diff --git a/.changes/header.tpl.md b/.changes/header.tpl.md new file mode 100644 index 000000000000..df8faa7b2d5a --- /dev/null +++ b/.changes/header.tpl.md @@ -0,0 +1,6 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), +and is generated by [Changie](https://github.com/miniscruff/changie). diff --git a/.changes/unreleased/.gitkeep b/.changes/unreleased/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/.changes/v0.11.0.md b/.changes/v0.11.0.md new file mode 100644 index 000000000000..9a88a0712935 --- /dev/null +++ b/.changes/v0.11.0.md @@ -0,0 +1,195 @@ +## v0.11.0 (05/10/2024) + +### Notice + +* The `--webserver` flag is now enabled by default in `tabby serve`. To turn off the webserver and only use OSS features, use the `--no-webserver` flag. +* The `/v1beta/chat/completions` endpoint has been moved to `/v1/chat/completions`, while the old endpoint is still available for backward compatibility. + +### Features +* Upgraded [llama.cpp](https://github.com/ggerganov/llama.cpp) to version [b2715](https://github.com/ggerganov/llama.cpp/releases/tag/b2715). +* Added support for integrating repositories from GitHub and GitLab using personal access tokens. +* Introduced a new **Activities** page to view user activities. +* Implemented incremental indexing for faster repository context updates. +* Added storage usage statistics in the **System** page. +* Included an `Ask Tabby` feature in the source code browser to provide in-context help from AI. + +### Fixes and Improvements +* Changed the default model filename from `q8_0.v2.gguf` to `model.gguf` in MODEL_SPEC.md. +* Excluded activities from deactivated users in reports. + +## v0.10.0 (04/22/2024) + +### Features +* Introduced the `--chat-device` flag to specify the device used to run the chat model. +* Added a "Reports" tab in the web interface, which provides team-wise statistics for Tabby IDE and Extensions usage (e.g., completions, acceptances). +* Enabled the use of segmented models with the `tabby download` command. +* Implemented the "Go to file" functionality in the Code Browser. + +### Fixes and Improvements +* Fix worker unregisteration misfunctioning caused by unmatched address. +* Accurate repository context filtering using fuzzy matching on `git_url` field. +* Support the use of client-side context, including function/class declarations from LSP, and relevant snippets from local changed files. + +## v0.9.1 (03/19/2024) + +### Fixes and Improvements +* Fix worker registration check against enterprise licenses. +* Fix default value of `disable_client_side_telemetry` when `--webserver` is not used. + +## v0.9.0 (03/06/2024) + +### Features + +* Support for SMTP configuration in the user management system. +* Support for SSO and team management as features in the Enterprise tier. +* Fully managed repository indexing using `--webserver`, with job history logging available in the web interface. + +## v0.8.3 (02/06/2024) + +### Fixes and Improvements + +* Ensure `~/.tabby/repositories` exists for tabby scheduler jobs: https://github.com/TabbyML/tabby/pull/1375 +* Add cpu only binary `tabby-cpu` to docker distribution. + +## v0.8.0 (02/02/2024) + +### Notice + +* Due to format changes, re-executing `tabby scheduler --now` is required to ensure that `Code Browser` functions properly. + +### Features + +* Introducing a preview release of the `Source Code Browser`, featuring visualization of code snippets utilized for code completion in RAG. +* Added a Windows CPU binary distribution. +* Added a Linux ROCm (AMD GPU) binary distribution. + +### Fixes and Improvements + +* Fixed an issue with cached permanent redirection in certain browsers (e.g., Chrome) when the `--webserver` flag is disabled. +* Introduced the `TABBY_MODEL_CACHE_ROOT` environment variable to individually override the model cache directory. +* The `/v1beta/chat/completions` API endpoint is now compatible with OpenAI's chat completion API. +* Models from our official registry can now be referred to without the TabbyML prefix. Therefore, for the model TabbyML/CodeLlama-7B, you can simply refer to it as CodeLlama-7B everywhere. + +## v0.7.0 (12/15/2023) + +### Features + +* Tabby now includes built-in user management and secure access, ensuring that it is only accessible to your team. +* The `--webserver` flag is a new addition to `tabby serve` that enables secure access to the tabby server. When this flag is on, IDE extensions will need to provide an authorization token to access the instance. + - Some functionalities that are bound to the webserver (e.g. playground) will also require the `--webserver` flag. + + +### Fixes and Improvements + +* Fix https://github.com/TabbyML/tabby/issues/1036, events log should be written to dated json files. + +## v0.6.0 (11/27/2023) + +### Features + +* Add distribution support (running completion / chat model on different process / machine). +* Add conversation history in chat playground. +* Add `/metrics` endpoint for prometheus metrics collection. + +### Fixes and Improvements + +* Fix the slow repository indexing due to constraint memory arena in tantivy index writer. +* Make `--model` optional, so users can create a chat only instance. +* Add `--parallelism` to control the throughput and VRAM usage: https://github.com/TabbyML/tabby/pull/727 + +## v0.5.5 (11/09/2023) + +### Fixes and Improvements + +### Notice + +* llama.cpp backend (CPU, Metal) now requires a redownload of gguf model due to upstream format changes: https://github.com/TabbyML/tabby/pull/645 https://github.com/ggerganov/llama.cpp/pull/3252 +* Due to indexing format changes, the `~/.tabby/index` needs to be manually removed before any further runs of `tabby scheduler`. +* `TABBY_REGISTRY` is replaced with `TABBY_DOWNLOAD_HOST` for the github based registry implementation. + +### Features + +* Improved dashboard UI. + +### Fixes and Improvements + +* Cpu backend is switched to llama.cpp: https://github.com/TabbyML/tabby/pull/638 +* add `server.completion_timeout` to control the code completion interface timeout: https://github.com/TabbyML/tabby/pull/637 +* Cuda backend is switched to llama.cpp: https://github.com/TabbyML/tabby/pull/656 +* Tokenizer implementation is switched to llama.cpp, so tabby no longer need to download additional tokenizer file: https://github.com/TabbyML/tabby/pull/683 +* Fix deadlock issue reported in https://github.com/TabbyML/tabby/issues/718 + +## v0.4.0 (10/24/2023) + +### Features + +* Supports golang: https://github.com/TabbyML/tabby/issues/553 +* Supports ruby: https://github.com/TabbyML/tabby/pull/597 +* Supports using local directory for `Repository.git_url`: use `file:///path/to/repo` to specify a local directory. +* A new UI design for webserver. + +### Fixes and Improvements + +* Improve snippets retrieval by dedup candidates to existing content + snippets: https://github.com/TabbyML/tabby/pull/582 + +## v0.3.1 (10/21/2023) +### Fixes and improvements + +* Fix GPU OOM issue caused the parallelism: https://github.com/TabbyML/tabby/issues/541, https://github.com/TabbyML/tabby/issues/587 +* Fix git safe directory check in docker: https://github.com/TabbyML/tabby/issues/569 + +## v0.3.0 (10/13/2023) + +### Features +#### Retrieval-Augmented Code Completion Enabled by Default + +The currently supported languages are: + +* Rust +* Python +* JavaScript / JSX +* TypeScript / TSX + +A blog series detailing the technical aspects of Retrieval-Augmented Code Completion will be published soon. Stay tuned! + +### Fixes and Improvements + +* Fix [Issue #511](https://github.com/TabbyML/tabby/issues/511) by marking ggml models as optional. +* Improve stop words handling by combining RegexSet into Regex for efficiency. + +## v0.2.2 (10/09/2023) +### Fixes and improvements + +* Fix a critical issue that might cause request dead locking in ctranslate2 backend (when loading is heavy) + +## v0.2.1 (10/03/2023) +### Features +#### Chat Model & Web Interface + +We have introduced a new argument, `--chat-model`, which allows you to specify the model for the chat playground located at http://localhost:8080/playground + +To utilize this feature, use the following command in the terminal: + +```bash +tabby serve --device metal --model TabbyML/StarCoder-1B --chat-model TabbyML/Mistral-7B +``` + +#### ModelScope Model Registry + +Mainland Chinese users have been facing challenges accessing Hugging Face due to various reasons. The Tabby team is actively working to address this issue by mirroring models to a hosting provider in mainland China called modelscope.cn. + +```bash +## Download from the Modelscope registry +TABBY_REGISTRY=modelscope tabby download --model TabbyML/WizardCoder-1B +``` + +### Fixes and improvements + +* Implemented more accurate UTF-8 incremental decoding in the [GitHub pull request](https://github.com/TabbyML/tabby/pull/491). +* Fixed the stop words implementation by utilizing RegexSet to isolate the stop word group. +* Improved model downloading logic; now Tabby will attempt to fetch the latest model version if there's a remote change, and the local cache key becomes stale. +* set default num_replicas_per_device for ctranslate2 backend to increase parallelism. + + + +No releases yet, this file will be updated when generating your first release. \ No newline at end of file diff --git a/.changes/v0.11.1.md b/.changes/v0.11.1.md new file mode 100644 index 000000000000..659ee944b55f --- /dev/null +++ b/.changes/v0.11.1.md @@ -0,0 +1,6 @@ +## v0.11.1 (2024-05-14) + +### Fixed and Improvements + +* Fixed display of files where the path contains special characters. ([#2081](https://github.com/TabbyML/tabby/issues/2081)) +* Fixed non-admin users not being able to see the repository in Code Browser. ([#2110](https://github.com/TabbyML/tabby/discussions/2110)) diff --git a/.changes/v0.12.0.md b/.changes/v0.12.0.md new file mode 100644 index 000000000000..e950d91c39f9 --- /dev/null +++ b/.changes/v0.12.0.md @@ -0,0 +1,16 @@ +## v0.12.0 (2024-05-31) + +### Features + +* Support Gitlab SSO +* Support connect with Self-Hosted Github / Gitlab +* Repository Context is now utilizied in "Code Browser" as well + +### Fixed and Improvements + +* llama-server from llama.cpp is now distributed as an individual binary, allowing for more flexible configuration +* HTTP API is out of experimental - you can connect tabby to models through HTTP API. Right now following APIs are supported: + - llama.cpp + - ollama + - mistral / codestral + - openai diff --git a/.changes/v0.13.0.md b/.changes/v0.13.0.md new file mode 100644 index 000000000000..df0c3d9b7e16 --- /dev/null +++ b/.changes/v0.13.0.md @@ -0,0 +1,17 @@ +## v0.13.0 (2024-06-28) + +### Features + +* Introduced a new Home page featuring the Answer Engine, which activates when the chat model is loaded. +* Enhanced the Answer Engine's context by indexing issues and pull requests. +* Supports web page crawling to further enrich the Answer Engine's context. +* Enabled navigation through various git trees in the git browser. + +### Fixed and Improvements + +* Turn on sha256 checksum verification for model downloading. +* Added an environment variable `TABBY_HUGGINGFACE_HOST_OVERRIDE` to override `huggingface.co` with compatible mirrors (e.g., `hf-mirror.com`) for model downloading. +* Bumped `llama.cpp` version to [b3166](https://github.com/ggerganov/llama.cpp/releases/tag/3166). +* Improved logging for the `llama.cpp` backend. +* Added support for triggering background jobs in the admin UI. +* Enhanced logging for backend jobs in the admin UI. diff --git a/.changes/v0.13.1.md b/.changes/v0.13.1.md new file mode 100644 index 000000000000..c13b7ad7160a --- /dev/null +++ b/.changes/v0.13.1.md @@ -0,0 +1,7 @@ +## v0.13.1 (2024-07-10) + +### Fixed and Improvements + +* Bump llama.cpp version to b3334, supporting Deepseek V2 series models. +* Turn on fast attention for Qwen2-1.5B model to fix the quantization error. +* Properly set number of GPU layers (to zero) when device is CPU. \ No newline at end of file diff --git a/.changes/v0.14.0.md b/.changes/v0.14.0.md new file mode 100644 index 000000000000..7a4ac55c9fce --- /dev/null +++ b/.changes/v0.14.0.md @@ -0,0 +1,11 @@ +## v0.14.0 (2024-07-23) + +### Features +* Code search functionality is now available in the `Code Browser` tab. Users can search for code using regex patterns and filter by language, repository, and branch. +* Initial experimental support for natural language to codebase conversation in `Answer Engine`. + +### Fixed and Improvements + +* Incremental issues / PRs indexing by checking `updated_at`. +* Canonicalize `git_url` before performing a relevant code search. Previously, for git_urls with credentials, the canonicalized git_url was used in the index, but the query still used the raw git_url. +* bump llama.cpp to b3370 - which fixes Qwen2 model series inference diff --git a/.changes/v0.15.0.md b/.changes/v0.15.0.md new file mode 100644 index 000000000000..2269def8c10a --- /dev/null +++ b/.changes/v0.15.0.md @@ -0,0 +1,13 @@ +## v0.15.0 (2024-08-08) + +### Features + +* The search bar in the Code Browser has been reworked and integrated with file navigation functionality. +* GraphQL syntax highlighting support in Code Browser. + +### Fixed and Improvements + +* For linked GitHub repositories, issues and PRs are now only returned when the repository is selected. +* Fixed GitLab issues/MRs indexing - no longer panics if the description field is null. +* When connecting to localhost model servers, proxy settings are now skipped. +* Allow set code completion's `max_input_length` and `max_output_tokens` in config.toml diff --git a/.changes/v0.16.1.md b/.changes/v0.16.1.md new file mode 100644 index 000000000000..2dc7926ebbac --- /dev/null +++ b/.changes/v0.16.1.md @@ -0,0 +1,13 @@ +## v0.16.1 (2024-08-27) + +### Notice +* Starting from this version, we are utilizing websockets for features that require streaming (e.g., Answer Engine and Chat Side Panel). If you are deploying tabby behind a reverse proxy, you may need to configure the proxy to support websockets. + +### Features + +* Discussion threads in the Answer Engine are now persisted, allowing users to share threads with others. + +### Fixed and Improvements + +* Fixed an issue where the llama-server subprocess was not being reused when reusing a model for Chat / Completion together (e.g., Codestral-22B) with the local model backend. +* Updated llama.cpp to version b3571 to support the jina series embedding models. diff --git a/.changes/v0.17.0.md b/.changes/v0.17.0.md new file mode 100644 index 000000000000..4f282ecd2771 --- /dev/null +++ b/.changes/v0.17.0.md @@ -0,0 +1,13 @@ +## v0.17.0 (2024-09-10) + +### Notice + +* We've reworked the `Web` (a beta feature) context provider into the `Developer Docs` context provider. Previously added context in the `Web` tab has been cleared and needs to be manually migrated to `Developer Docs`. + +### Features + +* Extensive rework has been done in the answer engine search box. + - Developer Docs / Web search is now triggered by `@`. + - Repository Context is now selected using `#`. + +* Supports OCaml diff --git a/.changes/v0.18.0.md b/.changes/v0.18.0.md new file mode 100644 index 000000000000..a0d6c2e63817 --- /dev/null +++ b/.changes/v0.18.0.md @@ -0,0 +1,11 @@ +## v0.18.0 (2024-10-08) + +### Notice + +* The Chat Side Panel implementation has been redesigned in version 0.18, necessitating an extension version bump for compatibility with 0.18.0. + - VSCode: >= 1.12.0 + - IntelliJ: >= 1.8.0 + +### Features + +* User Groups Access Control: Server Administrators can now assign user groups to specific context providers to precisely control which contexts can be accessed by which user groups. diff --git a/.changes/v0.19.0.md b/.changes/v0.19.0.md new file mode 100644 index 000000000000..57c439ccd89b --- /dev/null +++ b/.changes/v0.19.0.md @@ -0,0 +1,12 @@ +## v0.19.0 (2024-10-30) + +### Features + +* For Answer Engine, when the file content is reasonably short (e.g., less than 200 lines of code), include the entire file content directly instead of only the chunk ([#3096](https://github.com/TabbyML/tabby/issues/3096)). +* Allowed adding additional languages through the `config.toml` file. +* Allowed customizing the `system_prompt` for Answer Engine. + +### Fixes and Improvements + +* Redesigned homepage to make team activities (e.g., threads discussed in Answer Engine) discoverable. +* Supported downloading models with multiple partitions (e.g., Qwen-2.5 series). diff --git a/.changes/v0.20.0.md b/.changes/v0.20.0.md new file mode 100644 index 000000000000..6e5655817043 --- /dev/null +++ b/.changes/v0.20.0.md @@ -0,0 +1,12 @@ +## v0.20.0 (2024-11-08) + +### Features + +* Search results can now be edited directly. +* Allow switching backend chat models in Answer Engine. +* Added a connection test button in the `System` tab to test the connection to the backend LLM server. + +### Fixes and Improvements + +* Optimized CR-LF inference in code completion. ([#3279](https://github.com/TabbyML/tabby/issues/3279)) +* Bumped `llama.cpp` version to `b3995`. diff --git a/.changes/v0.21.0.md b/.changes/v0.21.0.md new file mode 100644 index 000000000000..901f4d03e334 --- /dev/null +++ b/.changes/v0.21.0.md @@ -0,0 +1,19 @@ +## v0.21.0 (2024-12-02) + +### Notice + +* Due to changes in the indexing format, the `~/.tabby/index` directory will be automatically removed before any further indexing jobs are run. It is expected that the indexing jobs will be re-run (instead of incrementally) after the upgrade. + +### Features + +* Support connecting to llamafile model backend. +* Display **Open** / **Closed** state for issues / pull requests in Answer Engine context card. +* Support deleting the entire thread in Answer Engine. +* Add rate limiter options for HTTP-powered model backends. + +### Fixed and Improvements + +* Fixed a panic that occurred when specifying a local model ([#3464](https://github.com/TabbyML/tabby/issues/3464)) +* Add pagination to Answer Engine threads. +* Fix Vulkan binary distributions. +* Improve the retry logic for chunk embedding computation in indexing job. diff --git a/.changes/v0.21.1.md b/.changes/v0.21.1.md new file mode 100644 index 000000000000..db20dff0f3a4 --- /dev/null +++ b/.changes/v0.21.1.md @@ -0,0 +1,9 @@ +## v0.21.1 (2024-12-09) + +### Notice + +* This is a patch release, please also check [the full release note](https://github.com/TabbyML/tabby/releases/tag/v0.21.0) for 0.21. + +### Fixed and Improvements + +* Fixed Gitlab Context Provider. \ No newline at end of file diff --git a/.changes/v0.21.2.md b/.changes/v0.21.2.md new file mode 100644 index 000000000000..438a97508086 --- /dev/null +++ b/.changes/v0.21.2.md @@ -0,0 +1,11 @@ +## v0.21.2 (2024-12-18) + +### Notice + +* This is a patch release, please also check [the full release note](https://github.com/TabbyML/tabby/releases/tag/v0.21.1) for 0.21.1. + +### Fixed and Improvements + +* Adapt extension side changes in new versions. + - VSCode: 1.16.0 + - IntelliJ Platform: 1.9.1 diff --git a/.changes/v0.22.0.md b/.changes/v0.22.0.md new file mode 100644 index 000000000000..c71565a317d6 --- /dev/null +++ b/.changes/v0.22.0.md @@ -0,0 +1,15 @@ +## v0.22.0 (2024-12-23) + +### Features + +* Introduce notification inbox on homepage and license expiration check. ([#3541](https://github.com/TabbyML/tabby/pull/3541)) ([#3566](https://github.com/TabbyML/tabby/pull/3566)) +* Display author information for issues / pull requests in Answer Engine context card ([#3513](https://github.com/TabbyML/tabby/pull/3513)) + +### Fixed and Improvements + +* Refactors the pull request indexing process to enhance the speed of incremental indexing for pull docs. ([#3538](https://github.com/TabbyML/tabby/pull/3538)) +* Optimize the rate limiter on the HTTP-powered model backend to reduce errors. ([#3567](https://github.com/TabbyML/tabby/pull/3567)) +* Introduce rate limiting at 60 requests per minute in the tabby-webserver. ([#3484](https://github.com/TabbyML/tabby/pull/3484)) +* Validate model capability prior to download. ([#3565](https://github.com/TabbyML/tabby/pull/3565)) +* Fix broken tree view on Windows in CodeBrowser. ([#3528](https://github.com/TabbyML/tabby/pull/3528)) +* Upgrade all Tabby Linux base images to manylinux_2_28. ([#3536](https://github.com/TabbyML/tabby/pull/3536)) diff --git a/.changes/v0.23.0.md b/.changes/v0.23.0.md new file mode 100644 index 000000000000..2ab81dc3096c --- /dev/null +++ b/.changes/v0.23.0.md @@ -0,0 +1,11 @@ +## v0.23.0 (2025-01-09) + +### Features + +* Add commit hash to code attachments, allowing redirection to the specific commit in code browser. ([#3627](https://github.com/TabbyML/tabby/pull/3627)) ([#3577](https://github.com/TabbyML/tabby/pull/3577)) +* Display the connection status of the gitUrl repository in the chat side panel. ([#3550](https://github.com/TabbyML/tabby/pull/3550)) + +### Fixed and Improvements + +* Perform database backups only when there are pending schema migrations. ([#3620](https://github.com/TabbyML/tabby/pull/3620)) +* Add repository selection and remove '#' mentions in the Answer Engine. ([#3619](https://github.com/TabbyML/tabby/pull/3619)) diff --git a/.changes/v0.23.1.md b/.changes/v0.23.1.md new file mode 100644 index 000000000000..a3a351c176de --- /dev/null +++ b/.changes/v0.23.1.md @@ -0,0 +1,5 @@ +## v0.23.1 (2025-01-31) + +### Fixed and Improvements + +* Fix the issue of being unable to download remote models due to changes in HuggingFace API. ([#3772](https://github.com/TabbyML/tabby/pull/3772)) diff --git a/.changes/v0.24.0.md b/.changes/v0.24.0.md new file mode 100644 index 000000000000..7d3db808530c --- /dev/null +++ b/.changes/v0.24.0.md @@ -0,0 +1,16 @@ +## v0.24.0 (2025-01-23) + +### Features + +* Implement LDAP Authentication Integration. ([#3650](https://github.com/TabbyML/tabby/pull/3650)) ([#3625](https://github.com/TabbyML/tabby/pull/3625)) +* Add Notifications for unsuccessful background jobs. ([#3713](https://github.com/TabbyML/tabby/pull/3713)) + +### Fixed and Improvements + +* Fixed a bug that prevented the client code context in historical messages from being added to the prompt. ([#3673](https://github.com/TabbyML/tabby/pull/3673)) +* Retain the job run and user event history only for the past three months. ([#3640](https://github.com/TabbyML/tabby/pull/3640)) +* Resolved an issue that caused integration errors with recent versions of Jan AI. ([#3649](https://github.com/TabbyML/tabby/pull/3649)) +* Resolved an issue where repositories specified in config.toml were not synchronizing correctly. ([#3703](https://github.com/TabbyML/tabby/pull/3703)) +* Set the active text tab as default context in Code Browser chat. ([#3729](https://github.com/TabbyML/tabby/pull/3729)) +* Resolved an issue that caused models download failures due to changes in HuggingFace API. ([#3772](https://github.com/TabbyML/tabby/pull/3772)) +* Omit indexing of GitHub Pull Request diffs that exceed 300 files. ([#3779](https://github.com/TabbyML/tabby/pull/3779)) diff --git a/.changes/v0.25.0.md b/.changes/v0.25.0.md new file mode 100644 index 000000000000..576100473ce4 --- /dev/null +++ b/.changes/v0.25.0.md @@ -0,0 +1,23 @@ +## v0.25.0 (2025-02-17) + +### Notice + +Significant changes have been implemented in this release; please consider adjusting them to fit your specific use case. + +* The default parallelism has been increased from 1 to 4, which might increase VRAM usage. ([#3832](https://github.com/TabbyML/tabby/pull/3832)) +* Introduce a new embedding kind `llama.cpp/before_b4356_embedding` for llamafile or other embedding services utilizing the legacy llama.cpp embedding API. ([#3828](https://github.com/TabbyML/tabby/pull/3828)) + +### Features + +* Expose thinking process of Answer Engine to the answers in thread message. ([#3785](https://github.com/TabbyML/tabby/pull/3785)) ([#3672](https://github.com/TabbyML/tabby/pull/3672)) +* Enable the Answer Engine to access the repository's directory file list as needed. ([#3796](https://github.com/TabbyML/tabby/pull/3796)) +* Enable the use of `@` to mention a symbol in Chat Sidebar. ([#3778](https://github.com/TabbyML/tabby/pull/3778)) +* Provide default question recommendations that are repository-aware on Answer Engine. ([#3815](https://github.com/TabbyML/tabby/pull/3815)) + +### Fixed and Improvements + +* Provide a configuration to truncate text content prior to dispatching it to embedding service.. ([#3816](https://github.com/TabbyML/tabby/pull/3816)) +* Bump llama.cpp version to b4651. ([#3798](https://github.com/TabbyML/tabby/pull/3798)) +* Automatically retry embedding when the service occasionally fails due to issues with llama.cpp. ([#3805](https://github.com/TabbyML/tabby/pull/3805)) +* Enhance the user interface experience for Answer Engine. ([#3845](https://github.com/TabbyML/tabby/pull/3845)) ([#3794](https://github.com/TabbyML/tabby/pull/3794)) +* Resolve the deserialization issue related to `finish_reason` in chat response from LiteLLM Proxy Server.([#3882](https://github.com/TabbyML/tabby/pull/3882)) diff --git a/.changes/v0.25.1.md b/.changes/v0.25.1.md new file mode 100644 index 000000000000..8ba71a452096 --- /dev/null +++ b/.changes/v0.25.1.md @@ -0,0 +1,9 @@ +## v0.25.1 (2025-02-25) + +### Notice + +* This is a patch release, please also check [the full release note](https://github.com/TabbyML/tabby/releases/tag/v0.25.0) for 0.25. + +### Fixed and Improvements + +* Refine the UI details in Answer Engine. ([#3888](https://github.com/TabbyML/tabby/pull/3888)) ([#3889](https://github.com/TabbyML/tabby/pull/3889)) ([#3891](https://github.com/TabbyML/tabby/pull/3891)) diff --git a/.changes/v0.25.2.md b/.changes/v0.25.2.md new file mode 100644 index 000000000000..916dc98925ee --- /dev/null +++ b/.changes/v0.25.2.md @@ -0,0 +1,9 @@ +## v0.25.2 (2025-02-28) + +### Notice + +* This is a patch release, please also check [the historical release note](https://github.com/TabbyML/tabby/releases/tag/v0.25.1) for 0.25.1. + +### Fixed and Improvements + +* Alter the LDAP login mechanism to query users across the Organizational Unit subtree rather than a single level. An LDAP directory is structured hierarchically as a tree of nodes. This modification empowers all users beneath the LDAP Organizational Unit (OU) subtree to authenticate, rather than restricting access to users within a specific OU. diff --git a/.changes/v0.26.0.md b/.changes/v0.26.0.md new file mode 100644 index 000000000000..d896b659d979 --- /dev/null +++ b/.changes/v0.26.0.md @@ -0,0 +1,14 @@ +## v0.26.0 (2025-03-16) + +### Notice + +* Bump minimum katana version to 1.1.2 + +### Features + +* Enable the Answer Engine to access the repository's commit history as needed. ([#3916](https://github.com/TabbyML/tabby/pull/3916)) +* Support the display of user chat history on Homepage and Chat Side Panel. ([#3897](https://github.com/TabbyML/tabby/pull/3897)) + +### Fixed and Improvements + +* Facilitate the crawling of developer documentation from llms-full.txt when available. ([#3880](https://github.com/TabbyML/tabby/pull/3880)) diff --git a/.changes/v0.27.0.md b/.changes/v0.27.0.md new file mode 100644 index 000000000000..3024920282c7 --- /dev/null +++ b/.changes/v0.27.0.md @@ -0,0 +1,17 @@ +## v0.27.0 (2025-03-29) + +### Notice + +* Prepare for the 1.0 release. Starting with this release, extensions version upgrades will maintain backward compatibility with the Tabby server. + +### Features + +* Add support for the ability to execute shell commands within Chat Panel. ([#3979](https://github.com/TabbyML/tabby/pull/3979)) +* Add support for `@changes` within Chat Panel to include uncommitted changes as contexts. ([#3988](https://github.com/TabbyML/tabby/pull/3988)) +* Add security option to hide password login in frontend. When enabled, the password login can be revealed by appending the URL search parameter `passwordSignIn=true`. ([#3996](https://github.com/TabbyML/tabby/pull/3996)) ([#4015](https://github.com/TabbyML/tabby/pull/4015)) + +### Fixed and Improvements + +* Store background job logs on disk to prevent disruptions during processing of large repositories. ([#3994](https://github.com/TabbyML/tabby/pull/3994)) +* Track chat usage frequency and display it in Reports page. ([#4025](https://github.com/TabbyML/tabby/pull/4025)) ([#4042](https://github.com/TabbyML/tabby/pull/4042)) +* Resolve the chat functionality issue involving OpenAI reasoning models such as `o3-mini` and `o1-mini`. ([#4049](https://github.com/TabbyML/tabby/pull/4049)) diff --git a/.changes/v0.27.1.md b/.changes/v0.27.1.md new file mode 100644 index 000000000000..4f77ea876c6f --- /dev/null +++ b/.changes/v0.27.1.md @@ -0,0 +1,11 @@ +## v0.27.1 (2025-04-07) + +### Notice + +* This is a patch release, please also check [the full release note](https://github.com/TabbyML/tabby/releases/tag/v0.27.0) for 0.27. + +### Fixed and Improvements + +* Resolved an issue where certain escaped characters could cause plugin crashes on Windows systems. ([#4114](https://github.com/TabbyML/tabby/pull/4114)) +* Resolved an issue where the Tabby server could hang while waiting for the registry file to download in offline environments. ([#4120](https://github.com/TabbyML/tabby/pull/4120)) +* Improved handling of file mentions from different sources in the Chat Panel. ([#4116](https://github.com/TabbyML/tabby/pull/4116)) diff --git a/.changes/v0.28.0.md b/.changes/v0.28.0.md new file mode 100644 index 000000000000..7c575225c146 --- /dev/null +++ b/.changes/v0.28.0.md @@ -0,0 +1,11 @@ +## v0.28.0 (2025-04-28) + +### Features + +* Add support to convert Answer Engine messages into Pages. ([#4194](https://github.com/TabbyML/tabby/pull/4194)) +* Add support for Doc Query in Chat Panel, allowing Dev Docs to be included as context. ([#4095](https://github.com/TabbyML/tabby/pull/4095)) + +### Fixed and Improvements + +* Enhance background task logging to boost performance and enrich user experience. ([#4066](https://github.com/TabbyML/tabby/pull/4066)) ([#4145](https://github.com/TabbyML/tabby/pull/4145)) +* Enhance the display of thinking processes in Answer Engine and Chat Panel. ([#4091](https://github.com/TabbyML/tabby/pull/4091)) diff --git a/.changes/v0.29.0.md b/.changes/v0.29.0.md new file mode 100644 index 000000000000..748c236d8a2e --- /dev/null +++ b/.changes/v0.29.0.md @@ -0,0 +1,11 @@ +## v0.29.0 (2025-05-19) + +### Features + +* Add RESTful APIs for ingesting custom documents and facilitating their removal. ([#4171](https://github.com/TabbyML/tabby/pull/4171)) ([#4203](https://github.com/TabbyML/tabby/pull/4203)) ([#4196](https://github.com/TabbyML/tabby/pull/4196)) + +### Fixed and Improvements + +* Enhance error details in Notification Inbox. ([#4184](https://github.com/TabbyML/tabby/pull/4184)) +* Add support for Automatic Jobs status polling. ([#4172](https://github.com/TabbyML/tabby/pull/4172)) +* Bump llama.cpp to b2500. ([#4200](https://github.com/TabbyML/tabby/pull/4200)) diff --git a/.changes/v0.30.0.md b/.changes/v0.30.0.md new file mode 100644 index 000000000000..b668b7828ca1 --- /dev/null +++ b/.changes/v0.30.0.md @@ -0,0 +1,14 @@ +## v0.30.0 (2025-07-02) + +### Features + +* Support indexing GitLab Merge Request as Context. [#4227](https://github.com/TabbyML/tabby/pull/4227) + +### Fixed and Improvements + +* Use CUDA 12 image as base image by default. [#4235](https://github.com/TabbyML/tabby/pull/4235) +* Leverage the Answer Engine logic to enrich the context when generating new pages, improving page quality. [#4237](https://github.com/TabbyML/tabby/pull/4237) +* Improve the model configuration details on the System page. [#4236](https://github.com/TabbyML/tabby/pull/4236) +* Resolve the flickering of buttons within code snippets in a chat response during answer generation. [#4233](https://github.com/TabbyML/tabby/pull/4233) +* Set the member filter bar always fixed at the top of the Reports page.[#4232](https://github.com/TabbyML/tabby/pull/4232) +* Resolve the issue when loading a multi-part model from local.[#4302](https://github.com/TabbyML/tabby/pull/4302) diff --git a/.changes/v0.30.1.md b/.changes/v0.30.1.md new file mode 100644 index 000000000000..d8497f3de34e --- /dev/null +++ b/.changes/v0.30.1.md @@ -0,0 +1,9 @@ +## v0.30.1 (2025-07-16) + +### Notice + +* This is a patch release, please also check [the full release note](https://github.com/TabbyML/tabby/releases/tag/v0.30.0) for 0.30. + +### Fixed and Improvements + +* Downgrade CUDA base image to version 12.4.1 to enhance compatibility. [#4314](https://github.com/TabbyML/tabby/pull/4314) diff --git a/.changes/v0.30.2.md b/.changes/v0.30.2.md new file mode 100644 index 000000000000..97ac94b1987c --- /dev/null +++ b/.changes/v0.30.2.md @@ -0,0 +1,11 @@ +## v0.30.2 (2025-07-31) + +### Notice + +* This is a patch release, please also check [the full release note](https://github.com/TabbyML/tabby/releases/tag/v0.30.0) for 0.30. + +### Fixed and Improvements + +* Use 0.7.3 sqlx to avoid database pool timeout. [#4328](https://github.com/TabbyML/tabby/pull/4328) +* Bump llama.cpp to b6047. [#4330](https://github.com/TabbyML/tabby/pull/4330) +* Expose the Flash Attention LLAMA flag as an environment variable. [#4323](https://github.com/TabbyML/tabby/pull/4323) diff --git a/.changes/v0.31.0.md b/.changes/v0.31.0.md new file mode 100644 index 000000000000..00702c687267 --- /dev/null +++ b/.changes/v0.31.0.md @@ -0,0 +1,5 @@ +## v0.31.0 (2025-08-19) + +### Features + +* Add support for custom branding with name and logo (Available Exclusively with an Enterprise License). [#4334](https://github.com/TabbyML/tabby/pull/4334) diff --git a/.changes/v0.31.2.md b/.changes/v0.31.2.md new file mode 100644 index 000000000000..9e873a6d1ed0 --- /dev/null +++ b/.changes/v0.31.2.md @@ -0,0 +1,9 @@ +## v0.31.2 (2025-09-25) + +### Notice + +* This is a patch release, please also check [the full release note](https://github.com/TabbyML/tabby/releases/tag/v0.31.0) for 0.31. + +### Fixed and Improvements + +* Added environment variable `TABBY_INDEX_REPO_IN_SHARD` to enable hourly sharded indexing when repository count exceeds 20. [#4366](https://github.com/TabbyML/tabby/pull/4366) diff --git a/.changie.yaml b/.changie.yaml new file mode 100644 index 000000000000..9679ab87c90a --- /dev/null +++ b/.changie.yaml @@ -0,0 +1,22 @@ +changesDir: .changes +unreleasedDir: unreleased +headerPath: header.tpl.md +changelogPath: CHANGELOG.md +versionExt: md +versionFormat: '## {{.Version}} ({{.Time.Format "2006-01-02"}})' +kindFormat: '### {{.Kind}}' +changeFormat: '* {{.Body}}' +kinds: +- label: Notice + auto: minor +- label: Features + auto: minor +- label: Fixed and Improvements + auto: patch +newlines: + afterChangelogHeader: 1 + afterKind: 1 + afterChangelogVersion: 1 + beforeKind: 1 + endOfVersion: 1 +envPrefix: CHANGIE_ \ No newline at end of file diff --git a/.github/workflows/ast-grep-lint.yml b/.github/workflows/ast-grep-lint.yml new file mode 100644 index 000000000000..638d03890944 --- /dev/null +++ b/.github/workflows/ast-grep-lint.yml @@ -0,0 +1,15 @@ +name: ast-grep lint +on: [push] + +jobs: + sg-lint: + runs-on: ubuntu-latest + name: ast-grep-lint + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: ast-grep lint step + uses: ast-grep/action@v1.4 + with: + version: 0.20.1 \ No newline at end of file diff --git a/.github/workflows/autofix-pnpm.yml b/.github/workflows/autofix-pnpm.yml new file mode 100644 index 000000000000..bdc11d3ea9da --- /dev/null +++ b/.github/workflows/autofix-pnpm.yml @@ -0,0 +1,57 @@ +name: autofix.ci + +on: + pull_request: + branches: ["main" ] + paths: + - '.github/workflows/autofix-pnpm.yml' + - 'clients/**' + - 'ee/tabby-ui/**' + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow_ref }}-${{ github.head_ref || github.ref_name }} + + # If this is enabled it will cancel current running and start latest + cancel-in-progress: true + +jobs: + autofix: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 18 + + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + version: 9 + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install + + - name: Fix lint + run: pnpm lint:fix + + - uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef diff --git a/.github/workflows/autofix-python.yml b/.github/workflows/autofix-python.yml index 62a77de5dada..73afce03fc7a 100644 --- a/.github/workflows/autofix-python.yml +++ b/.github/workflows/autofix-python.yml @@ -29,4 +29,4 @@ jobs: src: "./python" args: --fix - - uses: autofix-ci/action@d3e591514b99d0fca6779455ff8338516663f7cc + - uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef diff --git a/.github/workflows/autofix-rust.yml b/.github/workflows/autofix-rust.yml index 76299bff2601..917580316ed2 100644 --- a/.github/workflows/autofix-rust.yml +++ b/.github/workflows/autofix-rust.yml @@ -10,18 +10,24 @@ on: - 'crates/**' - 'ee/**' - '!ee/tabby-ui/**' + - '!ee/tabby-email/**' permissions: contents: read concurrency: - group: ${{ github.workflow_ref }}-${{ github.head_ref || github.ref_name }} - + group: ${{ github.workflow_ref }}-${{ github.head_ref || github.ref_name }} + # If this is enabled it will cancel current running and start latest cancel-in-progress: true jobs: autofix: + env: + CARGO_TERM_COLOR: always + SCCACHE_GHA_ENABLED: true + RUSTC_WRAPPER: sccache + CARGO_INCREMENTAL: 0 runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -34,30 +40,30 @@ jobs: toolchain: nightly components: rustfmt, clippy + - name: Sccache cache + uses: mozilla-actions/sccache-action@v0.0.9 + - name: Install cargo-machete uses: actions-rs/cargo@v1 with: command: install - args: cargo-machete - - - name: Sccache cache - uses: mozilla-actions/sccache-action@v0.0.3 - with: - version: "v0.4.0" + args: --version 0.7.0 cargo-machete - name: Cargo registry cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: - key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.toml') }}-${{ github.sha }} + key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-${{ github.sha }} restore-keys: | - cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.toml') }}- + cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}- cargo-${{ runner.os }}- path: | ~/.cargo/registry ~/.cargo/git - - run: bash ./ci/prepare_build_environment.sh + - run: sudo bash ./ci/prepare_build_environment.sh - run: make fix - - uses: autofix-ci/action@d3e591514b99d0fca6779455ff8338516663f7cc + - run: make update-graphql-schema + + - uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef diff --git a/.github/workflows/autofix-tabby-ui.yml b/.github/workflows/autofix-tabby-ui.yml deleted file mode 100644 index 227187540e5c..000000000000 --- a/.github/workflows/autofix-tabby-ui.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: autofix.ci - -on: - pull_request: - branches: ["main" ] - paths: - - '.github/workflows/autofix-tabby-ui.yml' - - 'ee/tabby-ui/**' - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow_ref }}-${{ github.head_ref || github.ref_name }} - - # If this is enabled it will cancel current running and start latest - cancel-in-progress: true - -jobs: - autofix: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - submodules: recursive - - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version: '18.x' - cache: yarn - cache-dependency-path: 'yarn.lock' - - - name: Install dependencies - working-directory: ./ee/tabby-ui - run: yarn install - - - name: Lint - working-directory: ./ee/tabby-ui - run: yarn lint:fix - - - name: Format - working-directory: ./ee/tabby-ui - run: yarn format:write - - - uses: autofix-ci/action@d3e591514b99d0fca6779455ff8338516663f7cc diff --git a/.github/workflows/bloat.yml b/.github/workflows/bloat.yml new file mode 100644 index 000000000000..9f20bbdbf5a6 --- /dev/null +++ b/.github/workflows/bloat.yml @@ -0,0 +1,65 @@ +on: # rebuild any PRs and main branch changes + pull_request: + branches: ["main"] + paths: + - '.github/workflows/bloat.yml' + - 'Cargo.toml' + - 'Cargo.lock' + - 'crates/**' + - 'ee/**' + - '!ee/tabby-ui/**' + push: + branches: + - main + +name: bloat + +permissions: write-all + +concurrency: + group: ${{ github.workflow_ref }}-${{ github.head_ref || github.ref_name }} + + # If this is enabled it will cancel current running and start latest + cancel-in-progress: true + +jobs: + cargo_bloat: + env: + CARGO_TERM_COLOR: always + SCCACHE_GHA_ENABLED: true + RUSTC_WRAPPER: sccache + CARGO_INCREMENTAL: 0 + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + + - name: Sccache cache + uses: mozilla-actions/sccache-action@v0.0.9 + + - name: Cargo registry cache + uses: actions/cache@v4 + with: + key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-${{ github.sha }} + restore-keys: | + cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}- + cargo-${{ runner.os }}- + path: | + ~/.cargo/registry + ~/.cargo/git + + - run: sudo bash ./ci/prepare_build_environment.sh + + - name: Run cargo bloat + uses: wsxiaoys/cargo-bloat-action@master + with: + token: ${{ secrets.GITHUB_TOKEN }} + include_packages: tabby \ No newline at end of file diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 1e7482119d18..f2af4232d353 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -3,23 +3,27 @@ name: Create and publish docker image on: workflow_dispatch: schedule: - - cron: '0 20 */1 * *' + - cron: "0 20 */1 * *" push: tags: - - 'v*' + - "v*" + - "!*-dev.*" + - "!vscode@*" + - '!vim@*' concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }} - + group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }} + # If this is enabled it will cancel current running and start latest cancel-in-progress: true env: - RUST_TOOLCHAIN: 1.73.0 + RUST_TOOLCHAIN: 1.82.0 + CUDA_VERSION: 12.4.1 jobs: release-docker: - runs-on: ubuntu-latest + runs-on: buildjet-2vcpu-ubuntu-2204 permissions: contents: read packages: write @@ -27,6 +31,13 @@ jobs: # with sigstore/fulcio when running outside of PRs. id-token: write + strategy: + matrix: + device-type: [cuda] + include: + - device-type: cuda + image-suffix: "" + steps: - name: Free Disk Space (Ubuntu) uses: jlumbroso/free-disk-space@main @@ -44,32 +55,34 @@ jobs: swap-storage: true - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive # Workaround: https://github.com/docker/build-push-action/issues/461 - name: Setup Docker buildx - uses: docker/setup-buildx-action@v2.0.0 + uses: docker/setup-buildx-action@v3 # Login against a Docker registry except on PR # https://github.com/docker/login-action - name: Log into GitHub Container registry - uses: docker/login-action@v2.0.0 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Log into Docker Hub - uses: docker/login-action@v2.0.0 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Generate image name + env: + IMAGE_SUFFIX: ${{ matrix.image-suffix }} run: | - echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >>${GITHUB_ENV} + echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}${IMAGE_SUFFIX}" >>${GITHUB_ENV} - uses: int128/docker-build-cache-config-action@v1 id: cache @@ -78,7 +91,7 @@ jobs: - name: Docker meta id: meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: # list of Docker images to use as base name for tags images: | @@ -95,21 +108,56 @@ jobs: # https://github.com/docker/build-push-action - name: Build and push Docker image id: build-and-push - uses: docker/build-push-action@v3.1.1 + uses: docker/build-push-action@v5 with: - file: Dockerfile + file: docker/Dockerfile.${{ matrix.device-type }} push: true context: . tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: ${{ steps.cache.outputs.cache-from }} cache-to: ${{ steps.cache.outputs.cache-to }} - build-args: RUST_TOOLCHAIN=${{ env.RUST_TOOLCHAIN }} + build-args: | + RUST_TOOLCHAIN=${{ env.RUST_TOOLCHAIN }} + CUDA_VERSION=${{ env.CUDA_VERSION }} + + - name: Docker meta for CUDA 11 + if: startsWith(github.ref, 'refs/tags/v') + id: meta-cuda11 + uses: docker/metadata-action@v5 + with: + images: | + ghcr.io/${{ env.IMAGE_NAME }} + ${{ env.IMAGE_NAME }} + # do not generate tags for latest since cuda12 is the latest + flavor: | + latest=false + suffix=-cuda11 + tags: | + type=semver,pattern={{version}} + + # Build and push Docker image with Buildx + # Only built on release tags for compatibility with previous versions + - name: Build and push Docker image for CUDA 11 + if: startsWith(github.ref, 'refs/tags/v') + id: build-and-push-cuda11 + uses: docker/build-push-action@v5 + with: + file: docker/Dockerfile.${{ matrix.device-type }} + push: true + context: . + tags: ${{ steps.meta-cuda11.outputs.tags }} + labels: ${{ steps.meta-cuda11.outputs.labels }} + cache-from: ${{ steps.cache.outputs.cache-from }} + cache-to: ${{ steps.cache.outputs.cache-to }} + build-args: | + RUST_TOOLCHAIN=${{ env.RUST_TOOLCHAIN }} + CUDA_VERSION=11.7.1 + - name: Docker Hub Description - uses: peter-evans/dockerhub-description@v3 + uses: peter-evans/dockerhub-description@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} repository: tabbyml/tabby - diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 9b8ff2b97efc..6ed16cd0efba 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -7,7 +7,7 @@ on: paths: - '.github/workflows/gh-pages.yml' - 'website/**' - + # Allows you to run this workflow manually from the Actions tab workflow_dispatch: @@ -42,7 +42,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v3 with: - node-version: 16.x + node-version: 18.x cache: yarn cache-dependency-path: website/yarn.lock - name: Install dependencies @@ -52,11 +52,11 @@ jobs: working-directory: website run: yarn build - name: Setup Pages - uses: actions/configure-pages@v1 + uses: actions/configure-pages@v5 - name: Upload artifact - uses: actions/upload-pages-artifact@v1 + uses: actions/upload-pages-artifact@v3 with: path: website/build - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v1 + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/gpt-translate.yml b/.github/workflows/gpt-translate.yml new file mode 100644 index 000000000000..f692d5d16d23 --- /dev/null +++ b/.github/workflows/gpt-translate.yml @@ -0,0 +1,25 @@ +name: GPT Translate + +on: + issue_comment: + types: [ created ] + +permissions: + pull-requests: write + issues: write + contents: write + +jobs: + gpt_translate: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Run GPT Translate + if: | + contains(github.event.comment.body, '/gpt-translate') || + contains(github.event.comment.body, '/gt') + uses: 3ru/gpt-translate@master + with: + apikey: ${{ secrets.OPENAI_API_KEY }} diff --git a/.github/workflows/release-intellij.yml b/.github/workflows/release-intellij.yml new file mode 100644 index 000000000000..754129025a3f --- /dev/null +++ b/.github/workflows/release-intellij.yml @@ -0,0 +1,150 @@ +name: Release Tabby Plugin for IntelliJ Platform + +on: + workflow_dispatch: + push: + tags: + - "intellij@*" + +concurrency: + group: ${{ github.workflow_ref }}-${{ github.head_ref || github.ref_name }} + + # If this is enabled it will cancel current running and start latest + cancel-in-progress: true + +jobs: + release-marketplace: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + lfs: true + + - name: Setup JDK + uses: actions/setup-java@v3 + with: + distribution: zulu + java-version: 17 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 18 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install + + - name: Determine Publish Channel + run: | + if [[ ${{ github.ref_name }} =~ ^intellij@[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "PUBLISH_CHANNEL=stable" >> $GITHUB_ENV + else + echo "PUBLISH_CHANNEL=alpha" >> $GITHUB_ENV + fi + + - name: Check Publish Channel + run: echo "Publish Channel is ${{ env.PUBLISH_CHANNEL }}" + + - name: Publish Plugin to Marketplace + env: + CERTIFICATE_CHAIN: ${{ secrets.INTELLIJ_PLUGIN_CERTIFICATE_CHAIN }} + PRIVATE_KEY: ${{ secrets.INTELLIJ_PLUGIN_PRIVATE_KEY }} + PUBLISH_TOKEN: ${{ secrets.INTELLIJ_PLUGIN_PUBLISH_TOKEN }} + PUBLISH_CHANNEL: ${{ env.PUBLISH_CHANNEL }} + uses: gradle/gradle-build-action@v2.4.2 + with: + arguments: publishPlugin + build-root-directory: clients/intellij + + release-github: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + lfs: true + + - name: Setup JDK + uses: actions/setup-java@v3 + with: + distribution: zulu + java-version: 17 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 18 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install + + - name: Build Signed Plugin + env: + CERTIFICATE_CHAIN: ${{ secrets.INTELLIJ_PLUGIN_CERTIFICATE_CHAIN }} + PRIVATE_KEY: ${{ secrets.INTELLIJ_PLUGIN_PRIVATE_KEY }} + uses: gradle/gradle-build-action@v2.4.2 + with: + arguments: signPlugin + build-root-directory: clients/intellij + + - name: Determine is stable release + run: | + if [[ ${{ github.ref_name }} =~ ^intellij@[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "STABLE_RELEASE=true" >> $GITHUB_ENV + else + echo "STABLE_RELEASE=false" >> $GITHUB_ENV + fi + + - name: Check if stable release + run: echo "Stable Release is ${{ env.STABLE_RELEASE }}" + + - name: Create GitHub Release + uses: ncipollo/release-action@v1 + with: + allowUpdates: true + prerelease: ${{ env.STABLE_RELEASE == 'false' }} + makeLatest: false + tag: ${{ github.ref_name }} + removeArtifacts: true + artifacts: "clients/intellij/build/distributions/intellij-tabby-signed.zip" diff --git a/.github/workflows/release-vim.yml b/.github/workflows/release-vim.yml index 9671139ef01b..35933a0a0cbd 100644 --- a/.github/workflows/release-vim.yml +++ b/.github/workflows/release-vim.yml @@ -2,6 +2,9 @@ name: Release Vim Plugin on: workflow_dispatch: + push: + tags: + - 'vim@*' jobs: release: @@ -21,13 +24,13 @@ jobs: path: vim-tabby - name: Copy files run: | + find vim-tabby/ -mindepth 1 ! -regex '^vim-tabby/\.git\(/.*\)?' -delete + cp -r tabby/clients/vim/.gitignore vim-tabby/ cp -r tabby/clients/vim/* vim-tabby/ - rm vim-tabby/package.json - name: Get version id: get_version run: | - cd vim-tabby/autoload/tabby/ - version=$(cat globals.vim | grep "let g:tabby_version" | sed -e 's/.*"\(.*\)".*/\1/') + version=$(cat vim-tabby/autoload/tabby.vim | grep "let g:tabby_version" | sed -e 's/.*"\(.*\)".*/\1/') echo "::set-output name=version::$version" - name: Commit files run: | diff --git a/.github/workflows/release-vscode.yml b/.github/workflows/release-vscode.yml new file mode 100644 index 000000000000..248ed0ddac10 --- /dev/null +++ b/.github/workflows/release-vscode.yml @@ -0,0 +1,55 @@ +name: Release vscode extension + +on: + workflow_dispatch: + push: + tags: + - 'vscode@*' + +concurrency: + group: ${{ github.workflow_ref }}-${{ github.head_ref || github.ref_name }} + + # If this is enabled it will cancel current running and start latest + cancel-in-progress: true + +jobs: + publish-vscode: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + lfs: true + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 18 + + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + version: 9 + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install + + - name: Publish + run: cd clients/vscode && pnpm run $(node scripts/publish.cjs) + env: + VSCE_PAT: ${{ secrets.VSCE_PAT }} + OVSX_PAT: ${{ secrets.OVSX_PAT }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0fa9ce6b1cd8..a388783d12ed 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,72 +6,54 @@ on: tags: - 'v*' - 'nightly' + - "!*-dev.*" + - '!vscode@*' + - '!vim@*' pull_request: branches: [ "main" ] paths: - '.github/workflows/release.yml' concurrency: - group: ${{ github.workflow_ref }}-${{ github.head_ref || github.ref_name }} - + group: ${{ github.workflow_ref }}-${{ github.head_ref || github.ref_name }} + # If this is enabled it will cancel current running and start latest cancel-in-progress: true env: - RUST_TOOLCHAIN: 1.73.0 + RUST_TOOLCHAIN: 1.82.0 jobs: - release-binary: + release-llama-server-binary: runs-on: ${{ matrix.os }} container: ${{ matrix.container }} strategy: matrix: - binary: [aarch64-apple-darwin, x86_64-manylinux2014, x86_64-windows-msvc, x86_64-manylinux2014-cuda117, x86_64-manylinux2014-cuda122, x86_64-windows-msvc-cuda117, x86_64-windows-msvc-cuda122, x86_64-manylinux2014-rocm57] + binary: + - aarch64-apple-darwin + - x86_64-manylinux_2_28 + - x86_64-manylinux_2_28-cuda123 include: - os: macos-latest target: aarch64-apple-darwin binary: aarch64-apple-darwin - - os: ubuntu-latest + build_args: --features binary + - os: buildjet-2vcpu-ubuntu-2204 target: x86_64-unknown-linux-gnu - binary: x86_64-manylinux2014 - container: quay.io/pypa/manylinux2014_x86_64 - build_args: --features static-ssl,prod-db - - os: ubuntu-latest + binary: x86_64-manylinux_2_28 + container: quay.io/pypa/manylinux_2_28_x86_64 + build_args: --features binary + - os: buildjet-2vcpu-ubuntu-2204 target: x86_64-unknown-linux-gnu - binary: x86_64-manylinux2014-cuda117 - container: sameli/manylinux2014_x86_64_cuda_11.7 - build_args: --features static-ssl --features cuda,prod-db - - os: ubuntu-latest - target: x86_64-unknown-linux-gnu - binary: x86_64-manylinux2014-cuda122 - container: sameli/manylinux2014_x86_64_cuda_12.2 - build_args: --features static-ssl --features cuda,prod-db - - os: windows-latest - target: x86_64-pc-windows-msvc - binary: x86_64-windows-msvc - ext: .exe - - os: windows-latest - target: x86_64-pc-windows-msvc - binary: x86_64-windows-msvc-cuda117 - ext: .exe - build_args: --features cuda,prod-db - windows_cuda: '11.7.1' - - os: windows-latest - target: x86_64-pc-windows-msvc - binary: x86_64-windows-msvc-cuda122 - ext: .exe - build_args: --features cuda,prod-db - windows_cuda: '12.2.0' - - os: ubuntu-latest - target: x86_64-unknown-linux-gnu - binary: x86_64-manylinux2014-rocm57 - container: ghcr.io/cromefire/hipblas-manylinux/2014/5.7:latest - build_args: --features static-ssl,rocm,prod-db + binary: x86_64-manylinux_2_28-cuda123 + container: sameli/manylinux_2_28_x86_64_cuda_12.3@sha256:e12416bf249ab312f9dcfdebd7939b968dd6f1b6f810abbede818df875e86a7c + build_args: --features binary,cuda env: SCCACHE_GHA_ENABLED: true RUSTC_WRAPPER: sccache CARGO_INCREMENTAL: 0 + ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true steps: - name: Checkout @@ -80,26 +62,32 @@ jobs: submodules: recursive - name: Install Rust + if: runner.os != 'Windows' uses: actions-rs/toolchain@v1 with: toolchain: ${{ env.RUST_TOOLCHAIN }} target: ${{ matrix.target }} components: clippy + - name: Install Rust for Windows + if: runner.os == 'Windows' + run: | + rustup update --no-self-update ${{ env.RUST_TOOLCHAIN }} + rustup target add ${{ matrix.target }} + rustup component add clippy --toolchain ${{ env.RUST_TOOLCHAIN }} + - name: Set default rust version run: rustup default ${{ env.RUST_TOOLCHAIN }} - name: Sccache cache - uses: mozilla-actions/sccache-action@v0.0.3 - with: - version: "v0.4.0" + uses: mozilla-actions/sccache-action@v0.0.9 - name: Cargo registry cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: - key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.toml') }}-${{ github.sha }} + key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-${{ github.sha }} restore-keys: | - cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.toml') }}- + cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}- cargo-${{ runner.os }}- path: | ~/.cargo/registry @@ -122,32 +110,226 @@ jobs: if: runner.os == 'Windows' && matrix.windows_cuda != '' - name: Bulid release binary + run: cargo build ${{ matrix.build_args }} --release --target ${{ matrix.target }} --package llama-cpp-server + + - name: Rename release binary + run: mv target/${{ matrix.target }}/release/llama-server${{ matrix.ext }} llama-server_${{ matrix.binary }}${{ matrix.ext }} + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + retention-days: 3 + name: llama-server_${{ matrix.binary }}${{ matrix.ext }} + path: llama-server_${{ matrix.binary }}${{ matrix.ext }} + + release-binary: + runs-on: ${{ matrix.os }} + container: ${{ matrix.container }} + strategy: + matrix: + binary: + - aarch64-apple-darwin + - x86_64-manylinux_2_28 + - x86_64-windows-msvc + include: + - os: macos-latest + target: aarch64-apple-darwin + binary: aarch64-apple-darwin + build_args: --no-default-features --features prod + - os: buildjet-2vcpu-ubuntu-2204 + target: x86_64-unknown-linux-gnu + binary: x86_64-manylinux_2_28 + container: quay.io/pypa/manylinux_2_28_x86_64 + build_args: --no-default-features --features static-ssl,prod + - os: windows-latest + target: x86_64-pc-windows-msvc + binary: x86_64-windows-msvc + build_args: --no-default-features --features prod + ext: .exe + + env: + SCCACHE_GHA_ENABLED: true + RUSTC_WRAPPER: sccache + CARGO_INCREMENTAL: 0 + ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true + + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Install Rust + if: runner.os != 'Windows' + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + target: ${{ matrix.target }} + components: clippy + + - name: Install Rust for Windows + if: runner.os == 'Windows' + run: | + rustup update --no-self-update ${{ env.RUST_TOOLCHAIN }} + rustup target add ${{ matrix.target }} + rustup component add clippy --toolchain ${{ env.RUST_TOOLCHAIN }} + + - name: Set default rust version + run: rustup default ${{ env.RUST_TOOLCHAIN }} + + - name: Sccache cache + uses: mozilla-actions/sccache-action@v0.0.9 + + - name: Cargo registry cache + uses: actions/cache@v4 + with: + key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-${{ github.sha }} + restore-keys: | + cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}- + cargo-${{ runner.os }}- + path: | + ~/.cargo/registry + ~/.cargo/git + + - name: Prepare build environment for macOS & Linux + run: bash ./ci/prepare_build_environment.sh + if: runner.os != 'Windows' + + - name: Prepare build environment for Windows + run: ./ci/prepare_build_environment.ps1 + if: runner.os == 'Windows' + + - name: Build release binary run: cargo build ${{ matrix.build_args }} --release --target ${{ matrix.target }} --package tabby - name: Rename release binary run: mv target/${{ matrix.target }}/release/tabby${{ matrix.ext }} tabby_${{ matrix.binary }}${{ matrix.ext }} - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: retention-days: 3 name: tabby_${{ matrix.binary }}${{ matrix.ext }} path: tabby_${{ matrix.binary }}${{ matrix.ext }} + package-from-upstream: + runs-on: ubuntu-latest + needs: [release-binary] + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Download all artifacts + uses: actions/download-artifact@v4 + + - name: Display structure of downloaded files + run: ls -R + + - name: Package CPU for Windows + run: > + LLAMA_CPP_PLATFORM=win-cpu-x64 OUTPUT_NAME=tabby_x86_64-windows-msvc-cpu ./ci/package-from-upstream.sh + + - name: Package CUDA 12.4 for Windows + run: > + LLAMA_CPP_PLATFORM=win-cuda-12.4-x64 OUTPUT_NAME=tabby_x86_64-windows-msvc-cuda124 ./ci/package-from-upstream.sh + + - name: Package Vulkan for Windows + run: > + LLAMA_CPP_PLATFORM=win-vulkan-x64 OUTPUT_NAME=tabby_x86_64-windows-msvc-vulkan ./ci/package-from-upstream.sh + + - name: Package Vulkan for Linux + run: > + LLAMA_CPP_PLATFORM=ubuntu-vulkan-x64 OUTPUT_NAME=tabby_x86_64-manylinux_2_28-vulkan ./ci/package-from-upstream.sh + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + retention-days: 3 + name: dist + path: dist/ + + pre-release: - if: github.event_name == 'push' - needs: release-binary + needs: [release-llama-server-binary, release-binary, package-from-upstream] runs-on: ubuntu-latest permissions: contents: write steps: - name: Download all artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 + + - name: Display structure of downloaded files + run: ls -R + + - name: Creating distribution bundles + run: | + get_file_extension() { + local filename="$1" + # Check if the file has an extension + if [[ "$filename" == *.* && ! "$filename" == .* ]]; then + echo ".${filename##*.}" + else + echo "" + fi + } + + dist_dir=$(pwd)/dist + mkdir -p $dist_dir + + for llama_server in llama-server_*/llama-server_*; do + for tabby in tabby_*/tabby_*; do + llamab=$(basename $llama_server) + tabbyb=$(basename $tabby) + extname=$(get_file_extension $tabbyb) + + llaman=${llamab%.*} + tabbyn=${tabbyb%.*} + + llamav=${llaman#llama-server_} + tabbyv=${tabbyn#tabby_} + + if [[ $llamav == *"$tabbyv"* ]]; then + echo "Creating bundle for $llamav" + + # the downloaded files may have the same folder name with release_dir + # put the release files in a new folder + build_dir=build + release_name=tabby_${llamav} + release_dir=$build_dir/$release_name + mkdir -p $release_dir + cp $llama_server $release_dir/llama-server${extname} + cp $tabby $release_dir/tabby${extname} + + pushd $build_dir + # Release zip for Windows, tar.gz for macOS and Linux + # use `extname` to determine the platform + if [[ "$extname" == ".exe" ]]; then + zip -r $release_name.zip $release_name + mv $release_name.zip $dist_dir/ + else + chmod +x $release_name/llama-server${extname} $release_name/tabby${extname} + tar zcvf $release_name.tar.gz $release_name + mv $release_name.tar.gz $dist_dir/ + fi + rm -rf "$release_name" + popd + fi + done + done + + - name: Display structure of created files + run: ls -R dist - uses: ncipollo/release-action@v1 + if: github.event_name == 'push' with: - allowUpdates: true + allowUpdates: true prerelease: true - artifacts: "tabby_*/tabby_*" + makeLatest: false + artifacts: "dist/tabby_*.zip,dist/tabby_*.tar.gz" tag: ${{ github.ref_name }} removeArtifacts: true diff --git a/.github/workflows/test-intellij.yml b/.github/workflows/test-intellij.yml index 1ede1ed5c218..2257f9a0869c 100644 --- a/.github/workflows/test-intellij.yml +++ b/.github/workflows/test-intellij.yml @@ -5,6 +5,7 @@ on: branches: [ main ] paths: - '.github/workflows/test-intellij.yml' + - 'clients/tabby-agent/**' - 'clients/intellij/**' concurrency: @@ -14,19 +15,48 @@ concurrency: cancel-in-progress: true jobs: - tests: + build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 with: lfs: true + - name: Setup JDK uses: actions/setup-java@v3 with: distribution: zulu java-version: 17 - - name: Test Build + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 18 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install + + - name: Build Plugin uses: gradle/gradle-build-action@v2.4.2 with: arguments: buildPlugin diff --git a/.github/workflows/test-pnpm.yml b/.github/workflows/test-pnpm.yml new file mode 100644 index 000000000000..258d9a81ab81 --- /dev/null +++ b/.github/workflows/test-pnpm.yml @@ -0,0 +1,61 @@ +name: Test Pnpm + +on: + pull_request: + branches: ["main" ] + paths: + - '.github/workflows/autofix-pnpm.yml' + - 'clients/**' + - 'ee/tabby-ui/**' + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow_ref }}-${{ github.head_ref || github.ref_name }} + + # If this is enabled it will cancel current running and start latest + cancel-in-progress: true + +jobs: + tests: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 18 + + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + version: 9 + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install + + - name: Lint + run: pnpm lint + + - name: Test + run: pnpm test + + - name: Build + run: pnpm build \ No newline at end of file diff --git a/.github/workflows/test-rust.yml b/.github/workflows/test-rust.yml index 4b322c0ce2ef..d95a37c581e2 100644 --- a/.github/workflows/test-rust.yml +++ b/.github/workflows/test-rust.yml @@ -1,6 +1,16 @@ name: Test Rust on: + push: + branches: [ "main" ] + paths: + - 'Cargo.toml' + - 'Cargo.lock' + - 'crates/**' + - 'ee/**' + - '!ee/tabby-ui/**' + - '.github/workflows/test-rust.yml' + pull_request: branches: [ "main" ] paths: @@ -12,18 +22,20 @@ on: - '.github/workflows/test-rust.yml' concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }} - + group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }} + # If this is enabled it will cancel current running and start latest cancel-in-progress: true env: - RUST_TOOLCHAIN: 1.73.0 + RUST_TOOLCHAIN: 1.82.0 jobs: tests: + if: ${{ github.event_name == 'pull_request' }} runs-on: ubuntu-latest env: + CARGO_TERM_COLOR: always SCCACHE_GHA_ENABLED: true RUSTC_WRAPPER: sccache CARGO_INCREMENTAL: 0 @@ -38,22 +50,75 @@ jobs: toolchain: ${{ env.RUST_TOOLCHAIN }} - name: Sccache cache - uses: mozilla-actions/sccache-action@v0.0.3 - with: - version: "v0.4.0" + uses: mozilla-actions/sccache-action@v0.0.9 - name: Cargo registry cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: - key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.toml') }}-${{ github.sha }} + key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-${{ github.sha }} restore-keys: | - cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.toml') }}- + cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}- cargo-${{ runner.os }}- path: | ~/.cargo/registry ~/.cargo/git - - run: bash ./ci/prepare_build_environment.sh + - run: sudo bash ./ci/prepare_build_environment.sh + + - name: Run doc tests + run: cargo test --doc + + - name: Run unit tests on community build + run: cargo test --bin tabby --no-default-features - name: Run unit tests run: cargo test --bin tabby --lib + + coverage: + runs-on: ubuntu-latest + env: + CARGO_TERM_COLOR: always + SCCACHE_GHA_ENABLED: true + RUSTC_WRAPPER: sccache + CARGO_INCREMENTAL: 0 + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + + - name: Install cargo-llvm-cov + uses: taiki-e/install-action@cargo-llvm-cov + + - name: Sccache cache + uses: mozilla-actions/sccache-action@v0.0.9 + + - name: Cargo registry cache + uses: actions/cache@v4 + with: + key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-${{ github.sha }} + restore-keys: | + cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}- + cargo-${{ runner.os }}- + path: | + ~/.cargo/registry + ~/.cargo/git + + - run: sudo bash ./ci/prepare_build_environment.sh + + - name: Generate code coverage + env: + CI_COVERAGE: 1 + run: cargo llvm-cov --bin tabby --lib --lcov --output-path lcov.info + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + with: + files: lcov.info + fail_ci_if_error: true \ No newline at end of file diff --git a/.github/workflows/test-tabby-agent.yml b/.github/workflows/test-tabby-agent.yml deleted file mode 100644 index 98573743e49d..000000000000 --- a/.github/workflows/test-tabby-agent.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Test Tabby Agent - -on: - pull_request: - branches: [ main ] - paths: - - '.github/workflows/test-tabby-agent.yml' - - 'package.json' - - 'yarn.lock' - - 'clients/tabby-agent/**' - -concurrency: - group: ${{ github.workflow_ref }}-${{ github.head_ref || github.ref_name }} - - # If this is enabled it will cancel current running and start latest - cancel-in-progress: true - -jobs: - tests: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - lfs: true - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version: '18.x' - cache: yarn - cache-dependency-path: 'yarn.lock' - - name: Install dependencies - working-directory: ./clients/tabby-agent - run: yarn install - - name: Lint - working-directory: ./clients/tabby-agent - run: yarn lint:check - - name: Run tests - working-directory: ./clients/tabby-agent - run: yarn test - - name: Test build - working-directory: ./clients/tabby-agent - run: yarn build - \ No newline at end of file diff --git a/.github/workflows/test-tabby-ui.yml b/.github/workflows/test-tabby-ui.yml deleted file mode 100644 index 75a34fc72c90..000000000000 --- a/.github/workflows/test-tabby-ui.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Test Tabby UI - -on: - pull_request: - branches: [ main ] - paths: - - '.github/workflows/test-tabby-ui.yml' - - 'ee/tabby-ui/**' - -concurrency: - group: ${{ github.workflow_ref }}-${{ github.head_ref || github.ref_name }} - - # If this is enabled it will cancel current running and start latest - cancel-in-progress: true - -jobs: - tests: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version: '18.x' - cache: yarn - cache-dependency-path: 'yarn.lock' - - name: Install dependencies - working-directory: ./ee/tabby-ui - run: yarn install - - name: Format - working-directory: ./ee/tabby-ui - run: yarn format:check - - name: Lint - working-directory: ./ee/tabby-ui - run: yarn lint - - name: Test build - working-directory: ./ee/tabby-ui - run: yarn build - \ No newline at end of file diff --git a/.github/workflows/test-vscode.yml b/.github/workflows/test-vscode.yml deleted file mode 100644 index 6068253dfd5f..000000000000 --- a/.github/workflows/test-vscode.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Test VSCode Extension - -on: - pull_request: - branches: [ main ] - paths: - - '.github/workflows/test-vscode.yml' - - 'package.json' - - 'yarn.lock' - - 'clients/tabby-agent/**' - - 'clients/vscode/**' - -concurrency: - group: ${{ github.workflow_ref }}-${{ github.head_ref || github.ref_name }} - - # If this is enabled it will cancel current running and start latest - cancel-in-progress: true - -jobs: - tests: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - lfs: true - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version: '18.x' - cache: yarn - cache-dependency-path: 'yarn.lock' - - name: Install dependencies - working-directory: ./clients/vscode - run: yarn install - - name: Lint - working-directory: ./clients/vscode - run: yarn lint:check - - name: Test build - working-directory: ./clients/vscode - run: yarn build - \ No newline at end of file diff --git a/.gitignore b/.gitignore index dcdc169416ac..595435d9d637 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,10 @@ node_modules .idea/ .DS_Store .vscode/ +local/ __pycache__ +.turbo + +# ignore cargo flamegraph temp files +flamegraph.svg +cargo-flamegraph.stacks diff --git a/.gitmodules b/.gitmodules index e150ac4796c6..29cc0b2944ba 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "crates/llama-cpp-bindings/llama.cpp"] - path = crates/llama-cpp-bindings/llama.cpp - url = https://github.com/TabbyML/llama.cpp +[submodule "crates/llama-cpp-server/llama.cpp"] + path = crates/llama-cpp-server/llama.cpp + url = https://github.com/ggml-org/llama.cpp.git diff --git a/.tmuxinator/tabby.yml b/.tmuxinator/tabby.yml new file mode 100644 index 000000000000..6def6f2689d3 --- /dev/null +++ b/.tmuxinator/tabby.yml @@ -0,0 +1,12 @@ +name: tabby +root: ./ + +windows: + - caddy: + panes: + - caddy run --watch --config ee/tabby-webserver/development/Caddyfile + - server: + layout: even-horizontal + panes: + - cargo run serve --port 8081 + - cd ee/tabby-ui && pnpm dev diff --git a/CHANGELOG.md b/CHANGELOG.md index f024ea8bf33f..de27a23ceb33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,72 +1,523 @@ -# v0.9.0 [Unreleased] +# Changelog +All notable changes to this project will be documented in this file. -## Features +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), +and is generated by [Changie](https://github.com/miniscruff/changie). -## Fixes and Improvements +## v0.31.2 (2025-09-25) -* The `user` field in the `~/tabby/events` log is now populated for users who authenticate via the `--webserver` feature. +### Notice -# v0.8.0 +* This is a patch release, please also check [the full release note](https://github.com/TabbyML/tabby/releases/tag/v0.31.0) for 0.31. -## Notice +### Fixed and Improvements + +* Added environment variable `TABBY_INDEX_REPO_IN_SHARD` to enable hourly sharded indexing when repository count exceeds 20. [#4366](https://github.com/TabbyML/tabby/pull/4366) + +## v0.31.0 (2025-08-19) + +### Features + +* Add support for custom branding with name and logo (Available Exclusively with an Enterprise License). [#4334](https://github.com/TabbyML/tabby/pull/4334) + +## v0.30.2 (2025-07-31) + +### Notice + +* This is a patch release, please also check [the full release note](https://github.com/TabbyML/tabby/releases/tag/v0.30.0) for 0.30. + +### Fixed and Improvements + +* Use 0.7.3 sqlx to avoid database pool timeout. [#4328](https://github.com/TabbyML/tabby/pull/4328) +* Bump llama.cpp to b6047. [#4330](https://github.com/TabbyML/tabby/pull/4330) +* Expose the Flash Attention LLAMA flag as an environment variable. [#4323](https://github.com/TabbyML/tabby/pull/4323) + +## v0.30.1 (2025-07-16) + +### Notice + +* This is a patch release, please also check [the full release note](https://github.com/TabbyML/tabby/releases/tag/v0.30.0) for 0.30. + +### Fixed and Improvements + +* Downgrade CUDA base image to version 12.4.1 to enhance compatibility. [#4314](https://github.com/TabbyML/tabby/pull/4314) + +## v0.30.0 (2025-07-02) + +### Features + +* Support indexing GitLab Merge Request as Context. [#4227](https://github.com/TabbyML/tabby/pull/4227) + +### Fixed and Improvements + +* Use CUDA 12 image as base image by default. [#4235](https://github.com/TabbyML/tabby/pull/4235) +* Leverage the Answer Engine logic to enrich the context when generating new pages, improving page quality. [#4237](https://github.com/TabbyML/tabby/pull/4237) +* Improve the model configuration details on the System page. [#4236](https://github.com/TabbyML/tabby/pull/4236) +* Resolve the flickering of buttons within code snippets in a chat response during answer generation. [#4233](https://github.com/TabbyML/tabby/pull/4233) +* Set the member filter bar always fixed at the top of the Reports page.[#4232](https://github.com/TabbyML/tabby/pull/4232) +* Resolve the issue when loading a multi-part model from local.[#4302](https://github.com/TabbyML/tabby/pull/4302) + +## v0.29.0 (2025-05-19) + +### Features + +* Add RESTful APIs for ingesting custom documents and facilitating their removal. ([#4171](https://github.com/TabbyML/tabby/pull/4171)) ([#4203](https://github.com/TabbyML/tabby/pull/4203)) ([#4196](https://github.com/TabbyML/tabby/pull/4196)) + +### Fixed and Improvements + +* Enhance error details in Notification Inbox. ([#4184](https://github.com/TabbyML/tabby/pull/4184)) +* Add support for Automatic Jobs status polling. ([#4172](https://github.com/TabbyML/tabby/pull/4172)) +* Bump llama.cpp to b2500. ([#4200](https://github.com/TabbyML/tabby/pull/4200)) + +## v0.28.0 (2025-04-28) + +### Features + +* Add support to convert Answer Engine messages into Pages. ([#4194](https://github.com/TabbyML/tabby/pull/4194)) +* Add support for Doc Query in Chat Panel, allowing Dev Docs to be included as context. ([#4095](https://github.com/TabbyML/tabby/pull/4095)) + +### Fixed and Improvements + +* Enhance background task logging to boost performance and enrich user experience. ([#4066](https://github.com/TabbyML/tabby/pull/4066)) ([#4145](https://github.com/TabbyML/tabby/pull/4145)) +* Enhance the display of thinking processes in Answer Engine and Chat Panel. ([#4091](https://github.com/TabbyML/tabby/pull/4091)) + +## v0.27.1 (2025-04-07) + +### Notice + +* This is a patch release, please also check [the full release note](https://github.com/TabbyML/tabby/releases/tag/v0.27.0) for 0.27. + +### Fixed and Improvements + +* Resolved an issue where certain escaped characters could cause plugin crashes on Windows systems. ([#4114](https://github.com/TabbyML/tabby/pull/4114)) +* Resolved an issue where the Tabby server could hang while waiting for the registry file to download in offline environments. ([#4120](https://github.com/TabbyML/tabby/pull/4120)) +* Improved handling of file mentions from different sources in the Chat Panel. ([#4116](https://github.com/TabbyML/tabby/pull/4116)) + +## v0.27.0 (2025-03-29) + +### Notice + +* Prepare for the 1.0 release. Starting with this release, extensions version upgrades will maintain backward compatibility with the Tabby server. + +### Features + +* Add support for the ability to execute shell commands within Chat Panel. ([#3979](https://github.com/TabbyML/tabby/pull/3979)) +* Add support for `@changes` within Chat Panel to include uncommitted changes as contexts. ([#3988](https://github.com/TabbyML/tabby/pull/3988)) +* Add security option to hide password login in frontend. When enabled, the password login can be revealed by appending the URL search parameter `passwordSignIn=true`. ([#3996](https://github.com/TabbyML/tabby/pull/3996)) ([#4015](https://github.com/TabbyML/tabby/pull/4015)) + +### Fixed and Improvements + +* Store background job logs on disk to prevent disruptions during processing of large repositories. ([#3994](https://github.com/TabbyML/tabby/pull/3994)) +* Track chat usage frequency and display it in Reports page. ([#4025](https://github.com/TabbyML/tabby/pull/4025)) ([#4042](https://github.com/TabbyML/tabby/pull/4042)) +* Resolve the chat functionality issue involving OpenAI reasoning models such as `o3-mini` and `o1-mini`. ([#4049](https://github.com/TabbyML/tabby/pull/4049)) + +## v0.26.0 (2025-03-16) + +### Notice + +* Bump minimum katana version to 1.1.2 + +### Features + +* Enable the Answer Engine to access the repository's commit history as needed. ([#3916](https://github.com/TabbyML/tabby/pull/3916)) +* Support the display of user chat history on Homepage and Chat Side Panel. ([#3897](https://github.com/TabbyML/tabby/pull/3897)) + +### Fixed and Improvements + +* Facilitate the crawling of developer documentation from llms-full.txt when available. ([#3880](https://github.com/TabbyML/tabby/pull/3880)) + +## v0.25.2 (2025-02-28) + +### Notice + +* This is a patch release, please also check [the historical release note](https://github.com/TabbyML/tabby/releases/tag/v0.25.1) for 0.25.1. + +### Fixed and Improvements + +* Alter the LDAP login mechanism to query users across the Organizational Unit subtree rather than a single level. An LDAP directory is structured hierarchically as a tree of nodes. This modification empowers all users beneath the LDAP Organizational Unit (OU) subtree to authenticate, rather than restricting access to users within a specific OU. + +## v0.25.1 (2025-02-25) + +### Notice + +* This is a patch release, please also check [the full release note](https://github.com/TabbyML/tabby/releases/tag/v0.25.0) for 0.25. + +### Fixed and Improvements + +* Refine the UI details in Answer Engine. ([#3888](https://github.com/TabbyML/tabby/pull/3888)) ([#3889](https://github.com/TabbyML/tabby/pull/3889)) ([#3891](https://github.com/TabbyML/tabby/pull/3891)) + +## v0.25.0 (2025-02-17) + +### Notice + +Significant changes have been implemented in this release; please consider adjusting them to fit your specific use case. + +* The default parallelism has been increased from 1 to 4, which might increase VRAM usage. ([#3832](https://github.com/TabbyML/tabby/pull/3832)) +* Introduce a new embedding kind `llama.cpp/before_b4356_embedding` for llamafile or other embedding services utilizing the legacy llama.cpp embedding API. ([#3828](https://github.com/TabbyML/tabby/pull/3828)) + +### Features + +* Expose thinking process of Answer Engine to the answers in thread message. ([#3785](https://github.com/TabbyML/tabby/pull/3785)) ([#3672](https://github.com/TabbyML/tabby/pull/3672)) +* Enable the Answer Engine to access the repository's directory file list as needed. ([#3796](https://github.com/TabbyML/tabby/pull/3796)) +* Enable the use of `@` to mention a symbol in Chat Sidebar. ([#3778](https://github.com/TabbyML/tabby/pull/3778)) +* Provide default question recommendations that are repository-aware on Answer Engine. ([#3815](https://github.com/TabbyML/tabby/pull/3815)) + +### Fixed and Improvements + +* Provide a configuration to truncate text content prior to dispatching it to embedding service.. ([#3816](https://github.com/TabbyML/tabby/pull/3816)) +* Bump llama.cpp version to b4651. ([#3798](https://github.com/TabbyML/tabby/pull/3798)) +* Automatically retry embedding when the service occasionally fails due to issues with llama.cpp. ([#3805](https://github.com/TabbyML/tabby/pull/3805)) +* Enhance the user interface experience for Answer Engine. ([#3845](https://github.com/TabbyML/tabby/pull/3845)) ([#3794](https://github.com/TabbyML/tabby/pull/3794)) +* Resolve the deserialization issue related to `finish_reason` in chat response from LiteLLM Proxy Server.([#3882](https://github.com/TabbyML/tabby/pull/3882)) + +## v0.24.0 (2025-01-23) + +### Features + +* Implement LDAP Authentication Integration. ([#3650](https://github.com/TabbyML/tabby/pull/3650)) ([#3625](https://github.com/TabbyML/tabby/pull/3625)) +* Add Notifications for unsuccessful background jobs. ([#3713](https://github.com/TabbyML/tabby/pull/3713)) + +### Fixed and Improvements + +* Fixed a bug that prevented the client code context in historical messages from being added to the prompt. ([#3673](https://github.com/TabbyML/tabby/pull/3673)) +* Retain the job run and user event history only for the past three months. ([#3640](https://github.com/TabbyML/tabby/pull/3640)) +* Resolved an issue that caused integration errors with recent versions of Jan AI. ([#3649](https://github.com/TabbyML/tabby/pull/3649)) +* Resolved an issue where repositories specified in config.toml were not synchronizing correctly. ([#3703](https://github.com/TabbyML/tabby/pull/3703)) +* Set the active text tab as default context in Code Browser chat. ([#3729](https://github.com/TabbyML/tabby/pull/3729)) +* Resolved an issue that caused models download failures due to changes in HuggingFace API. ([#3772](https://github.com/TabbyML/tabby/pull/3772)) +* Omit indexing of GitHub Pull Request diffs that exceed 300 files. ([#3779](https://github.com/TabbyML/tabby/pull/3779)) + +## v0.23.1 (2025-01-31) + +### Fixed and Improvements + +* Fix the issue of being unable to download remote models due to changes in HuggingFace API. ([#3772](https://github.com/TabbyML/tabby/pull/3772)) + +## v0.23.0 (2025-01-09) + +### Features + +* Add commit hash to code attachments, allowing redirection to the specific commit in code browser. ([#3627](https://github.com/TabbyML/tabby/pull/3627)) ([#3577](https://github.com/TabbyML/tabby/pull/3577)) +* Display the connection status of the gitUrl repository in the chat side panel. ([#3550](https://github.com/TabbyML/tabby/pull/3550)) + +### Fixed and Improvements + +* Perform database backups only when there are pending schema migrations. ([#3620](https://github.com/TabbyML/tabby/pull/3620)) +* Add repository selection and remove '#' mentions in the Answer Engine. ([#3619](https://github.com/TabbyML/tabby/pull/3619)) + +## v0.22.0 (2024-12-23) + +### Features + +* Introduce notification inbox on homepage and license expiration check. ([#3541](https://github.com/TabbyML/tabby/pull/3541)) ([#3566](https://github.com/TabbyML/tabby/pull/3566)) +* Display author information for issues / pull requests in Answer Engine context card ([#3513](https://github.com/TabbyML/tabby/pull/3513)) + +### Fixed and Improvements + +* Refactors the pull request indexing process to enhance the speed of incremental indexing for pull docs. ([#3538](https://github.com/TabbyML/tabby/pull/3538)) +* Optimize the rate limiter on the HTTP-powered model backend to reduce errors. ([#3567](https://github.com/TabbyML/tabby/pull/3567)) +* Introduce rate limiting at 60 requests per minute in the tabby-webserver. ([#3484](https://github.com/TabbyML/tabby/pull/3484)) +* Validate model capability prior to download. ([#3565](https://github.com/TabbyML/tabby/pull/3565)) +* Fix broken tree view on Windows in CodeBrowser. ([#3528](https://github.com/TabbyML/tabby/pull/3528)) +* Upgrade all Tabby Linux base images to manylinux_2_28. ([#3536](https://github.com/TabbyML/tabby/pull/3536)) + +## v0.21.2 (2024-12-18) + +### Notice + +* This is a patch release, please also check [the full release note](https://github.com/TabbyML/tabby/releases/tag/v0.21.1) for 0.21.1. + +### Fixed and Improvements + +* Adapt extension side changes in new versions. + - VSCode: 1.16.0 + - IntelliJ Platform: 1.9.1 + +## v0.21.1 (2024-12-09) + +### Notice + +* This is a patch release, please also check [the full release note](https://github.com/TabbyML/tabby/releases/tag/v0.21.0) for 0.21. + +### Fixed and Improvements + +* Fixed Gitlab Context Provider. +## v0.21.0 (2024-12-02) + +### Notice + +* Due to changes in the indexing format, the `~/.tabby/index` directory will be automatically removed before any further indexing jobs are run. It is expected that the indexing jobs will be re-run (instead of incrementally) after the upgrade. + +### Features + +* Support connecting to llamafile model backend. +* Display **Open** / **Closed** state for issues / pull requests in Answer Engine context card. +* Support deleting the entire thread in Answer Engine. +* Add rate limiter options for HTTP-powered model backends. + +### Fixed and Improvements + +* Fixed a panic that occurred when specifying a local model ([#3464](https://github.com/TabbyML/tabby/issues/3464)) +* Add pagination to Answer Engine threads. +* Fix Vulkan binary distributions. +* Improve the retry logic for chunk embedding computation in indexing job. + +## v0.20.0 (2024-11-08) + +### Features + +* Search results can now be edited directly. +* Allow switching backend chat models in Answer Engine. +* Added a connection test button in the `System` tab to test the connection to the backend LLM server. + +### Fixes and Improvements + +* Optimized CR-LF inference in code completion. ([#3279](https://github.com/TabbyML/tabby/issues/3279)) +* Bumped `llama.cpp` version to `b3995`. + +## v0.19.0 (2024-10-30) + +### Features + +* For Answer Engine, when the file content is reasonably short (e.g., less than 200 lines of code), include the entire file content directly instead of only the chunk ([#3096](https://github.com/TabbyML/tabby/issues/3096)). +* Allowed adding additional languages through the `config.toml` file. +* Allowed customizing the `system_prompt` for Answer Engine. + +### Fixes and Improvements + +* Redesigned homepage to make team activities (e.g., threads discussed in Answer Engine) discoverable. +* Supported downloading models with multiple partitions (e.g., Qwen-2.5 series). + +## v0.18.0 (2024-10-08) + +### Notice + +* The Chat Side Panel implementation has been redesigned in version 0.18, necessitating an extension version bump for compatibility with 0.18.0. + - VSCode: >= 1.12.0 + - IntelliJ: >= 1.8.0 + +### Features + +* User Groups Access Control: Server Administrators can now assign user groups to specific context providers to precisely control which contexts can be accessed by which user groups. + +## v0.17.0 (2024-09-10) + +### Notice + +* We've reworked the `Web` (a beta feature) context provider into the `Developer Docs` context provider. Previously added context in the `Web` tab has been cleared and needs to be manually migrated to `Developer Docs`. + +### Features + +* Extensive rework has been done in the answer engine search box. + - Developer Docs / Web search is now triggered by `@`. + - Repository Context is now selected using `#`. + +* Supports OCaml + +## v0.16.1 (2024-08-27) + +### Notice +* Starting from this version, we are utilizing websockets for features that require streaming (e.g., Answer Engine and Chat Side Panel). If you are deploying tabby behind a reverse proxy, you may need to configure the proxy to support websockets. + +### Features + +* Discussion threads in the Answer Engine are now persisted, allowing users to share threads with others. + +### Fixed and Improvements + +* Fixed an issue where the llama-server subprocess was not being reused when reusing a model for Chat / Completion together (e.g., Codestral-22B) with the local model backend. +* Updated llama.cpp to version b3571 to support the jina series embedding models. + +## v0.15.0 (2024-08-08) + +### Features + +* The search bar in the Code Browser has been reworked and integrated with file navigation functionality. +* GraphQL syntax highlighting support in Code Browser. + +### Fixed and Improvements + +* For linked GitHub repositories, issues and PRs are now only returned when the repository is selected. +* Fixed GitLab issues/MRs indexing - no longer panics if the description field is null. +* When connecting to localhost model servers, proxy settings are now skipped. +* Allow set code completion's `max_input_length` and `max_output_tokens` in config.toml + +## v0.14.0 (2024-07-23) + +### Features +* Code search functionality is now available in the `Code Browser` tab. Users can search for code using regex patterns and filter by language, repository, and branch. +* Initial experimental support for natural language to codebase conversation in `Answer Engine`. + +### Fixed and Improvements + +* Incremental issues / PRs indexing by checking `updated_at`. +* Canonicalize `git_url` before performing a relevant code search. Previously, for git_urls with credentials, the canonicalized git_url was used in the index, but the query still used the raw git_url. +* bump llama.cpp to b3370 - which fixes Qwen2 model series inference + +## v0.13.1 (2024-07-10) + +### Fixed and Improvements + +* Bump llama.cpp version to b3334, supporting Deepseek V2 series models. +* Turn on fast attention for Qwen2-1.5B model to fix the quantization error. +* Properly set number of GPU layers (to zero) when device is CPU. +## v0.13.0 (2024-06-28) + +### Features + +* Introduced a new Home page featuring the Answer Engine, which activates when the chat model is loaded. +* Enhanced the Answer Engine's context by indexing issues and pull requests. +* Supports web page crawling to further enrich the Answer Engine's context. +* Enabled navigation through various git trees in the git browser. + +### Fixed and Improvements + +* Turn on sha256 checksum verification for model downloading. +* Added an environment variable `TABBY_HUGGINGFACE_HOST_OVERRIDE` to override `huggingface.co` with compatible mirrors (e.g., `hf-mirror.com`) for model downloading. +* Bumped `llama.cpp` version to [b3166](https://github.com/ggerganov/llama.cpp/releases/tag/3166). +* Improved logging for the `llama.cpp` backend. +* Added support for triggering background jobs in the admin UI. +* Enhanced logging for backend jobs in the admin UI. + +## v0.12.0 (2024-05-31) + +### Features + +* Support Gitlab SSO +* Support connect with Self-Hosted Github / Gitlab +* Repository Context is now utilizied in "Code Browser" as well + +### Fixed and Improvements + +* llama-server from llama.cpp is now distributed as an individual binary, allowing for more flexible configuration +* HTTP API is out of experimental - you can connect tabby to models through HTTP API. Right now following APIs are supported: + - llama.cpp + - ollama + - mistral / codestral + - openai + +## v0.11.1 (2024-05-14) + +### Fixed and Improvements + +* Fixed display of files where the path contains special characters. ([#2081](https://github.com/TabbyML/tabby/issues/2081)) +* Fixed non-admin users not being able to see the repository in Code Browser. ([#2110](https://github.com/TabbyML/tabby/discussions/2110)) + +## v0.11.0 (05/10/2024) + +### Notice + +* The `--webserver` flag is now enabled by default in `tabby serve`. To turn off the webserver and only use OSS features, use the `--no-webserver` flag. +* The `/v1beta/chat/completions` endpoint has been moved to `/v1/chat/completions`, while the old endpoint is still available for backward compatibility. + +### Features +* Upgraded [llama.cpp](https://github.com/ggerganov/llama.cpp) to version [b2715](https://github.com/ggerganov/llama.cpp/releases/tag/b2715). +* Added support for integrating repositories from GitHub and GitLab using personal access tokens. +* Introduced a new **Activities** page to view user activities. +* Implemented incremental indexing for faster repository context updates. +* Added storage usage statistics in the **System** page. +* Included an `Ask Tabby` feature in the source code browser to provide in-context help from AI. + +### Fixes and Improvements +* Changed the default model filename from `q8_0.v2.gguf` to `model.gguf` in MODEL_SPEC.md. +* Excluded activities from deactivated users in reports. + +## v0.10.0 (04/22/2024) + +### Features +* Introduced the `--chat-device` flag to specify the device used to run the chat model. +* Added a "Reports" tab in the web interface, which provides team-wise statistics for Tabby IDE and Extensions usage (e.g., completions, acceptances). +* Enabled the use of segmented models with the `tabby download` command. +* Implemented the "Go to file" functionality in the Code Browser. + +### Fixes and Improvements +* Fix worker unregisteration misfunctioning caused by unmatched address. +* Accurate repository context filtering using fuzzy matching on `git_url` field. +* Support the use of client-side context, including function/class declarations from LSP, and relevant snippets from local changed files. + +## v0.9.1 (03/19/2024) + +### Fixes and Improvements +* Fix worker registration check against enterprise licenses. +* Fix default value of `disable_client_side_telemetry` when `--webserver` is not used. + +## v0.9.0 (03/06/2024) + +### Features + +* Support for SMTP configuration in the user management system. +* Support for SSO and team management as features in the Enterprise tier. +* Fully managed repository indexing using `--webserver`, with job history logging available in the web interface. + +## v0.8.3 (02/06/2024) + +### Fixes and Improvements + +* Ensure `~/.tabby/repositories` exists for tabby scheduler jobs: https://github.com/TabbyML/tabby/pull/1375 +* Add cpu only binary `tabby-cpu` to docker distribution. + +## v0.8.0 (02/02/2024) + +### Notice * Due to format changes, re-executing `tabby scheduler --now` is required to ensure that `Code Browser` functions properly. -## Features +### Features * Introducing a preview release of the `Source Code Browser`, featuring visualization of code snippets utilized for code completion in RAG. * Added a Windows CPU binary distribution. * Added a Linux ROCm (AMD GPU) binary distribution. -## Fixes and Improvements +### Fixes and Improvements * Fixed an issue with cached permanent redirection in certain browsers (e.g., Chrome) when the `--webserver` flag is disabled. * Introduced the `TABBY_MODEL_CACHE_ROOT` environment variable to individually override the model cache directory. * The `/v1beta/chat/completions` API endpoint is now compatible with OpenAI's chat completion API. * Models from our official registry can now be referred to without the TabbyML prefix. Therefore, for the model TabbyML/CodeLlama-7B, you can simply refer to it as CodeLlama-7B everywhere. -# v0.7.0 (12/15/2023) +## v0.7.0 (12/15/2023) -## Features +### Features * Tabby now includes built-in user management and secure access, ensuring that it is only accessible to your team. * The `--webserver` flag is a new addition to `tabby serve` that enables secure access to the tabby server. When this flag is on, IDE extensions will need to provide an authorization token to access the instance. - Some functionalities that are bound to the webserver (e.g. playground) will also require the `--webserver` flag. -## Fixes and Improvements +### Fixes and Improvements * Fix https://github.com/TabbyML/tabby/issues/1036, events log should be written to dated json files. -# v0.6.0 (11/27/2023) +## v0.6.0 (11/27/2023) -## Features +### Features * Add distribution support (running completion / chat model on different process / machine). * Add conversation history in chat playground. * Add `/metrics` endpoint for prometheus metrics collection. -## Fixes and Improvements +### Fixes and Improvements * Fix the slow repository indexing due to constraint memory arena in tantivy index writer. * Make `--model` optional, so users can create a chat only instance. * Add `--parallelism` to control the throughput and VRAM usage: https://github.com/TabbyML/tabby/pull/727 -# v0.5.5 (11/09/2023) +## v0.5.5 (11/09/2023) -## Fixes and Improvements +### Fixes and Improvements -## Notice +### Notice * llama.cpp backend (CPU, Metal) now requires a redownload of gguf model due to upstream format changes: https://github.com/TabbyML/tabby/pull/645 https://github.com/ggerganov/llama.cpp/pull/3252 * Due to indexing format changes, the `~/.tabby/index` needs to be manually removed before any further runs of `tabby scheduler`. * `TABBY_REGISTRY` is replaced with `TABBY_DOWNLOAD_HOST` for the github based registry implementation. -## Features +### Features * Improved dashboard UI. -## Fixes and Improvements +### Fixes and Improvements * Cpu backend is switched to llama.cpp: https://github.com/TabbyML/tabby/pull/638 * add `server.completion_timeout` to control the code completion interface timeout: https://github.com/TabbyML/tabby/pull/637 @@ -74,29 +525,29 @@ * Tokenizer implementation is switched to llama.cpp, so tabby no longer need to download additional tokenizer file: https://github.com/TabbyML/tabby/pull/683 * Fix deadlock issue reported in https://github.com/TabbyML/tabby/issues/718 -# v0.4.0 (10/24/2023) +## v0.4.0 (10/24/2023) -## Features +### Features * Supports golang: https://github.com/TabbyML/tabby/issues/553 * Supports ruby: https://github.com/TabbyML/tabby/pull/597 * Supports using local directory for `Repository.git_url`: use `file:///path/to/repo` to specify a local directory. * A new UI design for webserver. -## Fixes and Improvements +### Fixes and Improvements * Improve snippets retrieval by dedup candidates to existing content + snippets: https://github.com/TabbyML/tabby/pull/582 -# v0.3.1 (10/21/2023) -## Fixes and improvements +## v0.3.1 (10/21/2023) +### Fixes and improvements * Fix GPU OOM issue caused the parallelism: https://github.com/TabbyML/tabby/issues/541, https://github.com/TabbyML/tabby/issues/587 * Fix git safe directory check in docker: https://github.com/TabbyML/tabby/issues/569 -# v0.3.0 (10/13/2023) +## v0.3.0 (10/13/2023) -## Features -### Retrieval-Augmented Code Completion Enabled by Default +### Features +#### Retrieval-Augmented Code Completion Enabled by Default The currently supported languages are: @@ -107,19 +558,19 @@ The currently supported languages are: A blog series detailing the technical aspects of Retrieval-Augmented Code Completion will be published soon. Stay tuned! -## Fixes and Improvements +### Fixes and Improvements * Fix [Issue #511](https://github.com/TabbyML/tabby/issues/511) by marking ggml models as optional. * Improve stop words handling by combining RegexSet into Regex for efficiency. -# v0.2.2 (10/09/2023) -## Fixes and improvements +## v0.2.2 (10/09/2023) +### Fixes and improvements * Fix a critical issue that might cause request dead locking in ctranslate2 backend (when loading is heavy) -# v0.2.1 (10/03/2023) -## Features -### Chat Model & Web Interface +## v0.2.1 (10/03/2023) +### Features +#### Chat Model & Web Interface We have introduced a new argument, `--chat-model`, which allows you to specify the model for the chat playground located at http://localhost:8080/playground @@ -129,18 +580,22 @@ To utilize this feature, use the following command in the terminal: tabby serve --device metal --model TabbyML/StarCoder-1B --chat-model TabbyML/Mistral-7B ``` -### ModelScope Model Registry +#### ModelScope Model Registry Mainland Chinese users have been facing challenges accessing Hugging Face due to various reasons. The Tabby team is actively working to address this issue by mirroring models to a hosting provider in mainland China called modelscope.cn. ```bash -# Download from the Modelscope registry +## Download from the Modelscope registry TABBY_REGISTRY=modelscope tabby download --model TabbyML/WizardCoder-1B ``` -## Fixes and improvements +### Fixes and improvements * Implemented more accurate UTF-8 incremental decoding in the [GitHub pull request](https://github.com/TabbyML/tabby/pull/491). * Fixed the stop words implementation by utilizing RegexSet to isolate the stop word group. * Improved model downloading logic; now Tabby will attempt to fetch the latest model version if there's a remote change, and the local cache key becomes stale. * set default num_replicas_per_device for ctranslate2 backend to increase parallelism. + + + +No releases yet, this file will be updated when generating your first release. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 854b5c451806..21f51553f3bc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,16 +29,23 @@ apt-get install protobuf-compiler libopenblas-dev choco install protoc ``` +Some of the tests require mailpit SMTP server which you can install following this [instruction](https://github.com/axllent/mailpit?tab=readme-ov-file#installation) + Before proceeding, ensure that all tests are passing locally: ``` cargo test -- --skip golden ``` -Golden tests should be skipped on all platforms except Apple silicon (M1/M2), because they have not been created for other platforms yet. - This will help ensure everything is working correctly and avoid surprises with local breakages. +Golden tests, which run models and check their outputs against previous "golden snapshots", should be skipped for most development purposes, as they take a very long time to run (especially the tests running the models on CPU). You may still want to run them if your changes relate to the functioning of or integration with the generative models, but skipping them is recommended otherwise. + +Optionally, to use a GPU make sure you have the correct drivers and libraries installed for your device: + +> **CUDA for Nvidia** - [Linux](https://docs.nvidia.com/cuda/cuda-installation-guide-linux/index.html), [Windows](https://docs.nvidia.com/cuda/cuda-installation-guide-microsoft-windows/index.html) +> **ROCm for AMD** - [Linux](https://rocm.docs.amd.com/projects/install-on-linux/en/latest/tutorial/quick-start.html), [Windows](https://rocm.docs.amd.com/projects/install-on-linux/en/latest/) + ## Building and Running Tabby can be run through `cargo` in much the same manner as docker: @@ -47,7 +54,7 @@ Tabby can be run through `cargo` in much the same manner as docker: cargo run serve --model TabbyML/StarCoder-1B ``` -This will run Tabby locally on CPU, which is not optimal for performance. Depending on your GPU and its compatibility, you may be able to run Tabby with GPU acceleration. Please make sure you have CUDA or ROCm installed, for Nvidia or AMD graphics cards respectively. No extra library installation is necessary for Apple silicon (M1/M2) using Metal. +This will run Tabby locally on CPU, which is not optimal for performance. Depending on your GPU and its compatibilities, you may be able to run Tabby with GPU acceleration. First insure you have the dependencies for your Nvidia or AMD GPU installed. No extra library installation is necessary for Apple silicon (M1/M2) using Metal. To run Tabby locally with CUDA (NVIDIA): @@ -61,6 +68,12 @@ To run Tabby locally with ROCm (AMD): cargo run --features rocm serve --model TabbyML/StarCoder-1B --device rocm ``` +To run Tabby locally with Vulkan: + +``` +cargo run --features vulkan serve --model TabbyML/StarCoder-1B --device vulkan +``` + To run Tabby locally with Metal (Apple M1/M2): ``` @@ -78,13 +91,14 @@ By default, Tabby will start on `localhost:8080` and serve requests. Tabby is broken up into several crates, each responsible for a different part of the functionality. These crates fall into two categories: Fully open source features, and enterprise features. All open-source feature crates are located in the `/crates` folder in the repository root, and all enterprise feature crates are located in `/ee`. ### Crates + - `crates/tabby` - The core tabby application, this is the main binary crate defining CLI behavior and driving the API - `crates/tabby-common` - Interfaces and type definitions shared across most other tabby crates, especially types used for serialization - `crates/tabby-download` - Very small crate, responsible for downloading models at runtime - `crates/tabby-scheduler` - Defines jobs that need to run periodically for syncing and indexing code - `crates/tabby-inference` - Defines interfaces for interacting with text generation models - `crates/llama-cpp-bindings` - Raw bindings to talk with the actual models in C++ from Rust -- `ee/tabby-webserver` - The webserver for Tabby with privilege management and a chatbot playground. Also includes GraphQL API implementation. Must use `--webserver` on CLI to enable +- `ee/tabby-webserver` - The webserver for Tabby with privilege management and a chatbot playground. Also includes GraphQL API implementation. - `ee/tabby-db` - The database backing the webserver - `ee/tabby-ui` - Frontend for the Tabby webserver @@ -100,3 +114,17 @@ Most issues will have a link to the related location in the code, and if they do You can feel free to open PRs that aren't quite ready yet, to work on them. If you do this, please make sure to [mark the pull request as a draft](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/changing-the-stage-of-a-pull-request). Once your PR is ready, please request review from one of the [Tabby team members](https://github.com/orgs/TabbyML/people), and watch for replies asking for any changes. Once approved, you can merge your code into Tabby! + +# Changelog + +Tabby used [changie](https://changie.dev/) to track unreleased features, it's preferred the changelog is added as part of implementation pr. To create an unreleased feature, use `changie new` command. + +# Contributing to Docs + +To begin contributing to Tabby's docs website, make sure you installed node lts and yarn: + +``` +cd website +yarn install +yarn start +``` \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 97fd4a5cf0d2..5b04a299fe51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,12 +1,12 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" -version = "0.19.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] @@ -19,12 +19,12 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" -version = "0.8.7" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom 0.2.11", + "getrandom 0.2.15", "once_cell", "version_check", "zerocopy", @@ -32,16 +32,16 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "aim-downloader" -version = "0.8.0" +version = "0.33.0-dev.0" dependencies = [ "async-stream", "clap", @@ -63,9 +63,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" [[package]] name = "android-tzdata" @@ -82,80 +82,81 @@ dependencies = [ "libc", ] -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", -] - [[package]] name = "anstream" -version = "0.6.4" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.0" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" -version = "0.2.0" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.1" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" dependencies = [ "anstyle", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "anyhow" -version = "1.0.71" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + +[[package]] +name = "arbitrary" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +dependencies = [ + "derive_arbitrary", +] [[package]] name = "arc-swap" -version = "1.6.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" [[package]] name = "argon2" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ba4cac0a46bc1d2912652a751c47f2a9f3a7fe89bcae2275d418f5270402f9" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" dependencies = [ "base64ct", "blake2", @@ -185,6 +186,41 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" +[[package]] +name = "async-convert" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d416feee97712e43152cd42874de162b8f9b77295b1c85e5d92725cc8310bae" +dependencies = [ + "async-trait", +] + +[[package]] +name = "async-openai-alt" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ec478b16393f68acab53c252f3de7d8e7047c4ef63e8f17315f4e6afc08d9c" +dependencies = [ + "async-convert", + "backoff", + "base64 0.22.1", + "bytes", + "derive_builder", + "eventsource-stream", + "futures", + "rand 0.8.5", + "reqwest", + "reqwest-eventsource", + "secrecy 0.8.0", + "serde", + "serde_json", + "thiserror 1.0.61", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", +] + [[package]] name = "async-stream" version = "0.3.5" @@ -204,18 +240,18 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.100", ] [[package]] name = "async-trait" -version = "0.1.74" +version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.100", ] [[package]] @@ -227,40 +263,92 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "atomic-write-file" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edcdbedc2236483ab103a53415653d6b4442ea6141baf1ffa85df29635e88436" +checksum = "cbf54d4588732bdfc5ebc3eb9f74f20e027112fc31de412fc7ff0cd1c6896dae" dependencies = [ "nix", "rand 0.8.5", ] +[[package]] +name = "auto_enums" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1899bfcfd9340ceea3533ea157360ba8fa864354eccbceab58e1006ecab35393" +dependencies = [ + "derive_utils", + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "autocfg" -version = "1.1.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "axum" -version = "0.6.20" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" dependencies = [ "async-trait", - "axum-core", - "base64 0.21.5", - "bitflags 1.3.2", + "axum-core 0.4.3", "bytes", "futures-util", - "headers", - "http", - "http-body", - "hyper", - "itoa", - "matchit", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.3.1", + "hyper-util", + "itoa 1.0.11", + "matchit 0.7.3", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "tokio", + "tower 0.4.13", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de45108900e1f9b9242f7f2e254aa3e2c029c921c258fe9e6b4217eeebd54288" +dependencies = [ + "axum-core 0.5.2", + "base64 0.22.1", + "bytes", + "form_urlencoded", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.3.1", + "hyper-util", + "itoa 1.0.11", + "matchit 0.8.4", "memchr", "mime", "percent-encoding", @@ -271,96 +359,147 @@ dependencies = [ "serde_path_to_error", "serde_urlencoded", "sha1", - "sync_wrapper", + "sync_wrapper 1.0.1", "tokio", - "tokio-tungstenite", - "tower", + "tokio-tungstenite 0.26.2", + "tower 0.5.2", "tower-layer", "tower-service", + "tracing", ] [[package]] name = "axum-core" -version = "0.3.4" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" dependencies = [ "async-trait", "bytes", "futures-util", - "http", - "http-body", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 0.1.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" +dependencies = [ + "bytes", + "futures-core", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 1.0.1", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-extra" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45bf463831f5131b7d3c756525b305d40f1185b688565648a92e1392ca35713d" +dependencies = [ + "axum 0.8.3", + "axum-core 0.5.2", + "bytes", + "futures-util", + "headers", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", "mime", + "pin-project-lite", "rustversion", + "serde", + "tower 0.5.2", "tower-layer", "tower-service", ] [[package]] name = "axum-prometheus" -version = "0.4.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97def327c5481791abb57ac295bfc70f2e1a0727675b7dbf74bd1b27a72b6fd8" +checksum = "b683cbc43010e9a3d72c2f31ca464155ff4f95819e88a32924b0f47a43898978" dependencies = [ - "axum", - "axum-core", + "axum 0.7.5", "bytes", "futures", "futures-core", - "http", - "http-body", - "matchit", + "http 1.1.0", + "http-body 1.0.0", + "matchit 0.7.3", "metrics", "metrics-exporter-prometheus", "once_cell", "pin-project", "tokio", - "tower", - "tower-http 0.4.0", + "tower 0.4.13", + "tower-http 0.5.2", ] [[package]] -name = "axum-tracing-opentelemetry" -version = "0.10.0" +name = "backoff" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "164b95427e83b79583c7699a72b4a6b485a12bbdef5b5c054ee5ff2296d82f52" +checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" dependencies = [ - "axum", - "futures", - "http", - "opentelemetry", - "tower", - "tower-http 0.3.5", - "tracing", - "tracing-opentelemetry", + "futures-core", + "getrandom 0.2.15", + "instant", + "pin-project-lite", + "rand 0.8.5", + "tokio", ] [[package]] name = "backtrace" -version = "0.3.67" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ "addr2line", "cc", "cfg-if", "libc", - "miniz_oxide 0.6.2", + "miniz_oxide", "object", "rustc-demangle", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" -version = "0.13.1" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64" -version = "0.21.5" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" @@ -385,18 +524,18 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" dependencies = [ "serde", ] [[package]] name = "bitpacking" -version = "0.8.4" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c7d2ac73c167c06af4a5f37e6e59d84148d57ccbe4480b76f0273eefea82d7" +checksum = "4c1d3e2bfd8d06048a179f7b17afc3188effa10385e7b00dc65af6aae732ea92" dependencies = [ "crunchy", ] @@ -420,78 +559,92 @@ dependencies = [ ] [[package]] -name = "bson" -version = "1.2.4" +name = "bstr" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0aa578035b938855a710ba58d43cfb4d435f3619f99236fb35922a574d6cb1" +checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" dependencies = [ - "base64 0.13.1", - "chrono", - "hex", - "lazy_static", - "linked-hash-map", - "rand 0.7.3", + "memchr", + "regex-automata 0.4.6", "serde", - "serde_json", - "uuid 0.8.2", ] [[package]] -name = "bstr" -version = "1.7.0" +name = "build-target" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c79ad7fb2dd38f3dabd76b09c6a5a20c038fc0213ef1e9afd30eb777f120f019" -dependencies = [ - "memchr", - "serde", -] +checksum = "832133bbabbbaa9fbdba793456a2827627a7d2b8fb96032fa1e7666d7895832b" [[package]] name = "bumpalo" -version = "3.13.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] -name = "cargo-lock" -version = "9.0.0" +name = "cached" +version = "0.49.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11c675378efb449ed3ce8de78d75d0d80542fc98487c26aba28eb3b82feac72" +checksum = "8e8e463fceca5674287f32d252fb1d94083758b8709c160efae66d263e5f4eba" dependencies = [ - "petgraph", - "semver", - "serde", - "toml", - "url", + "ahash", + "async-trait", + "cached_proc_macro", + "cached_proc_macro_types", + "futures", + "hashbrown 0.14.5", + "instant", + "once_cell", + "thiserror 1.0.61", + "tokio", +] + +[[package]] +name = "cached_proc_macro" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad9f16c0d84de31a2ab7fdf5f7783c14631f7075cf464eb3bb43119f61c9cb2a" +dependencies = [ + "darling 0.14.4", + "proc-macro2", + "quote", + "syn 1.0.109", ] +[[package]] +name = "cached_proc_macro_types" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" + [[package]] name = "cc" -version = "1.0.83" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" dependencies = [ "jobserver", "libc", + "once_cell", ] [[package]] name = "census" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fafee10a5dd1cffcb5cc560e0d0df8803d7355a2b12272e3557dee57314cb6e" +checksum = "4f4c707c6a209cbe82d10abd08e1ea8995e9ea937d2550646e02798948992be0" [[package]] name = "cfg-if" @@ -499,11 +652,17 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "chrono" -version = "0.4.31" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", @@ -511,7 +670,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.48.0", + "windows-targets 0.52.5", ] [[package]] @@ -520,15 +679,15 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" dependencies = [ - "hashbrown 0.14.3", + "hashbrown 0.14.5", "stacker", ] [[package]] name = "clap" -version = "4.4.11" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" dependencies = [ "clap_builder", "clap_derive", @@ -536,33 +695,44 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.11" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim 0.10.0", + "strsim 0.11.1", ] [[package]] name = "clap_derive" -version = "4.4.7" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.100", ] [[package]] name = "clap_lex" -version = "0.6.0" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" + +[[package]] +name = "clocksource" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" +checksum = "129026dd5a8a9592d96916258f3a5379589e513ea5e86aeb0bd2530286e44e9e" +dependencies = [ + "libc", + "time", + "winapi", +] [[package]] name = "cmake" @@ -574,20 +744,37 @@ dependencies = [ ] [[package]] -name = "codespan-reporting" -version = "0.11.1" +name = "color-eyre" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5" dependencies = [ - "termcolor", - "unicode-width", + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", ] [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "combine" @@ -604,15 +791,15 @@ dependencies = [ [[package]] name = "console" -version = "0.15.7" +version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" dependencies = [ "encode_unicode", "lazy_static", "libc", "unicode-width", - "windows-sys 0.45.0", + "windows-sys 0.52.0", ] [[package]] @@ -621,11 +808,27 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + [[package]] name = "core-foundation" -version = "0.9.3" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" dependencies = [ "core-foundation-sys", "libc", @@ -633,24 +836,24 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] [[package]] name = "crc" -version = "3.0.1" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" dependencies = [ "crc-catalog", ] @@ -663,67 +866,50 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] [[package]] name = "cron" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab00a636277f7ea5d8dd92ac7a5099fc9a46e5327bba84d3640b41ae127eada9" -dependencies = [ - "chrono", - "error-chain", - "nom 4.1.1", -] - -[[package]] -name = "cron" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ff76b51e4c068c52bfd2866e1567bee7c567ae8f24ada09fd4307019e25eab7" +checksum = "6f8c3e73077b4b4a6ab1ea5047c37c57aee77657bc8ecd6f29b0af082d0b0c07" dependencies = [ "chrono", - "nom 7.1.3", + "nom", "once_cell", ] [[package]] name = "crossbeam-channel" -version = "0.5.8" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" dependencies = [ - "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" dependencies = [ - "cfg-if", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" -version = "0.9.14" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ - "autocfg", - "cfg-if", "crossbeam-utils", - "memoffset", - "scopeguard", ] [[package]] @@ -737,9 +923,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" @@ -747,6 +933,18 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -758,64 +956,64 @@ dependencies = [ ] [[package]] -name = "custom_error" -version = "1.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f8a51dd197fa6ba5b4dc98a990a43cc13693c23eb0089ebb0fcc1f04152bca6" - -[[package]] -name = "cxx" -version = "1.0.95" +name = "cssparser" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "109308c20e8445959c2792e81871054c6a17e6976489a93d2769641a2ba5839c" +checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" dependencies = [ - "cc", - "cxxbridge-flags", - "cxxbridge-macro", - "link-cplusplus", + "cssparser-macros", + "dtoa-short", + "itoa 0.4.8", + "matches", + "phf 0.8.0", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", ] [[package]] -name = "cxx-build" -version = "1.0.95" +name = "cssparser-macros" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf4c6755cdf10798b97510e0e2b3edb9573032bd9379de8fffa59d68165494f" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ - "cc", - "codespan-reporting", - "once_cell", - "proc-macro2", "quote", - "scratch", - "syn 2.0.32", + "syn 2.0.100", ] [[package]] -name = "cxxbridge-flags" -version = "1.0.95" +name = "curve25519-dalek" +version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "882074421238e84fe3b4c65d0081de34e5b323bf64555d3e61991f76eb64a7bb" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] [[package]] -name = "cxxbridge-macro" -version = "1.0.95" +name = "curve25519-dalek-derive" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a076022ece33e7686fb76513518e219cca4fce5750a8ae6d1ce6c0f48fd1af9" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.100", ] [[package]] -name = "darling" -version = "0.10.2" +name = "custom_error" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" -dependencies = [ - "darling_core 0.10.2", - "darling_macro 0.10.2", -] +checksum = "4f8a51dd197fa6ba5b4dc98a990a43cc13693c23eb0089ebb0fcc1f04152bca6" [[package]] name = "darling" @@ -828,17 +1026,13 @@ dependencies = [ ] [[package]] -name = "darling_core" -version = "0.10.2" +name = "darling" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" +checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.9.3", - "syn 1.0.109", + "darling_core 0.20.9", + "darling_macro 0.20.9", ] [[package]] @@ -856,14 +1050,17 @@ dependencies = [ ] [[package]] -name = "darling_macro" -version = "0.10.2" +name = "darling_core" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" +checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" dependencies = [ - "darling_core 0.10.2", + "fnv", + "ident_case", + "proc-macro2", "quote", - "syn 1.0.109", + "strsim 0.11.1", + "syn 2.0.100", ] [[package]] @@ -877,6 +1074,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "darling_macro" +version = "0.20.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" +dependencies = [ + "darling_core 0.20.9", + "quote", + "syn 2.0.100", +] + [[package]] name = "dashmap" version = "5.5.3" @@ -884,7 +1092,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", @@ -892,15 +1100,15 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.4.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" [[package]] name = "der" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", "pem-rfc7468", @@ -909,53 +1117,78 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.8" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ + "powerfmt", "serde", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "derive_builder" -version = "0.12.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8" +checksum = "0350b5cb0331628a5916d6c5c0b72e97393b8b6b03b47a9284f4e7f5a405ffd7" dependencies = [ "derive_builder_macro", ] [[package]] name = "derive_builder_core" -version = "0.12.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" +checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d" dependencies = [ - "darling 0.14.4", + "darling 0.20.9", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.100", ] [[package]] name = "derive_builder_macro" -version = "0.12.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e" +checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" dependencies = [ "derive_builder_core", + "syn 2.0.100", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", "syn 1.0.109", ] [[package]] name = "derive_utils" -version = "0.11.2" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532b4c15dccee12c7044f1fcad956e98410860b22231e44a3b827464797ca7bf" +checksum = "61bb5a1014ce6dfc2a378578509abe775a5aa06bff584a547555d9efdb81b926" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.100", ] [[package]] @@ -971,53 +1204,113 @@ dependencies = [ ] [[package]] -name = "dirs" -version = "4.0.0" +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dtoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" + +[[package]] +name = "dtoa-short" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +checksum = "dbaceec3c6e4211c79e7b1800fb9680527106beb2f9c51904a3210c03a448c74" dependencies = [ - "dirs-sys", + "dtoa", ] [[package]] -name = "dirs-sys" -version = "0.3.7" +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecdsa" +version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "libc", - "redox_users", - "winapi", + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", ] [[package]] -name = "dotenvy" -version = "0.15.7" +name = "ed25519" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] [[package]] -name = "downcast-rs" -version = "1.2.0" +name = "ed25519-dalek" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] [[package]] name = "either" -version = "1.8.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" dependencies = [ "serde", ] +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "email-encoding" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbfb21b9878cf7a348dcb8559109aabc0ec40d69924bd706fa5149846c4fef75" +checksum = "60d1d33cdaede7e24091f039632eb5d3c7469fe5b066a985281a34fc70fa317f" dependencies = [ - "base64 0.21.5", + "base64 0.22.1", "memchr", ] @@ -1034,48 +1327,55 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] -name = "encoding_rs" -version = "0.8.32" +name = "encoder" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +checksum = "03f6928ad5c6efcdae42eb068dff8a555ef2f057c92bbd491ddf5610f6444987" dependencies = [ - "cfg-if", + "encoder-ryu", + "indexmap 2.2.6", + "serde_json", + "simd-json", ] [[package]] -name = "equivalent" -version = "1.0.1" +name = "encoder-ryu" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "5e27addc39f5f73c85604bfe21b59fe93717f9765194015d92bde1db11e8ccef" [[package]] -name = "errno" -version = "0.3.1" +name = "encoding_rs" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys 0.48.0", + "cfg-if", ] [[package]] -name = "errno-dragonfly" -version = "0.1.2" +name = "encoding_rs_io" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +checksum = "1cc3c5651fb62ab8aa3103998dade57efdd028544bd300516baa31840c252a83" dependencies = [ - "cc", - "libc", + "encoding_rs", ] [[package]] -name = "error-chain" -version = "0.10.0" +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9435d864e017c3c6afeac1654189b06cdb491cf2ff73dbf0d73b0f292f42ff8" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ - "backtrace", + "libc", + "windows-sys 0.52.0", ] [[package]] @@ -1096,65 +1396,80 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] -name = "fastdivide" -version = "0.4.0" +name = "eventsource-stream" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25c7df09945d65ea8d70b3321547ed414bbc540aad5bac6883d021b970f35b04" +checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab" +dependencies = [ + "futures-core", + "nom", + "pin-project-lite", +] [[package]] -name = "fastrand" -version = "1.9.0" +name = "eyre" +version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" dependencies = [ - "instant", + "indenter", + "once_cell", ] [[package]] -name = "fastrand" -version = "2.0.1" +name = "fastdivide" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "59668941c55e5c186b8b58c391629af56774ec768f73c08bbcd56f09348eb00b" [[package]] -name = "file-rotate" -version = "0.7.5" +name = "fastrand" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddf221ceec4517f3cb764dae3541b2bd87666fc8832e51322fbb97250b468c71" -dependencies = [ - "chrono", - "flate2", -] +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] -name = "filenamify" -version = "0.1.0" +name = "ff" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b781e8974b2cc71ac3c587c881c11ee5fe9a379f43503674e1e1052647593b4c" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "regex", + "rand_core 0.6.4", + "subtle", ] [[package]] -name = "finl_unicode" -version = "1.2.0" +name = "fiat-crypto" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] -name = "fixedbitset" -version = "0.4.2" +name = "fid-rs" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +checksum = "6c28658c0c3420305705adde833a0d2d614207507d013a5f25707553fb2ae2cd" +dependencies = [ + "rayon", +] [[package]] name = "flate2" -version = "1.0.26" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", - "miniz_oxide 0.7.1", + "miniz_oxide", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", ] [[package]] @@ -1200,19 +1515,45 @@ dependencies = [ [[package]] name = "fs4" -version = "0.6.6" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eeb4ed9e12f43b7fa0baae3f9cdda28352770132ef2e09a23760c29cae8bd47" +checksum = "73969b81e8bc90a3828d913dd3973d80771bfb9d7fbe1a78a79122aad456af15" dependencies = [ - "rustix 0.38.17", - "windows-sys 0.48.0", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "fslock" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04412b8935272e3a9bae6f48c7bfff74c2911f60525404edfdd28e49884c3bfb" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", ] [[package]] name = "futures" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", @@ -1225,9 +1566,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -1235,26 +1576,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" - -[[package]] -name = "futures-enum" -version = "0.1.17" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3422d14de7903a52e9dbc10ae05a7e14445ec61890100e098754e120b2bd7b1e" -dependencies = [ - "derive_utils", - "quote", - "syn 1.0.109", -] +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", @@ -1274,38 +1604,44 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-macro" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.100", ] [[package]] name = "futures-sink" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] -name = "futures-util" -version = "0.3.29" +name = "futures-timer" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -1319,17 +1655,26 @@ dependencies = [ "slab", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "generator" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3e123d9ae7c02966b4d892e550bdc32164f05853cd40ab570650ad600596a8a" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" dependencies = [ "cc", "libc", "log", "rustversion", - "windows", + "windows 0.48.0", ] [[package]] @@ -1340,6 +1685,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -1355,20 +1701,75 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.11" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] name = "gimli" -version = "0.27.2" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "git2" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "232e6a7bfe35766bf715e55a88b39a700596c0ccfd88cd3680b4cdb40d66ef70" +dependencies = [ + "bitflags 2.6.0", + "libc", + "libgit2-sys", + "log", + "openssl-probe", + "openssl-sys", + "url", +] + +[[package]] +name = "gitlab" +version = "0.1700.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65f36adc608cffeec1cc08ecf1977c038cbb1af9184b2174ca006ebeb817991c" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bytes", + "chrono", + "cron", + "derive_builder", + "futures-util", + "graphql_client", + "http 1.1.0", + "itertools 0.12.1", + "log", + "percent-encoding", + "reqwest", + "serde", + "serde_json", + "serde_urlencoded", + "thiserror 1.0.61", + "url", +] [[package]] name = "glob" @@ -1378,50 +1779,178 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "globset" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d" +checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" dependencies = [ "aho-corasick", "bstr", - "fnv", "log", - "regex", + "regex-automata 0.4.6", + "regex-syntax 0.8.3", ] [[package]] -name = "globwalk" -version = "0.7.3" +name = "graphql-introspection-query" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9db17aec586697a93219b19726b5b68307eba92898c34b170857343fe67c99d" +checksum = "7f2a4732cf5140bd6c082434494f785a19cfb566ab07d1382c3671f5812fed6d" dependencies = [ - "ignore", - "walkdir", + "serde", ] [[package]] name = "graphql-parser" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1abd4ce5247dfc04a03ccde70f87a048458c9356c7e41d21ad8c407b3dde6f2" +checksum = "d2ebc8013b4426d5b81a4364c419a95ed0b404af2b82e2457de52d9348f0e474" dependencies = [ "combine", - "thiserror", + "thiserror 1.0.61", +] + +[[package]] +name = "graphql_client" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a50cfdc7f34b7f01909d55c2dcb71d4c13cbcbb4a1605d6c8bd760d654c1144b" +dependencies = [ + "graphql_query_derive", + "serde", + "serde_json", +] + +[[package]] +name = "graphql_client_codegen" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e27ed0c2cf0c0cc52c6bcf3b45c907f433015e580879d14005386251842fb0a" +dependencies = [ + "graphql-introspection-query", + "graphql-parser", + "heck 0.4.1", + "lazy_static", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 1.0.109", +] + +[[package]] +name = "graphql_query_derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83febfa838f898cfa73dfaa7a8eb69ff3409021ac06ee94cfb3d622f6eeb1a97" +dependencies = [ + "graphql_client_codegen", + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "grep" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2b024ec1e686cb64d78beb852030b0e632af93817f1ed25be0173af0e94939" +dependencies = [ + "grep-cli", + "grep-matcher", + "grep-printer", + "grep-regex", + "grep-searcher", +] + +[[package]] +name = "grep-cli" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea40788c059ab8b622c4d074732750bfb3bd2912e2dd58eabc11798a4d5ad725" +dependencies = [ + "bstr", + "globset", + "libc", + "log", + "termcolor", + "winapi-util", +] + +[[package]] +name = "grep-matcher" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47a3141a10a43acfedc7c98a60a834d7ba00dfe7bec9071cbfc19b55b292ac02" +dependencies = [ + "memchr", +] + +[[package]] +name = "grep-printer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743c12a03c8aee38b6e5bd0168d8ebb09345751323df4a01c56e792b1f38ceb2" +dependencies = [ + "bstr", + "grep-matcher", + "grep-searcher", + "log", + "serde", + "serde_json", + "termcolor", +] + +[[package]] +name = "grep-regex" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f748bb135ca835da5cbc67ca0e6955f968db9c5df74ca4f56b18e1ddbc68230d" +dependencies = [ + "bstr", + "grep-matcher", + "log", + "regex-automata 0.4.6", + "regex-syntax 0.8.3", +] + +[[package]] +name = "grep-searcher" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba536ae4f69bec62d8839584dd3153d3028ef31bb229f04e09fb5a9e5a193c54" +dependencies = [ + "bstr", + "encoding_rs", + "encoding_rs_io", + "grep-matcher", + "log", + "memchr", + "memmap2", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", ] [[package]] name = "h2" -version = "0.3.19" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d357c7ae988e7d2182f7d7871d0b963962420b0678b0997ce7de72001aeab782" +checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" dependencies = [ + "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "futures-util", - "http", - "indexmap 1.9.3", + "http 1.1.0", + "indexmap 2.2.6", "slab", "tokio", "tokio-util", @@ -1429,25 +1958,30 @@ dependencies = [ ] [[package]] -name = "hashbrown" -version = "0.12.3" +name = "halfbrown" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +checksum = "8588661a8607108a5ca69cab034063441a0413a0b041c13618a7dd348021ef6f" +dependencies = [ + "hashbrown 0.14.5", + "serde", +] + +[[package]] +name = "hash-ids" +version = "0.33.0-dev.0" [[package]] name = "hashbrown" -version = "0.13.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ff8ae62cd3a9102e5637afc8452c55acf3844001bd5374e0b0bd7b6616c038" -dependencies = [ - "ahash", -] +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", "allocator-api2", @@ -1459,20 +1993,19 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown 0.14.3", + "hashbrown 0.14.5", ] [[package]] name = "headers" -version = "0.3.8" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" +checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" dependencies = [ - "base64 0.13.1", - "bitflags 1.3.2", + "base64 0.21.7", "bytes", "headers-core", - "http", + "http 1.1.0", "httpdate", "mime", "sha1", @@ -1480,11 +2013,11 @@ dependencies = [ [[package]] name = "headers-core" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" dependencies = [ - "http", + "http 1.1.0", ] [[package]] @@ -1497,19 +2030,16 @@ dependencies = [ ] [[package]] -name = "hermit-abi" -version = "0.2.6" +name = "heck" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.3.1" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" @@ -1537,22 +2067,60 @@ dependencies = [ [[package]] name = "home" -version = "0.5.5" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "hostname" -version = "0.3.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba" dependencies = [ + "cfg-if", "libc", - "match_cfg", - "winapi", + "windows 0.52.0", +] + +[[package]] +name = "htmd" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fff09744b11deed5946dd1b61c806bb9a94ded93cf28da819935599b69b987" +dependencies = [ + "html5ever 0.27.0", + "markup5ever_rcdom", +] + +[[package]] +name = "html5ever" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5c13fb08e5d4dfc151ee5e88bae63f7773d61852f3bdc73c9f4b9e1bde03148" +dependencies = [ + "log", + "mac", + "markup5ever 0.10.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "html5ever" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" +dependencies = [ + "log", + "mac", + "markup5ever 0.12.1", + "proc-macro2", + "quote", + "syn 2.0.100", ] [[package]] @@ -1563,46 +2131,87 @@ checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" [[package]] name = "http" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes", "fnv", - "itoa", + "itoa 1.0.11", +] + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa 1.0.11", ] [[package]] name = "http-api-bindings" -version = "0.8.0" +version = "0.33.0-dev.0" dependencies = [ "anyhow", + "async-openai-alt", + "async-stream", "async-trait", "futures", + "leaky-bucket", + "ollama-api-bindings", "reqwest", + "reqwest-eventsource", "serde", "serde_json", + "tabby-common", "tabby-inference", "tokio", + "tokio-retry", "tracing", ] [[package]] name = "http-body" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +dependencies = [ + "bytes", + "futures-core", + "http 1.1.0", + "http-body 1.0.0", "pin-project-lite", ] [[package]] name = "http-range-header" -version = "0.3.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" [[package]] name = "httparse" @@ -1612,77 +2221,157 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "humantime" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" [[package]] name = "hyper" -version = "0.14.27" +version = "0.14.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" dependencies = [ "bytes", "futures-channel", "futures-core", "futures-util", - "h2", - "http", - "http-body", + "http 0.2.12", + "http-body 0.4.6", "httparse", "httpdate", - "itoa", + "itoa 1.0.11", "pin-project-lite", - "socket2 0.4.9", + "socket2", "tokio", "tower-service", "tracing", "want", ] +[[package]] +name = "hyper" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http 1.1.0", + "http-body 1.0.0", + "httparse", + "httpdate", + "itoa 1.0.11", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" +dependencies = [ + "futures-util", + "http 1.1.0", + "hyper 1.3.1", + "hyper-util", + "rustls 0.22.4", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.25.0", + "tower-service", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +dependencies = [ + "futures-util", + "http 1.1.0", + "hyper 1.3.1", + "hyper-util", + "log", + "rustls 0.23.20", + "rustls-native-certs 0.8.1", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.1", + "tower-service", +] + [[package]] name = "hyper-timeout" -version = "0.4.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +checksum = "3203a961e5c83b6f5498933e78b6b263e208c197b63e9c6c53cc82ffd3f63793" dependencies = [ - "hyper", + "hyper 1.3.1", + "hyper-util", "pin-project-lite", "tokio", - "tokio-io-timeout", + "tower-service", ] [[package]] name = "hyper-tls" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", - "hyper", + "http-body-util", + "hyper 1.3.1", + "hyper-util", "native-tls", "tokio", "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "hyper 1.3.1", + "pin-project-lite", + "socket2", + "tokio", + "tower 0.4.13", + "tower-service", + "tracing", ] [[package]] name = "iana-time-zone" -version = "0.1.57" +version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows", + "windows-core 0.52.0", ] [[package]] @@ -1700,16 +2389,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" -[[package]] -name = "idna" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - [[package]] name = "idna" version = "0.5.0" @@ -1720,29 +2399,28 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "if_chain" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" - [[package]] name = "ignore" -version = "0.4.20" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492" +checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1" dependencies = [ + "crossbeam-deque", "globset", - "lazy_static", "log", "memchr", - "regex", + "regex-automata 0.4.6", "same-file", - "thread_local", "walkdir", "winapi-util", ] +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + [[package]] name = "indexmap" version = "1.9.3" @@ -1756,19 +2434,20 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.0.1" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad227c3af19d4914570ad36d30409928b75967c298feb9ea1969db3a610bb14e" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown 0.14.5", + "serde", ] [[package]] name = "indicatif" -version = "0.17.7" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb28741c9db9a713d93deb3bb9515c20788cef5815265bee4980e87bde7e0f25" +checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" dependencies = [ "console", "instant", @@ -1779,9 +2458,9 @@ dependencies = [ [[package]] name = "insta" -version = "1.34.0" +version = "1.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d64600be34b2fcfc267740a243fa7744441bb4947a619ac4e5bb6507f35fbfc" +checksum = "810ae6042d48e2c9e9215043563a58a80b877bc863228a74cf10c49d4620a6f5" dependencies = [ "console", "lazy_static", @@ -1790,14 +2469,13 @@ dependencies = [ "pest_derive", "serde", "similar", - "yaml-rust", ] [[package]] name = "instant" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", "js-sys", @@ -1806,21 +2484,26 @@ dependencies = [ ] [[package]] -name = "io-lifetimes" -version = "1.0.11" +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "iri-string" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +checksum = "7f5f6c2df22c009ac44f6f1499308e7a3ac7ba42cd2378475cc691510e1eef1b" dependencies = [ - "hermit-abi 0.3.1", - "libc", - "windows-sys 0.48.0", + "memchr", + "serde", ] [[package]] -name = "ipnet" -version = "2.7.2" +name = "is_terminal_polyfill" +version = "1.70.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" [[package]] name = "itertools" @@ -1833,66 +2516,62 @@ dependencies = [ [[package]] name = "itertools" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] [[package]] name = "itertools" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] -name = "job_scheduler" -version = "1.2.1" +name = "itoa" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51f368c9c76dde2282714ae32dc274b79c27527a0c06c816f6dda048904d0d7c" -dependencies = [ - "chrono", - "cron 0.6.1", - "uuid 0.8.2", -] +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jobserver" -version = "0.1.26" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.64" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] [[package]] name = "jsonwebtoken" -version = "9.1.0" +version = "9.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "155c4d7e39ad04c172c5e3a99c434ea3b4a7ba7960b38ecd562b270b097cce09" +checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" dependencies = [ - "base64 0.21.5", + "base64 0.21.7", + "js-sys", "pem", - "ring", + "ring 0.17.8", "serde", "serde_json", "simple_asn1", @@ -1900,31 +2579,33 @@ dependencies = [ [[package]] name = "juniper" -version = "0.15.11" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52adf17d43d0b526eed31fac15d9312941c5c2558ffbfb105811690b96d6e2f1" +checksum = "943306315b1a7a03d27af9dfb0c288d9f4da8830c17df4bceb7d50a47da0982c" dependencies = [ "async-trait", - "bson", + "auto_enums", "chrono", "fnv", "futures", - "futures-enum", "graphql-parser", - "indexmap 1.9.3", + "indexmap 2.2.6", "juniper_codegen", "serde", "smartstring", "static_assertions", - "url", - "uuid 0.8.2", + "void", ] [[package]] -name = "juniper-axum" -version = "0.8.0" +name = "juniper_axum" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63fb283e7f51b7f8ed9c012f3b4c4d4c8c9423e277d06162ceb1c3bc9628aa8" dependencies = [ - "axum", + "axum 0.8.3", + "bytes", + "futures", "juniper", "juniper_graphql_ws", "serde", @@ -1933,21 +2614,21 @@ dependencies = [ [[package]] name = "juniper_codegen" -version = "0.15.9" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aee97671061ad50301ba077d054d295e01d31a1868fbd07902db651f987e71db" +checksum = "760dbe46660494d469023d661e8d268f413b2cb68c999975dcc237407096a693" dependencies = [ - "proc-macro-error", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.100", + "url", ] [[package]] name = "juniper_graphql_ws" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5526c2f2a9c40f08841dc559971641fdd71c008a265745d18bb0c8b7e105b3" +checksum = "709eb11c716072f5c9fcbfa705dd684bd3c070943102f9fc56ccb812a36ba017" dependencies = [ "juniper", "juniper_subscriptions", @@ -1957,22 +2638,24 @@ dependencies = [ [[package]] name = "juniper_subscriptions" -version = "0.16.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2983b26a1e12b691c17432aee3881d8bec4a94d6c64bc933c0eaf6d9e3429f13" +checksum = "e6208a839bd4ca2131924a238311d088d6604ea267c0917903392bad7b70a92c" dependencies = [ "futures", "juniper", ] [[package]] -name = "kdam" -version = "0.5.0" +name = "kuchiki" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e352f4e1acc6a3d0919eaeb014ca63e5da9450a12ef7106fe2936a07a1648d44" +checksum = "1ea8e9c6e031377cff82ee3001dc8026cdf431ed4e2e6b51f98ab8c73484a358" dependencies = [ - "terminal_size", - "windows-sys 0.48.0", + "cssparser", + "html5ever 0.25.2", + "matches", + "selectors", ] [[package]] @@ -1984,27 +2667,76 @@ dependencies = [ "spin 0.5.2", ] +[[package]] +name = "lber" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2df7f9fd9f64cf8f59e1a4a0753fe7d575a5b38d3d7ac5758dcee9357d83ef0a" +dependencies = [ + "bytes", + "nom", +] + +[[package]] +name = "ldap3" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "166199a8207874a275144c8a94ff6eed5fcbf5c52303e4d9b4d53a0c7ac76554" +dependencies = [ + "async-trait", + "bytes", + "futures", + "futures-util", + "lazy_static", + "lber", + "log", + "native-tls", + "nom", + "percent-encoding", + "thiserror 1.0.61", + "tokio", + "tokio-native-tls", + "tokio-stream", + "tokio-util", + "url", +] + +[[package]] +name = "leaky-bucket" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a396bb213c2d09ed6c5495fd082c991b6ab39c9daf4fff59e6727f85c73e4c5" +dependencies = [ + "parking_lot", + "pin-project-lite", + "tokio", +] + [[package]] name = "lettre" -version = "0.11.3" +version = "0.11.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5aaf628956b6b0852e12ac3505d20d7a12ecc1e32d5ea921f002af4a74036a5" +checksum = "1a62049a808f1c4e2356a2a380bd5f2aca3b011b0b482cf3b914ba1731426969" dependencies = [ - "base64 0.21.5", + "async-trait", + "base64 0.22.1", "chumsky", "email-encoding", "email_address", - "fastrand 2.0.1", + "fastrand", + "futures-io", "futures-util", "hostname", "httpdate", - "idna 0.5.0", + "idna", "mime", "native-tls", - "nom 7.1.3", + "nom", + "percent-encoding", "quoted_printable", - "socket2 0.5.5", + "socket2", "tokio", + "tokio-native-tls", "url", ] @@ -2014,11 +2746,89 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25" +[[package]] +name = "lexical-core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cde5de06e8d4c2faabc400238f9ae1c74d5412d03a7bd067645ccbc47070e46" +dependencies = [ + "lexical-parse-float", + "lexical-parse-integer", + "lexical-util", + "lexical-write-float", + "lexical-write-integer", +] + +[[package]] +name = "lexical-parse-float" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683b3a5ebd0130b8fb52ba0bdc718cc56815b6a097e28ae5a6997d0ad17dc05f" +dependencies = [ + "lexical-parse-integer", + "lexical-util", + "static_assertions", +] + +[[package]] +name = "lexical-parse-integer" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d0994485ed0c312f6d965766754ea177d07f9c00c9b82a5ee62ed5b47945ee9" +dependencies = [ + "lexical-util", + "static_assertions", +] + +[[package]] +name = "lexical-util" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5255b9ff16ff898710eb9eb63cb39248ea8a5bb036bea8085b1a767ff6c4e3fc" +dependencies = [ + "static_assertions", +] + +[[package]] +name = "lexical-write-float" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accabaa1c4581f05a3923d1b4cfd124c329352288b7b9da09e766b0668116862" +dependencies = [ + "lexical-util", + "lexical-write-integer", + "static_assertions", +] + +[[package]] +name = "lexical-write-integer" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b6f3d1f4422866b68192d62f77bc5c700bee84f3069f2469d7bc8c77852446" +dependencies = [ + "lexical-util", + "static_assertions", +] + [[package]] name = "libc" -version = "0.2.149" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "libgit2-sys" +version = "0.16.2+1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8" +dependencies = [ + "cc", + "libc", + "libssh2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", +] [[package]] name = "libloading" @@ -2048,12 +2858,29 @@ dependencies = [ ] [[package]] -name = "link-cplusplus" -version = "1.0.8" +name = "libssh2-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" +checksum = "c15da26e5af7e25c90b37a2d75cdbf940cf4a55316de9d84c679c9b8bfabf82e" dependencies = [ "cc", + "libc", + "pkg-config", + "vcpkg", ] [[package]] @@ -2064,37 +2891,36 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.3.8" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] -name = "linux-raw-sys" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" - -[[package]] -name = "llama-cpp-bindings" -version = "0.8.0" +name = "llama-cpp-server" +version = "0.33.0-dev.0" dependencies = [ - "async-stream", + "anyhow", + "async-openai-alt", "async-trait", "cmake", - "cxx", - "cxx-build", - "derive_builder", "futures", + "http-api-bindings", + "omnicopy_to_output", + "reqwest", + "serde", + "serdeconv", + "tabby-common", "tabby-inference", "tokio", "tracing", + "which", ] [[package]] name = "lock_api" -version = "0.4.10" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -2102,9 +2928,20 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "logkit" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "b517d00135d2ea552dc1f6cbc5da9d8953f895c91db680c288d53c50ca309767" +dependencies = [ + "backtrace", + "chrono", + "encoder", +] [[package]] name = "loom" @@ -2117,46 +2954,83 @@ dependencies = [ "pin-utils", "scoped-tls", "tracing", - "tracing-subscriber 0.3.17", + "tracing-subscriber", +] + +[[package]] +name = "louds-rs" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16a91fb20f74b6d9a758a0103a2884af525a2fa34fbfe19f4b3c5482a4a54e9" +dependencies = [ + "fid-rs", ] [[package]] name = "lru" -version = "0.11.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a83fb7698b3643a0e34f9ae6f2e8f0178c0fd42f8b59d493aa271ff3a5bf21" +checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" dependencies = [ - "hashbrown 0.14.3", + "hashbrown 0.14.5", ] [[package]] name = "lz4_flex" -version = "0.11.1" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ea9b256699eda7b0387ffbc776dd625e28bde3918446381781245b7a50349d8" +checksum = "75761162ae2b0e580d7e7c390558127e5f01b4194debd6221fd8c207fc80e3f5" [[package]] -name = "mach2" -version = "0.4.1" +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "markup5ever" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d0d1830bcd151a6fc4aea1369af235b36c1528fe976b8ff678683c9995eade8" +checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd" dependencies = [ - "libc", + "log", + "phf 0.8.0", + "phf_codegen 0.8.0", + "string_cache", + "string_cache_codegen", + "tendril", ] [[package]] -name = "match_cfg" -version = "0.1.0" +name = "markup5ever" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" +checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" +dependencies = [ + "log", + "phf 0.11.2", + "phf_codegen 0.11.2", + "string_cache", + "string_cache_codegen", + "tendril", +] [[package]] -name = "matchers" -version = "0.0.1" +name = "markup5ever_rcdom" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f099785f7595cc4b4553a174ce30dd7589ef93391ff414dbb67f62392b9e0ce1" +checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18" dependencies = [ - "regex-automata 0.1.10", + "html5ever 0.27.0", + "markup5ever 0.12.1", + "tendril", + "xml5ever", ] [[package]] @@ -2168,11 +3042,23 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + [[package]] name = "matchit" -version = "0.7.0" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "matchit" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "md-5" @@ -2186,9 +3072,9 @@ dependencies = [ [[package]] name = "measure_time" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56220900f1a0923789ecd6bf25fbae8af3b2f1ff3e9e297fc9b6b8674dd4d852" +checksum = "dbefd235b0aadd181626f281e1d684e116972988c14c264e42069d5e8a5775cc" dependencies = [ "instant", "log", @@ -2196,83 +3082,55 @@ dependencies = [ [[package]] name = "memchr" -version = "2.6.2" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5486aed0026218e61b8a01d5fbd5a0a134649abb71a0e53b7bc088529dced86e" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "memmap2" -version = "0.7.1" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f49388d20533534cd19360ad3d6a7dadc885944aa802ba3995040c5ec11288c6" +checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" dependencies = [ "libc", ] -[[package]] -name = "memo-map" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374c335b2df19e62d4cb323103473cbc6510980253119180de862d89184f6a83" - -[[package]] -name = "memoffset" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" -dependencies = [ - "autocfg", -] - [[package]] name = "metrics" -version = "0.21.1" +version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fde3af1a009ed76a778cb84fdef9e7dbbdf5775ae3e4cc1f434a6a307f6f76c5" +checksum = "2be3cbd384d4e955b231c895ce10685e3d8260c5ccffae898c96c723b0772835" dependencies = [ "ahash", - "metrics-macros", "portable-atomic", ] [[package]] name = "metrics-exporter-prometheus" -version = "0.12.1" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a4964177ddfdab1e3a2b37aec7cf320e14169abb0ed73999f558136409178d5" +checksum = "9bf4e7146e30ad172c42c39b3246864bd2d3c6396780711a1baf749cfe423e21" dependencies = [ - "base64 0.21.5", - "hyper", - "indexmap 1.9.3", + "base64 0.21.7", + "hyper 0.14.28", + "indexmap 2.2.6", "ipnet", "metrics", "metrics-util", "quanta", - "thiserror", + "thiserror 1.0.61", "tokio", - "tracing", -] - -[[package]] -name = "metrics-macros" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddece26afd34c31585c74a4db0630c376df271c285d682d1e55012197830b6df" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.32", ] [[package]] name = "metrics-util" -version = "0.15.1" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4de2ed6e491ed114b40b732e4d1659a9d53992ebd87490c44a6ffe23739d973e" +checksum = "8b07a5eb561b8cbc16be2d216faf7757f9baf3bfb94dbb0fae3df8387a5bb47f" dependencies = [ "crossbeam-epoch", "crossbeam-utils", - "hashbrown 0.13.1", + "hashbrown 0.14.5", "metrics", "num_cpus", "quanta", @@ -2295,17 +3153,6 @@ dependencies = [ "unicase", ] -[[package]] -name = "minijinja" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80084fa3099f58b7afab51e5f92e24c2c2c68dcad26e96ad104bd6011570461d" -dependencies = [ - "memo-map", - "self_cell", - "serde", -] - [[package]] name = "minimal-lexical" version = "0.2.1" @@ -2314,59 +3161,43 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" -dependencies = [ - "adler", -] - -[[package]] -name = "miniz_oxide" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.10" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] -[[package]] -name = "multimap" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" - [[package]] name = "murmurhash32" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9380db4c04d219ac5c51d14996bbf2c2e9a15229771b53f8671eb6c83cf44df" +checksum = "2195bf6aa996a481483b29d62a7663eed3fe39600c460e323f8ff41e90bdd89b" [[package]] name = "native-tls" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" dependencies = [ - "lazy_static", "libc", "log", "openssl", "openssl-probe", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.0", "security-framework-sys", "tempfile", ] @@ -2377,25 +3208,29 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9a91b326434fca226707ed8ec1fd22d4e1c96801abdf10c412afdc7d97116e0" +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nix" -version = "0.27.1" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.6.0", "cfg-if", + "cfg_aliases", "libc", ] [[package]] -name = "nom" -version = "4.1.1" +name = "nodrop" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c349f68f25f596b9f44cf0e7c69752a5c633b0550c3ff849518bfba0233774a" -dependencies = [ - "memchr", -] +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" [[package]] name = "nom" @@ -2426,13 +3261,33 @@ dependencies = [ "winapi", ] +[[package]] +name = "nucleo" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5262af4c94921c2646c5ac6ff7900c2af9cbb08dc26a797e18130a7019c039d4" +dependencies = [ + "nucleo-matcher", + "parking_lot", + "rayon", +] + +[[package]] +name = "nucleo-matcher" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf33f538733d1a5a3494b836ba913207f14d9d4a1d3cd67030c5061bdd2cac85" +dependencies = [ + "memchr", + "unicode-segmentation", +] + [[package]] name = "num-bigint" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" dependencies = [ - "autocfg", "num-integer", "num-traits", ] @@ -2455,31 +3310,25 @@ dependencies = [ ] [[package]] -name = "num-derive" -version = "0.3.3" +name = "num-conv" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-integer" -version = "0.1.45" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "autocfg", "num-traits", ] [[package]] name = "num-iter" -version = "0.1.43" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ "autocfg", "num-integer", @@ -2488,9 +3337,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.17" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", "libm", @@ -2498,19 +3347,19 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.2.6", + "hermit-abi", "libc", ] [[package]] name = "num_threads" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" dependencies = [ "libc", ] @@ -2531,7 +3380,7 @@ dependencies = [ "libloading", "nvml-wrapper-sys", "static_assertions", - "thiserror", + "thiserror 1.0.61", "wrapcenum-derive", ] @@ -2544,37 +3393,166 @@ dependencies = [ "libloading", ] +[[package]] +name = "oauth2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" +dependencies = [ + "base64 0.22.1", + "chrono", + "getrandom 0.2.15", + "http 1.1.0", + "rand 0.8.5", + "reqwest", + "serde", + "serde_json", + "serde_path_to_error", + "sha2", + "thiserror 1.0.61", + "url", +] + [[package]] name = "object" -version = "0.30.3" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] +[[package]] +name = "octocrab" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b97f949a7cb04608441c2ddb28e15a377e8b5142c2d1835ad2686d434de8558" +dependencies = [ + "arc-swap", + "async-trait", + "base64 0.22.1", + "bytes", + "cfg-if", + "chrono", + "either", + "futures", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.3.1", + "hyper-rustls 0.27.3", + "hyper-timeout", + "hyper-util", + "jsonwebtoken", + "once_cell", + "percent-encoding", + "pin-project", + "secrecy 0.10.3", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "snafu", + "tokio", + "tower 0.5.2", + "tower-http 0.6.2", + "tracing", + "url", + "web-time", +] + +[[package]] +name = "ollama-api-bindings" +version = "0.33.0-dev.0" +dependencies = [ + "anyhow", + "async-stream", + "async-trait", + "futures", + "ollama-rs", + "tabby-common", + "tabby-inference", + "tracing", +] + +[[package]] +name = "ollama-rs" +version = "0.1.9" +source = "git+https://github.com/pepperoni21/ollama-rs.git?rev=56e8157d98d4185bc171fe9468d3d09bc56e9dd3#56e8157d98d4185bc171fe9468d3d09bc56e9dd3" +dependencies = [ + "reqwest", + "serde", + "serde_json", + "tokio", + "tokio-stream", + "url", +] + +[[package]] +name = "omnicopy_to_output" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10aff4d07c3656c416a997301d51ed83be62cbb256b421f86b014931217f2393" +dependencies = [ + "anyhow", + "build-target", + "fs_extra", + "project-root", +] + [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "oneshot" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc22d22931513428ea6cc089e942d38600e3d00976eef8c86de6b8a3aadec6eb" +version = "0.1.6" +source = "git+https://github.com/fulmicoton/oneshot.git?rev=b208f49#b208f493e505f5f37e180b5cdec4c22b6f99abaa" dependencies = [ "loom", ] +[[package]] +name = "openidconnect" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c6709ba2ea764bbed26bce1adf3c10517113ddea6f2d4196e4851757ef2b2" +dependencies = [ + "base64 0.21.7", + "chrono", + "dyn-clone", + "ed25519-dalek", + "hmac", + "http 1.1.0", + "itertools 0.10.5", + "log", + "oauth2", + "p256", + "p384", + "rand 0.8.5", + "rsa", + "serde", + "serde-value", + "serde_json", + "serde_path_to_error", + "serde_plain", + "serde_with", + "sha2", + "subtle", + "thiserror 1.0.61", + "url", +] + [[package]] name = "openssl" -version = "0.10.61" +version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b8419dc8cc6d866deb801274bba2e6f8f6108c1bb7fcc10ee5ab864931dbb45" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.6.0", "cfg-if", "foreign-types", "libc", @@ -2591,7 +3569,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.100", ] [[package]] @@ -2602,18 +3580,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.2.1+3.2.0" +version = "300.3.0+3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fe476c29791a5ca0d1273c697e96085bbabbbea2ef7afd5617e78a4b40332d3" +checksum = "eba8804a1c5765b18c4b3f907e6897ebabeedebc9830e1a0046c4a4cf44663e1" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.97" +version = "0.9.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3eaad34cdd97d81de97964fc7f29e2d104f483840d906ef56daa1912338460b" +checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" dependencies = [ "cc", "libc", @@ -2629,55 +3607,73 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69d6c3d7288a106c0a363e4b0e8d308058d56902adefb16f4936f417ffef086e" dependencies = [ "opentelemetry_api", - "opentelemetry_sdk", + "opentelemetry_sdk 0.18.0", +] + +[[package]] +name = "opentelemetry" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab70038c28ed37b97d8ed414b6429d343a8bbf44c9f79ec854f3a643029ba6d7" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", + "thiserror 1.0.61", + "tracing", ] [[package]] name = "opentelemetry-otlp" -version = "0.11.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1c928609d087790fc936a1067bdc310ae702bdf3b090c3f281b713622c8bbde" +checksum = "91cf61a1868dacc576bf2b2a1c3e9ab150af7272909e80085c3173384fe11f76" dependencies = [ "async-trait", - "futures", - "futures-util", - "http", - "opentelemetry", + "futures-core", + "http 1.1.0", + "opentelemetry 0.27.1", "opentelemetry-proto", + "opentelemetry_sdk 0.27.1", "prost", - "thiserror", + "thiserror 1.0.61", "tokio", "tonic", + "tracing", ] [[package]] name = "opentelemetry-proto" -version = "0.1.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d61a2f56df5574508dd86aaca016c917489e589ece4141df1b5e349af8d66c28" +checksum = "a6e05acbfada5ec79023c85368af14abd0b307c015e9064d249b2a950ef459a6" dependencies = [ - "futures", - "futures-util", - "opentelemetry", + "opentelemetry 0.27.1", + "opentelemetry_sdk 0.27.1", "prost", "tonic", - "tonic-build", ] +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc1b6902ff63b32ef6c489e8048c5e253e2e4a803ea3ea7e783914536eb15c52" + [[package]] name = "opentelemetry_api" version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c24f96e21e7acc813c7a8394ee94978929db2bcc46cf6b5014fc612bf7760c22" dependencies = [ - "fnv", "futures-channel", "futures-util", "indexmap 1.9.3", "js-sys", "once_cell", "pin-project-lite", - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -2688,8 +3684,6 @@ checksum = "1ca41c4933371b61c2a2f214bf16931499af4ec90543604ec828f7a625c09113" dependencies = [ "async-trait", "crossbeam-channel", - "dashmap", - "fnv", "futures-channel", "futures-executor", "futures-util", @@ -2697,11 +3691,38 @@ dependencies = [ "opentelemetry_api", "percent-encoding", "rand 0.8.5", - "thiserror", + "thiserror 1.0.61", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231e9d6ceef9b0b2546ddf52335785ce41252bc7474ee8ba05bfad277be13ab8" +dependencies = [ + "async-trait", + "futures-channel", + "futures-executor", + "futures-util", + "glob", + "opentelemetry 0.27.1", + "percent-encoding", + "rand 0.8.5", + "serde_json", + "thiserror 1.0.61", "tokio", "tokio-stream", ] +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + [[package]] name = "overload" version = "0.1.1" @@ -2710,18 +3731,47 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "ownedbytes" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e8a72b918ae8198abb3a18c190288123e1d442b6b9a7d709305fd194688b4b7" +version = "0.7.0" +source = "git+https://github.com/quickwit-oss/tantivy?rev=4143d31#4143d31865cbae9a9a7a286b0420a95814408ec7" dependencies = [ "stable_deref_trait", ] +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -2729,15 +3779,25 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.8" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.3.5", + "redox_syscall 0.5.1", "smallvec", - "windows-targets 0.48.0", + "windows-targets 0.52.5", +] + +[[package]] +name = "parse-git-url" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cd626725d3855a68fdede6483fae43429129bf246f42d8db598911c8036cf47" +dependencies = [ + "tracing", + "url", ] [[package]] @@ -2753,17 +3813,17 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.12" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pem" -version = "3.0.2" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3163d2912b7c3b52d651a055f2c7eec9ba5cd22d26ef75b8dd3a59980b185923" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" dependencies = [ - "base64 0.21.5", + "base64 0.22.1", "serde", ] @@ -2784,20 +3844,20 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.5" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae9cee2a55a544be8b89dc6848072af97a20f2422603c10865be2a42b580fff5" +checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8" dependencies = [ "memchr", - "thiserror", + "thiserror 1.0.61", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.7.5" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81d78524685f5ef2a3b3bd1cafbc9fcabb036253d9b1463e726a91cd16e2dfc2" +checksum = "26293c9193fbca7b1a3bf9b79dc1e388e927e6cacaa78b4a3ab705a1d3d41459" dependencies = [ "pest", "pest_generator", @@ -2805,22 +3865,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.5" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68bd1206e71118b5356dae5ddc61c8b11e28b09ef6a31acbd15ea48a28e0c227" +checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.100", ] [[package]] name = "pest_meta" -version = "2.7.5" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c747191d4ad9e4a4ab9c8798f1e82a39affe7ef9648390b7e5548d18e099de6" +checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd" dependencies = [ "once_cell", "pest", @@ -2828,40 +3888,141 @@ dependencies = [ ] [[package]] -name = "petgraph" -version = "0.6.3" +name = "phf" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "fixedbitset", - "indexmap 1.9.3", + "phf_macros", + "phf_shared 0.8.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared 0.11.2", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", ] [[package]] name = "pin-project" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.100", ] [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -2892,15 +4053,21 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.27" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "portable-atomic" -version = "1.3.2" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" + +[[package]] +name = "powerfmt" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc59d1bcc64fc5d021d67521f818db868368028108d37f0e98d74e33f68297b5" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" @@ -2909,13 +4076,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] -name = "prettyplease" -version = "0.1.25" +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "primeorder" +version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8646e95016a7a6c4adea95bafa8a16baab64b583356217f2c85db4a39d9a86" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" dependencies = [ - "proc-macro2", - "syn 1.0.109", + "elliptic-curve", ] [[package]] @@ -2942,67 +4114,48 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] [[package]] -name = "prost" -version = "0.11.9" +name = "project-root" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" -dependencies = [ - "bytes", - "prost-derive", -] +checksum = "8bccbff07d5ed689c4087d20d7307a52ab6141edeedf487c3876a55b86cf63df" [[package]] -name = "prost-build" -version = "0.11.9" +name = "prost" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "119533552c9a7ffacc21e099c24a0ac8bb19c2a2a3f363de84cd9b844feab270" +checksum = "2c0fef6c4230e4ccf618a35c59d7ede15dea37de8427500f50aff708806e42ec" dependencies = [ "bytes", - "heck", - "itertools 0.10.5", - "lazy_static", - "log", - "multimap", - "petgraph", - "prettyplease", - "prost", - "prost-types", - "regex", - "syn 1.0.109", - "tempfile", - "which", + "prost-derive", ] [[package]] name = "prost-derive" -version = "0.11.9" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" +checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools 0.13.0", "proc-macro2", "quote", - "syn 1.0.109", -] - -[[package]] -name = "prost-types" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13" -dependencies = [ - "prost", + "syn 2.0.100", ] [[package]] @@ -3016,13 +4169,12 @@ dependencies = [ [[package]] name = "quanta" -version = "0.11.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17e662a7a8291a865152364c20c7abc5e60486ab2001e8ec10b24862de0b9ab" +checksum = "8e5167a477619228a0b284fac2674e3c388cba90631d7b7de620e6f1fcd08da5" dependencies = [ "crossbeam-utils", "libc", - "mach2", "once_cell", "raw-cpuid", "wasi 0.11.0+wasi-snapshot-preview1", @@ -3038,9 +4190,9 @@ checksum = "9318ead08c799aad12a55a3e78b82e0b6167271ffd1f627b758891282f739187" [[package]] name = "quote" -version = "1.0.32" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -3051,6 +4203,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79ec282e887b434b68c18fe5c121d38e72a5cf35119b59e54ec5b992ea9c8eb0" +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + [[package]] name = "rand" version = "0.7.3" @@ -3062,6 +4220,7 @@ dependencies = [ "rand_chacha 0.2.2", "rand_core 0.5.1", "rand_hc", + "rand_pcg", ] [[package]] @@ -3075,6 +4234,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -3095,6 +4264,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + [[package]] name = "rand_core" version = "0.5.1" @@ -3110,7 +4289,26 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.11", + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.2", +] + +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand 0.8.5", ] [[package]] @@ -3122,20 +4320,40 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "ratelimit" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36ea961700fd7260e7fa3701c8287d901b2172c51f9c1421fa0f21d7f7e184b7" +dependencies = [ + "clocksource", + "parking_lot", + "thiserror 1.0.61", +] + [[package]] name = "raw-cpuid" -version = "10.7.0" +version = "11.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c297679cb867470fa8c9f67dbba74a78d78e3e98d7cf2b08d6d71540f797332" +checksum = "e29830cbb1290e404f24c73af91c5d8d631ce7e128691e9477556b540cd01ecd" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", ] [[package]] name = "rayon" -version = "1.7.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", @@ -3143,55 +4361,76 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.11.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ - "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", - "num_cpus", +] + +[[package]] +name = "readable-readability" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17015928a25bff296b0471dfa7a784e406664e1d091781db66e885b18708a8d" +dependencies = [ + "html5ever 0.25.2", + "kuchiki", + "lazy_static", + "log", + "regex", + "url", ] [[package]] name = "redox_syscall" -version = "0.2.16" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "redox_syscall" -version = "0.3.5" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", ] [[package]] -name = "redox_users" -version = "0.4.3" +name = "ref-cast" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf0a6f84d5f1d581da8b41b47ec8600871962f2a528115b542b362d4b744931" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6" dependencies = [ - "getrandom 0.2.11", - "redox_syscall 0.2.16", - "thiserror", + "proc-macro2", + "quote", + "syn 2.0.100", ] [[package]] name = "regex" -version = "1.10.2" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.3", - "regex-syntax 0.8.2", + "regex-automata 0.4.6", + "regex-syntax 0.8.3", ] [[package]] @@ -3205,13 +4444,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.3" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.2", + "regex-syntax 0.8.3", ] [[package]] @@ -3222,53 +4461,51 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" - -[[package]] -name = "requirements" -version = "0.3.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2643e903f79d8e6bc310ee0def974d12a33561d14e0728511b6ba5e8be0791c3" -dependencies = [ - "globwalk", - "pest", - "pest_derive", - "regex", - "walkdir", -] +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "reqwest" -version = "0.11.22" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" +checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" dependencies = [ - "base64 0.21.5", + "base64 0.22.1", "bytes", "encoding_rs", + "futures-channel", "futures-core", "futures-util", "h2", - "http", - "http-body", - "hyper", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.3.1", + "hyper-rustls 0.26.0", "hyper-tls", + "hyper-util", "ipnet", "js-sys", "log", "mime", + "mime_guess", "native-tls", "once_cell", "percent-encoding", "pin-project-lite", + "rustls 0.22.4", + "rustls-native-certs 0.7.0", + "rustls-pemfile", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", + "sync_wrapper 0.1.2", "system-configuration", "tokio", "tokio-native-tls", + "tokio-rustls 0.25.0", "tokio-util", "tower-service", "url", @@ -3276,28 +4513,71 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", + "webpki-roots", "winreg", ] +[[package]] +name = "reqwest-eventsource" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "632c55746dbb44275691640e7b40c907c16a2dc1a5842aa98aaec90da6ec6bde" +dependencies = [ + "eventsource-stream", + "futures-core", + "futures-timer", + "mime", + "nom", + "pin-project-lite", + "reqwest", + "thiserror 1.0.61", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted 0.7.1", + "web-sys", + "winapi", +] + [[package]] name = "ring" -version = "0.17.5" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", - "getrandom 0.2.11", + "cfg-if", + "getrandom 0.2.15", "libc", "spin 0.9.8", - "untrusted", - "windows-sys 0.48.0", + "untrusted 0.9.0", + "windows-sys 0.52.0", ] [[package]] name = "rmp" -version = "0.8.11" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44519172358fd6d58656c86ab8e7fbc9e1490c3e8f14d35ed78ca0dd07403c9f" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" dependencies = [ "byteorder", "num-traits", @@ -3306,9 +4586,9 @@ dependencies = [ [[package]] name = "rmp-serde" -version = "1.1.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5b13be192e0220b8afb7222aa5813cb62cc269ebb5cac346ca6487681d2913e" +checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" dependencies = [ "byteorder", "rmp", @@ -3337,68 +4617,33 @@ dependencies = [ [[package]] name = "rust-embed" -version = "6.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b68543d5527e158213414a92832d2aab11a84d2571a5eb021ebe22c43aab066" -dependencies = [ - "rust-embed-impl 6.5.0", - "rust-embed-utils 7.5.0", - "walkdir", -] - -[[package]] -name = "rust-embed" -version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e7d90385b59f0a6bf3d3b757f3ca4ece2048265d70db20a2016043d4509a40" -dependencies = [ - "rust-embed-impl 8.0.0", - "rust-embed-utils 8.0.0", - "walkdir", -] - -[[package]] -name = "rust-embed-impl" -version = "6.5.0" +version = "8.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d4e0f0ced47ded9a68374ac145edd65a6c1fa13a96447b873660b2a568a0fd7" +checksum = "19549741604902eb99a7ed0ee177a0663ee1eda51a29f71401f166e47e77806a" dependencies = [ - "proc-macro2", - "quote", - "rust-embed-utils 7.5.0", - "shellexpand", - "syn 1.0.109", + "rust-embed-impl", + "rust-embed-utils", "walkdir", ] [[package]] name = "rust-embed-impl" -version = "8.0.0" +version = "8.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3d8c6fd84090ae348e63a84336b112b5c3918b3bf0493a581f7bd8ee623c29" +checksum = "cb9f96e283ec64401f30d3df8ee2aaeb2561f34c824381efa24a35f79bf40ee4" dependencies = [ "proc-macro2", "quote", - "rust-embed-utils 8.0.0", - "syn 2.0.32", - "walkdir", -] - -[[package]] -name = "rust-embed-utils" -version = "7.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512b0ab6853f7e14e3c8754acb43d6f748bb9ced66aa5915a6553ac8213f7731" -dependencies = [ - "sha2", + "rust-embed-utils", + "syn 2.0.100", "walkdir", ] [[package]] name = "rust-embed-utils" -version = "8.0.0" +version = "8.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "873feff8cb7bf86fdf0a71bb21c95159f4e4a37dd7a4bd1855a940909b583ada" +checksum = "38c74a686185620830701348de757fd36bef4aa9680fd23c49fc539ddcc1af32" dependencies = [ "sha2", "walkdir", @@ -3416,9 +4661,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" @@ -3427,43 +4672,131 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] -name = "rustix" -version = "0.37.19" +name = "rustc_version" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys 0.3.8", - "windows-sys 0.48.0", + "semver", ] [[package]] name = "rustix" -version = "0.38.17" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f25469e9ae0f3d0047ca8b93fc56843f38e6774f0914a107ff8b41be8be8e0b7" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.6.0", "errno", "libc", - "linux-raw-sys 0.4.10", - "windows-sys 0.48.0", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.20.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" +dependencies = [ + "log", + "ring 0.16.20", + "sct", + "webpki", +] + +[[package]] +name = "rustls" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "log", + "ring 0.17.8", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls" +version = "0.23.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b" +dependencies = [ + "log", + "once_cell", + "ring 0.17.8", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "rustls-pki-types", + "schannel", + "security-framework 2.11.0", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.0.1", +] + +[[package]] +name = "rustls-pemfile" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +dependencies = [ + "base64 0.22.1", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring 0.17.8", + "rustls-pki-types", + "untrusted 0.9.0", ] [[package]] name = "rustversion" -version = "1.0.14" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "ryu" -version = "1.0.13" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" @@ -3474,13 +4807,46 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scc" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ad2bbb0ae5100a07b7a6f2ed7ab5fd0045551a4c507989b7a620046ea3efdc" +dependencies = [ + "sdd", +] + [[package]] name = "schannel" -version = "0.1.21" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" +checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" dependencies = [ - "windows-sys 0.42.0", + "dyn-clone", + "ref-cast", + "serde", + "serde_json", ] [[package]] @@ -3491,134 +4857,243 @@ checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" [[package]] name = "scopeguard" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] -name = "scratch" -version = "1.0.5" +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring 0.17.8", + "untrusted 0.9.0", +] + +[[package]] +name = "sdd" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84345e4c9bd703274a082fb80caaa99b7612be48dfaa1dd9266577ec412309d" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "serde", + "zeroize", +] + +[[package]] +name = "secrecy" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "zeroize", +] [[package]] name = "security-framework" -version = "2.9.2" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" dependencies = [ - "bitflags 1.3.2", - "core-foundation", + "bitflags 2.6.0", + "core-foundation 0.9.4", "core-foundation-sys", "libc", "security-framework-sys", ] [[package]] -name = "security-framework-sys" -version = "2.9.1" +name = "security-framework" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +checksum = "e1415a607e92bec364ea2cf9264646dcce0f91e6d65281bd6f2819cca3bf39c8" dependencies = [ + "bitflags 2.6.0", + "core-foundation 0.10.0", "core-foundation-sys", "libc", + "security-framework-sys", ] [[package]] -name = "self_cell" -version = "1.0.1" +name = "security-framework-sys" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c309e515543e67811222dbc9e3dd7e1056279b782e1dacffe4242b718734fb6" +checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" +dependencies = [ + "core-foundation-sys", + "libc", +] [[package]] -name = "semver" -version = "1.0.20" +name = "selectors" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" +checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" dependencies = [ - "serde", + "bitflags 1.3.2", + "cssparser", + "derive_more", + "fxhash", + "log", + "matches", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc", + "smallvec", + "thin-slice", ] +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + [[package]] name = "serde" -version = "1.0.171" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] [[package]] name = "serde-jsonlines" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4684abdec99c1de7e507a3516c82385ff74d54c385973846b079bfd9f5920d35" +checksum = "e228faf5f94badfe42723177b62cfb9b187351994cb4e852cd4a6a4c96dbeea8" dependencies = [ "serde", "serde_json", ] [[package]] -name = "serde-jsonlines" -version = "0.5.0" +name = "serde-value" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e228faf5f94badfe42723177b62cfb9b187351994cb4e852cd4a6a4c96dbeea8" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" dependencies = [ + "ordered-float", "serde", - "serde_json", ] [[package]] name = "serde_derive" -version = "1.0.171" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.100", ] [[package]] name = "serde_json" -version = "1.0.107" +version = "1.0.142" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" dependencies = [ - "indexmap 2.0.1", - "itoa", + "itoa 1.0.11", + "memchr", "ryu", "serde", ] [[package]] name = "serde_path_to_error" -version = "0.1.11" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa 1.0.11", + "serde", +] + +[[package]] +name = "serde_plain" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7f05c1d5476066defcdfacce1f52fc3cae3af1d3089727100c02ae92e5abbe0" +checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" dependencies = [ "serde", ] [[package]] name = "serde_spanned" -version = "0.6.2" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa 1.0.11", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d" +checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.2.6", + "schemars 0.9.0", + "schemars 1.0.4", "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", ] [[package]] -name = "serde_urlencoded" -version = "0.7.1" +name = "serde_with_macros" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", + "darling 0.20.9", + "proc-macro2", + "quote", + "syn 2.0.100", ] [[package]] @@ -3650,16 +5125,17 @@ dependencies = [ [[package]] name = "serial_test" -version = "3.0.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ad9342b3aaca7cb43c45c097dd008d4907070394bd0751a0aa8817e5a018d" +checksum = "4b4b487fe2acf240a021cf57c6b2b4903b1e78ca0ecd862a71b71d2a51fed77d" dependencies = [ - "dashmap", + "fslock", "futures", - "lazy_static", "log", + "once_cell", "parking_lot", - "serial_test_derive 3.0.0", + "scc", + "serial_test_derive 3.1.1", ] [[package]] @@ -3670,18 +5146,28 @@ checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.100", ] [[package]] name = "serial_test_derive" -version = "3.0.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b93fb4adc70021ac1b47f7d45e8cc4169baaa7ea58483bc5b721d19a26202212" +checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.100", +] + +[[package]] +name = "servo_arc" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432" +dependencies = [ + "nodrop", + "stable_deref_trait", ] [[package]] @@ -3706,42 +5192,20 @@ dependencies = [ "digest", ] -[[package]] -name = "sha256" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7895c8ae88588ccead14ff438b939b0c569cd619116f14b4d13fdff7b8333386" -dependencies = [ - "async-trait", - "bytes", - "hex", - "sha2", - "tokio", -] - [[package]] name = "sharded-slab" -version = "0.1.4" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ "lazy_static", ] -[[package]] -name = "shellexpand" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ccc8076840c4da029af4f87e4e8daeb0fca6b87bbb02e10cb60b791450e11e4" -dependencies = [ - "dirs", -] - [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] @@ -3756,11 +5220,39 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simd-json" +version = "0.13.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "570c430b3d902ea083097e853263ae782dfe40857d93db019a12356c8e8143fa" +dependencies = [ + "getrandom 0.2.15", + "halfbrown", + "lexical-core", + "ref-cast", + "serde", + "serde_json", + "simdutf8", + "value-trait", +] + +[[package]] +name = "simdutf8" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" + [[package]] name = "similar" -version = "2.3.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aeaf503862c419d66959f5d7ca015337d864e9c49485d771b732e2a20453597" +checksum = "fa42c91313f1d05da9b26f267f931cf178d4aba455b4c4622dd7355eb80c6640" [[package]] name = "simple_asn1" @@ -3770,33 +5262,39 @@ checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" dependencies = [ "num-bigint", "num-traits", - "thiserror", + "thiserror 1.0.61", "time", ] +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "sketches-ddsketch" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a406c1882ed7f29cd5e248c9848a80e7cb6ae0fea82346d2746f2f941c07e1" +checksum = "85636c14b73d81f541e525f585c0a2109e6744e1565b5c1668e31c70c10ed65c" dependencies = [ "serde", ] [[package]] name = "slab" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "smallvec" -version = "1.10.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "smartstring" @@ -3810,23 +5308,34 @@ dependencies = [ ] [[package]] -name = "socket2" -version = "0.4.9" +name = "snafu" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +checksum = "418b8136fec49956eba89be7da2847ec1909df92a9ae4178b5ff0ff092c8d95e" dependencies = [ - "libc", - "winapi", + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a4812a669da00d17d8266a0439eddcacbc88b17f732f927e52eeb9d196f7fb5" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.100", ] [[package]] name = "socket2" -version = "0.5.5" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -3844,6 +5353,17 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spinners" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0ef947f358b9c238923f764c72a4a9d42f2d637c46e059dbd319d6e7cfb4f82" +dependencies = [ + "lazy_static", + "maplit", + "strum 0.24.1", +] + [[package]] name = "spki" version = "0.7.3" @@ -3854,22 +5374,27 @@ dependencies = [ "der", ] +[[package]] +name = "sql_query_builder" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a85dbaf3c8d08abe8a95a51860550236a07bd6fc097e2bff054ad8c2bf9a0df5" + [[package]] name = "sqlformat" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c" dependencies = [ - "itertools 0.12.0", - "nom 7.1.3", + "itertools 0.12.1", + "nom", "unicode_categories", ] [[package]] name = "sqlx" version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dba03c279da73694ef99763320dea58b51095dfe87d001b1d4b5fe78ba8763cf" +source = "git+https://github.com/wsxiaoys/sqlx?rev=77eb94d#77eb94dd672531bf1fec190aadf2f742cfb5a8db" dependencies = [ "sqlx-core", "sqlx-macros", @@ -3881,8 +5406,7 @@ dependencies = [ [[package]] name = "sqlx-core" version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d84b0a3c3739e220d94b3239fd69fb1f74bc36e16643423bd99de3b43c21bfbd" +source = "git+https://github.com/wsxiaoys/sqlx?rev=77eb94d#77eb94dd672531bf1fec190aadf2f742cfb5a8db" dependencies = [ "ahash", "atoi", @@ -3901,7 +5425,7 @@ dependencies = [ "futures-util", "hashlink", "hex", - "indexmap 2.0.1", + "indexmap 2.2.6", "log", "memchr", "once_cell", @@ -3912,7 +5436,7 @@ dependencies = [ "sha2", "smallvec", "sqlformat", - "thiserror", + "thiserror 1.0.61", "tokio", "tokio-stream", "tracing", @@ -3922,8 +5446,7 @@ dependencies = [ [[package]] name = "sqlx-macros" version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89961c00dc4d7dffb7aee214964b065072bff69e36ddb9e2c107541f75e4f2a5" +source = "git+https://github.com/wsxiaoys/sqlx?rev=77eb94d#77eb94dd672531bf1fec190aadf2f742cfb5a8db" dependencies = [ "proc-macro2", "quote", @@ -3935,13 +5458,12 @@ dependencies = [ [[package]] name = "sqlx-macros-core" version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0bd4519486723648186a08785143599760f7cc81c52334a55d6a83ea1e20841" +source = "git+https://github.com/wsxiaoys/sqlx?rev=77eb94d#77eb94dd672531bf1fec190aadf2f742cfb5a8db" dependencies = [ "atomic-write-file", "dotenvy", "either", - "heck", + "heck 0.4.1", "hex", "once_cell", "proc-macro2", @@ -3959,15 +5481,26 @@ dependencies = [ "url", ] +[[package]] +name = "sqlx-migrate-validate" +version = "0.33.0-dev.0" +dependencies = [ + "anyhow", + "async-trait", + "sqlx", + "sqlx-rt", + "thiserror 1.0.61", + "tokio", +] + [[package]] name = "sqlx-mysql" version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e37195395df71fd068f6e2082247891bc11e3289624bbc776a0cdfa1ca7f1ea4" +source = "git+https://github.com/wsxiaoys/sqlx?rev=77eb94d#77eb94dd672531bf1fec190aadf2f742cfb5a8db" dependencies = [ "atoi", - "base64 0.21.5", - "bitflags 2.4.0", + "base64 0.21.7", + "bitflags 2.6.0", "byteorder", "bytes", "chrono", @@ -3983,7 +5516,7 @@ dependencies = [ "hex", "hkdf", "hmac", - "itoa", + "itoa 1.0.11", "log", "md-5", "memchr", @@ -3997,7 +5530,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 1.0.61", "tracing", "whoami", ] @@ -4005,12 +5538,11 @@ dependencies = [ [[package]] name = "sqlx-postgres" version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6ac0ac3b7ccd10cc96c7ab29791a7dd236bd94021f31eec7ba3d46a74aa1c24" +source = "git+https://github.com/wsxiaoys/sqlx?rev=77eb94d#77eb94dd672531bf1fec190aadf2f742cfb5a8db" dependencies = [ "atoi", - "base64 0.21.5", - "bitflags 2.4.0", + "base64 0.21.7", + "bitflags 2.6.0", "byteorder", "chrono", "crc", @@ -4024,7 +5556,7 @@ dependencies = [ "hkdf", "hmac", "home", - "itoa", + "itoa 1.0.11", "log", "md-5", "memchr", @@ -4037,16 +5569,26 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 1.0.61", "tracing", "whoami", ] +[[package]] +name = "sqlx-rt" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804d3f245f894e61b1e6263c84b23ca675d96753b5abfd5cc8597d86806e8024" +dependencies = [ + "once_cell", + "tokio", + "tokio-rustls 0.23.4", +] + [[package]] name = "sqlx-sqlite" version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "210976b7d948c7ba9fced8ca835b11cbb2d677c59c79de41ac0d397e14547490" +source = "git+https://github.com/wsxiaoys/sqlx?rev=77eb94d#77eb94dd672531bf1fec190aadf2f742cfb5a8db" dependencies = [ "atoi", "chrono", @@ -4097,28 +5639,54 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a8348af2d9fc3258c8733b8d9d8db2e56f54b2363a4b5b81585c7875ed65e65" +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot", + "phf_shared 0.10.0", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro2", + "quote", +] + [[package]] name = "stringprep" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" dependencies = [ - "finl_unicode", "unicode-bidi", "unicode-normalization", + "unicode-properties", ] [[package]] name = "strsim" -version = "0.9.3" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "strsim" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" @@ -4126,7 +5694,16 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" dependencies = [ - "strum_macros", + "strum_macros 0.24.3", +] + +[[package]] +name = "strum" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" +dependencies = [ + "strum_macros 0.26.2", ] [[package]] @@ -4135,13 +5712,26 @@ version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "rustversion", "syn 1.0.109", ] +[[package]] +name = "strum_macros" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.100", +] + [[package]] name = "subtle" version = "2.5.0" @@ -4161,9 +5751,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.32" +version = "2.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239814284fd6f1a4ffe4ca893952cdd93c224b6a1571c9a9eadd670295c0c9e2" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" dependencies = [ "proc-macro2", "quote", @@ -4176,19 +5766,24 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" + [[package]] name = "sysinfo" -version = "0.29.8" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d10ed79c22663a35a255d289a7fdcb43559fc77ff15df5ce6c341809e7867528" +checksum = "948512566b1895f93b1592c7574baeb2de842f224f2aab158799ecadb8ebbb46" dependencies = [ - "cfg-if", "core-foundation-sys", "libc", + "memchr", "ntapi", - "once_cell", "rayon", - "winapi", + "windows 0.57.0", ] [[package]] @@ -4198,7 +5793,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys", ] @@ -4214,202 +5809,348 @@ dependencies = [ [[package]] name = "tabby" -version = "0.8.0" +version = "0.33.0-dev.0" dependencies = [ "anyhow", "assert-json-diff", + "async-openai-alt", "async-stream", "async-trait", - "axum", + "axum 0.8.3", + "axum-extra", "axum-prometheus", - "axum-tracing-opentelemetry", "chrono", "clap", + "color-eyre", "futures", "http-api-bindings", - "hyper", + "hyper 1.3.1", "insta", "lazy_static", - "llama-cpp-bindings", - "minijinja", + "llama-cpp-server", "nvml-wrapper", "openssl", - "opentelemetry", + "opentelemetry 0.27.1", "opentelemetry-otlp", + "opentelemetry-semantic-conventions", + "opentelemetry_sdk 0.27.1", "regex", "reqwest", + "reqwest-eventsource", "serde", - "serde-jsonlines 0.5.0", + "serde-jsonlines", "serde_json", "serdeconv", - "serial_test 3.0.0", + "serial_test 3.1.1", + "spinners", "strfmt", - "strum", + "strum 0.24.1", "sysinfo", "tabby-common", "tabby-download", "tabby-inference", - "tabby-scheduler", "tabby-webserver", "tantivy", - "textdistance", - "thiserror", + "thiserror 1.0.61", "tokio", - "tower-http 0.4.0", + "tower-http 0.5.2", "tracing", - "tracing-opentelemetry", - "tracing-subscriber 0.3.17", + "tracing-opentelemetry 0.28.0", + "tracing-subscriber", "utoipa", "utoipa-swagger-ui", - "uuid 1.6.1", + "uuid", "vergen", ] [[package]] name = "tabby-common" -version = "0.8.0" +version = "0.33.0-dev.0" dependencies = [ "anyhow", "async-trait", - "filenamify", - "glob", + "axum 0.8.3", + "axum-extra", + "chrono", + "derive_builder", + "hash-ids", "home", + "humantime", "lazy_static", - "regex", + "parse-git-url", "reqwest", "serde", - "serde-jsonlines 0.4.0", "serde_json", "serdeconv", "tantivy", - "thiserror", + "temp_testdir", + "thiserror 1.0.61", + "tokio", + "tracing", + "url", "utoipa", - "uuid 1.6.1", + "uuid", + "validator", +] + +[[package]] +name = "tabby-crawler" +version = "0.33.0-dev.0" +dependencies = [ + "anyhow", + "async-stream", + "futures", + "htmd", + "logkit", + "percent-encoding", + "readable-readability", + "regex", + "reqwest", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-test", + "url", ] [[package]] name = "tabby-db" -version = "0.8.0" +version = "0.33.0-dev.0" dependencies = [ "anyhow", "assert_matches", + "cached", "chrono", + "hash-ids", + "lazy_static", + "serde", + "serde_json", + "sql_query_builder", "sqlx", + "sqlx-migrate-validate", + "tabby-db-macros", + "tokio", + "uuid", +] + +[[package]] +name = "tabby-db-macros" +version = "0.33.0-dev.0" +dependencies = [ + "quote", + "syn 2.0.100", +] + +[[package]] +name = "tabby-download" +version = "0.33.0-dev.0" +dependencies = [ + "aim-downloader", + "anyhow", + "serial_test 3.1.1", + "tabby-common", + "tokio-retry", + "tracing", +] + +[[package]] +name = "tabby-git" +version = "0.33.0-dev.0" +dependencies = [ + "anyhow", + "assert_matches", + "async-stream", + "axum 0.8.3", + "chrono", + "futures", + "git2", + "grep", + "ignore", + "mime_guess", + "nucleo", + "serde", + "serde_json", + "temp_testdir", + "tokio", + "tracing", +] + +[[package]] +name = "tabby-index" +version = "0.33.0-dev.0" +dependencies = [ + "anyhow", + "async-stream", + "async-trait", + "chrono", + "futures", + "git2", + "ignore", + "insta", + "lazy_static", + "logkit", + "serde", + "serde_json", + "serial_test 3.1.1", "tabby-common", + "tabby-git", + "tabby-inference", + "tantivy", + "temp_testdir", + "text-splitter", "tokio", - "uuid 1.6.1", + "tracing", + "tracing-subscriber", + "tracing-test", + "tree-sitter-c", + "tree-sitter-c-sharp", + "tree-sitter-cpp", + "tree-sitter-elixir", + "tree-sitter-gdscript", + "tree-sitter-go", + "tree-sitter-java", + "tree-sitter-kotlin", + "tree-sitter-lua", + "tree-sitter-python", + "tree-sitter-ruby", + "tree-sitter-rust", + "tree-sitter-scala", + "tree-sitter-solidity", + "tree-sitter-tags", + "tree-sitter-typescript", ] [[package]] -name = "tabby-download" -version = "0.8.0" +name = "tabby-index-cli" +version = "0.33.0-dev.0" dependencies = [ - "aim-downloader", "anyhow", - "sha256", + "clap", + "serde", + "serde_json", "tabby-common", - "tokio-retry", - "tracing", + "tantivy", ] [[package]] name = "tabby-inference" -version = "0.8.0" +version = "0.33.0-dev.0" dependencies = [ + "anyhow", + "async-openai-alt", "async-stream", "async-trait", "dashmap", "derive_builder", "futures", - "regex", + "reqwest", + "secrecy 0.8.0", "tabby-common", + "tracing", + "trie-rs", ] [[package]] -name = "tabby-scheduler" -version = "0.8.0" +name = "tabby-schema" +version = "0.33.0-dev.0" dependencies = [ "anyhow", - "cargo-lock", - "file-rotate", - "ignore", - "job_scheduler", - "kdam", + "async-openai-alt", + "async-trait", + "axum 0.8.3", + "base64 0.22.1", + "chrono", + "futures", + "hash-ids", + "juniper", "lazy_static", - "requirements", - "serde-jsonlines 0.4.0", - "serde_json", - "serdeconv", + "ldap3", + "regex", + "serde", + "strum 0.24.1", "tabby-common", - "tantivy", - "temp_testdir", + "tabby-db", + "tabby-inference", + "thiserror 1.0.61", "tokio", "tracing", - "tracing-test", - "tree-sitter-c", - "tree-sitter-c-sharp", - "tree-sitter-cpp", - "tree-sitter-go", - "tree-sitter-java", - "tree-sitter-kotlin", - "tree-sitter-python", - "tree-sitter-ruby", - "tree-sitter-rust", - "tree-sitter-tags", - "tree-sitter-typescript", + "url", + "validator", ] [[package]] name = "tabby-webserver" -version = "0.8.0" +version = "0.33.0-dev.0" dependencies = [ "anyhow", "argon2", "assert_matches", + "async-openai-alt", + "async-stream", "async-trait", - "axum", + "axum 0.8.3", + "axum-extra", "bincode", + "cached", "chrono", + "cron", + "fs_extra", "futures", - "hyper", + "gitlab", + "humantime", + "hyper 1.3.1", + "insta", "jsonwebtoken", "juniper", - "juniper-axum", + "juniper_axum", + "juniper_graphql_ws", "lazy_static", + "ldap3", "lettre", + "logkit", "mime_guess", + "octocrab", + "openidconnect", "pin-project", "querystring", + "ratelimit", "reqwest", - "rust-embed 8.0.0", + "rust-embed", "serde", "serde_json", + "serial_test 3.1.1", + "strum 0.24.1", "tabby-common", + "tabby-crawler", "tabby-db", + "tabby-git", + "tabby-index", + "tabby-inference", + "tabby-schema", "tarpc", - "thiserror", + "temp_testdir", + "thiserror 1.0.61", "tokio", - "tokio-cron-scheduler", - "tokio-tungstenite", - "tower", - "tower-http 0.4.0", + "tokio-tungstenite 0.21.0", + "tower 0.5.2", + "tower-http 0.5.2", "tracing", - "unicase", + "url", "urlencoding", - "uuid 1.6.1", + "utoipa", + "uuid", "validator", ] [[package]] name = "tantivy" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1d4675fed6fe2218ce11445374e181e864a8ffd0f28e7e0591ccfc38cd000ae" +version = "0.23.0" +source = "git+https://github.com/quickwit-oss/tantivy?rev=4143d31#4143d31865cbae9a9a7a286b0420a95814408ec7" dependencies = [ "aho-corasick", "arc-swap", - "async-trait", - "base64 0.21.5", + "base64 0.22.1", "bitpacking", "byteorder", "census", @@ -4417,17 +6158,16 @@ dependencies = [ "crossbeam-channel", "downcast-rs", "fastdivide", + "fnv", "fs4", "htmlescape", - "itertools 0.11.0", + "itertools 0.13.0", "levenshtein_automata", "log", "lru", "lz4_flex", "measure_time", "memmap2", - "murmurhash32", - "num_cpus", "once_cell", "oneshot", "rayon", @@ -4446,30 +6186,28 @@ dependencies = [ "tantivy-stacker", "tantivy-tokenizer-api", "tempfile", - "thiserror", + "thiserror 1.0.61", "time", - "uuid 1.6.1", + "uuid", "winapi", ] [[package]] name = "tantivy-bitpacker" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecb164321482301f514dd582264fa67f70da2d7eb01872ccd71e35e0d96655a" +version = "0.6.0" +source = "git+https://github.com/quickwit-oss/tantivy?rev=4143d31#4143d31865cbae9a9a7a286b0420a95814408ec7" dependencies = [ "bitpacking", ] [[package]] name = "tantivy-columnar" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d85f8019af9a78b3118c11298b36ffd21c2314bd76bbcd9d12e00124cbb7e70" +version = "0.3.0" +source = "git+https://github.com/quickwit-oss/tantivy?rev=4143d31#4143d31865cbae9a9a7a286b0420a95814408ec7" dependencies = [ + "downcast-rs", "fastdivide", - "fnv", - "itertools 0.11.0", + "itertools 0.13.0", "serde", "tantivy-bitpacker", "tantivy-common", @@ -4479,9 +6217,8 @@ dependencies = [ [[package]] name = "tantivy-common" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af4a3a975e604a2aba6b1106a04505e1e7a025e6def477fab6e410b4126471e1" +version = "0.7.0" +source = "git+https://github.com/quickwit-oss/tantivy?rev=4143d31#4143d31865cbae9a9a7a286b0420a95814408ec7" dependencies = [ "async-trait", "byteorder", @@ -4492,30 +6229,29 @@ dependencies = [ [[package]] name = "tantivy-fst" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc3c506b1a8443a3a65352df6382a1fb6a7afe1a02e871cee0d25e2c3d5f3944" +checksum = "d60769b80ad7953d8a7b2c70cdfe722bbcdcac6bccc8ac934c40c034d866fc18" dependencies = [ "byteorder", - "regex-syntax 0.6.29", + "regex-syntax 0.8.3", "utf8-ranges", ] [[package]] name = "tantivy-query-grammar" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d39c5a03100ac10c96e0c8b07538e2ab8b17da56434ab348309b31f23fada77" +version = "0.22.0" +source = "git+https://github.com/quickwit-oss/tantivy?rev=4143d31#4143d31865cbae9a9a7a286b0420a95814408ec7" dependencies = [ - "nom 7.1.3", + "nom", ] [[package]] name = "tantivy-sstable" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0c1bb43e5e8b8e05eb8009610344dbf285f06066c844032fbb3e546b3c71df" +version = "0.3.0" +source = "git+https://github.com/quickwit-oss/tantivy?rev=4143d31#4143d31865cbae9a9a7a286b0420a95814408ec7" dependencies = [ + "tantivy-bitpacker", "tantivy-common", "tantivy-fst", "zstd", @@ -4523,19 +6259,18 @@ dependencies = [ [[package]] name = "tantivy-stacker" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2c078595413f13f218cf6f97b23dcfd48936838f1d3d13a1016e05acd64ed6c" +version = "0.3.0" +source = "git+https://github.com/quickwit-oss/tantivy?rev=4143d31#4143d31865cbae9a9a7a286b0420a95814408ec7" dependencies = [ "murmurhash32", + "rand_distr", "tantivy-common", ] [[package]] name = "tantivy-tokenizer-api" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "347b6fb212b26d3505d224f438e3c4b827ab8bd847fe9953ad5ac6b8f9443b66" +version = "0.3.0" +source = "git+https://github.com/quickwit-oss/tantivy?rev=4143d31#4143d31865cbae9a9a7a286b0420a95814408ec7" dependencies = [ "serde", ] @@ -4550,18 +6285,18 @@ dependencies = [ "fnv", "futures", "humantime", - "opentelemetry", + "opentelemetry 0.18.0", "pin-project", "rand 0.8.5", "serde", "static_assertions", "tarpc-plugins", - "thiserror", + "thiserror 1.0.61", "tokio", "tokio-serde", "tokio-util", "tracing", - "tracing-opentelemetry", + "tracing-opentelemetry 0.18.0", ] [[package]] @@ -4583,67 +6318,105 @@ checksum = "921f1e9c427802414907a48b21a6504ff6b3a15a1a3cf37e699590949ad9befc" [[package]] name = "tempfile" -version = "3.5.0" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", - "fastrand 1.9.0", - "redox_syscall 0.3.5", - "rustix 0.37.19", - "windows-sys 0.45.0", + "fastrand", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", ] [[package]] name = "termcolor" -version = "1.2.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ "winapi-util", ] [[package]] -name = "terminal_size" -version = "0.3.0" +name = "text-splitter" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" +checksum = "2ab9dc04b7cf08eb01c07c272bf699fa55679a326ddf7dd075e14094efc80fb9" dependencies = [ - "rustix 0.38.17", - "windows-sys 0.48.0", + "ahash", + "auto_enums", + "either", + "itertools 0.13.0", + "once_cell", + "regex", + "strum 0.26.2", + "thiserror 1.0.61", + "tree-sitter", + "unicode-segmentation", ] [[package]] -name = "textdistance" -version = "1.0.2" +name = "thin-slice" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" + +[[package]] +name = "thiserror" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d321c8576c2b47e43953e9cce236550d4cd6af0a6ce518fe084340082ca6037b" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +dependencies = [ + "thiserror-impl 1.0.61", +] [[package]] name = "thiserror" -version = "1.0.49" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ - "thiserror-impl", + "proc-macro2", + "quote", + "syn 2.0.100", ] [[package]] name = "thiserror-impl" -version = "1.0.49" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.100", ] [[package]] name = "thread_local" -version = "1.1.7" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ "cfg-if", "once_cell", @@ -4651,14 +6424,16 @@ dependencies = [ [[package]] name = "time" -version = "0.3.26" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a79d09ac6b08c1ab3906a2f7cc2e81a0e27c7ae89c63812df75e52bef0751e07" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", - "itoa", + "itoa 1.0.11", "libc", + "num-conv", "num_threads", + "powerfmt", "serde", "time-core", "time-macros", @@ -4666,16 +6441,17 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.12" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75c65469ed6b3a4809d987a41eb1dc918e9bc1d92211cbad7ae82931846f7451" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ + "num-conv", "time-core", ] @@ -4696,57 +6472,31 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.35.0" +version = "1.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d45b238a16291a4e1584e61820b8ae57d696cc5015c459c229ccc6990cc1c" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" dependencies = [ "backtrace", "bytes", "libc", "mio", - "num_cpus", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.5", + "socket2", "tokio-macros", - "windows-sys 0.48.0", -] - -[[package]] -name = "tokio-cron-scheduler" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de2c1fd54a857b29c6cd1846f31903d0ae8e28175615c14a277aed45c58d8e27" -dependencies = [ - "chrono", - "cron 0.12.0", - "num-derive", - "num-traits", - "tokio", - "tracing", - "uuid 1.6.1", -] - -[[package]] -name = "tokio-io-timeout" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" -dependencies = [ - "pin-project-lite", - "tokio", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.100", ] [[package]] @@ -4770,6 +6520,38 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls 0.20.9", + "tokio", + "webpki", +] + +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls 0.22.4", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +dependencies = [ + "rustls 0.23.20", + "tokio", +] + [[package]] name = "tokio-serde" version = "0.8.0" @@ -4784,9 +6566,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.14" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite", @@ -4795,39 +6577,50 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.20.1" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite 0.21.0", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" dependencies = [ "futures-util", "log", "tokio", - "tungstenite", + "tungstenite 0.26.2", ] [[package]] name = "tokio-util" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", "futures-io", "futures-sink", "futures-util", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "pin-project-lite", "slab", "tokio", - "tracing", ] [[package]] name = "toml" -version = "0.7.4" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6135d499e69981f9ff0ef2167955a5333c35e36f6937d382974566b3d5b94ec" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" dependencies = [ "serde", "serde_spanned", @@ -4837,20 +6630,20 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.2" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.19.10" +version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380d56e8670370eee6566b0bfd4265f65b3f432e8c6d85623f728d4fa31f739" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 1.9.3", + "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", @@ -4859,47 +6652,32 @@ dependencies = [ [[package]] name = "tonic" -version = "0.8.3" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f219fad3b929bef19b1f86fbc0358d35daed8f2cac972037ac0dc10bbb8d5fb" +checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" dependencies = [ "async-stream", "async-trait", - "axum", - "base64 0.13.1", + "axum 0.7.5", + "base64 0.22.1", "bytes", - "futures-core", - "futures-util", "h2", - "http", - "http-body", - "hyper", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.3.1", "hyper-timeout", + "hyper-util", "percent-encoding", "pin-project", "prost", - "prost-derive", + "socket2", "tokio", "tokio-stream", - "tokio-util", - "tower", + "tower 0.4.13", "tower-layer", "tower-service", "tracing", - "tracing-futures", -] - -[[package]] -name = "tonic-build" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf5e9b9c0f7e0a7c027dcfaba7b2c60816c7049171f679d99ee2ff65d0de8c4" -dependencies = [ - "prettyplease", - "proc-macro2", - "prost-build", - "quote", - "syn 1.0.109", ] [[package]] @@ -4923,19 +6701,17 @@ dependencies = [ ] [[package]] -name = "tower-http" -version = "0.3.5" +name = "tower" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f873044bf02dd1e8239e9c1293ea39dad76dc594ec16185d0a1bf31d8dc8d858" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ - "bitflags 1.3.2", - "bytes", "futures-core", "futures-util", - "http", - "http-body", - "http-range-header", "pin-project-lite", + "sync_wrapper 1.0.1", + "tokio", + "tokio-util", "tower-layer", "tower-service", "tracing", @@ -4943,16 +6719,16 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.4.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d1d42a9b3f3ec46ba828e8d376aec14592ea199f70a06a548587ecd1c4ab658" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "bytes", - "futures-core", "futures-util", - "http", - "http-body", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", "http-range-header", "httpdate", "mime", @@ -4966,25 +6742,43 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" +dependencies = [ + "bitflags 2.6.0", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "iri-string", + "pin-project-lite", + "tower 0.5.2", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower-layer" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if", "log", "pin-project-lite", "tracing-attributes", @@ -4993,43 +6787,43 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.24" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.100", ] [[package]] name = "tracing-core" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", "valuable", ] [[package]] -name = "tracing-futures" -version = "0.2.5" +name = "tracing-error" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" dependencies = [ - "pin-project", "tracing", + "tracing-subscriber", ] [[package]] name = "tracing-log" -version = "0.1.3" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ - "lazy_static", "log", + "once_cell", "tracing-core", ] @@ -5040,52 +6834,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21ebb87a95ea13271332df069020513ab70bdb5637ca42d6e492dc3bbbad48de" dependencies = [ "once_cell", - "opentelemetry", + "opentelemetry 0.18.0", "tracing", "tracing-core", - "tracing-log", - "tracing-subscriber 0.3.17", -] - -[[package]] -name = "tracing-serde" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" -dependencies = [ - "serde", - "tracing-core", + "tracing-subscriber", ] [[package]] -name = "tracing-subscriber" -version = "0.2.25" +name = "tracing-opentelemetry" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71" +checksum = "97a971f6058498b5c0f1affa23e7ea202057a7301dbff68e968b2d578bcbd053" dependencies = [ - "ansi_term", - "chrono", - "lazy_static", - "matchers 0.0.1", - "regex", - "serde", - "serde_json", - "sharded-slab", + "js-sys", + "once_cell", + "opentelemetry 0.27.1", + "opentelemetry_sdk 0.27.1", "smallvec", - "thread_local", "tracing", "tracing-core", "tracing-log", - "tracing-serde", + "tracing-subscriber", + "web-time", ] [[package]] name = "tracing-subscriber" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ - "matchers 0.1.0", + "matchers", "nu-ansi-term", "once_cell", "regex", @@ -5099,21 +6878,21 @@ dependencies = [ [[package]] name = "tracing-test" -version = "0.1.0" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3b48778c2d401c6a7fcf38a0e3c55dc8e8e753cbd381044a8cdb6fd69a29f53" +checksum = "3a2c0ff408fe918a94c428a3f2ad04e4afd5c95bbc08fcf868eff750c15728a4" dependencies = [ "lazy_static", "tracing-core", - "tracing-subscriber 0.2.25", + "tracing-subscriber", "tracing-test-macro", ] [[package]] name = "tracing-test-macro" -version = "0.1.0" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c49adbab879d2e0dd7f75edace5f0ac2156939ecb7e6a1e8fa14e53728328c48" +checksum = "258bc1c4f8e2e73a977812ab339d503e6feeb92700f6d07a6de4d321522d5c08" dependencies = [ "lazy_static", "quote", @@ -5141,9 +6920,9 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.20.10" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e747b1f9b7b931ed39a548c1fae149101497de3c1fc8d9e18c62c1a66c683d3d" +checksum = "df7cc499ceadd4dcdf7ec6d4cbc34ece92c3fa07821e287aedecd4416c516dca" dependencies = [ "cc", "regex", @@ -5151,8 +6930,8 @@ dependencies = [ [[package]] name = "tree-sitter-c" -version = "0.20.6" -source = "git+https://github.com/tree-sitter/tree-sitter-c/?rev=212a80f#212a80f86452bb1316324fa0db730cf52f29e05a" +version = "0.21.3" +source = "git+https://github.com/tree-sitter/tree-sitter-c/?rev=00ed08f#00ed08f1a6c18141bfd7a81638e4d239a0bb55cc" dependencies = [ "cc", "tree-sitter", @@ -5160,9 +6939,9 @@ dependencies = [ [[package]] name = "tree-sitter-c-sharp" -version = "0.20.0" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ab3dc608f34924fa9e10533a95f62dbc14b6de0ddd7107722eba66fe19ae31" +checksum = "ff899037068a1ffbb891891b7e94db1400ddf12c3d934b85b8c9e30be5cd18da" dependencies = [ "cc", "tree-sitter", @@ -5170,8 +6949,27 @@ dependencies = [ [[package]] name = "tree-sitter-cpp" -version = "0.20.3" -source = "git+https://github.com/tree-sitter/tree-sitter-cpp?rev=a714740#a71474021410973b29bfe99440d57bcd750246b1" +version = "0.22.1" +source = "git+https://github.com/tree-sitter/tree-sitter-cpp?rev=d29fbff#d29fbff09a8c9ff4f3074de2595dfca12cb33da9" +dependencies = [ + "cc", + "tree-sitter", +] + +[[package]] +name = "tree-sitter-elixir" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94bf7f057768b1cab2ee1f14812ed4ae33f9e04d09254043eeaa797db4ef70" +dependencies = [ + "cc", + "tree-sitter", +] + +[[package]] +name = "tree-sitter-gdscript" +version = "0.0.1" +source = "git+https://github.com/faceCutWall/tree-sitter-gdscript?rev=8a8c067899d734840e8ce86fdeeeadbe8088446b#8a8c067899d734840e8ce86fdeeeadbe8088446b" dependencies = [ "cc", "tree-sitter", @@ -5179,9 +6977,9 @@ dependencies = [ [[package]] name = "tree-sitter-go" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad6d11f19441b961af2fda7f12f5d0dac325f6d6de83836a1d3750018cc5114" +checksum = "55cb318be5ccf75f44e054acf6898a5c95d59b53443eed578e16be0cd7ec037f" dependencies = [ "cc", "tree-sitter", @@ -5189,9 +6987,9 @@ dependencies = [ [[package]] name = "tree-sitter-java" -version = "0.20.2" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2adc5696bf5abf761081d7457d2bb82d0e3b28964f4214f63fd7e720ef462653" +checksum = "33bc21adf831a773c075d9d00107ab43965e6a6ea7607b47fd9ec6f3db4b481b" dependencies = [ "cc", "tree-sitter", @@ -5199,9 +6997,19 @@ dependencies = [ [[package]] name = "tree-sitter-kotlin" -version = "0.3.1" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c88dfbb22333118a5d5c5c10b19f93d115a6fa3c8a69dd0e6a260a64f9f5a79b" +dependencies = [ + "cc", + "tree-sitter", +] + +[[package]] +name = "tree-sitter-lua" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5f367466210220a194a2d8831fc12d15aa13305e7bcdf2dba47714aa328e86" +checksum = "3b9fe6fc87bd480e1943fc1fcb02453fb2da050e4e8ce0daa67d801544046856" dependencies = [ "cc", "tree-sitter", @@ -5209,9 +7017,9 @@ dependencies = [ [[package]] name = "tree-sitter-python" -version = "0.20.2" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dda114f58048f5059dcf158aff691dffb8e113e6d2b50d94263fd68711975287" +checksum = "b4066c6cf678f962f8c2c4561f205945c84834cce73d981e71392624fdc390a9" dependencies = [ "cc", "tree-sitter", @@ -5219,9 +7027,9 @@ dependencies = [ [[package]] name = "tree-sitter-ruby" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ac30cbb1560363ae76e1ccde543d6d99087421e228cc47afcec004b86bb711a" +checksum = "c0031f687c0772f2dad7b77104c43428611099a1804c81244ada21560f41f0b1" dependencies = [ "cc", "tree-sitter", @@ -5229,9 +7037,28 @@ dependencies = [ [[package]] name = "tree-sitter-rust" -version = "0.20.3" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "277690f420bf90741dea984f3da038ace46c4fe6047cba57a66822226cde1c93" +dependencies = [ + "cc", + "tree-sitter", +] + +[[package]] +name = "tree-sitter-scala" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "797842733e252dc11ae5d403a18060bf337b822fc2ae5ddfaa6ff4d9cc20bda6" +checksum = "a464d8e2e1837cf20b34204c51c369da3483e55c3ea013c6db81a04439e17895" +dependencies = [ + "cc", + "tree-sitter", +] + +[[package]] +name = "tree-sitter-solidity" +version = "1.2.6" +source = "git+https://github.com/JoranHonig/tree-sitter-solidity?rev=0e86ae647bda22c9bee00ec59752df7b3d3b000b#0e86ae647bda22c9bee00ec59752df7b3d3b000b" dependencies = [ "cc", "tree-sitter", @@ -5239,56 +7066,82 @@ dependencies = [ [[package]] name = "tree-sitter-tags" -version = "0.20.2" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccb3f1376219530a37a809751ecf65aa35fd8b9c1c4ab6d4faf5f6a9eeda2c05" +checksum = "34380416097ab36d1b4cd83f887d9e150ea4feaeb6ee9a5ecfe53d26839acc69" dependencies = [ "memchr", "regex", - "thiserror", + "thiserror 1.0.61", "tree-sitter", ] [[package]] name = "tree-sitter-typescript" -version = "0.20.3" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75049f0aafabb2aac205d7bb24da162b53dcd0cfb326785f25a2f32efa8071a" +checksum = "f07523e51e3b88529360a89038c0cca7ee877db40a40141514eece8b4cddcbb4" dependencies = [ "cc", "tree-sitter", ] +[[package]] +name = "trie-rs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5096c019d49566aff57593a06e401c7f588da84e9a575d0ed2ac0913f51928c0" +dependencies = [ + "louds-rs", +] + [[package]] name = "try-lock" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" -version = "0.20.1" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" dependencies = [ "byteorder", "bytes", "data-encoding", - "http", + "http 1.1.0", "httparse", "log", "rand 0.8.5", "sha1", - "thiserror", + "thiserror 1.0.61", "url", "utf-8", ] +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http 1.1.0", + "httparse", + "log", + "rand 0.9.1", + "sha1", + "thiserror 2.0.12", + "utf-8", +] + [[package]] name = "typenum" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "ucd-trie" @@ -5307,36 +7160,42 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.13" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" -version = "1.0.9" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-properties" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" + [[package]] name = "unicode-segmentation" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-width" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" [[package]] name = "unicode_categories" @@ -5353,6 +7212,12 @@ dependencies = [ "void", ] +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" @@ -5366,15 +7231,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", - "idna 0.5.0", + "idna", "percent-encoding", + "serde", ] [[package]] name = "url-parse" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d375da66174ba9b3697f36468fb6b9a981074537569a87ad2dc43de2a598063" +checksum = "865ece61c15cae30f180636ae551daa25c318c181938da07f3ab3ed06750bdd2" dependencies = [ "regex", ] @@ -5405,11 +7271,11 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "utoipa" -version = "3.3.0" +version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ae74ef183fae36d650f063ae7bde1cacbe1cd7e72b617cbe1e985551878b98" +checksum = "435c6f69ef38c9017b4b4eea965dfb91e71e53d869e896db40d1cf2441dd75c0" dependencies = [ - "indexmap 1.9.3", + "indexmap 2.2.6", "serde", "serde_json", "utoipa-gen", @@ -5417,48 +7283,41 @@ dependencies = [ [[package]] name = "utoipa-gen" -version = "3.3.0" +version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ea8ac818da7e746a63285594cce8a96f5e00ee31994e655bd827569cb8b137b" +checksum = "a77d306bc75294fd52f3e99b13ece67c02c1a2789190a6f31d32f736624326f7" dependencies = [ - "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.32", + "regex", + "syn 2.0.100", ] [[package]] name = "utoipa-swagger-ui" -version = "3.1.3" +version = "9.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "062bba5a3568e126ac72049a63254f4cb1da2eb713db0c1ab2a4c76be191db8c" +checksum = "d29519b3c485df6b13f4478ac909a491387e9ef70204487c3b64b53749aec0be" dependencies = [ - "axum", + "axum 0.8.3", + "base64 0.22.1", "mime_guess", "regex", - "rust-embed 6.6.1", + "rust-embed", "serde", "serde_json", + "url", "utoipa", "zip", ] [[package]] name = "uuid" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" -dependencies = [ - "getrandom 0.2.11", -] - -[[package]] -name = "uuid" -version = "1.6.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" dependencies = [ - "getrandom 0.2.11", + "getrandom 0.2.15", "rand 0.8.5", "serde", "uuid-macro-internal", @@ -5466,23 +7325,23 @@ dependencies = [ [[package]] name = "uuid-macro-internal" -version = "1.6.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f49e7f3f3db8040a100710a11932239fd30697115e2ba4107080d8252939845e" +checksum = "9881bea7cbe687e36c9ab3b778c36cd0487402e270304e8b1296d5085303c1a2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.100", ] [[package]] name = "validator" -version = "0.16.1" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b92f40481c04ff1f4f61f304d61793c7b56ff76ac1469f1beb199b1445b253bd" +checksum = "db79c75af171630a3148bd3e6d7c4f42b6a9a014c2945bc5ed0020cbb8d9478e" dependencies = [ - "idna 0.4.0", - "lazy_static", + "idna", + "once_cell", "regex", "serde", "serde_derive", @@ -5493,28 +7352,16 @@ dependencies = [ [[package]] name = "validator_derive" -version = "0.16.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc44ca3088bb3ba384d9aecf40c6a23a676ce23e09bdaca2073d99c207f864af" +checksum = "55591299b7007f551ed1eb79a684af7672c19c3193fb9e0a31936987bb2438ec" dependencies = [ - "if_chain", - "lazy_static", + "darling 0.20.9", + "once_cell", "proc-macro-error", "proc-macro2", "quote", - "regex", - "syn 1.0.109", - "validator_types", -] - -[[package]] -name = "validator_types" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "111abfe30072511849c5910134e8baf8dc05de4c0e5903d681cbd5c9c4d611e3" -dependencies = [ - "proc-macro2", - "syn 1.0.109", + "syn 2.0.100", ] [[package]] @@ -5523,6 +7370,18 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "value-trait" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad8db98c1e677797df21ba03fca7d3bf9bec3ca38db930954e4fe6e1ea27eb4" +dependencies = [ + "float-cmp", + "halfbrown", + "itoa 1.0.11", + "ryu", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -5531,11 +7390,12 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vergen" -version = "8.2.4" +version = "8.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbc5ad0d9d26b2c49a5ab7da76c3e79d3ee37e7821799f8223fcb8f2f391a2e7" +checksum = "e27d6bdd219887a9eadd19e1c34f32e47fa332301184935c6d9bca26f3cca525" dependencies = [ "anyhow", + "cfg-if", "rustversion", "time", ] @@ -5554,9 +7414,9 @@ checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" [[package]] name = "walkdir" -version = "2.3.3" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", @@ -5564,11 +7424,10 @@ dependencies = [ [[package]] name = "want" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ - "log", "try-lock", ] @@ -5584,11 +7443,26 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -5596,24 +7470,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.100", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.37" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ "cfg-if", "js-sys", @@ -5623,9 +7497,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5633,28 +7507,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.100", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "wasm-streams" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4609d447824375f43e1ffbc051b50ad8f4b3ae8219680c94452ea05eb240ac7" +checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" dependencies = [ "futures-util", "js-sys", @@ -5665,30 +7539,65 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.64" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ "js-sys", + "serde", "wasm-bindgen", ] +[[package]] +name = "webpki" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" +dependencies = [ + "ring 0.17.8", + "untrusted 0.9.0", +] + +[[package]] +name = "webpki-roots" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "which" -version = "4.4.0" +version = "6.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +checksum = "8211e4f58a2b2805adfbefbc07bab82958fc91e3836339b1ab7ae32465dce0d7" dependencies = [ "either", - "libc", - "once_cell", + "home", + "rustix", + "winsafe", ] [[package]] name = "whoami" -version = "1.4.1" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" +checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" +dependencies = [ + "redox_syscall 0.4.1", + "wasite", +] [[package]] name = "winapi" @@ -5708,11 +7617,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -5727,31 +7636,79 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets 0.48.0", + "windows-targets 0.48.5", ] [[package]] -name = "windows-sys" -version = "0.42.0" +name = "windows" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", + "windows-core 0.52.0", + "windows-targets 0.52.5", ] [[package]] -name = "windows-sys" -version = "0.45.0" +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "windows-result" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" dependencies = [ - "windows-targets 0.42.2", + "windows-targets 0.52.5", ] [[package]] @@ -5760,227 +7717,273 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.0", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.5", ] [[package]] name = "windows-targets" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" -version = "0.48.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" -version = "0.4.6" +version = "0.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" dependencies = [ "memchr", ] [[package]] name = "winreg" -version = "0.50.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" dependencies = [ "cfg-if", "windows-sys 0.48.0", ] +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.6.0", +] + [[package]] name = "wrapcenum-derive" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bcc065c85ad2c3bd12aa4118bf164835712e25080c392557801a13292c60aec" +checksum = "a76ff259533532054cfbaefb115c613203c73707017459206380f03b3b3f266e" dependencies = [ - "darling 0.10.2", + "darling 0.20.9", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.100", ] [[package]] -name = "yaml-rust" -version = "0.4.5" +name = "xml5ever" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +checksum = "7c376f76ed09df711203e20c3ef5ce556f0166fa03d39590016c0fd625437fad" dependencies = [ - "linked-hash-map", + "log", + "mac", + "markup5ever 0.12.1", ] [[package]] name = "zerocopy" -version = "0.7.32" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.32" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.100", ] [[package]] name = "zeroize" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" [[package]] name = "zip" -version = "0.6.6" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +checksum = "1dcb24d0152526ae49b9b96c1dcf71850ca1e0b882e4e28ed898a93c41334744" dependencies = [ - "byteorder", + "arbitrary", "crc32fast", "crossbeam-utils", "flate2", + "indexmap 2.2.6", + "memchr", + "zopfli", +] + +[[package]] +name = "zopfli" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", ] [[package]] name = "zstd" -version = "0.12.4" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a27595e173641171fc74a1232b7b1c7a7cb6e18222c11e9dfb9888fa424c53c" +checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" -version = "6.0.6" +version = "7.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee98ffd0b48ee95e6c5168188e44a54550b1564d9d530ee21d5f0eaed1069581" +checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a" dependencies = [ - "libc", "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.8+zstd.1.5.5" +version = "2.0.10+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" +checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa" dependencies = [ "cc", - "libc", "pkg-config", ] diff --git a/Cargo.toml b/Cargo.toml index a618d5a59696..77e784e2a6b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,46 +3,94 @@ resolver = "1" members = [ "crates/tabby", "crates/tabby-common", - "crates/tabby-scheduler", "crates/tabby-download", + "crates/tabby-git", "crates/tabby-inference", - "crates/llama-cpp-bindings", - "crates/http-api-bindings", + "crates/tabby-index", + "crates/tabby-crawler", + "crates/aim-downloader", - "crates/juniper-axum", + "crates/http-api-bindings", + "crates/llama-cpp-server", + "crates/ollama-api-bindings", + "crates/tabby-index-cli", + "crates/hash-ids", + "crates/sqlx-migrate-validate", + "ee/tabby-webserver", "ee/tabby-db", + "ee/tabby-db-macros", + "ee/tabby-schema", ] [workspace.package] -version = "0.8.0" +version = "0.33.0-dev.0" edition = "2021" -authors = ["Meng Zhang"] +authors = ["TabbyML Team"] homepage = "https://github.com/TabbyML/tabby" [workspace.dependencies] +cached = "0.49.3" lazy_static = "1.4.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1" serdeconv = "0.4.1" tokio = "1.28" +tokio-retry = "0.3.0" +tokio-util = { version = "0.7.10", features = ["full"] } tracing = "0.1" +tokio-cron-scheduler = "0.9.4" tracing-subscriber = "0.3" anyhow = "1.0.71" -serde-jsonlines = "0.4.0" -tantivy = "0.21.0" +tantivy = { git = "https://github.com/quickwit-oss/tantivy", rev = "4143d31" } async-trait = "0.1.72" -reqwest = { version = "0.11.18" } -derive_builder = "0.12.0" -futures = "0.3.28" +reqwest = { version = "0.12" } +derive_builder = "0.20" +futures = "0.3.30" async-stream = "0.3.5" regex = "1.10.0" +strum = { version = "0.24", features = ["derive"] } thiserror = "1.0.49" -utoipa = "3.3" -axum = "0.6" -hyper = "0.14" -juniper = "0.15" +utoipa = "5.3" +axum = "0.8" +axum-extra = "0.10" +hyper = "1.1" +humantime = "2.2.0" +juniper = "0.16" chrono = "0.4" +reqwest-eventsource = "0.6" +serial_test = { version = "3.0.0", features = ["file_locks"] } +hash-ids = { path = "./crates/hash-ids" } +ignore = "0.4.20" +nucleo = "0.5.0" +url = "2.5.0" +temp_testdir = "0.2" +git2 = "0.18.3" +tower-http = "0.5" +tower = { version = "0.5", features = ["util"] } +mime_guess = "2.0.4" +assert_matches = "1.5" +insta = "1.34.0" +logkit = "0.3" +ldap3 = "0.11.0" +async-openai-alt = "0.26.2" +tracing-test = "0.2" +clap = "4.3.0" +ratelimit = "0.10" +tracing-opentelemetry = "0.28.0" +opentelemetry = { version = "0.27.0", features = ["trace", "metrics"] } +opentelemetry_sdk = { version = "0.27.0", default-features = false, features = [ + "trace", + "rt-tokio", +] } +opentelemetry-otlp = { version = "0.27.0" } +opentelemetry-semantic-conventions = { version = "0.27.0", features = [ + "semconv_experimental", +] } +# https://github.com/launchbadge/sqlx/issues/3241#issuecomment-2260797292 +# https://github.com/wsxiaoys/sqlx/tree/fix-0-7-3-remove-date-time-encoding +sqlx = { git = "https://github.com/wsxiaoys/sqlx", rev = "77eb94d" } +validator = { version = "0.18.1", features = ["derive"] } [workspace.dependencies.uuid] version = "1.3.3" diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 9ca69a1fab67..000000000000 --- a/Dockerfile +++ /dev/null @@ -1,76 +0,0 @@ -ARG UBUNTU_VERSION=22.04 -# This needs to generally match the container host's environment. -ARG CUDA_VERSION=11.7.1 -# Target the CUDA build image -ARG BASE_CUDA_DEV_CONTAINER=nvidia/cuda:${CUDA_VERSION}-devel-ubuntu${UBUNTU_VERSION} -# Target the CUDA runtime image -ARG BASE_CUDA_RUN_CONTAINER=nvidia/cuda:${CUDA_VERSION}-runtime-ubuntu${UBUNTU_VERSION} - -FROM ${BASE_CUDA_DEV_CONTAINER} as build - -# Rust toolchain version -ARG RUST_TOOLCHAIN=stable - -ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update && \ - apt-get install -y --no-install-recommends \ - curl \ - pkg-config \ - libssl-dev \ - protobuf-compiler \ - git \ - cmake \ - && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/* - -# setup rust. -RUN curl https://sh.rustup.rs -sSf | bash -s -- --default-toolchain ${RUST_TOOLCHAIN} -y -ENV PATH="/root/.cargo/bin:${PATH}" - -WORKDIR /root/workspace - -RUN mkdir -p /opt/tabby/bin -RUN mkdir -p /opt/tabby/lib -RUN mkdir -p target - -COPY . . - -RUN --mount=type=cache,target=/usr/local/cargo/registry \ - --mount=type=cache,target=/root/workspace/target \ - cargo build --features cuda,prod-db --release --package tabby && \ - cp target/release/tabby /opt/tabby/bin/ - -FROM ${BASE_CUDA_RUN_CONTAINER} as runtime - -RUN apt-get update && \ - apt-get install -y --no-install-recommends \ - git \ - openssh-client \ - ca-certificates \ - && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/* - -# Disable safe directory in docker -# Context: https://github.com/git/git/commit/8959555cee7ec045958f9b6dd62e541affb7e7d9 -RUN git config --system --add safe.directory "*" - -# Automatic platform ARGs in the global scope -# https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope -ARG TARGETARCH - -# AMD64 only: -# Make link to libnvidia-ml.so (NVML) library -# so that we could get GPU stats. -RUN if [ "$TARGETARCH" = "amd64" ]; then \ - ln -s /usr/lib/x86_64-linux-gnu/libnvidia-ml.so.1 \ - /usr/lib/x86_64-linux-gnu/libnvidia-ml.so; \ - fi - -COPY --from=build /opt/tabby /opt/tabby - -ENV PATH="$PATH:/opt/tabby/bin" -ENV TABBY_ROOT=/data - -ENTRYPOINT ["/opt/tabby/bin/tabby"] diff --git a/LICENSE b/LICENSE index dc1f4fa76943..fe1321487adb 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2024 TabbyML, Inc. +Copyright (c) 2025 TabbyML, Inc. Portions of this software are licensed as follows: diff --git a/MODEL_SPEC.md b/MODEL_SPEC.md index d214d61e2365..60cec8794b80 100644 --- a/MODEL_SPEC.md +++ b/MODEL_SPEC.md @@ -1,12 +1,12 @@ -# Tabby Model Specification (Unstable) +# Tabby Model Specification -Tabby organizes the model within a directory. This document provides an explanation of the necessary contents for supporting model serving. An example model directory can be found at https://huggingface.co/TabbyML/StarCoder-1B - -The minimal Tabby model directory should include the following contents: +Tabby organizes the models within a directory. +This document provides an explanation of the necessary contents for supporting model serving. +A minimal Tabby model directory should include the following contents: ``` -ggml/ tabby.json +ggml/model-00001-of-00001.gguf ``` ### tabby.json @@ -28,6 +28,11 @@ The **chat_template** field is optional. When it is present, it is assumed that ### ggml/ -This directory contains binary files used by the [llama.cpp](https://github.com/ggerganov/llama.cpp) inference engine. Tabby utilizes ggml for inference on `cpu`, `cuda` and `metal` devices. +This directory contains binary files used by the [llama.cpp](https://github.com/ggml-org/llama.cpp) inference engine. +Tabby utilizes GGML for inference on `cpu`, `cuda` and `metal` devices. + +Tabby saves GGUF model files in the format `model-{index}-of-{count}.gguf`, following the llama.cpp naming convention. +Please note that the index is 1-based, +by default, Tabby names a single file model as `model-00001-of-00001.gguf`. -Currently, only `q8_0.v2.gguf` in this directory is in use. You can refer to the instructions in llama.cpp to learn how to acquire it. +For more details about GGUF models, please refer to the instructions in llama.cpp. diff --git a/Makefile b/Makefile index b8e207ac2bc3..aa80b518900b 100644 --- a/Makefile +++ b/Makefile @@ -1,33 +1,45 @@ fix: cargo machete --fix || true cargo +nightly fmt - cargo +nightly clippy --fix --allow-dirty --allow-staged + cargo clippy --fix --allow-dirty --allow-staged fix-ui: - cd ee/tabby-ui && yarn format:write && yarn lint:fix + pnpm lint:fix update-ui: - cd ee/tabby-ui && yarn build + pnpm build rm -rf ee/tabby-webserver/ui && cp -R ee/tabby-ui/out ee/tabby-webserver/ui + rm -rf ee/tabby-webserver/email_templates && cp -R ee/tabby-email/out ee/tabby-webserver/email_templates +update-db-schema: + sqlite3 ee/tabby-db/schema.sqlite ".schema --indent" > ee/tabby-db/schema/schema.sql + sqlite3 ee/tabby-db/schema.sqlite -init ee/tabby-db/schema/sqlite-schema-visualize.sql "" > schema.dot + dot -Tsvg schema.dot > ee/tabby-db/schema/schema.svg + rm schema.dot + +dev: + tmuxinator start -p .tmuxinator/tabby.yml + bump-version: - cargo ws version --no-git-tag --force "*" + cargo ws version --force "*" --no-individual-tags --allow-branch "main" bump-release-version: cargo ws version --allow-branch "r*" --no-individual-tags --force "*" update-openapi-doc: - curl http://localhost:8080/api-docs/openapi.json | jq ' \ - delpaths([ \ - ["paths", "/v1beta/chat/completions"], \ - ["paths", "/v1beta/search"], \ - ["components", "schemas", "CompletionRequest", "properties", "prompt"], \ - ["components", "schemas", "CompletionRequest", "properties", "debug_options"], \ - ["components", "schemas", "CompletionResponse", "properties", "debug_data"], \ - ["components", "schemas", "DebugData"], \ - ["components", "schemas", "DebugOptions"] \ - ])' | jq '.servers[0] |= { url: "https://playground.app.tabbyml.com", description: "Playground server" }' \ + curl http://localhost:8080/api-docs/openapi.json | jq ' \ + delpaths([ \ + ["paths", "/v1beta/chat/completions"], \ + ["paths", "/v1beta/search"], \ + ["paths", "/v1beta/server_setting"], \ + ["components", "schemas", "CompletionRequest", "properties", "prompt"], \ + ["components", "schemas", "CompletionRequest", "properties", "debug_options"], \ + ["components", "schemas", "CompletionResponse", "properties", "debug_data"], \ + ["components", "schemas", "DebugData"], \ + ["components", "schemas", "DebugOptions"], \ + ["components", "schemas", "ServerSetting"] \ + ])' | jq '.servers[0] |= { url: "https://playground.app.tabbyml.com", description: "Playground server" }' \ > website/static/openapi.json update-graphql-schema: - cargo run --package tabby-webserver --example update-schema + cargo run --package tabby-schema --example update-schema --features=schema-language diff --git a/README-ja.md b/README-ja.md new file mode 100644 index 000000000000..232ba464bc1c --- /dev/null +++ b/README-ja.md @@ -0,0 +1,134 @@ +
+ +# 🐾 Tabby + +[📚 ドキュメント](https://tabby.tabbyml.com/docs/welcome/) • [💬 Slack](https://links.tabbyml.com/join-slack) • [🗺️ ロードマップ](https://tabby.tabbyml.com/docs/roadmap/) + +[![最新リリース](https://shields.io/github/v/release/TabbyML/tabby)](https://github.com/TabbyML/tabby/releases/latest) +[![PR歓迎](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://makeapullrequest.com) +[![Docker pulls](https://img.shields.io/docker/pulls/tabbyml/tabby)](https://hub.docker.com/r/tabbyml/tabby) +[![codecov](https://codecov.io/gh/TabbyML/tabby/graph/badge.svg?token=WYVVH8MKK3)](https://codecov.io/gh/TabbyML/tabby) + +[English](/README.md) | +[简体中文](/README-zh.md) | +[日本語](/README-ja.md) + +
+ +Tabbyは、GitHub Copilotのオープンソースでオンプレミスな代替手段を提供する、セルフホスト型AIコーディングアシスタントです。いくつかの主要な特徴を備えています: +* DBMSやクラウドサービスが不要な自己完結型。 +* OpenAPIインターフェースにより、既存のインフラストラクチャ(例:クラウドIDE)との統合が容易。 +* コンシューマーグレードのGPUをサポート。 + +

+ ライブデモを開く +

+ +

+ デモ +

+ +## 🔥 新着情報 +* **2025/03/31** チャットサイドパネルにより豊富な`@`メニューを備えた[v0.27](https://github.com/TabbyML/tabby/releases/tag/v0.27.0)がリリースされました。 +* **2025/02/05** LDAP認証とバックグラウンドジョブのより良い通知がTabby[v0.24.0](https://github.com/TabbyML/tabby/releases/tag/v0.24.0)に登場!✨ +* **2025/02/04** [VSCode 1.20.0](https://marketplace.visualstudio.com/items/TabbyML.vscode-tabby/changelog)アップグレード!ファイルを@メンションしてチャットコンテキストに追加し、新しい右クリックオプションでインライン編集が可能に! +* **2025/01/10** Tabby[v0.23.0](https://github.com/TabbyML/tabby/releases/tag/v0.23.0)は、強化されたコードブラウザ体験とチャットサイドパネルの改善を特徴としています! + +
+ アーカイブ +* **2024/12/24** Tabby[v0.22.0](https://github.com/TabbyML/tabby/releases/tag/v0.22.0)に**通知ボックス**を導入! +* **2024/12/06** Llamafileデプロイメント統合と強化されたアンサーエンジンユーザー体験がTabby[v0.21.0](https://github.com/TabbyML/tabby/releases/tag/v0.21.0)に登場!🚀 +* **2024/11/10** 異なるバックエンドチャットモデル間の切り替えがTabby[v0.20.0](https://github.com/TabbyML/tabby/releases/tag/v0.20.0)のアンサーエンジンでサポートされました! +* **2024/10/30** Tabby[v0.19.0](https://github.com/TabbyML/tabby/releases/tag/v0.19.0)は、メインページに最近共有されたスレッドを表示し、その発見性を向上させます。 +* **2024/07/09** 🎉[TabbyでのCodestral統合](https://tabby.tabbyml.com/blog/2024/07/09/tabby-codestral/)を発表! +* **2024/07/05** Tabby[v0.13.0](https://github.com/TabbyML/tabby/releases/tag/v0.13.0)は、内部エンジニアリングチームのための中央知識エンジンである***アンサーエンジン***を導入します。開発チームの内部データとシームレスに統合し、開発者に信頼性の高い正確な回答を提供します。 +* **2024/06/13** [VSCode 1.7](https://marketplace.visualstudio.com/items/TabbyML.vscode-tabby/changelog)は、コーディング体験全体を通じて多用途なチャット体験を提供する重要なマイルストーンです。最新の**サイドパネルでのチャット**と**チャットコマンドによる編集**をお試しください! +* **2024/06/10** 最新の📃ブログ投稿がTabbyでの[強化されたコードコンテキスト理解](https://tabby.tabbyml.com/blog/2024/06/11/rank-fusion-in-tabby-code-completion/)について公開されました! +* **2024/06/06** Tabby[v0.12.0](https://github.com/TabbyML/tabby/releases/tag/v0.12.0)リリースは、🔗**シームレスな統合**(Gitlab SSO、セルフホストGitHub/GitLabなど)、⚙️**柔軟な設定**(HTTP API統合)、🌐**拡張された機能**(コードブラウザでのリポジトリコンテキスト)をもたらします。 +* **2024/05/22** Tabby[VSCode 1.6](https://marketplace.visualstudio.com/items?itemName=TabbyML.vscode-tabby)は、インライン補完での**複数の選択肢**と、**自動生成されたコミットメッセージ**🐱💻を提供します! +* **2024/05/11** [v0.11.0](https://github.com/TabbyML/tabby/releases/tag/v0.11.0)は、📊**ストレージ使用量**統計、🔗**GitHub & GitLab**統合、📋**アクティビティ**ページ、待望の🤖**Ask Tabby**機能を含む重要なエンタープライズアップグレードをもたらします! +* **2024/04/22** [v0.10.0](https://github.com/TabbyML/tabby/releases/tag/v0.10.0)がリリースされ、チームごとの分析を提供する最新の**レポート**タブを特徴としています。 +* **2024/04/19** 📣 Tabbyは、コード補完のために[ローカルに関連するスニペット](https://github.com/TabbyML/tabby/pull/1844)(ローカルLSPからの宣言や最近変更されたコード)を組み込むようになりました! +* **2024/04/17** CodeGemmaとCodeQwenモデルシリーズが[公式レジストリ](https://tabby.tabbyml.com/docs/models/)に追加されました! +* **2024/03/20** [v0.9](https://github.com/TabbyML/tabby/releases/tag/v0.9.1)がリリースされ、フル機能の管理UIを強調しています。 +* **2023/12/23** [SkyServe](https://skypilot.readthedocs.io/en/latest/serving/sky-serve.html) 🛫を使用して、[任意のクラウドでTabbyをシームレスにデプロイ](https://tabby.tabbyml.com/docs/installation/skypilot/)します。 +* **2023/12/15** [v0.7.0](https://github.com/TabbyML/tabby/releases/tag/v0.7.0)がリリースされ、チーム管理と安全なアクセスを提供します! +* **2023/10/15** RAGベースのコード補完が[v0.3.0](https://github.com/TabbyML/tabby/releases/tag/v0.3.0)で詳細に有効化されました🎉!Tabbyがリポジトリレベルのコンテキストを利用してさらにスマートになる方法を説明する[ブログ投稿](https://tabby.tabbyml.com/blog/2023/10/16/repository-context-for-code-completion/)をチェックしてください! +* **2023/11/27** [v0.6.0](https://github.com/TabbyML/tabby/releases/tag/v0.6.0)がリリースされました! +* **2023/11/09** [v0.5.5](https://github.com/TabbyML/tabby/releases/tag/v0.5.5)がリリースされました!UIの再設計とパフォーマンスの向上を伴います。 +* **2023/10/24** ⛳️ [VSCode/Vim/IntelliJ](https://tabby.tabbyml.com/docs/extensions)向けのTabby IDEプラグインの主要なアップデート! +* **2023/10/04** Tabbyがサポートする最新のモデルを確認するには、[モデルディレクトリ](https://tabby.tabbyml.com/docs/models/)をチェックしてください。 +* **2023/09/18** AppleのM1/M2 Metal推論サポートが[v0.1.1](https://github.com/TabbyML/tabby/releases/tag/v0.1.1)に登場しました! +* **2023/08/31** Tabbyの最初の安定版リリース[v0.0.1](https://github.com/TabbyML/tabby/releases/tag/v0.0.1) 🥳。 +* **2023/08/28** [CodeLlama 7B](https://github.com/TabbyML/tabby/issues/370)の実験的サポート。 +* **2023/08/24** Tabbyが[JetBrains Marketplace](https://plugins.jetbrains.com/plugin/22379-tabby)に登場! + +
+ +## 👋 はじめに + +ドキュメントは[こちら](https://tabby.tabbyml.com/docs/getting-started)でご覧いただけます。 +- 📚 [インストール](https://tabby.tabbyml.com/docs/installation/) +- 💻 [IDE/エディタ拡張](https://tabby.tabbyml.com/docs/extensions/) +- ⚙️ [設定](https://tabby.tabbyml.com/docs/configuration) + +### 1分でTabbyを実行 +Tabbyサーバーを開始する最も簡単な方法は、次のDockerコマンドを使用することです: + +```bash +docker run -it \ + --gpus all -p 8080:8080 -v $HOME/.tabby:/data \ + tabbyml/tabby \ + serve --model StarCoder-1B --device cuda --chat-model Qwen2-1.5B-Instruct +``` +追加のオプション(例:推論タイプ、並列処理)については、[ドキュメントページ](https://tabbyml.github.io/tabby)を参照してください。 + +## 🤝 コントリビューション + +詳細なガイドは[CONTRIBUTING.md](https://github.com/TabbyML/tabby/blob/main/CONTRIBUTING.md)をご覧ください。 + +### コードを取得 + +```bash +git clone --recurse-submodules https://github.com/TabbyML/tabby +cd tabby +``` + +すでにリポジトリをクローンしている場合は、`git submodule update --recursive --init`コマンドを実行してすべてのサブモジュールを取得できます。 + +### ビルド + +1. この[チュートリアル](https://www.rust-lang.org/learn/get-started)に従ってRust環境をセットアップします。 + +2. 必要な依存関係をインストールします: +```bash +# MacOSの場合 +brew install protobuf + +# Ubuntu / Debianの場合 +apt install protobuf-compiler libopenblas-dev +``` + +3. 便利なツールをインストールします: +```bash +# Ubuntuの場合 +apt install make sqlite3 graphviz +``` + +4. これで、`cargo build`コマンドを実行してTabbyをビルドできます。 + +### ハッキングを始めよう! +... そして、[プルリクエスト](https://github.com/TabbyML/tabby/compare)を提出するのを忘れないでください。 + +## 🌍 コミュニティ +- 🎤 [Twitter / X](https://twitter.com/Tabby_ML) - TabbyMLとあらゆる可能性について交流 +- 📚 [LinkedIn](https://www.linkedin.com/company/tabbyml/) - コミュニティからの最新情報をフォロー +- 💌 [ニュースレター](https://newsletter.tabbyml.com/archive) - Tabbyの洞察と秘密を解き明かすために購読 + +### 🔆 アクティビティ + +![Gitリポジトリアクティビティ](https://repobeats.axiom.co/api/embed/e4ef0fbd12e586ef9ea7d72d1fb4f5c5b88d78d5.svg "Repobeats分析画像") + +### 🌟 スター履歴 + +[![スター履歴チャート](https://api.star-history.com/svg?repos=tabbyml/tabby&type=Date)](https://star-history.com/#tabbyml/tabby&Date) diff --git a/README-zh.md b/README-zh.md new file mode 100644 index 000000000000..0be95f54d630 --- /dev/null +++ b/README-zh.md @@ -0,0 +1,134 @@ +
+ +# 🐾 Tabby + +[📚 文档](https://tabby.tabbyml.com/docs/welcome/) • [💬 Slack](https://links.tabbyml.com/join-slack) • [🗺️ 路线图](https://tabby.tabbyml.com/docs/roadmap/) + +[![最新版本](https://shields.io/github/v/release/TabbyML/tabby)](https://github.com/TabbyML/tabby/releases/latest) +[![欢迎 PR](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://makeapullrequest.com) +[![Docker 下载量](https://img.shields.io/docker/pulls/tabbyml/tabby)](https://hub.docker.com/r/tabbyml/tabby) +[![代码覆盖率](https://codecov.io/gh/TabbyML/tabby/graph/badge.svg?token=WYVVH8MKK3)](https://codecov.io/gh/TabbyML/tabby) + +[English](/README.md) | +[简体中文](/README-zh.md) | +[日本語](/README-ja.md) + +
+ +Tabby 是一个自托管的 AI 编码助手,提供 GitHub Copilot 的开源和本地替代方案。它具有以下几个关键特性: +* 独立运行,无需 DBMS 或云服务。 +* OpenAPI 接口,易于与现有基础设施集成(例如云 IDE)。 +* 支持消费级 GPU。 + +

+ 打开在线演示 +

+ +

+ 演示 +

+ +## 🔥 最新动态 +* **2025/03/31** [v0.27](https://github.com/TabbyML/tabby/releases/tag/v0.27.0) 发布,聊天侧边栏中新增更丰富的 `@` 菜单。 +* **2025/02/05** Tabby [v0.24.0](https://github.com/TabbyML/tabby/releases/tag/v0.24.0) 中引入 LDAP 认证和更好的后台作业通知!✨ +* **2025/02/04** [VSCode 1.20.0](https://marketplace.visualstudio.com/items/TabbyML.vscode-tabby/changelog) 升级!可以通过 @ 提及文件将其添加为聊天上下文,并通过新的右键选项进行内联编辑! +* **2025/01/10** Tabby [v0.23.0](https://github.com/TabbyML/tabby/releases/tag/v0.23.0) 提供增强的代码浏览体验和聊天侧边栏改进! + +
+ 存档 +* **2024/12/24** 在 Tabby [v0.22.0](https://github.com/TabbyML/tabby/releases/tag/v0.22.0) 中引入 **通知框**! +* **2024/12/06** Tabby [v0.21.0](https://github.com/TabbyML/tabby/releases/tag/v0.21.0) 中即将推出 Llamafile 部署集成和增强的答案引擎用户体验!🚀 +* **2024/11/10** 在 Tabby [v0.20.0](https://github.com/TabbyML/tabby/releases/tag/v0.20.0) 中,答案引擎支持在不同的后端聊天模型之间切换! +* **2024/10/30** Tabby [v0.19.0](https://github.com/TabbyML/tabby/releases/tag/v0.19.0) 在主页上展示最近共享的线程,以提高其可发现性。 +* **2024/07/09** 🎉宣布 [Tabby 中的 Codestral 集成](https://tabby.tabbyml.com/blog/2024/07/09/tabby-codestral/)! +* **2024/07/05** Tabby [v0.13.0](https://github.com/TabbyML/tabby/releases/tag/v0.13.0) 引入了 ***答案引擎***,这是一个面向内部工程团队的中央知识引擎。它与开发团队的内部数据无缝集成,提供可靠和精确的答案以增强开发人员的能力。 +* **2024/06/13** [VSCode 1.7](https://marketplace.visualstudio.com/items/TabbyML.vscode-tabby/changelog) 标志着一个重要的里程碑,提供了贯穿整个编码体验的多功能聊天体验。来试试最新的 **侧边栏聊天** 和 **通过聊天命令编辑**! +* **2024/06/10** 最新 📃博客文章发布,关于 Tabby 中 [增强的代码上下文理解](https://tabby.tabbyml.com/blog/2024/06/11/rank-fusion-in-tabby-code-completion/)! +* **2024/06/06** Tabby [v0.12.0](https://github.com/TabbyML/tabby/releases/tag/v0.12.0) 发布,带来 🔗**无缝集成**(Gitlab SSO,自托管 GitHub/GitLab 等),到 ⚙️**灵活配置**(HTTP API 集成)和 🌐**扩展功能**(代码浏览器中的仓库上下文)! +* **2024/05/22** Tabby [VSCode 1.6](https://marketplace.visualstudio.com/items?itemName=TabbyML.vscode-tabby) 提供 **多种选择** 的内联补全和 **自动生成的提交信息**🐱💻! +* **2024/05/11** [v0.11.0](https://github.com/TabbyML/tabby/releases/tag/v0.11.0) 带来了重要的企业升级,包括 📊**存储使用**统计,🔗**GitHub & GitLab** 集成,📋**活动**页面,以及期待已久的 🤖**询问 Tabby** 功能! +* **2024/04/22** [v0.10.0](https://github.com/TabbyML/tabby/releases/tag/v0.10.0) 发布,推出最新的 **报告** 标签,提供团队使用 Tabby 的分析。 +* **2024/04/19** 📣 Tabby 现在结合了 [本地相关代码片段](https://github.com/TabbyML/tabby/pull/1844)(来自本地 LSP 的声明和最近修改的代码)用于代码补全! +* **2024/04/17** CodeGemma 和 CodeQwen 模型系列现已添加到 [官方注册表](https://tabby.tabbyml.com/docs/models/)! +* **2024/03/20** [v0.9](https://github.com/TabbyML/tabby/releases/tag/v0.9.1) 发布,重点推出完整功能的管理员 UI。 +* **2023/12/23** 通过 [SkyServe](https://skypilot.readthedocs.io/en/latest/serving/sky-serve.html) 🛫 从 SkyPilot 无缝 [在任何云上部署 Tabby](https://tabby.tabbyml.com/docs/installation/skypilot/)。 +* **2023/12/15** [v0.7.0](https://github.com/TabbyML/tabby/releases/tag/v0.7.0) 发布,带有团队管理和安全访问! +* **2023/10/15** 在 [v0.3.0](https://github.com/TabbyML/tabby/releases/tag/v0.3.0) 中启用了基于 RAG 的代码补全🎉!查看 [博客文章](https://tabby.tabbyml.com/blog/2023/10/16/repository-context-for-code-completion/) 了解 Tabby 如何利用仓库级上下文变得更智能! +* **2023/11/27** [v0.6.0](https://github.com/TabbyML/tabby/releases/tag/v0.6.0) 发布! +* **2023/11/09** [v0.5.5](https://github.com/TabbyML/tabby/releases/tag/v0.5.5) 发布!UI 重新设计 + 性能改进。 +* **2023/10/24** ⛳️ Tabby IDE 插件的重大更新,适用于 [VSCode/Vim/IntelliJ](https://tabby.tabbyml.com/docs/extensions)! +* **2023/10/04** 查看 [模型目录](https://tabby.tabbyml.com/docs/models/) 了解 Tabby 支持的最新模型。 +* **2023/09/18** 苹果 M1/M2 Metal 推理支持已在 [v0.1.1](https://github.com/TabbyML/tabby/releases/tag/v0.1.1) 中推出! +* **2023/08/31** Tabby 的第一个稳定版本 [v0.0.1](https://github.com/TabbyML/tabby/releases/tag/v0.0.1) 🥳。 +* **2023/08/28** 对 [CodeLlama 7B](https://github.com/TabbyML/tabby/issues/370) 的实验性支持。 +* **2023/08/24** Tabby 现已在 [JetBrains Marketplace](https://plugins.jetbrains.com/plugin/22379-tabby) 上架! + +
+ +## 👋 入门指南 + +您可以在[这里](https://tabby.tabbyml.com/docs/getting-started)找到我们的文档。 +- 📚 [安装](https://tabby.tabbyml.com/docs/installation/) +- 💻 [IDE/编辑器扩展](https://tabby.tabbyml.com/docs/extensions/) +- ⚙️ [配置](https://tabby.tabbyml.com/docs/configuration) + +### 1 分钟运行 Tabby +启动 Tabby 服务器的最简单方法是使用以下 Docker 命令: + +```bash +docker run -it \ + --gpus all -p 8080:8080 -v $HOME/.tabby:/data \ + tabbyml/tabby \ + serve --model StarCoder-1B --device cuda --chat-model Qwen2-1.5B-Instruct +``` +有关其他选项(例如推理类型、并行性),请参阅[文档页面](https://tabbyml.github.io/tabby)。 + +## 🤝 贡献 + +完整指南请参见 [CONTRIBUTING.md](https://github.com/TabbyML/tabby/blob/main/CONTRIBUTING.md); + +### 获取代码 + +```bash +git clone --recurse-submodules https://github.com/TabbyML/tabby +cd tabby +``` + +如果您已经克隆了仓库,可以运行 `git submodule update --recursive --init` 命令来获取所有子模块。 + +### 构建 + +1. 按照此 [教程](https://www.rust-lang.org/learn/get-started) 设置 Rust 环境。 + +2. 安装所需的依赖项: +```bash +# 对于 MacOS +brew install protobuf + +# 对于 Ubuntu / Debian +apt install protobuf-compiler libopenblas-dev +``` + +3. 安装有用的工具: +```bash +# 对于 Ubuntu +apt install make sqlite3 graphviz +``` + +4. 现在,您可以通过运行命令 `cargo build` 来构建 Tabby。 + +### 开始开发! +... 别忘了提交一个 [Pull Request](https://github.com/TabbyML/tabby/compare) + +## 🌍 社区 +- 🎤 [Twitter / X](https://twitter.com/Tabby_ML) - 与 TabbyML 互动,探索所有可能性 +- 📚 [LinkedIn](https://www.linkedin.com/company/tabbyml/) - 关注社区的最新动态 +- 💌 [新闻通讯](https://newsletter.tabbyml.com/archive) - 订阅以解锁 Tabby 的见解和秘密 + +### 🔆 活动 + +![Git 仓库活动](https://repobeats.axiom.co/api/embed/e4ef0fbd12e586ef9ea7d72d1fb4f5c5b88d78d5.svg "Repobeats 分析图") + +### 🌟 星标历史 + +[![星标历史图](https://api.star-history.com/svg?repos=tabbyml/tabby&type=Date)](https://star-history.com/#tabbyml/tabby&Date) diff --git a/README.md b/README.md index b53425ff5501..a3b62afcb9d6 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,17 @@
- + # 🐾 Tabby -[![latest release](https://shields.io/github/v/release/TabbyML/tabby?sort=semver)](https://github.com/TabbyML/tabby/releases/latest) +[📚 Docs](https://tabby.tabbyml.com/docs/welcome/) • [💬 Slack](https://links.tabbyml.com/join-slack) • [🗺️ Roadmap](https://tabby.tabbyml.com/docs/roadmap/) + +[![latest release](https://shields.io/github/v/release/TabbyML/tabby)](https://github.com/TabbyML/tabby/releases/latest) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://makeapullrequest.com) [![Docker pulls](https://img.shields.io/docker/pulls/tabbyml/tabby)](https://hub.docker.com/r/tabbyml/tabby) +[![codecov](https://codecov.io/gh/TabbyML/tabby/graph/badge.svg?token=WYVVH8MKK3)](https://codecov.io/gh/TabbyML/tabby) -[![Slack Community](https://shields.io/badge/Join-Tabby%20Slack-red?logo=slack)](https://links.tabbyml.com/join-slack) -[![Office Hours](https://img.shields.io/badge/Book-Office%20Hours-purple?logo=googlecalendar&logoColor=white)](https://calendly.com/tabby_ml/chat-with-tabbyml) +[English](/README.md) | +[简体中文](/README-zh.md) | +[日本語](/README-ja.md)
@@ -17,7 +21,7 @@ Tabby is a self-hosted AI coding assistant, offering an open-source and on-premi * Supports consumer-grade GPUs.

- Open in Playground + Open Live Demo

@@ -25,19 +29,41 @@ Tabby is a self-hosted AI coding assistant, offering an open-source and on-premi

## 🔥 What's New - -* **12/23/2023** Seamlessly [deploy Tabby on any cloud](https://tabby.tabbyml.com/docs/installation/skypilot/) with [SkyServe](https://skypilot.readthedocs.io/en/latest/serving/sky-serve.html) 🛫 from SkyPilot. -* **12/15/2023** [v0.7.0](https://github.com/TabbyML/tabby/releases/tag/v0.7.0) released with team management and secured access! -* **10/24/2023** ⛳️ Major updates for Tabby IDE plugins across [VSCode/Vim/IntelliJ](https://tabby.tabbyml.com/docs/extensions)! - +* **12/12/2025** Get your GitHub issues implemented by connecting them to [Pochi](https://github.com/TabbyML/pochi) tasks and create PRs directly from the sidebar with a breakdown of CI/Lint/Test results [vscode@0.20.0](https://github.com/TabbyML/pochi/releases/tag/vscode%400.20.0). +* **07/02/2025** [v0.30](https://github.com/TabbyML/tabby/releases/tag/v0.30.0) supports indexing GitLab Merge Request as Context! +* **05/25/2025** 💡Interested in joining [Agent](https://links.tabbyml.com/pochi-github-readme) private preview? DM in [X](https://x.com/getpochi) for early waitlist approval!🎫 +* **05/20/2025** Enhance Tabby with your own documentation📃 through REST APIs in [v0.29](https://github.com/TabbyML/tabby/releases/tag/v0.29.0)! 🎉 +* **05/01/2025** [v0.28](https://github.com/TabbyML/tabby/releases/tag/v0.28.0) transforming Answer Engine messages into persistent, shareable Pages +* **03/31/2025** [v0.27](https://github.com/TabbyML/tabby/releases/tag/v0.27.0) released with a richer `@` menu in the chat side panel.
Archived +* **02/05/2025** LDAP Authentication and better notification for background jobs coming in Tabby [v0.24.0](https://github.com/TabbyML/tabby/releases/tag/v0.24.0)!✨ +* **02/04/2025** [VSCode 1.20.0](https://marketplace.visualstudio.com/items/TabbyML.vscode-tabby/changelog) upgrade! @-mention files to add them as chat context, and edit inline with a new right-click option are available! +* **01/10/2025** Tabby [v0.23.0](https://github.com/TabbyML/tabby/releases/tag/v0.23.0) featuring enhanced code browser experience and chat side panel improvements! +* **12/24/2024** Introduce **Notification Box** in Tabby [v0.22.0](https://github.com/TabbyML/tabby/releases/tag/v0.22.0)! +* **12/06/2024** Llamafile deployment integration and enhanced Answer Engine user experience are coming in Tabby [v0.21.0](https://github.com/TabbyML/tabby/releases/tag/v0.21.0)!🚀 +* **11/10/2024** Switching between different backend chat models is supported in Answer Engine with Tabby [v0.20.0](https://github.com/TabbyML/tabby/releases/tag/v0.20.0)! +* **10/30/2024** Tabby [v0.19.0](https://github.com/TabbyML/tabby/releases/tag/v0.19.0) featuring recent shared threads on the main page to improve their discoverability. +* **07/09/2024** 🎉Announce [Codestral integration in Tabby](https://tabby.tabbyml.com/blog/2024/07/09/tabby-codestral/)! +* **07/05/2024** Tabby [v0.13.0](https://github.com/TabbyML/tabby/releases/tag/v0.13.0) introduces ***Answer Engine***, a central knowledge engine for internal engineering teams. It seamlessly integrates with dev team's internal data, delivering reliable and precise answers to empower developers. +* **06/13/2024** [VSCode 1.7](https://marketplace.visualstudio.com/items/TabbyML.vscode-tabby/changelog) marks a significant milestone with a versatile Chat experience throughout your coding experience. Come and they the latest **chat in side-panel** and **editing via chat command**! +* **06/10/2024** Latest 📃blogpost drop on [an enhanced code context understanding](https://tabby.tabbyml.com/blog/2024/06/11/rank-fusion-in-tabby-code-completion/) in Tabby! +* **06/06/2024** Tabby [v0.12.0](https://github.com/TabbyML/tabby/releases/tag/v0.12.0) release brings 🔗**seamless integrations** (Gitlab SSO, Self-hosted GitHub/GitLab, etc.), to ⚙️**flexible configurations** (HTTP API integration) and 🌐**expanded capabilities** (repo-context in Code Browser)! +* **05/22/2024** Tabby [VSCode 1.6](https://marketplace.visualstudio.com/items?itemName=TabbyML.vscode-tabby) comes with **multiple choices** in inline completion, and the **auto-generated commit messages**🐱💻! +* **05/11/2024** [v0.11.0](https://github.com/TabbyML/tabby/releases/tag/v0.11.0) brings significant enterprise upgrades, including 📊**storage usage** stats, 🔗**GitHub & GitLab** integration, 📋**Activities** page, and the long-awaited 🤖**Ask Tabby** feature! +* **04/22/2024** [v0.10.0](https://github.com/TabbyML/tabby/releases/tag/v0.10.0) released, featuring the latest **Reports** tab with team-wise analytics for Tabby usage. +* **04/19/2024** 📣 Tabby now incorporates [locally relevant snippets](https://github.com/TabbyML/tabby/pull/1844)(declarations from local LSP, and recently modified code) for code completion! +* **04/17/2024** CodeGemma and CodeQwen model series have now been added to the [official registry](https://tabby.tabbyml.com/docs/models/)! +* **03/20/2024** [v0.9](https://github.com/TabbyML/tabby/releases/tag/v0.9.1) released, highlighting a full feature admin UI. +* **12/23/2023** Seamlessly [deploy Tabby on any cloud](https://tabby.tabbyml.com/docs/installation/skypilot/) with [SkyServe](https://skypilot.readthedocs.io/en/latest/serving/sky-serve.html) 🛫 from SkyPilot. +* **12/15/2023** [v0.7.0](https://github.com/TabbyML/tabby/releases/tag/v0.7.0) released with team management and secured access! * **10/15/2023** RAG-based code completion is enabled by detail in [v0.3.0](https://github.com/TabbyML/tabby/releases/tag/v0.3.0)🎉! Check out the [blogpost](https://tabby.tabbyml.com/blog/2023/10/16/repository-context-for-code-completion/) explaining how Tabby utilizes repo-level context to get even smarter! * **11/27/2023** [v0.6.0](https://github.com/TabbyML/tabby/releases/tag/v0.6.0) released! * **11/09/2023** [v0.5.5](https://github.com/TabbyML/tabby/releases/tag/v0.5.5) released! With a redesign of UI + performance improvement. +* **10/24/2023** ⛳️ Major updates for Tabby IDE plugins across [VSCode/Vim/IntelliJ](https://tabby.tabbyml.com/docs/extensions)! * **10/04/2023** Check out the [model directory](https://tabby.tabbyml.com/docs/models/) for the latest models supported by Tabby. * **09/18/2023** Apple's M1/M2 Metal inference support has landed in [v0.1.1](https://github.com/TabbyML/tabby/releases/tag/v0.1.1)! * **08/31/2023** Tabby's first stable release [v0.0.1](https://github.com/TabbyML/tabby/releases/tag/v0.0.1) 🥳. @@ -60,7 +86,7 @@ The easiest way to start a Tabby server is by using the following Docker command docker run -it \ --gpus all -p 8080:8080 -v $HOME/.tabby:/data \ tabbyml/tabby \ - serve --model TabbyML/StarCoder-1B --device cuda + serve --model StarCoder-1B --device cuda --chat-model Qwen2-1.5B-Instruct ``` For additional options (e.g inference type, parallelism), please refer to the [documentation page](https://tabbyml.github.io/tabby). @@ -87,21 +113,28 @@ If you have already cloned the repository, you could run the `git submodule upda brew install protobuf # For Ubuntu / Debian -apt-get install protobuf-compiler libopenblas-dev +apt install protobuf-compiler libopenblas-dev +``` + +3. Install useful tools: +```bash +# For Ubuntu +apt install make sqlite3 graphviz ``` -3. Now, you can build Tabby by running the command `cargo build`. +4. Now, you can build Tabby by running the command `cargo build`. ### Start Hacking! ... and don't forget to submit a [Pull Request](https://github.com/TabbyML/tabby/compare) ## 🌍 Community -- #️⃣ [Slack](https://links.tabbyml.com/join-slack-extensions) - connect with the TabbyML community - 🎤 [Twitter / X](https://twitter.com/Tabby_ML) - engage with TabbyML for all things possible - 📚 [LinkedIn](https://www.linkedin.com/company/tabbyml/) - follow for the latest from the community - 💌 [Newsletter](https://newsletter.tabbyml.com/archive) - subscribe to unlock Tabby insights and secrets +### 🔆 Activity +![Git Repository Activity](https://repobeats.axiom.co/api/embed/e4ef0fbd12e586ef9ea7d72d1fb4f5c5b88d78d5.svg "Repobeats analytics image") ### 🌟 Star History diff --git a/ci/package-from-upstream.sh b/ci/package-from-upstream.sh new file mode 100755 index 000000000000..a6c6c7fe68ce --- /dev/null +++ b/ci/package-from-upstream.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +set -e + +# get current bash file directory +PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +LLAMA_CPP_PATH="${PROJECT_ROOT}/crates/llama-cpp-server/llama.cpp" + +# Input variables +LLAMA_CPP_VERSION=${LLAMA_CPP_VERSION:-$(cd ${LLAMA_CPP_PATH} && git fetch --tags origin >/dev/null && git describe --tags --abbrev=0)} +echo "LLAMA_CPP_VERSION=${LLAMA_CPP_VERSION}" +LLAMA_CPP_PLATFORM=${LLAMA_CPP_PLATFORM:-win-cuda-12.4-x64} + +NAME=llama-${LLAMA_CPP_VERSION}-bin-${LLAMA_CPP_PLATFORM} +ZIP_FILE=${NAME}.zip + +OUTPUT_NAME=${OUTPUT_NAME:-tabby_x86_64-windows-msvc-cuda124} + +if [[ ${LLAMA_CPP_PLATFORM} == win* ]]; then + TABBY_BINARY=${TABBY_BINARY:-tabby_x86_64-windows-msvc.exe} + TABBY_EXTENSION=.exe +else + TABBY_BINARY=${TABBY_BINARY:-tabby_x86_64-manylinux_2_28} + TABBY_EXTENSION="" +fi + +curl https://github.com/ggml-org/llama.cpp/releases/download/${LLAMA_CPP_VERSION}/${ZIP_FILE} -L -o ${ZIP_FILE} +unzip ${ZIP_FILE} -d ${OUTPUT_NAME} +cp "./${TABBY_BINARY}"/${TABBY_BINARY} ${OUTPUT_NAME}/tabby${TABBY_EXTENSION} + +pushd ${OUTPUT_NAME} +if [[ ${LLAMA_CPP_PLATFORM} == win* ]]; then + rm -f $(ls *.exe | grep -v -e "tabby" -e "llama-server") + + popd + zip -r ${OUTPUT_NAME}.zip ${OUTPUT_NAME} +else + # upstream release linux package within build/bin directory + mv build/bin/* . + rm -r build + + rm -f $(ls . | grep -v -e "tabby" -e "llama-server" -e '.so$' -e "LICENSE") + mv LICENSE LICENSE-llama-server + chmod +x llama-server tabby + + popd + tar -czvf ${OUTPUT_NAME}.tar.gz ${OUTPUT_NAME} +fi + +rm -rf "${OUTPUT_NAME}" + +mkdir -p dist +mv ${OUTPUT_NAME}.* dist/ diff --git a/ci/prepare_build_environment.ps1 b/ci/prepare_build_environment.ps1 index 7235782df0a4..3c7fc7781fb4 100644 --- a/ci/prepare_build_environment.ps1 +++ b/ci/prepare_build_environment.ps1 @@ -1 +1 @@ -choco install --yes protoc +choco install --yes protoc \ No newline at end of file diff --git a/ci/prepare_build_environment.sh b/ci/prepare_build_environment.sh index cdcf2f637eeb..67e8802e8b5c 100755 --- a/ci/prepare_build_environment.sh +++ b/ci/prepare_build_environment.sh @@ -1,9 +1,5 @@ #!/bin/bash -if [[ "$OSTYPE" == "darwin"* ]]; then - brew install protobuf -fi - install_protobuf_centos() { PB_REL="https://github.com/protocolbuffers/protobuf/releases" curl -LO $PB_REL/download/v3.15.8/protoc-3.15.8-linux-x86_64.zip @@ -11,16 +7,39 @@ install_protobuf_centos() { rm protoc-3.15.8-linux-x86_64.zip } +install_mailpit() { + bash < <(curl -sL https://raw.githubusercontent.com/axllent/mailpit/develop/install.sh) +} + +if [[ "$OSTYPE" == "darwin"* ]]; then + brew install protobuf + install_mailpit +fi + if [[ "$OSTYPE" == "linux"* ]]; then if command -v apt-get ; then - sudo apt-get -y install protobuf-compiler libopenblas-dev + apt-get -y install protobuf-compiler libopenblas-dev sqlite3 graphviz libcurl4-openssl-dev else - # Build from manylinux2014 container - yum -y install openblas-devel perl-IPC-Cmd unzip curl openssl-devel + # Build from manylinux_2_28 container + + # CentOS 7 is EOL after 2024 06, need to update to vault.centos.org + sed -i -e 's/mirrorlist/#mirrorlist/g' -e 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-* + yum -y install openblas-devel perl-IPC-Cmd unzip openssl-devel + + # Install newer version of curl to support `--fail-with-body` on vulkan-sdk-install + # The outdated release RPMs will be cleaned up from the city-fan.org mirror; therefore, we must first obtain the latest version. + base_url="https://mirror.city-fan.org/ftp/contrib/sysutils/Mirroring" + curl_rpm=$(curl -s $base_url/ | grep -oE 'curl-[0-9\.-]+\.cf\.rhel8\.x86_64\.rpm' | sort -V -u | tail -n 1) + libcurl_rpm=$(curl -s $base_url/ | grep -oE 'libcurl-[0-9\.-]+\.cf\.rhel8\.x86_64\.rpm' | sort -V -u | tail -n 1) + curl -SLO $base_url/$curl_rpm + curl -SLO $base_url/$libcurl_rpm + rpm -Uvh $curl_rpm $libcurl_rpm # Disable safe directory in docker git config --system --add safe.directory "*" install_protobuf_centos fi + + install_mailpit fi diff --git a/clients/eclipse/.gitignore b/clients/eclipse/.gitignore new file mode 100644 index 000000000000..a2698ab27ad3 --- /dev/null +++ b/clients/eclipse/.gitignore @@ -0,0 +1,4 @@ +dist +node_modules +plugin/tabby-agent +plugin/chat-panel/tabby-chat-panel.min.js diff --git a/clients/eclipse/README.md b/clients/eclipse/README.md new file mode 100644 index 000000000000..4261428217bd --- /dev/null +++ b/clients/eclipse/README.md @@ -0,0 +1,37 @@ +# Tabby Plugin for Eclipse + +**Note:** This repository is currently undergoing extensive development. + +## Development Setup + +1. Install Eclipse with the [Plug-in Development Environment (PDE)](https://projects.eclipse.org/projects/eclipse.pde). It is recommended to download the [Eclipse IDE for Eclipse Committers](https://www.eclipse.org/downloads/packages/release/2024-06/r/eclipse-ide-eclipse-committers) for a more streamlined setup. + +2. Ensure you have [Node.js](https://nodejs.org/en/download/) version 18 or higher installed. For package management, install [pnpm](https://pnpm.io/installation). Using [corepack](https://pnpm.io/installation#using-corepack) is the preferred method for installation. + +3. Clone the Tabby repository and install its dependencies: + ```bash + git clone https://github.com/TabbyML/tabby.git + cd tabby + pnpm install + cd clients/eclipse + pnpm turbo build + ``` + This process will also build the `tabby-agent` and place it into the `clients/eclipse/plugin/tabby-agent/` directory. + +4. Import the project into your Eclipse workspace. + a. Open `File -> Import...`, select the `General -> Existing Projects into Workspace` option, and click `Next`. + ![Import Project into Workspace](docs/eclipse-import-project-select-wizard.jpg) + b. Select the `clients/eclipse/plugin` as the root directory, check the item to be imported, and click `Finish`. + ![Import Project into Workspace](docs/eclipse-import-project-select-project.jpg) + c. Then import the `clients/eclipse/feature` directory as well. + +5. Open the `clients/eclipse/plugin/plugin.xml` file in Eclipse. It should open as a plugin project overview. In the `Testing` section, click on `Launch an Eclipse application` to start the plugin. + ![Launch an Eclipse application](docs/eclipse-launch-application.jpg) + +## Export a ZIP Archive + +1. In Eclipse, open `File -> Export...`, select the `Plug-in Development -> Deployable features` option, and click `Next`. + ![Export Archive](docs/eclipse-export-archive-select-wizard.jpg) +2. Check the `com.tabbyml.features.tabby4eclipse`, then select `Archive file` with a specified file path, and click `Finish`. + ![Export Archive](docs/eclipse-export-archive-select-features.jpg) +3. The exported ZIP archive should be located at the specified file path. It can be installed by navigating to `Help -> Install New Software...`. diff --git a/clients/eclipse/docs/eclipse-export-archive-select-features.jpg b/clients/eclipse/docs/eclipse-export-archive-select-features.jpg new file mode 100644 index 000000000000..991907b9e713 Binary files /dev/null and b/clients/eclipse/docs/eclipse-export-archive-select-features.jpg differ diff --git a/clients/eclipse/docs/eclipse-export-archive-select-wizard.jpg b/clients/eclipse/docs/eclipse-export-archive-select-wizard.jpg new file mode 100644 index 000000000000..51aff9016a31 Binary files /dev/null and b/clients/eclipse/docs/eclipse-export-archive-select-wizard.jpg differ diff --git a/clients/eclipse/docs/eclipse-import-project-select-project.jpg b/clients/eclipse/docs/eclipse-import-project-select-project.jpg new file mode 100644 index 000000000000..2099025b42b8 Binary files /dev/null and b/clients/eclipse/docs/eclipse-import-project-select-project.jpg differ diff --git a/clients/eclipse/docs/eclipse-import-project-select-wizard.jpg b/clients/eclipse/docs/eclipse-import-project-select-wizard.jpg new file mode 100644 index 000000000000..7ab91e0d8327 Binary files /dev/null and b/clients/eclipse/docs/eclipse-import-project-select-wizard.jpg differ diff --git a/clients/eclipse/docs/eclipse-launch-application.jpg b/clients/eclipse/docs/eclipse-launch-application.jpg new file mode 100644 index 000000000000..1d0c5c004dac Binary files /dev/null and b/clients/eclipse/docs/eclipse-launch-application.jpg differ diff --git a/clients/eclipse/feature/.project b/clients/eclipse/feature/.project new file mode 100644 index 000000000000..96f9c78a9c89 --- /dev/null +++ b/clients/eclipse/feature/.project @@ -0,0 +1,17 @@ + + + features.tabby4eclipse + + + + + + org.eclipse.pde.FeatureBuilder + + + + + + org.eclipse.pde.FeatureNature + + diff --git a/clients/eclipse/feature/build.properties b/clients/eclipse/feature/build.properties new file mode 100644 index 000000000000..64f93a9f0b73 --- /dev/null +++ b/clients/eclipse/feature/build.properties @@ -0,0 +1 @@ +bin.includes = feature.xml diff --git a/clients/eclipse/feature/feature.xml b/clients/eclipse/feature/feature.xml new file mode 100644 index 000000000000..f3336298fd82 --- /dev/null +++ b/clients/eclipse/feature/feature.xml @@ -0,0 +1,24 @@ + + + + + [Enter Feature Description here.] + + + + [Enter Copyright Description here.] + + + + [Enter License Description here.] + + + + + diff --git a/clients/eclipse/package.json b/clients/eclipse/package.json new file mode 100644 index 000000000000..bc6c1389cec7 --- /dev/null +++ b/clients/eclipse/package.json @@ -0,0 +1,12 @@ +{ + "name": "tabby4eclipse", + "private": true, + "scripts": { + "build": "node scripts/copy-dependencies.js" + }, + "devDependencies": { + "tabby-agent": "workspace:*", + "tabby-chat-panel": "workspace:*", + "fs-extra": "^11.1.1" + } +} diff --git a/clients/eclipse/plugin/.classpath b/clients/eclipse/plugin/.classpath new file mode 100644 index 000000000000..2869279ea3b4 --- /dev/null +++ b/clients/eclipse/plugin/.classpath @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/clients/eclipse/plugin/.gitignore b/clients/eclipse/plugin/.gitignore new file mode 100644 index 000000000000..ae3c1726048c --- /dev/null +++ b/clients/eclipse/plugin/.gitignore @@ -0,0 +1 @@ +/bin/ diff --git a/clients/eclipse/plugin/.project b/clients/eclipse/plugin/.project new file mode 100644 index 000000000000..20e1c6bc01f4 --- /dev/null +++ b/clients/eclipse/plugin/.project @@ -0,0 +1,45 @@ + + + tabby4eclipse + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.pde.api.tools.apiAnalysisBuilder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + org.eclipse.pde.api.tools.apiAnalysisNature + + + + 1721162964200 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + + diff --git a/clients/eclipse/plugin/.settings/org.eclipse.core.resources.prefs b/clients/eclipse/plugin/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 000000000000..99f26c0203a7 --- /dev/null +++ b/clients/eclipse/plugin/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/clients/eclipse/plugin/.settings/org.eclipse.jdt.core.prefs b/clients/eclipse/plugin/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000000..62ef3488cc04 --- /dev/null +++ b/clients/eclipse/plugin/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,9 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=17 diff --git a/clients/eclipse/plugin/META-INF/MANIFEST.MF b/clients/eclipse/plugin/META-INF/MANIFEST.MF new file mode 100644 index 000000000000..1320dbcb1da6 --- /dev/null +++ b/clients/eclipse/plugin/META-INF/MANIFEST.MF @@ -0,0 +1,21 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Tabby Plugin for Eclipse +Bundle-SymbolicName: com.tabbyml.tabby4eclipse;singleton:=true +Bundle-Version: 0.0.2.38 +Bundle-Activator: com.tabbyml.tabby4eclipse.Activator +Bundle-Vendor: com.tabbyml +Require-Bundle: org.eclipse.ui, + org.eclipse.ui.ide, + org.eclipse.ui.workbench.texteditor, + org.eclipse.core.resources, + org.eclipse.core.runtime, + org.eclipse.jface.text, + org.eclipse.lsp4e, + org.eclipse.lsp4j;bundle-version="0.23.1", + org.eclipse.lsp4j.jsonrpc;bundle-version="0.23.1", + org.eclipse.jgit;resolution:=optional, + com.google.gson;bundle-version="2.11.0" +Bundle-RequiredExecutionEnvironment: JavaSE-17 +Automatic-Module-Name: com.tabbyml.tabby4eclipse +Bundle-ActivationPolicy: lazy diff --git a/clients/eclipse/plugin/build.properties b/clients/eclipse/plugin/build.properties new file mode 100644 index 000000000000..7d7e9114a2c8 --- /dev/null +++ b/clients/eclipse/plugin/build.properties @@ -0,0 +1,8 @@ +source.. = src/ +output.. = bin/ +bin.includes = plugin.xml,\ + META-INF/,\ + images/, \ + tabby-agent/,\ + chat-panel/,\ + .\ diff --git a/clients/eclipse/plugin/chat-panel/index.html b/clients/eclipse/plugin/chat-panel/index.html new file mode 100644 index 000000000000..6a3c962a1dd0 --- /dev/null +++ b/clients/eclipse/plugin/chat-panel/index.html @@ -0,0 +1,110 @@ + + + + + + + + + + +
+
+

Welcome to Tabby Chat

+

+ Reload +
+
+ + + + + + \ No newline at end of file diff --git a/clients/eclipse/plugin/images/add_obj.png b/clients/eclipse/plugin/images/add_obj.png new file mode 100644 index 000000000000..c6aeae4d462d Binary files /dev/null and b/clients/eclipse/plugin/images/add_obj.png differ diff --git a/clients/eclipse/plugin/images/add_obj@2x.png b/clients/eclipse/plugin/images/add_obj@2x.png new file mode 100644 index 000000000000..a5fbbb07183b Binary files /dev/null and b/clients/eclipse/plugin/images/add_obj@2x.png differ diff --git a/clients/eclipse/plugin/images/chat.png b/clients/eclipse/plugin/images/chat.png new file mode 100644 index 000000000000..0add23e6fa58 Binary files /dev/null and b/clients/eclipse/plugin/images/chat.png differ diff --git a/clients/eclipse/plugin/images/chat@2x.png b/clients/eclipse/plugin/images/chat@2x.png new file mode 100644 index 000000000000..dbf5a8568036 Binary files /dev/null and b/clients/eclipse/plugin/images/chat@2x.png differ diff --git a/clients/eclipse/plugin/images/check_tsk.png b/clients/eclipse/plugin/images/check_tsk.png new file mode 100644 index 000000000000..6d5b4e3c4316 Binary files /dev/null and b/clients/eclipse/plugin/images/check_tsk.png differ diff --git a/clients/eclipse/plugin/images/check_tsk@2x.png b/clients/eclipse/plugin/images/check_tsk@2x.png new file mode 100644 index 000000000000..2f7682f1b773 Binary files /dev/null and b/clients/eclipse/plugin/images/check_tsk@2x.png differ diff --git a/clients/eclipse/plugin/images/hprio_tsk.png b/clients/eclipse/plugin/images/hprio_tsk.png new file mode 100644 index 000000000000..4e2910d5c920 Binary files /dev/null and b/clients/eclipse/plugin/images/hprio_tsk.png differ diff --git a/clients/eclipse/plugin/images/hprio_tsk@2x.png b/clients/eclipse/plugin/images/hprio_tsk@2x.png new file mode 100644 index 000000000000..fafd371e259c Binary files /dev/null and b/clients/eclipse/plugin/images/hprio_tsk@2x.png differ diff --git a/clients/eclipse/plugin/images/progress_task.png b/clients/eclipse/plugin/images/progress_task.png new file mode 100644 index 000000000000..b4f61de8a120 Binary files /dev/null and b/clients/eclipse/plugin/images/progress_task.png differ diff --git a/clients/eclipse/plugin/images/progress_task@2x.png b/clients/eclipse/plugin/images/progress_task@2x.png new file mode 100644 index 000000000000..1c8333bbdb50 Binary files /dev/null and b/clients/eclipse/plugin/images/progress_task@2x.png differ diff --git a/clients/eclipse/plugin/images/settings.png b/clients/eclipse/plugin/images/settings.png new file mode 100644 index 000000000000..3e17f5861055 Binary files /dev/null and b/clients/eclipse/plugin/images/settings.png differ diff --git a/clients/eclipse/plugin/images/settings@2x.png b/clients/eclipse/plugin/images/settings@2x.png new file mode 100644 index 000000000000..c487c260cb91 Binary files /dev/null and b/clients/eclipse/plugin/images/settings@2x.png differ diff --git a/clients/eclipse/plugin/images/time_obj.png b/clients/eclipse/plugin/images/time_obj.png new file mode 100644 index 000000000000..57af2c2a4b1f Binary files /dev/null and b/clients/eclipse/plugin/images/time_obj.png differ diff --git a/clients/eclipse/plugin/images/time_obj@2x.png b/clients/eclipse/plugin/images/time_obj@2x.png new file mode 100644 index 000000000000..07a3eabee256 Binary files /dev/null and b/clients/eclipse/plugin/images/time_obj@2x.png differ diff --git a/clients/eclipse/plugin/images/warn_tsk.png b/clients/eclipse/plugin/images/warn_tsk.png new file mode 100644 index 000000000000..b900f3b909fc Binary files /dev/null and b/clients/eclipse/plugin/images/warn_tsk.png differ diff --git a/clients/eclipse/plugin/images/warn_tsk@2x.png b/clients/eclipse/plugin/images/warn_tsk@2x.png new file mode 100644 index 000000000000..97339df8e0b8 Binary files /dev/null and b/clients/eclipse/plugin/images/warn_tsk@2x.png differ diff --git a/clients/eclipse/plugin/plugin.xml b/clients/eclipse/plugin/plugin.xml new file mode 100644 index 000000000000..cd2673aa3cb1 --- /dev/null +++ b/clients/eclipse/plugin/plugin.xml @@ -0,0 +1,319 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/Activator.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/Activator.java new file mode 100644 index 000000000000..39bcfb13e146 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/Activator.java @@ -0,0 +1,38 @@ +package com.tabbyml.tabby4eclipse; + +import org.eclipse.ui.plugin.AbstractUIPlugin; +import org.osgi.framework.BundleContext; + +/** + * The activator class controls the plug-in life cycle + */ +public class Activator extends AbstractUIPlugin { + // The plug-in ID + public static final String PLUGIN_ID = "com.tabbyml.tabby4eclipse"; + + // The shared instance + private static Activator plugin; + + public static Activator getDefault() { + return plugin; + } + + private Logger logger = new Logger("Activator"); + + public Activator() { + } + + @Override + public void start(BundleContext context) throws Exception { + super.start(context); + plugin = this; + logger.info("Tabby plugin is starting."); + } + + @Override + public void stop(BundleContext context) throws Exception { + logger.info("Tabby plugin is stopping."); + plugin = null; + super.stop(context); + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/DebouncedRunnable.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/DebouncedRunnable.java new file mode 100644 index 000000000000..4855972b4a2b --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/DebouncedRunnable.java @@ -0,0 +1,30 @@ +package com.tabbyml.tabby4eclipse; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +public class DebouncedRunnable { + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + private ScheduledFuture future; + private final long delay; + private final Runnable task; + + public DebouncedRunnable(Runnable task, long delay) { + this.task = task; + this.delay = delay; + } + + public synchronized void call() { + if (future != null && !future.isDone()) { + future.cancel(true); + } + future = scheduler.schedule(task, delay, TimeUnit.MILLISECONDS); + } + + // FIXME: scheduler shutdown not called + public void shutdown() { + scheduler.shutdown(); + } +} \ No newline at end of file diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/Images.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/Images.java new file mode 100644 index 000000000000..58510d66412a --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/Images.java @@ -0,0 +1,41 @@ +package com.tabbyml.tabby4eclipse; + +import java.net.URL; + +import org.eclipse.core.runtime.FileLocator; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.Platform; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.resource.ImageRegistry; +import org.eclipse.swt.graphics.Image; +import org.osgi.framework.Bundle; + +public class Images { + private static Bundle bundle = Platform.getBundle(Activator.PLUGIN_ID); + private static ImageRegistry icons = Activator.getDefault().getImageRegistry(); + + public static final String ICON_CHAT = "chat.png"; + public static final String ICON_CHECK = "check_tsk.png"; + public static final String ICON_ERROR = "hprio_tsk.png"; + public static final String ICON_WARN = "warn_tsk.png"; + public static final String ICON_LOADING = "progress_task.png"; + public static final String ICON_SETTINGS = "settings.png"; + public static final String ICON_ADD = "add_obj.png"; + public static final String ICON_HISTORY = "time_obj.png"; + + public static Image getIcon(String filename) { + Image icon = icons.get(filename); + if (icon == null) { + icon = createImageFromFile(filename); + icons.put(filename, icon); + } + return icon; + } + + private static Image createImageFromFile(String filename) { + String path = "images/" + filename; + URL url = FileLocator.find(bundle, new Path(path)); + ImageDescriptor imageDesc = ImageDescriptor.createFromURL(url); + return imageDesc.createImage(); + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/Logger.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/Logger.java new file mode 100644 index 000000000000..3fa70e5433c8 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/Logger.java @@ -0,0 +1,56 @@ +package com.tabbyml.tabby4eclipse; + +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; + +public class Logger { + private static String TABBY4ECLIPSE_LOG_DEBUG = System.getenv("TABBY4ECLIPSE_LOG_DEBUG"); + private static boolean LOG_DEBUG = TABBY4ECLIPSE_LOG_DEBUG != null && !TABBY4ECLIPSE_LOG_DEBUG.isEmpty(); + + private String tag; + + public Logger(String tag) { + this.tag = tag; + } + + public void trace(String message, Object obj) { + System.out.println(tagString(message)); + System.out.println(obj); + } + + public void debug(String message) { + System.out.println(tagString(message)); + if (LOG_DEBUG) { + logStatus(IStatus.INFO, String.format("[DEBUG] %s", message), null); + } + } + + public void info(String message) { + logStatus(IStatus.INFO, message, null); + } + + public void warn(String message) { + logStatus(IStatus.WARNING, message, null); + } + + public void warn(String message, Throwable throwable) { + logStatus(IStatus.WARNING, message, throwable); + } + + public void error(String message) { + logStatus(IStatus.ERROR, message, null); + } + + public void error(String message, Throwable throwable) { + logStatus(IStatus.ERROR, message, throwable); + } + + private void logStatus(int severity, String message, Throwable throwable) { + Status status = new Status(severity, Activator.PLUGIN_ID, tagString(message), throwable); + Activator.getDefault().getLog().log(status); + } + + private String tagString(String message) { + return "[" + tag + "] " + message; + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/Startup.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/Startup.java new file mode 100644 index 000000000000..d501364dce35 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/Startup.java @@ -0,0 +1,24 @@ +package com.tabbyml.tabby4eclipse; + +import org.eclipse.ui.IStartup; + +import com.tabbyml.tabby4eclipse.editor.WorkbenchPartListener; +import com.tabbyml.tabby4eclipse.lsp.LanguageServerService; +import com.tabbyml.tabby4eclipse.preferences.PreferencesService; + +public class Startup implements IStartup { + + private Logger logger = new Logger("Startup"); + + @Override + public void earlyStartup() { + logger.info("Running startup actions."); + LanguageServerService lsService = LanguageServerService.getInstance(); + lsService.init(); + WorkbenchPartListener workbenchPartListener = WorkbenchPartListener.getInstance(); + workbenchPartListener.init(); + PreferencesService preferenceService = PreferencesService.getInstance(); + preferenceService.init(); + logger.info("Finished running startup actions."); + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/StringUtils.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/StringUtils.java new file mode 100644 index 000000000000..88a03b2c8eda --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/StringUtils.java @@ -0,0 +1,71 @@ +package com.tabbyml.tabby4eclipse; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.swt.graphics.RGB; + +public class StringUtils { + public static String escapeCharacters(String jsonString) { + return jsonString.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r") + .replace("\t", "\\t"); + } + + public static String toHsl(RGB rgb) { + double r = rgb.red / 255.0; + double g = rgb.green / 255.0; + double b = rgb.blue / 255.0; + double max = Math.max(r, Math.max(g, b)); + double min = Math.min(r, Math.min(g, b)); + double l = (max + min) / 2.0; + double h, s; + if (max == min) { + h = 0; + s = 0; + } else { + double delta = max - min; + s = l > 0.5 ? delta / (2.0 - max - min) : delta / (max + min); + if (max == r) { + h = (g - b) / delta + (g < b ? 6 : 0); + } else if (max == g) { + h = (b - r) / delta + 2; + } else { + h = (r - g) / delta + 4; + } + h /= 6; + } + h *= 360; + s *= 100; + l *= 100; + return String.format("%.0f %.0f%% %.0f%%", h, s, l); + } + + public static class TextWithTabs { + private int tabs; + private String text; + + public TextWithTabs(int tabs, String text) { + this.tabs = tabs; + this.text = text; + } + + public int getTabs() { + return tabs; + } + + public String getText() { + return text; + } + } + + static final Pattern PATTERN_LEADING_TABS = Pattern.compile("^(\\t*)(.*)$"); + + public static TextWithTabs splitLeadingTabs(String text) { + Matcher matcher = PATTERN_LEADING_TABS.matcher(text); + if (matcher.matches()) { + return new TextWithTabs(matcher.group(1).length(), matcher.group(2)); + } else { + return new TextWithTabs(0, text); + } + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/Utils.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/Utils.java new file mode 100644 index 000000000000..d417de680936 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/Utils.java @@ -0,0 +1,7 @@ +package com.tabbyml.tabby4eclipse; + +public class Utils { + public static boolean isWindows() { + return System.getProperty("os.name").toLowerCase().contains("win"); + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/Version.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/Version.java new file mode 100644 index 000000000000..c567a1dd9860 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/Version.java @@ -0,0 +1,81 @@ +package com.tabbyml.tabby4eclipse; + +public class Version { + private int major; + private int minor; + private int patch; + + public Version(String versionStr) { + int startIndex = 0; + while (startIndex < versionStr.length() && !Character.isDigit(versionStr.charAt(startIndex))) { + startIndex++; + } + if (startIndex >= versionStr.length()) { + return; + } + int endIndex = versionStr.indexOf("-"); + String numPart = (endIndex != -1) ? versionStr.substring(startIndex, endIndex) + : versionStr.substring(startIndex); + + String[] parts = numPart.split("\\."); + if (parts.length > 0) { + this.major = parseInt(parts[0]); + } + if (parts.length > 1) { + this.minor = parseInt(parts[1]); + } + if (parts.length > 2) { + this.patch = parseInt(parts[2]); + } + } + + public int getMajor() { + return major; + } + + public int getMinor() { + return minor; + } + + public int getPatch() { + return patch; + } + + public boolean isGreaterOrEqualThan(Version other) { + if (this.major > other.major) { + return true; + } else if (this.major < other.major) { + return false; + } else { + if (this.minor > other.minor) { + return true; + } else if (this.minor < other.minor) { + return false; + } else { + return this.patch >= other.patch; + } + } + } + + public boolean isEqual(Version other, boolean ignorePatch) { + if (this.major != other.major || this.minor != other.minor) { + return false; + } + if (ignorePatch) { + return true; + } + return this.patch == other.patch; + } + + public boolean isZero() { + return this.major == 0 && this.minor == 0 && this.patch == 0; + } + + private int parseInt(String str) { + try { + return Integer.parseInt(str); + } catch (NumberFormatException e) { + return 0; + } + } +} \ No newline at end of file diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/ChatCommand.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/ChatCommand.java new file mode 100644 index 000000000000..6c4c4e0560ae --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/ChatCommand.java @@ -0,0 +1,8 @@ +package com.tabbyml.tabby4eclipse.chat; + +public abstract class ChatCommand { + public static final String EXPLAIN = "explain"; + public static final String FIX = "fix"; + public static final String GENERATE_DOCS = "generate-docs"; + public static final String GENERATE_TESTS = "generate-tests"; +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/ChatView.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/ChatView.java new file mode 100644 index 000000000000..e1643264a56a --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/ChatView.java @@ -0,0 +1,778 @@ +package com.tabbyml.tabby4eclipse.chat; + +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.core.runtime.FileLocator; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.Platform; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.IToolBarManager; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.text.ITextSelection; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.swt.SWT; +import org.eclipse.swt.browser.Browser; +import org.eclipse.swt.browser.BrowserFunction; +import org.eclipse.swt.browser.ProgressAdapter; +import org.eclipse.swt.browser.ProgressEvent; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.graphics.RGB; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.ISelectionListener; +import org.eclipse.ui.IWorkbenchPart; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.part.ViewPart; +import org.eclipse.ui.themes.ColorUtil; +import org.osgi.framework.Bundle; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.tabbyml.tabby4eclipse.Activator; +import com.tabbyml.tabby4eclipse.DebouncedRunnable; +import com.tabbyml.tabby4eclipse.Images; +import com.tabbyml.tabby4eclipse.Logger; +import com.tabbyml.tabby4eclipse.StringUtils; +import com.tabbyml.tabby4eclipse.Utils; +import com.tabbyml.tabby4eclipse.chat.ChatViewUtils.RangeStrategy; +import com.tabbyml.tabby4eclipse.editor.EditorUtils; +import com.tabbyml.tabby4eclipse.lsp.LanguageServerService; +import com.tabbyml.tabby4eclipse.lsp.ServerConfigHolder; +import com.tabbyml.tabby4eclipse.lsp.StatusInfoHolder; +import com.tabbyml.tabby4eclipse.lsp.protocol.Config; +import com.tabbyml.tabby4eclipse.lsp.protocol.ILanguageServer; +import com.tabbyml.tabby4eclipse.lsp.protocol.IStatusService; +import com.tabbyml.tabby4eclipse.lsp.protocol.StatusInfo; +import com.tabbyml.tabby4eclipse.lsp.protocol.StatusRequestParams; + +@SuppressWarnings("serial") +public class ChatView extends ViewPart { + private Logger logger = new Logger("ChatView"); + private Gson gson = new Gson(); + + private StatusInfoHolder statusInfoHolder = StatusInfoHolder.getInstance(); + private ServerConfigHolder serverConfigHolder = ServerConfigHolder.getInstance(); + + private Browser browser; + private List browserFunctions = new ArrayList<>(); + + private boolean isHtmlLoaded = false; + private boolean isChatPanelLoaded = false; + private String chatPanelApiVersion = null; + private Config.ServerConfig currentConfig; + + private List pendingScripts = new ArrayList<>(); + private Map> pendingChatPanelRequest = new HashMap<>(); + + private List toolbarActions = new ArrayList<>(); + + private boolean isDark; + private RGB browserBgColor; + private RGB bgColor; + private RGB fgColor; + private RGB borderColor; + private RGB inputColor; + private RGB inputBorderColor; + private RGB primaryColor; + private RGB primaryFgColor; + private RGB popoverColor; + private RGB popoverFgColor; + private RGB accentColor; + private RGB accentFgColor; + private RGB ringColor; + private String font; + private int fontSize = 13; + + @Override + public void createPartControl(Composite parent) { + parent.setLayout(new FillLayout()); + + // Tool bar + IToolBarManager toolbarManager = getViewSite().getActionBars().getToolBarManager(); + Action newChat = new Action("New") { + @Override + public void run() { + chatPanelClientInvoke(ChatViewUtils.API_0_8_0, "navigate", new ArrayList<>() { + { + add(ChatViewType.NEW_CHAT); + } + }); + } + }; + newChat.setImageDescriptor(ImageDescriptor.createFromImage(Images.getIcon(Images.ICON_ADD))); + newChat.setToolTipText("Start a new chat."); + newChat.setEnabled(false); + toolbarManager.add(newChat); + toolbarActions.add(newChat); + + Action history = new Action("History") { + @Override + public void run() { + chatPanelClientInvoke(ChatViewUtils.API_0_8_0, "navigate", new ArrayList<>() { + { + add(ChatViewType.HISTORY); + } + }); + } + }; + history.setImageDescriptor(ImageDescriptor.createFromImage(Images.getIcon(Images.ICON_HISTORY))); + history.setToolTipText("Show chat history."); + newChat.setEnabled(false); + toolbarManager.add(history); + toolbarActions.add(history); + + // Browser + browser = new Browser(parent, Utils.isWindows() ? SWT.EDGE : SWT.WEBKIT); + setupThemeStyle(); + browser.setBackground(new Color(browserBgColor)); + browser.setVisible(false); + + browser.addProgressListener(new ProgressAdapter() { + @Override + public void completed(ProgressEvent event) { + handleLoaded(); + } + }); + + injectFunctions(); + load(); + serverConfigHolder.addConfigDidChangeListener(() -> { + reloadContent(false); + }); + statusInfoHolder.addStatusDidChangeListener(() -> { + reloadContent(false); + }); + + PlatformUI.getWorkbench().getActiveWorkbenchWindow().getSelectionService() + .addSelectionListener(new ISelectionListener() { + @Override + public void selectionChanged(IWorkbenchPart part, ISelection selection) { + if (selection instanceof ITextSelection) { + syncActiveSelectionRunnable.call(); + } + } + }); + } + + private DebouncedRunnable syncActiveSelectionRunnable = new DebouncedRunnable(() -> { + if (!isChatPanelLoaded) { + return; + } + EditorUtils.asyncExec(() -> { + try { + chatPanelClientInvoke(ChatViewUtils.API_0_8_0, "updateActiveSelection", new ArrayList<>() { + { + add(ChatViewUtils.getActiveEditorFileContext()); + } + }); + } catch (Exception e) { + // ignore + } + }); + }, 100); + + @Override + public void setFocus() { + browser.forceFocus(); + } + + @Override + public void dispose() { + if (browser != null && !browser.isDisposed()) { + browser.dispose(); + } + if (!browserFunctions.isEmpty()) { + browserFunctions.forEach(f -> f.dispose()); + browserFunctions.clear(); + } + super.dispose(); + } + + public void explainSelectedText() { + chatPanelClientInvoke(ChatViewUtils.API_0_8_0, "executeCommand", new ArrayList<>() { + { + add(ChatCommand.EXPLAIN); + } + }); + } + + public void fixSelectedText() { + // FIXME(@icycodes): collect the diagnostic message provided by IDE or LSP + chatPanelClientInvoke(ChatViewUtils.API_0_8_0, "executeCommand", new ArrayList<>() { + { + add(ChatCommand.FIX); + } + }); + } + + public void generateDocsForSelectedText() { + chatPanelClientInvoke(ChatViewUtils.API_0_8_0, "executeCommand", new ArrayList<>() { + { + add(ChatCommand.GENERATE_DOCS); + } + }); + } + + public void generateTestsForSelectedText() { + chatPanelClientInvoke(ChatViewUtils.API_0_8_0, "executeCommand", new ArrayList<>() { + { + add(ChatCommand.GENERATE_TESTS); + } + }); + } + + public void addSelectedTextAsContext() { + chatPanelClientInvoke(ChatViewUtils.API_0_8_0, "addRelevantContext", new ArrayList<>() { + { + add(ChatViewUtils.getActiveEditorFileContext(RangeStrategy.SELECTION)); + } + }); + } + + public void addActiveEditorAsContext() { + chatPanelClientInvoke(ChatViewUtils.API_0_8_0, "addRelevantContext", new ArrayList<>() { + { + add(ChatViewUtils.getActiveEditorFileContext(RangeStrategy.FILE)); + } + }); + } + + private RGB getColor(int coloirId, RGB defaultColor) { + Display display = browser.getDisplay(); + Color swtColor = display.getSystemColor(coloirId); + if (swtColor != null) { + return swtColor.getRGB(); + } + return defaultColor; + } + + private void setupThemeStyle() { + bgColor = getColor(SWT.COLOR_WIDGET_BACKGROUND, new RGB(32, 32, 32)); + isDark = (bgColor.red + bgColor.green + bgColor.blue) / 3 < 128; + + browserBgColor = getColor(SWT.COLOR_LIST_BACKGROUND, ColorUtil.blend(bgColor, new RGB(127, 127, 127), 75)); + fgColor = getColor(SWT.COLOR_LIST_FOREGROUND, isDark ? new RGB(255, 255, 255) : new RGB(0, 0, 0)); + borderColor = isDark ? new RGB(64, 64, 64) : new RGB(192, 192, 192); + inputColor = browserBgColor; + inputBorderColor = borderColor; + + primaryColor = getColor(SWT.COLOR_LINK_FOREGROUND, isDark ? new RGB(55, 148, 255) : new RGB(26, 133, 255)); + primaryFgColor = new RGB(255, 255, 255); + popoverColor = browserBgColor; + popoverFgColor = fgColor; + accentColor = isDark ? new RGB(4, 57, 94) : ColorUtil.blend(browserBgColor, new RGB(0, 0, 0), 80); + accentFgColor = fgColor; + ringColor = primaryColor; + + FontData[] fontData = browser.getDisplay().getSystemFont().getFontData(); + if (fontData.length > 0) { + font = fontData[0].getName(); + fontSize = fontData[0].getHeight(); + } + } + + private String buildCss() { + String css = ""; + if (browserBgColor != null) { + css += String.format("--sidebar-background: %s;", StringUtils.toHsl(browserBgColor)); + } + if (bgColor != null) { + css += String.format("--background: %s;", StringUtils.toHsl(bgColor)); + } + if (fgColor != null) { + css += String.format("--foreground: %s;", StringUtils.toHsl(fgColor)); + } + if (borderColor != null) { + css += String.format("--border: %s;", StringUtils.toHsl(borderColor)); + } + if (inputColor != null) { + css += String.format("--input: %s;", StringUtils.toHsl(inputColor)); + } + if (inputBorderColor != null) { + css += String.format("--input-border: %s;", StringUtils.toHsl(inputBorderColor)); + } + if (ringColor != null) { + css += String.format("--ring: %s;", StringUtils.toHsl(ringColor)); + } + if (primaryColor != null) { + css += String.format("--primary: %s;", StringUtils.toHsl(primaryColor)); + } + if (primaryFgColor != null) { + css += String.format("--primary-foreground: %s;", StringUtils.toHsl(primaryFgColor)); + } + if (popoverColor != null) { + css += String.format("--popover: %s;", StringUtils.toHsl(popoverColor)); + } + if (popoverFgColor != null) { + css += String.format("--popover-foreground: %s;", StringUtils.toHsl(popoverFgColor)); + } + if (accentColor != null) { + css += String.format("--accent: %s;", StringUtils.toHsl(accentColor)); + } + if (accentFgColor != null) { + css += String.format("--accent-foreground: %s;", StringUtils.toHsl(accentFgColor)); + } + if (font != null) { + css += String.format("font: %s;", font); + } + css += String.format("font-size: %spt;", fontSize); + return css; + } + + private List parseArguments(final Object[] arguments) { + if (arguments.length < 1) { + return List.of(); + } + return gson.fromJson(arguments[0].toString(), new TypeToken>() { + }); + } + + private Object serializeResult(final Object result) { + return gson.toJson(result); + } + + private void injectFunctions() { + browserFunctions.add(new BrowserFunction(browser, "tabbyChatPanelHandleChatPanelClientCreated") { + @Override + public Object function(Object[] arguments) { + List params = parseArguments(arguments); + logger.debug("chatPanelClientCreated: " + params); + if (params.size() < 1) { + return null; + } + initChatPanel((String) params.get(0)); + setToolbarItemsEnabled(true); + return null; + } + }); + + browserFunctions.add(new BrowserFunction(browser, "handleTabbyChatPanelResponse") { + @Override + public Object function(Object[] arguments) { + List params = parseArguments(arguments); + logger.debug("Response from chat panel: " + params); + if (params.size() < 3) { + return null; + } + String uuid = (String) params.get(0); + String errorMessage = (String) params.get(1); + Object result = params.get(2); + + CompletableFuture future = pendingChatPanelRequest.remove(uuid); + if (future == null) { + return null; + } + + if (errorMessage != null && !errorMessage.isEmpty()) { + future.completeExceptionally(new Exception(errorMessage)); + } else { + future.complete(result); + } + return null; + } + }); + + browserFunctions.add(new BrowserFunction(browser, "handleReload") { + @Override + public Object function(Object[] arguments) { + logger.debug("handleReload"); + reloadContent(true); + return null; + } + }); + + browserFunctions.add(new BrowserFunction(browser, "tabbyChatPanelRefresh") { + @Override + public Object function(Object[] arguments) { + logger.debug("tabbyChatPanelRefresh"); + reloadContent(true); + return null; + } + }); + + browserFunctions.add(new BrowserFunction(browser, "tabbyChatPanelOnApplyInEditor") { + @Override + public Object function(Object[] arguments) { + List params = parseArguments(arguments); + logger.debug("tabbyChatPanelOnApplyInEditor: " + params); + if (params.size() < 1) { + return null; + } + String content = (String) params.get(0); + ChatViewUtils.applyContentInEditor(content); + return null; + } + }); + + browserFunctions.add(new BrowserFunction(browser, "tabbyChatPanelOnCopy") { + @Override + public Object function(Object[] arguments) { + List params = parseArguments(arguments); + logger.debug("tabbyChatPanelOnCopy: " + params); + if (params.size() < 1) { + return null; + } + String content = (String) params.get(0); + ChatViewUtils.setClipboardContent(content); + return null; + } + }); + + browserFunctions.add(new BrowserFunction(browser, "tabbyChatPanelOnKeyboardEvent") { + @Override + public Object function(Object[] arguments) { + // FIXME: For macOS and windows, the eclipse keyboard shortcuts are not + // available when browser is focused, + // we should handle keyboard events here. + return null; + } + }); + + browserFunctions.add(new BrowserFunction(browser, "tabbyChatPanelOpenInEditor") { + @Override + public Object function(Object[] arguments) { + List params = parseArguments(arguments); + logger.debug("tabbyChatPanelOpenInEditor: " + params); + if (params.size() < 1) { + return null; + } + FileLocation fileLocation = ChatViewUtils.asFileLocation(params.get(0)); + boolean success = ChatViewUtils.openInEditor(fileLocation); + Object result = serializeResult(success); + logger.debug("tabbyChatPanelOpenInEditor result: " + result); + return result; + } + }); + + browserFunctions.add(new BrowserFunction(browser, "tabbyChatPanelOpenExternal") { + @Override + public Object function(Object[] arguments) { + List params = parseArguments(arguments); + logger.debug("tabbyChatPanelOpenExternal: " + params); + if (params.size() < 1) { + return null; + } + String url = (String) params.get(0); + ChatViewUtils.openExternal(url); + return null; + } + }); + + browserFunctions.add(new BrowserFunction(browser, "tabbyChatPanelReadWorkspaceGitRepositories") { + @Override + public Object function(Object[] arguments) { + logger.debug("tabbyChatPanelReadWorkspaceGitRepositories"); + List repositories = ChatViewUtils.readGitRepositoriesInWorkspace(); + Object result = serializeResult(repositories); + logger.debug("tabbyChatPanelReadWorkspaceGitRepositories result: " + result); + return result; + } + }); + + browserFunctions.add(new BrowserFunction(browser, "tabbyChatPanelGetActiveEditorSelection") { + @Override + public Object function(Object[] arguments) { + logger.debug("tabbyChatPanelGetActiveEditorSelection"); + EditorFileContext context = ChatViewUtils.getActiveEditorFileContext(); + Object result = serializeResult(context); + logger.debug("tabbyChatPanelGetActiveEditorSelection result: " + result); + return result; + } + }); + } + + private void load() { + try { + // Find chat panel html file + Bundle bundle = Platform.getBundle(Activator.PLUGIN_ID); + URL chatPanelPath = FileLocator.find(bundle, new Path("chat-panel/index.html")); + if (chatPanelPath == null) { + logger.error("Failed to find chat panel html file."); + return; + } + URL url = FileLocator.toFileURL(chatPanelPath); + browser.getDisplay().asyncExec(() -> { + logger.info("Load url: " + url.toString()); + browser.setUrl(url.toString()); + }); + } catch (Exception e) { + logger.error("Failed to load chat panel html file.", e); + } + } + + private void handleLoaded() { + isHtmlLoaded = true; + isChatPanelLoaded = false; + applyStyle(); + createChatPanelClient(); + reloadContent(false); + } + + private void reloadContent(boolean force) { + if (!isHtmlLoaded) { + return; + } + if (force) { + reloadContentForStatus(StatusInfo.Status.CONNECTING, false); + // delay to make the loading indicator visible for a bit + browser.getDisplay().timerExec(500, () -> { + LanguageServerService.getInstance().getServer().execute((server) -> { + IStatusService statusService = ((ILanguageServer) server).getStatusService(); + StatusRequestParams params = new StatusRequestParams(); + params.setRecheckConnection(true); + return statusService.getStatus(params); + }).thenAccept((statusInfo) -> { + String status = statusInfo.getStatus(); + reloadContentForStatus(status, true); + }); + }); + } else { + String status = statusInfoHolder.getStatusInfo().getStatus(); + reloadContentForStatus(status, false); + } + } + + private void reloadContentForStatus(String status, boolean force) { + if (status.equals(StatusInfo.Status.DISCONNECTED)) { + currentConfig = null; + updateContentToMessage("Cannot connect to Tabby server, please check your settings."); + } else if (status.equals(StatusInfo.Status.CONNECTING)) { + currentConfig = null; + updateContentToMessage("Connecting to Tabby server..."); + } else if (status.equals(StatusInfo.Status.UNAUTHORIZED)) { + currentConfig = null; + updateContentToMessage("Authorization required, please set your token in settings."); + } else { + Map serverHealth = statusInfoHolder.getStatusInfo().getServerHealth(); + String error = ChatViewUtils.checkServerHealth(serverHealth); + if (error != null) { + currentConfig = null; + updateContentToMessage(error); + } else { + // Load main + Config.ServerConfig config = serverConfigHolder.getConfig().getServer(); + if (config == null) { + currentConfig = null; + updateContentToMessage("Initializing..."); + } else if (force || currentConfig == null || currentConfig.getEndpoint() != config.getEndpoint() + || currentConfig.getToken() != config.getToken()) { + updateContentToMessage("Loading chat panel..."); + isChatPanelLoaded = false; + currentConfig = config; + loadChatPanel(); + } + } + } + } + + private void updateContentToMessage(String message) { + showMessage(message); + showChatPanel(false); + setToolbarItemsEnabled(false); + } + + private void updateContentToChatPanel() { + showMessage(null); + showChatPanel(true); + } + + private void setToolbarItemsEnabled(Boolean enabled) { + toolbarActions.forEach((action) -> { + action.setEnabled(enabled); + }); + } + + // execute js functions + + private void executeScript(String script) { + browser.getDisplay().asyncExec(() -> { + browser.execute(script); + }); + } + + private void showMessage(String message) { + if (message != null) { + executeScript(String.format("showMessage('%s')", message)); + } else { + executeScript("showMessage(undefined)"); + } + } + + private void showChatPanel(boolean visible) { + executeScript(String.format("showChatPanel(%s)", visible ? "true" : "false")); + } + + private void loadChatPanel() { + String chatUrl = String.format("%s/chat?client=eclipse", currentConfig.getEndpoint()); + executeScript(String.format("loadChatPanel('%s')", chatUrl)); + } + + private void applyStyle() { + String theme = isDark ? "dark" : "light"; + String css = buildCss(); + String json = gson.toJson(new HashMap<>() { + { + put("theme", theme); + put("css", css); + } + }); + executeScript(String.format("applyStyle('%s')", json)); + browser.setVisible(true); + } + + private void initChatPanel(String version) { + isChatPanelLoaded = true; + chatPanelApiVersion = version; + browser.getDisplay().timerExec(100, () -> { + updateContentToChatPanel(); + pendingScripts.forEach((script) -> { + executeScript(script); + }); + pendingScripts.clear(); + }); + chatPanelClientInvoke(ChatViewUtils.API_0_8_0, "init", new ArrayList<>() { + { + add(new HashMap<>() { + { + put("fetcherOptions", new HashMap<>() { + { + put("authorization", currentConfig.getToken()); + } + }); + } + }); + } + }); + chatPanelClientInvoke(ChatViewUtils.API_0_8_0, "updateTheme", new ArrayList<>() { + { + add(buildCss()); + add(isDark ? "dark" : "light"); + } + }); + } + + private String wrapJsFunction(String name) { + return String.format( + String.join("\n", + "function(...args) {", + " return new Promise((resolve, reject) => {", + " const paramsJson = JSON.stringify(args)", + " const result = %s(paramsJson)", + " resolve(JSON.parse(result))", + " });", + "}" + ), + name + ); + } + + private void createChatPanelClient() { + String script = String.format( + String.join("\n", + "TabbyChatPanel.createClient(getChatPanel(), {", + " refresh: %s,", + " onApplyInEditor: %s,", + " onCopy: %s,", + " onKeyboardEvent: %s,", + " openInEditor: %s,", + " openExternal: %s,", + " readWorkspaceGitRepositories: %s,", + " getActiveEditorSelection: %s,", + "}).then((client) => {", + " window.tabbyChatPanelClient = client;", + " const getVersion = client && client['0.9.0'] && client['0.9.0']['getVersion'];", + " if (getVersion && typeof getVersion === 'function') {", + " return getVersion();", + " } else {", + " return undefined;", + " }", + "}).then((version) => {", + " console.log('Tabby Chat Panel API version: ' + version);", + " const callback = %s;", + " callback(version);", + "});" + ), + wrapJsFunction("tabbyChatPanelRefresh"), + wrapJsFunction("tabbyChatPanelOnApplyInEditor"), + wrapJsFunction("tabbyChatPanelOnCopy"), + wrapJsFunction("tabbyChatPanelOnKeyboardEvent"), + wrapJsFunction("tabbyChatPanelOpenInEditor"), + wrapJsFunction("tabbyChatPanelOpenExternal"), + wrapJsFunction("tabbyChatPanelReadWorkspaceGitRepositories"), + wrapJsFunction("tabbyChatPanelGetActiveEditorSelection"), + wrapJsFunction("tabbyChatPanelHandleChatPanelClientCreated") + ); + executeScript(script); + } + + private CompletableFuture chatPanelClientInvoke(String version, String method, List params) { + CompletableFuture future = new CompletableFuture<>(); + String uuid = UUID.randomUUID().toString(); + pendingChatPanelRequest.put(uuid, future); + String paramsJson = StringUtils.escapeCharacters(gson.toJson(params)); + String responseCallbackFunction = "handleTabbyChatPanelResponse(results)"; + String script = String.format( + String.join("\n", + "(function() {", + " const client = window.tabbyChatPanelClient;", + " if (client && typeof client === 'object') {", + " const func = client['%s'] && client['%s']['%s']", + " if (func && typeof func === 'function') {", + " const params = JSON.parse('%s')", + " const resultPromise = func(...params)", + " if (resultPromise && typeof resultPromise.then === 'function') {", + " resultPromise.then(result => {", + " const results = JSON.stringify(['%s', null, result])", + " %s", + " }).catch(error => {", + " const results = JSON.stringify(['%s', error.message, null])", + " %s", + " })", + " } else {", + " const results = JSON.stringify(['%s', null, resultPromise])", + " %s", + " }", + " } else {", + " const results = JSON.stringify(['%s', 'Method not found: %s %s', null])", + " %s", + " }", + " } else {", + " const results = JSON.stringify(['%s', 'Tabby chat panel client is not connected.', null])", + " %s", + " }", + "})();" + ), + version, + version, + method, + paramsJson, + uuid, + responseCallbackFunction, + uuid, + responseCallbackFunction, + uuid, + responseCallbackFunction, + uuid, + version, + method, + responseCallbackFunction, + uuid, + responseCallbackFunction + ); + logger.debug("Request to chat panel: " + uuid + ", " + version + "," + method + ", " + paramsJson); + if (isChatPanelLoaded) { + executeScript(script); + } else { + pendingScripts.add(script); + } + return future; + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/ChatViewType.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/ChatViewType.java new file mode 100644 index 000000000000..11fc99de8b71 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/ChatViewType.java @@ -0,0 +1,6 @@ +package com.tabbyml.tabby4eclipse.chat; + +public abstract class ChatViewType { + public static final String NEW_CHAT = "new-chat"; + public static final String HISTORY = "history"; +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/ChatViewUtils.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/ChatViewUtils.java new file mode 100644 index 000000000000..df04e1cc7afb --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/ChatViewUtils.java @@ -0,0 +1,403 @@ +package com.tabbyml.tabby4eclipse.chat; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.Path; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.ITextSelection; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.swt.dnd.Clipboard; +import org.eclipse.swt.dnd.TextTransfer; +import org.eclipse.swt.dnd.Transfer; +import org.eclipse.swt.program.Program; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.ide.IDE; +import org.eclipse.ui.ide.ResourceUtil; +import org.eclipse.ui.texteditor.ITextEditor; + +import com.google.gson.Gson; +import com.tabbyml.tabby4eclipse.Logger; +import com.tabbyml.tabby4eclipse.Version; +import com.tabbyml.tabby4eclipse.editor.EditorUtils; +import com.tabbyml.tabby4eclipse.git.GitProvider; +import com.tabbyml.tabby4eclipse.lsp.protocol.GitRepositoryParams; + +public class ChatViewUtils { + private static final String ID = "com.tabbyml.tabby4eclipse.views.chat"; + + private static final String MIN_SERVER_VERSION = "0.27.0"; + private static Logger logger = new Logger("ChatView"); + + private static final Gson gson = new Gson(); + private static final Map gitRemoteUrlToLocalRoot = new HashMap<>(); + + public static final String API_0_8_0 = "0.8.0"; + public static final String API_0_9_0 = "0.9.0"; + + public static ChatView openChatView() { + IWorkbenchPage page = EditorUtils.getActiveWorkbenchPage(); + if (page != null) { + try { + page.showView(ID); + return (ChatView) page.findView(ID); + } catch (PartInitException e) { + logger.error("Failed to open chat view.", e); + } + } + return null; + } + + public static ChatView findOpenedView() { + IWorkbenchPage page = EditorUtils.getActiveWorkbenchPage(); + if (page != null && page.findView(ID) instanceof ChatView chatView) { + return chatView; + } + return null; + } + + public static String checkServerHealth(Map serverHealth) { + if (serverHealth == null) { + return "Connecting to Tabby server..."; + } + + if (serverHealth.get("webserver") == null || serverHealth.get("chat_model") == null) { + return "You need to launch the server with the chat model enabled; for example, use `--chat-model Qwen2-1.5B-Instruct`."; + } + + if (serverHealth.containsKey("version")) { + String version = null; + Object versionObj = serverHealth.get("version"); + if (versionObj instanceof String versionStr) { + version = versionStr; + } else if (versionObj instanceof Map versionMap) { + if (versionMap.containsKey("git_describe") + && versionMap.get("git_describe") instanceof String versionStr) { + version = versionStr; + } + } + if (version != null) { + Version parsedVersion = new Version(version); + Version requiredVersion = new Version(MIN_SERVER_VERSION); + if (!parsedVersion.isZero() && !parsedVersion.isGreaterOrEqualThan(requiredVersion)) { + return String.format( + "Tabby Chat requires Tabby server version %s or later. Your server is running version %s.", + MIN_SERVER_VERSION, version); + } + } + } + return null; + } + + // default: use selection if available, otherwise use the whole file + // selection: use selection if available, otherwise return null + // file: use the whole file + public static enum RangeStrategy { + DEFAULT, SELECTION, FILE + } + + public static EditorFileContext getActiveEditorFileContext() { + return getActiveEditorFileContext(RangeStrategy.DEFAULT); + } + + public static EditorFileContext getActiveEditorFileContext(RangeStrategy rangeStrategy) { + ITextEditor activeTextEditor = EditorUtils.getActiveTextEditor(); + if (activeTextEditor == null) { + return null; + } + IFile file = ResourceUtil.getFile(activeTextEditor.getEditorInput()); + ISelection selection = activeTextEditor.getSelectionProvider().getSelection(); + boolean hasSelection = false; + if (selection instanceof ITextSelection textSelection) { + if (!textSelection.isEmpty()) { + String content = textSelection.getText(); + if (!content.isBlank()) { + hasSelection = true; + } + } + } + + if (rangeStrategy == RangeStrategy.SELECTION || (rangeStrategy == RangeStrategy.DEFAULT && hasSelection)) { + if (selection instanceof ITextSelection textSelection) { + if (!textSelection.isEmpty()) { + String content = textSelection.getText(); + if (!content.isBlank()) { + return new EditorFileContext(fileToChatPanelFilepath(file), + new LineRange(textSelection.getStartLine() + 1, textSelection.getEndLine() + 1), + content); + } + } + } + } else { + IDocument document = EditorUtils.getDocument(activeTextEditor); + String content = document.get(); + if (!content.isBlank()) { + return new EditorFileContext(fileToChatPanelFilepath(file), null, content); + } + } + return null; + } + + public static boolean openInEditor(FileLocation fileLocation) { + if (fileLocation == null) { + return false; + } + Filepath filepath = fileLocation.getFilepath(); + try { + IFile file = chatPanelFilepathToFile(filepath); + if (file != null && file.exists()) { + IEditorPart editorPart = IDE.openEditor(EditorUtils.getActiveWorkbenchPage(), file); + + if (editorPart instanceof ITextEditor textEditor) { + IDocument document = textEditor.getDocumentProvider().getDocument(textEditor.getEditorInput()); + Object location = fileLocation.getLocation(); + Position position; + + if (location instanceof Number lineNumberValue) { + position = new Position(lineNumberValue.intValue() - 1, 0); + } else if (location instanceof Position positionValue) { + position = new Position(positionValue.getLine() - 1, positionValue.getCharacter() - 1); + } else if (location instanceof LineRange lineRangeValue) { + position = new Position(lineRangeValue.getStart() - 1, 0); + } else if (location instanceof PositionRange positionRangeValue) { + position = new Position(positionRangeValue.getStart().getLine() - 1, + positionRangeValue.getStart().getCharacter() - 1); + } else { + position = null; + } + + if (position != null) { + int offset = document.getLineOffset(position.getLine()) + position.getCharacter(); + textEditor.selectAndReveal(offset, 0); + } + } + return true; + } else { + return false; + } + } catch (Exception e) { + logger.error("Failed to open in editor.", e); + return false; + } + } + + public static void openExternal(String url) { + Program.launch(url); + } + + public static List readGitRepositoriesInWorkspace() { + List repositories = new ArrayList<>(); + IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); + IProject[] projects = workspaceRoot.getProjects(); + + for (IProject project : projects) { + try { + URI projectRootUri = project.getLocation().toFile().toURI(); + com.tabbyml.tabby4eclipse.lsp.protocol.GitRepository repo = GitProvider.getInstance() + .getRepository(new GitRepositoryParams(projectRootUri.toString())); + if (repo != null) { + repositories.add(new GitRepository(repo.getRemoteUrl())); + } + } catch (Exception e) { + logger.warn("Error when read git repository.", e); + } + } + return repositories; + } + + public static void setClipboardContent(String content) { + Display display = Display.getCurrent(); + if (display == null) { + display = Display.getDefault(); + } + + Clipboard clipboard = new Clipboard(display); + TextTransfer textTransfer = TextTransfer.getInstance(); + clipboard.setContents(new Object[] { content }, new Transfer[] { textTransfer }); + clipboard.dispose(); + } + + public static void applyContentInEditor(String content) { + logger.info("Apply content to the active text editor."); + ITextEditor activeTextEditor = EditorUtils.getActiveTextEditor(); + if (activeTextEditor != null) { + try { + IDocument document = activeTextEditor.getDocumentProvider() + .getDocument(activeTextEditor.getEditorInput()); + ITextSelection selection = (ITextSelection) activeTextEditor.getSelectionProvider().getSelection(); + document.replace(selection.getOffset(), selection.getLength(), content); + } catch (Exception e) { + logger.error("Failed to apply content to the active text editor.", e); + } + } + } + + public static Filepath fileToChatPanelFilepath(IFile file) { + if (file == null) { + return null; + } + URI fileUri = file.getLocationURI(); + String fileUriString = fileUri.toString(); + + com.tabbyml.tabby4eclipse.lsp.protocol.GitRepository gitRepo = GitProvider.getInstance() + .getRepository(new GitRepositoryParams(fileUriString)); + String gitUrl = (gitRepo != null) ? gitRepo.getRemoteUrl() : null; + if (gitUrl != null) { + gitRemoteUrlToLocalRoot.put(gitUrl, gitRepo.getRoot()); + } + if (gitUrl != null && fileUriString.startsWith(gitRepo.getRoot())) { + try { + String relativePath = new URI(gitRepo.getRoot()).relativize(fileUri).getPath(); + return new FilepathInGitRepository(relativePath, gitUrl); + } catch (Exception e) { + // nothing + } + } + + IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); + IProject[] projects = workspaceRoot.getProjects(); + for (IProject project : projects) { + try { + URI projectRootUri = project.getLocation().toFile().toURI(); + String projectRootUriString = projectRootUri.toString(); + if (fileUriString.startsWith(projectRootUriString)) { + String relativePath = projectRootUri.relativize(fileUri).getPath(); + return new FilepathInWorkspace(relativePath, projectRootUriString); + } + } catch (Exception e) { + // nothing + } + } + + return new FilepathUri(fileUriString); + } + + public static IFile chatPanelFilepathToFile(Filepath filepath) { + IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); + + switch (filepath.getKind()) { + case Filepath.Kind.URI: { + FilepathUri filepathUri = (FilepathUri) filepath; + try { + URI fileUri = new URI(filepathUri.getUri()); + IFile file = workspaceRoot.getFileForLocation(new Path(fileUri.getPath())); + if (file != null && file.exists()) { + return file; + } + } catch (URISyntaxException e) { + IProject[] projects = workspaceRoot.getProjects(); + for (IProject project : projects) { + URI projectRootUri = project.getLocation().toFile().toURI(); + URI fileUri = projectRootUri.resolve(filepathUri.getUri()); + IFile file = workspaceRoot.getFileForLocation(new Path(fileUri.getPath())); + if (file != null && file.exists()) { + return file; + } + } + } + break; + } + + case Filepath.Kind.WORKSPACE: { + FilepathInWorkspace filepathInWorkspace = (FilepathInWorkspace) filepath; + try { + URI fileUri = new URI(filepathInWorkspace.getBaseDir()).resolve(filepathInWorkspace.getFilepath()); + IFile file = workspaceRoot.getFileForLocation(new Path(fileUri.getPath())); + if (file != null && file.exists()) { + return file; + } + } catch (Exception e) { + // nothing + } + break; + } + + case Filepath.Kind.GIT: + FilepathInGitRepository filepathInGit = (FilepathInGitRepository) filepath; + String gitLocalRoot = gitRemoteUrlToLocalRoot.get(filepathInGit.getGitUrl()); + if (gitLocalRoot != null) { + try { + URI fileUri = new URI(gitLocalRoot).resolve(filepathInGit.getFilepath()); + IFile file = workspaceRoot.getFileForLocation(new Path(fileUri.getPath())); + if (file != null && file.exists()) { + return file; + } + } catch (Exception e) { + // nothing + } + } + break; + + default: + break; + } + + logger.warn("Failed to parse filepath: " + gson.toJson(filepath)); + return null; + } + + public static FileLocation asFileLocation(Object obj) { + if (!(obj instanceof Map)) { + return null; + } + + Map map = (Map) obj; + + if (!map.containsKey("filepath")) { + return null; + } + + Object filepathValue = map.get("filepath"); + Filepath filepath = null; + + if (filepathValue instanceof Map) { + Map filepathMap = (Map) filepathValue; + if (filepathMap.containsKey("kind")) { + String kind = (String) filepathMap.get("kind"); + if (Filepath.Kind.GIT.equals(kind)) { + filepath = gson.fromJson(gson.toJson(filepathValue), FilepathInGitRepository.class); + } else if (Filepath.Kind.WORKSPACE.equals(kind)) { + filepath = gson.fromJson(gson.toJson(filepathValue), FilepathInWorkspace.class); + } else if (Filepath.Kind.URI.equals(kind)) { + filepath = gson.fromJson(gson.toJson(filepathValue), FilepathUri.class); + } + } + } + + if (filepath == null) { + return null; + } + + Object locationValue = map.get("location"); + Object location = null; + + if (locationValue instanceof Number) { + location = locationValue; + } else if (locationValue instanceof Map) { + Map locationMap = (Map) locationValue; + if (locationMap.containsKey("line")) { + location = gson.fromJson(gson.toJson(locationValue), Position.class); + } else if (locationMap.containsKey("start")) { + Object startValue = locationMap.get("start"); + if (startValue instanceof Number) { + location = gson.fromJson(gson.toJson(locationValue), LineRange.class); + } else if (startValue instanceof Map) { + location = gson.fromJson(gson.toJson(locationValue), PositionRange.class); + } + } + } + + return new FileLocation(filepath, location); + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/EditorFileContext.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/EditorFileContext.java new file mode 100644 index 000000000000..a09fecc192a8 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/EditorFileContext.java @@ -0,0 +1,31 @@ +package com.tabbyml.tabby4eclipse.chat; + +public class EditorFileContext { + private final String kind; + private final Filepath filepath; + private final Range range; + private final String content; + + public EditorFileContext(Filepath filepath, Range range, String content) { + this.kind = "file"; + this.filepath = filepath; + this.range = range; + this.content = content; + } + + public String getKind() { + return kind; + } + + public Filepath getFilepath() { + return filepath; + } + + public Range getRange() { + return range; + } + + public String getContent() { + return content; + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/FileLocation.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/FileLocation.java new file mode 100644 index 000000000000..215595116add --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/FileLocation.java @@ -0,0 +1,19 @@ +package com.tabbyml.tabby4eclipse.chat; + +public class FileLocation { + private final Filepath filepath; + private final Object location; + + public FileLocation(Filepath filepath, Object location) { + this.filepath = filepath; + this.location = location; + } + + public Filepath getFilepath() { + return filepath; + } + + public Object getLocation() { + return location; + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/Filepath.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/Filepath.java new file mode 100644 index 000000000000..84c67166b4bc --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/Filepath.java @@ -0,0 +1,19 @@ +package com.tabbyml.tabby4eclipse.chat; + +public abstract class Filepath { + private final String kind; + + protected Filepath(String kind) { + this.kind = kind; + } + + public String getKind() { + return kind; + } + + public static class Kind { + public static final String GIT = "git"; + public static final String WORKSPACE = "workspace"; + public static final String URI = "uri"; + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/FilepathInGitRepository.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/FilepathInGitRepository.java new file mode 100644 index 000000000000..8c59edbbd12f --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/FilepathInGitRepository.java @@ -0,0 +1,30 @@ +package com.tabbyml.tabby4eclipse.chat; + +public class FilepathInGitRepository extends Filepath { + private final String filepath; + private final String gitUrl; + private final String revision; + + public FilepathInGitRepository(String filepath, String gitUrl) { + this(filepath, gitUrl, null); + } + + public FilepathInGitRepository(String filepath, String gitUrl, String revision) { + super(Kind.GIT); + this.filepath = filepath; + this.gitUrl = gitUrl; + this.revision = revision; + } + + public String getFilepath() { + return filepath; + } + + public String getGitUrl() { + return gitUrl; + } + + public String getRevision() { + return revision; + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/FilepathInWorkspace.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/FilepathInWorkspace.java new file mode 100644 index 000000000000..825df798cc2e --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/FilepathInWorkspace.java @@ -0,0 +1,20 @@ +package com.tabbyml.tabby4eclipse.chat; + +public class FilepathInWorkspace extends Filepath { + private final String filepath; + private final String baseDir; + + public FilepathInWorkspace(String filepath, String baseDir) { + super(Kind.WORKSPACE); + this.filepath = filepath; + this.baseDir = baseDir; + } + + public String getFilepath() { + return filepath; + } + + public String getBaseDir() { + return baseDir; + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/FilepathUri.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/FilepathUri.java new file mode 100644 index 000000000000..c47cfe6c6b9f --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/FilepathUri.java @@ -0,0 +1,14 @@ +package com.tabbyml.tabby4eclipse.chat; + +public class FilepathUri extends Filepath { + private final String uri; + + public FilepathUri(String uri) { + super(Kind.URI); + this.uri = uri; + } + + public String getUri() { + return uri; + } +} \ No newline at end of file diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/GitRepository.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/GitRepository.java new file mode 100644 index 000000000000..32f5c2d31ce5 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/GitRepository.java @@ -0,0 +1,13 @@ +package com.tabbyml.tabby4eclipse.chat; + +public class GitRepository { + private final String url; + + public GitRepository(String url) { + this.url = url; + } + + public String getUrl() { + return url; + } +} \ No newline at end of file diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/LineRange.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/LineRange.java new file mode 100644 index 000000000000..9fc0958077c4 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/LineRange.java @@ -0,0 +1,19 @@ +package com.tabbyml.tabby4eclipse.chat; + +public class LineRange extends Range { + private final int start; + private final int end; + + public LineRange(int start, int end) { + this.start = start; + this.end = end; + } + + public int getStart() { + return start; + } + + public int getEnd() { + return end; + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/Position.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/Position.java new file mode 100644 index 000000000000..cdfc4c2ff86c --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/Position.java @@ -0,0 +1,19 @@ +package com.tabbyml.tabby4eclipse.chat; + +public class Position { + private final int line; + private final int character; + + public Position(int line, int character) { + this.line = line; + this.character = character; + } + + public int getLine() { + return line; + } + + public int getCharacter() { + return character; + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/PositionRange.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/PositionRange.java new file mode 100644 index 000000000000..8d216a636e16 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/PositionRange.java @@ -0,0 +1,19 @@ +package com.tabbyml.tabby4eclipse.chat; + +public class PositionRange extends Range { + private final Position start; + private final Position end; + + public PositionRange(Position start, Position end) { + this.start = start; + this.end = end; + } + + public Position getStart() { + return start; + } + + public Position getEnd() { + return end; + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/Range.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/Range.java new file mode 100644 index 000000000000..5abc6534fd45 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/Range.java @@ -0,0 +1,4 @@ +package com.tabbyml.tabby4eclipse.chat; + +public abstract class Range { +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/OpenPreferences.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/OpenPreferences.java new file mode 100644 index 000000000000..e57f7f807b5b --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/OpenPreferences.java @@ -0,0 +1,16 @@ +package com.tabbyml.tabby4eclipse.commands; + +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; + +import com.tabbyml.tabby4eclipse.preferences.MainPreferencesPage; + +public class OpenPreferences extends AbstractHandler { + + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + MainPreferencesPage.openPreferences(); + return null; + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/chat/AddFileToChat.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/chat/AddFileToChat.java new file mode 100644 index 000000000000..d045ef670942 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/chat/AddFileToChat.java @@ -0,0 +1,30 @@ +package com.tabbyml.tabby4eclipse.commands.chat; + +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; + +import com.tabbyml.tabby4eclipse.Logger; +import com.tabbyml.tabby4eclipse.chat.ChatView; +import com.tabbyml.tabby4eclipse.chat.ChatViewUtils; +import com.tabbyml.tabby4eclipse.editor.EditorUtils; + +public class AddFileToChat extends AbstractHandler { + private Logger logger = new Logger("Commands.Chat.AddFileToChat"); + + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + logger.debug("Open chat view and send message: AddFileToChat"); + ChatView chatView = ChatViewUtils.openChatView(); + if (chatView != null) { + chatView.addActiveEditorAsContext(); + } + return null; + } + + @Override + public boolean isEnabled() { + return EditorUtils.getActiveTextEditor() != null; + } + +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/chat/AddSelectionToChat.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/chat/AddSelectionToChat.java new file mode 100644 index 000000000000..9503da501525 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/chat/AddSelectionToChat.java @@ -0,0 +1,31 @@ +package com.tabbyml.tabby4eclipse.commands.chat; + +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; + +import com.tabbyml.tabby4eclipse.Logger; +import com.tabbyml.tabby4eclipse.chat.ChatView; +import com.tabbyml.tabby4eclipse.chat.ChatViewUtils; +import com.tabbyml.tabby4eclipse.editor.EditorUtils; + +public class AddSelectionToChat extends AbstractHandler { + private Logger logger = new Logger("Commands.Chat.AddSelectionToChat"); + + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + logger.debug("Open chat view and send message: AddSelectionToChat"); + ChatView chatView = ChatViewUtils.openChatView(); + if (chatView != null) { + chatView.addSelectedTextAsContext(); + } + return null; + } + + @Override + public boolean isEnabled() { + String selectedText = EditorUtils.getSelectedText(); + return selectedText != null && !selectedText.isBlank(); + } + +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/chat/Explain.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/chat/Explain.java new file mode 100644 index 000000000000..1e7c5362f436 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/chat/Explain.java @@ -0,0 +1,31 @@ +package com.tabbyml.tabby4eclipse.commands.chat; + +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; + +import com.tabbyml.tabby4eclipse.Logger; +import com.tabbyml.tabby4eclipse.chat.ChatView; +import com.tabbyml.tabby4eclipse.chat.ChatViewUtils; +import com.tabbyml.tabby4eclipse.editor.EditorUtils; + +public class Explain extends AbstractHandler { + private Logger logger = new Logger("Commands.Chat.Expalin"); + + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + logger.debug("Open chat view and send message: Expalin"); + ChatView chatView = ChatViewUtils.openChatView(); + if (chatView != null) { + chatView.explainSelectedText(); + } + return null; + } + + @Override + public boolean isEnabled() { + String selectedText = EditorUtils.getSelectedText(); + return selectedText != null && !selectedText.isBlank(); + } + +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/chat/Fix.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/chat/Fix.java new file mode 100644 index 000000000000..01dd2290b577 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/chat/Fix.java @@ -0,0 +1,31 @@ +package com.tabbyml.tabby4eclipse.commands.chat; + +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; + +import com.tabbyml.tabby4eclipse.Logger; +import com.tabbyml.tabby4eclipse.chat.ChatView; +import com.tabbyml.tabby4eclipse.chat.ChatViewUtils; +import com.tabbyml.tabby4eclipse.editor.EditorUtils; + +public class Fix extends AbstractHandler { + private Logger logger = new Logger("Commands.Chat.Fix"); + + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + logger.debug("Open chat view and send message: Fix"); + ChatView chatView = ChatViewUtils.openChatView(); + if (chatView != null) { + chatView.fixSelectedText(); + } + return null; + } + + @Override + public boolean isEnabled() { + String selectedText = EditorUtils.getSelectedText(); + return selectedText != null && !selectedText.isBlank(); + } + +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/chat/GenerateDocs.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/chat/GenerateDocs.java new file mode 100644 index 000000000000..1b5830fe5816 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/chat/GenerateDocs.java @@ -0,0 +1,31 @@ +package com.tabbyml.tabby4eclipse.commands.chat; + +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; + +import com.tabbyml.tabby4eclipse.Logger; +import com.tabbyml.tabby4eclipse.chat.ChatView; +import com.tabbyml.tabby4eclipse.chat.ChatViewUtils; +import com.tabbyml.tabby4eclipse.editor.EditorUtils; + +public class GenerateDocs extends AbstractHandler { + private Logger logger = new Logger("Commands.Chat.GenerateDocs"); + + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + logger.debug("Open chat view and send message: GenerateDocs"); + ChatView chatView = ChatViewUtils.openChatView(); + if (chatView != null) { + chatView.generateDocsForSelectedText(); + } + return null; + } + + @Override + public boolean isEnabled() { + String selectedText = EditorUtils.getSelectedText(); + return selectedText != null && !selectedText.isBlank(); + } + +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/chat/GenerateTests.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/chat/GenerateTests.java new file mode 100644 index 000000000000..b6d399a637e1 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/chat/GenerateTests.java @@ -0,0 +1,31 @@ +package com.tabbyml.tabby4eclipse.commands.chat; + +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; + +import com.tabbyml.tabby4eclipse.Logger; +import com.tabbyml.tabby4eclipse.chat.ChatView; +import com.tabbyml.tabby4eclipse.chat.ChatViewUtils; +import com.tabbyml.tabby4eclipse.editor.EditorUtils; + +public class GenerateTests extends AbstractHandler { + private Logger logger = new Logger("Commands.Chat.GenerateTests"); + + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + logger.debug("Open chat view and send message: GenerateTests"); + ChatView chatView = ChatViewUtils.openChatView(); + if (chatView != null) { + chatView.generateTestsForSelectedText(); + } + return null; + } + + @Override + public boolean isEnabled() { + String selectedText = EditorUtils.getSelectedText(); + return selectedText != null && !selectedText.isBlank(); + } + +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/chat/OpenChatView.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/chat/OpenChatView.java new file mode 100644 index 000000000000..a9d7a30a666a --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/chat/OpenChatView.java @@ -0,0 +1,25 @@ +package com.tabbyml.tabby4eclipse.commands.chat; + +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; + +import com.tabbyml.tabby4eclipse.Logger; +import com.tabbyml.tabby4eclipse.chat.ChatViewUtils; + +public class OpenChatView extends AbstractHandler { + private Logger logger = new Logger("Commands.Chat.OpenChatView"); + + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + logger.debug("Open chat view."); + ChatViewUtils.openChatView(); + return null; + } + + @Override + public boolean isEnabled() { + return true; + } + +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/chat/ToggleChatView.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/chat/ToggleChatView.java new file mode 100644 index 000000000000..7a1d7cb7cc9b --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/chat/ToggleChatView.java @@ -0,0 +1,65 @@ +package com.tabbyml.tabby4eclipse.commands.chat; + +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IWorkbenchPage; + +import com.tabbyml.tabby4eclipse.DebouncedRunnable; +import com.tabbyml.tabby4eclipse.Logger; +import com.tabbyml.tabby4eclipse.chat.ChatView; +import com.tabbyml.tabby4eclipse.chat.ChatViewUtils; +import com.tabbyml.tabby4eclipse.editor.EditorUtils; + +public class ToggleChatView extends AbstractHandler { + private Logger logger = new Logger("Commands.Chat.ToggleChatView"); + private static final int DEBOUNCE_INTERVAL = 50; // ms + + // prevent toggle command called many times within a short time + private DebouncedRunnable runnable = new DebouncedRunnable(() -> { + EditorUtils.syncExec(() -> { + logger.debug("Toggle chat view."); + IWorkbenchPage page = EditorUtils.getActiveWorkbenchPage(); + if (page != null) { + boolean chatPanelFocused = page.getActivePart() == ChatViewUtils.findOpenedView(); + if (chatPanelFocused) { + // FIXME: Toggle between chat view and editor using keyboard shortcut is tested + // on Linux only. + // For macOS and windows, the eclipse keyboard shortcuts in not available when + // chat view web browser is focused, + // so this action can only switch to chat panel but cannot switch back for now. + logger.debug("Switch to Editor."); + IEditorPart editorPart = page.getActiveEditor(); + if (editorPart != null) { + page.activate(editorPart); + } + } else { + logger.debug("Switch to ChatView."); + ChatView chatView = ChatViewUtils.openChatView(); + if (chatView != null) { + page.activate(chatView); + + String selectedText = EditorUtils.getSelectedText(); + if (selectedText != null && !selectedText.isBlank()) { + logger.debug("Send message: AddSelectionToChat"); + chatView.addSelectedTextAsContext(); + } + } + } + } + }); + }, DEBOUNCE_INTERVAL); + + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + runnable.call(); + return null; + } + + @Override + public boolean isEnabled() { + return true; + } + +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/inlineCompletion/Accept.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/inlineCompletion/Accept.java new file mode 100644 index 000000000000..109e19954d6a --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/inlineCompletion/Accept.java @@ -0,0 +1,26 @@ +package com.tabbyml.tabby4eclipse.commands.inlineCompletion; + +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; + +import com.tabbyml.tabby4eclipse.Logger; +import com.tabbyml.tabby4eclipse.inlineCompletion.IInlineCompletionService.AcceptType; +import com.tabbyml.tabby4eclipse.inlineCompletion.InlineCompletionService; + +public class Accept extends AbstractHandler { + private Logger logger = new Logger("Commands.InlineCompletion.Accept"); + + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + logger.debug("Accept the current inline completion."); + InlineCompletionService.getInstance().accept(AcceptType.FULL_COMPLETION); + return null; + } + + @Override + public boolean isEnabled() { + return InlineCompletionService.getInstance().isCompletionItemVisible(); + } + +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/inlineCompletion/AcceptNextLine.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/inlineCompletion/AcceptNextLine.java new file mode 100644 index 000000000000..b013ad0771a0 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/inlineCompletion/AcceptNextLine.java @@ -0,0 +1,26 @@ +package com.tabbyml.tabby4eclipse.commands.inlineCompletion; + +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; + +import com.tabbyml.tabby4eclipse.Logger; +import com.tabbyml.tabby4eclipse.inlineCompletion.IInlineCompletionService.AcceptType; +import com.tabbyml.tabby4eclipse.inlineCompletion.InlineCompletionService; + +public class AcceptNextLine extends AbstractHandler { + private Logger logger = new Logger("Commands.InlineCompletion.AcceptNextLine"); + + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + logger.debug("Accept next line of the current inline completion."); + InlineCompletionService.getInstance().accept(AcceptType.NEXT_LINE); + return null; + } + + @Override + public boolean isEnabled() { + return InlineCompletionService.getInstance().isCompletionItemVisible(); + } + +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/inlineCompletion/AcceptNextWord.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/inlineCompletion/AcceptNextWord.java new file mode 100644 index 000000000000..4ddb31d2666f --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/inlineCompletion/AcceptNextWord.java @@ -0,0 +1,26 @@ +package com.tabbyml.tabby4eclipse.commands.inlineCompletion; + +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; + +import com.tabbyml.tabby4eclipse.Logger; +import com.tabbyml.tabby4eclipse.inlineCompletion.IInlineCompletionService.AcceptType; +import com.tabbyml.tabby4eclipse.inlineCompletion.InlineCompletionService; + +public class AcceptNextWord extends AbstractHandler { + private Logger logger = new Logger("Commands.InlineCompletion.AcceptNextLine"); + + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + logger.debug("Accept next word of the current inline completion."); + InlineCompletionService.getInstance().accept(AcceptType.NEXT_WORD); + return null; + } + + @Override + public boolean isEnabled() { + return InlineCompletionService.getInstance().isCompletionItemVisible(); + } + +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/inlineCompletion/Dismiss.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/inlineCompletion/Dismiss.java new file mode 100644 index 000000000000..5875817f69de --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/inlineCompletion/Dismiss.java @@ -0,0 +1,25 @@ +package com.tabbyml.tabby4eclipse.commands.inlineCompletion; + +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; + +import com.tabbyml.tabby4eclipse.Logger; +import com.tabbyml.tabby4eclipse.inlineCompletion.InlineCompletionService; + +public class Dismiss extends AbstractHandler { + private Logger logger = new Logger("Commands.InlineCompletion.Dismiss"); + + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + logger.debug("Dismiss the current inline completion."); + InlineCompletionService.getInstance().dismiss(); + return null; + } + + @Override + public boolean isEnabled() { + return InlineCompletionService.getInstance().isCompletionItemVisible(); + } + +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/inlineCompletion/Next.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/inlineCompletion/Next.java new file mode 100644 index 000000000000..0458ea2f3d35 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/inlineCompletion/Next.java @@ -0,0 +1,25 @@ +package com.tabbyml.tabby4eclipse.commands.inlineCompletion; + +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; + +import com.tabbyml.tabby4eclipse.Logger; +import com.tabbyml.tabby4eclipse.inlineCompletion.InlineCompletionService; + +public class Next extends AbstractHandler { + private Logger logger = new Logger("Commands.InlineCompletion.Next"); + + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + logger.debug("Cycle inline completion choices: Next."); + InlineCompletionService.getInstance().next(); + return null; + } + + @Override + public boolean isEnabled() { + return InlineCompletionService.getInstance().isCompletionItemVisible(); + } + +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/inlineCompletion/Previous.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/inlineCompletion/Previous.java new file mode 100644 index 000000000000..1723bd2e5cf6 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/inlineCompletion/Previous.java @@ -0,0 +1,25 @@ +package com.tabbyml.tabby4eclipse.commands.inlineCompletion; + +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; + +import com.tabbyml.tabby4eclipse.Logger; +import com.tabbyml.tabby4eclipse.inlineCompletion.InlineCompletionService; + +public class Previous extends AbstractHandler { + private Logger logger = new Logger("Commands.InlineCompletion.Next"); + + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + logger.debug("Cycle inline completion choices: Previous."); + InlineCompletionService.getInstance().previous(); + return null; + } + + @Override + public boolean isEnabled() { + return InlineCompletionService.getInstance().isCompletionItemVisible(); + } + +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/inlineCompletion/Trigger.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/inlineCompletion/Trigger.java new file mode 100644 index 000000000000..bcb43b7c13ac --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/inlineCompletion/Trigger.java @@ -0,0 +1,26 @@ +package com.tabbyml.tabby4eclipse.commands.inlineCompletion; + +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; + +import com.tabbyml.tabby4eclipse.Logger; +import com.tabbyml.tabby4eclipse.editor.EditorUtils; +import com.tabbyml.tabby4eclipse.inlineCompletion.InlineCompletionService; + +public class Trigger extends AbstractHandler { + private Logger logger = new Logger("Commands.InlineCompletion.Trigger"); + + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + logger.debug("Trigger inline completion manually."); + InlineCompletionService.getInstance().trigger(true); + return null; + } + + @Override + public boolean isEnabled() { + return EditorUtils.getActiveTextEditor().isEditable(); + } + +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/editor/EditorUtils.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/editor/EditorUtils.java new file mode 100644 index 000000000000..6d9d83388210 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/editor/EditorUtils.java @@ -0,0 +1,149 @@ +package com.tabbyml.tabby4eclipse.editor; + +import java.net.URI; + +import org.eclipse.core.resources.IFile; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IDocumentExtension4; +import org.eclipse.jface.text.ITextSelection; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.jface.text.ITextViewerExtension5; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.lsp4e.LSPEclipseUtils; +import org.eclipse.swt.SwtCallable; +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.contexts.IContextService; +import org.eclipse.ui.texteditor.ITextEditor; + +public class EditorUtils { + public static IWorkbenchPage getActiveWorkbenchPage() { + IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); + if (window != null) { + IWorkbenchPage page = window.getActivePage(); + return page; + } + return null; + } + + public static ITextEditor getActiveTextEditor() { + IWorkbenchPage page = getActiveWorkbenchPage(); + if (page != null) { + IEditorPart activeEditor = page.getActiveEditor(); + if (activeEditor instanceof ITextEditor textEditor) { + return textEditor; + } + } + return null; + } + + public static boolean isActiveEditor(ITextEditor textEditor) { + return textEditor == EditorUtils.getActiveTextEditor(); + } + + public static ITextViewer getTextViewer(ITextEditor textEditor) { + return (ITextViewer) textEditor.getAdapter(ITextViewer.class); + } + + public static IDocument getDocument(ITextEditor textEditor) { + return LSPEclipseUtils.getDocument(textEditor.getEditorInput()); + } + + public static URI getUri(ITextEditor textEditor) { + IDocument document = getDocument(textEditor); + return LSPEclipseUtils.toUri(document); + } + + public static StyledText getStyledTextWidget(ITextEditor textEditor) { + return getTextViewer(textEditor).getTextWidget(); + } + + public static Display getDisplay(ITextEditor textEditor) { + return getStyledTextWidget(textEditor).getDisplay(); + } + + public static IContextService getContextService(ITextEditor textEditor) { + return textEditor.getSite().getService(IContextService.class); + } + + public static void asyncExec(Runnable runnable) { + PlatformUI.getWorkbench().getDisplay().asyncExec(runnable); + } + + public static void asyncExec(ITextEditor textEditor, Runnable runnable) { + getDisplay(textEditor).asyncExec(runnable); + } + + public static void syncExec(Runnable runnable) { + PlatformUI.getWorkbench().getDisplay().syncExec(runnable); + } + + public static void syncExec(ITextEditor textEditor, Runnable runnable) { + getDisplay(textEditor).syncExec(runnable); + } + + public static T syncCall(SwtCallable callable) throws E { + return PlatformUI.getWorkbench().getDisplay().syncCall(callable); + } + + public static T syncCall(ITextEditor textEditor, SwtCallable callable) throws E { + return getDisplay(textEditor).syncCall(callable); + } + + public static long getDocumentModificationStamp(ITextEditor textEditor) { + IDocument document = getDocument(textEditor); + if (document instanceof IDocumentExtension4 documentExt) { + return documentExt.getModificationStamp(); + } else if (document != null) { + IFile file = LSPEclipseUtils.getFile(document); + if (file != null) { + return file.getModificationStamp(); + } + } + return IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP; + } + + public static int getCurrentOffsetInDocument(ITextEditor textEditor) throws IllegalStateException { + return syncCall(textEditor, () -> { + int offset = -1; + ISelection selection = textEditor.getSelectionProvider().getSelection(); + if (selection instanceof ITextSelection textSelection) { + offset = textSelection.getOffset(); + } + if (offset != -1) { + return offset; + } + + int offsetInWidget = getStyledTextWidget(textEditor).getCaretOffset(); + ITextViewer textViewer = getTextViewer(textEditor); + if (textViewer instanceof ITextViewerExtension5 textViewerExt) { + offset = textViewerExt.widgetOffset2ModelOffset(offsetInWidget); + } + if (offset != -1) { + return offset; + } + + throw new IllegalStateException("Failed to get current offset in document."); + }); + } + + public static String getSelectedText() { + ITextEditor editor = getActiveTextEditor(); + if (editor != null) { + return getSelectedText(editor); + } + return null; + } + + public static String getSelectedText(ITextEditor textEditor) { + ISelection selection = textEditor.getSelectionProvider().getSelection(); + if (selection instanceof ITextSelection textSelection) { + return textSelection.getText(); + } + return null; + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/editor/WorkbenchPartListener.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/editor/WorkbenchPartListener.java new file mode 100644 index 000000000000..4160ee36f32a --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/editor/WorkbenchPartListener.java @@ -0,0 +1,120 @@ +package com.tabbyml.tabby4eclipse.editor; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jface.text.IDocument; +import org.eclipse.lsp4e.LanguageServerWrapper; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IEditorReference; +import org.eclipse.ui.IPartListener; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchPart; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.texteditor.ITextEditor; + +import com.tabbyml.tabby4eclipse.Logger; +import com.tabbyml.tabby4eclipse.inlineCompletion.trigger.InlineCompletionTrigger; +import com.tabbyml.tabby4eclipse.lsp.LanguageServerService; + +public class WorkbenchPartListener implements IPartListener { + public static WorkbenchPartListener getInstance() { + return LazyHolder.INSTANCE; + } + + private static class LazyHolder { + private static final WorkbenchPartListener INSTANCE = new WorkbenchPartListener(); + } + + private Logger logger = new Logger("WorkbenchPartListener"); + private List editors = new ArrayList<>(); + + public void init() { + try { + logger.debug("Init WorkbenchListener."); + IWorkbenchWindow[] windows = PlatformUI.getWorkbench().getWorkbenchWindows(); + for (IWorkbenchWindow window : windows) { + IWorkbenchPage[] pages = window.getPages(); + for (IWorkbenchPage page : pages) { + IEditorReference[] editorReferences = page.getEditorReferences(); + + page.addPartListener(this); + + for (IEditorReference editorRef : editorReferences) { + IEditorPart editorPart = editorRef.getEditor(false); + if (editorPart instanceof ITextEditor textEditor) { + IDocument document = EditorUtils.getDocument(textEditor); + URI uri = EditorUtils.getUri(textEditor); + getLanguageServerWrapper().connectDocument(document); + logger.info("Connect " + uri.toString() + " to LS when init."); + + InlineCompletionTrigger.getInstance().register(textEditor); + } + } + } + } + } catch (Exception e) { + logger.error("Failed to init WorkbenchListener.", e); + } + } + + @Override + public void partOpened(IWorkbenchPart part) { + try { + if (part != null) { + logger.debug("Handle event partOpened: " + part.toString()); + ITextEditor textEditor = (ITextEditor) part.getAdapter(ITextEditor.class); + if (textEditor != null) { + IDocument document = EditorUtils.getDocument(textEditor); + URI uri = EditorUtils.getUri(textEditor); + getLanguageServerWrapper().connectDocument(document); + logger.info("Connect " + uri.toString() + " to LS when partOpened."); + + InlineCompletionTrigger.getInstance().register(textEditor); + editors.add(textEditor); + } + } + } catch (Exception e) { + logger.error("Failed to handle event partOpened.", e); + } + } + + @Override + public void partClosed(IWorkbenchPart part) { + try { + if (part != null) { + logger.debug("Handle event partClosed: " + part.toString()); + ITextEditor textEditor = (ITextEditor) part.getAdapter(ITextEditor.class); + if (editors.contains(textEditor)) { + URI uri = EditorUtils.getUri(textEditor); + getLanguageServerWrapper().disconnect(uri); + logger.info("Disconnect " + uri.toString() + " from LS."); + + InlineCompletionTrigger.getInstance().unregister(textEditor); + editors.remove(textEditor); + } + } + } catch (Exception e) { + logger.error("Failed to handle event partClosed.", e); + } + } + + @Override + public void partActivated(IWorkbenchPart part) { + } + + @Override + public void partDeactivated(IWorkbenchPart part) { + } + + @Override + public void partBroughtToTop(IWorkbenchPart part) { + } + + private LanguageServerWrapper getLanguageServerWrapper() { + LanguageServerWrapper server = LanguageServerService.getInstance().getServer(); + return server; + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/git/EclipseJGitProvider.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/git/EclipseJGitProvider.java new file mode 100644 index 000000000000..611fbe78790f --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/git/EclipseJGitProvider.java @@ -0,0 +1,99 @@ +package com.tabbyml.tabby4eclipse.git; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.net.URI; +import java.util.List; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.storage.file.FileRepositoryBuilder; +import org.eclipse.jgit.transport.RemoteConfig; + +import com.tabbyml.tabby4eclipse.Logger; +import com.tabbyml.tabby4eclipse.lsp.protocol.GitDiffParams; +import com.tabbyml.tabby4eclipse.lsp.protocol.GitDiffResult; +import com.tabbyml.tabby4eclipse.lsp.protocol.GitRepository; +import com.tabbyml.tabby4eclipse.lsp.protocol.GitRepositoryParams; + +public class EclipseJGitProvider implements IGitProvider { + private Logger logger = new Logger("EclipseJGitProvider"); + + @Override + public boolean isAvailable() { + try { + FileRepositoryBuilder builder = new FileRepositoryBuilder(); + return true; + } catch (NoClassDefFoundError e) { + logger.error("org.eclipse.jgit is not available."); + return false; + } + } + + @Override + public GitRepository getRepository(GitRepositoryParams params) { + try { + URI uri = new URI(params.getUri()); + File file = new File(uri); + FileRepositoryBuilder builder = new FileRepositoryBuilder(); + Repository repository = builder.findGitDir(file).build(); + if (repository != null) { + GitRepository gitRepository = new GitRepository(); + + String root = repository.getDirectory().getParentFile().toURI().toString(); + gitRepository.setRoot(root); + + List remotes = RemoteConfig.getAllRemoteConfigs(repository.getConfig()); + String firstRemoteUrl = !remotes.isEmpty() ? remotes.get(0).getURIs().get(0).toString() : null; + String originRemoteUrl = null; + String upstreamRemoteUrl = null; + for (RemoteConfig remote : remotes) { + if (remote.getName().equals("origin")) { + originRemoteUrl = remote.getURIs().get(0).toString(); + } else if (remote.getName().equals("upstream")) { + upstreamRemoteUrl = remote.getURIs().get(0).toString(); + } + } + if (originRemoteUrl != null) { + gitRepository.setRemoteUrl(originRemoteUrl); + } else if (upstreamRemoteUrl != null) { + gitRepository.setRemoteUrl(upstreamRemoteUrl); + } else { + gitRepository.setRemoteUrl(firstRemoteUrl); + } + return gitRepository; + } else { + return null; + } + } catch (Exception e) { + logger.debug("Failed to get repository for: " + params.getUri()); + return null; + } + } + + @Override + public GitDiffResult getDiff(GitDiffParams params) { + try { + URI repoPath = new URI(params.getRepository()); + File repoDir = new File(repoPath); + FileRepositoryBuilder builder = new FileRepositoryBuilder(); + Repository repository = builder.findGitDir(repoDir).build(); + if (repository != null) { + try (Git git = new Git(repository)) { + ByteArrayOutputStream outStream = new ByteArrayOutputStream(); + git.diff().setCached(params.getCached()).setOutputStream(outStream).call(); + String diff = outStream.toString("UTF-8"); + + GitDiffResult result = new GitDiffResult(diff); + outStream.close(); + return result; + } + } else { + return null; + } + } catch (Exception e) { + logger.debug("Failed to get diff for: " + params.getRepository()); + return null; + } + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/git/GitProvider.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/git/GitProvider.java new file mode 100644 index 000000000000..40c3d2f0386e --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/git/GitProvider.java @@ -0,0 +1,30 @@ +package com.tabbyml.tabby4eclipse.git; + +import com.tabbyml.tabby4eclipse.Logger; + +public class GitProvider { + public static IGitProvider getInstance() { + return LazyHolder.INSTANCE; + } + + private static class LazyHolder { + private static final IGitProvider INSTANCE = createInstance(); + } + + private static Logger logger = new Logger("GitProvider"); + + public static IGitProvider createInstance() { + try { + IGitProvider eclipseJGitProvider = new EclipseJGitProvider(); + if (eclipseJGitProvider.isAvailable()) { + return eclipseJGitProvider; + } else { + logger.info("Eclipse JGit Provider is not available."); + } + } catch (NoClassDefFoundError e) { + logger.info("Eclipse JGit Provider is not available."); + } + return new NoOpGitProvider(); + } + +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/git/IGitProvider.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/git/IGitProvider.java new file mode 100644 index 000000000000..e154bd56992e --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/git/IGitProvider.java @@ -0,0 +1,23 @@ +package com.tabbyml.tabby4eclipse.git; + +import com.tabbyml.tabby4eclipse.lsp.protocol.GitDiffParams; +import com.tabbyml.tabby4eclipse.lsp.protocol.GitDiffResult; +import com.tabbyml.tabby4eclipse.lsp.protocol.GitRepository; +import com.tabbyml.tabby4eclipse.lsp.protocol.GitRepositoryParams; + +public interface IGitProvider { + /** + * Return false or throw NoClassDefFoundError if this provider is not available + */ + default public boolean isAvailable() throws NoClassDefFoundError { + return false; + } + + default public GitRepository getRepository(GitRepositoryParams params) { + return null; + } + + default public GitDiffResult getDiff(GitDiffParams params) { + return null; + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/git/NoOpGitProvider.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/git/NoOpGitProvider.java new file mode 100644 index 000000000000..4d70679a58e4 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/git/NoOpGitProvider.java @@ -0,0 +1,5 @@ +package com.tabbyml.tabby4eclipse.git; + +public class NoOpGitProvider implements IGitProvider { + +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/IInlineCompletionService.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/IInlineCompletionService.java new file mode 100644 index 000000000000..cc192ea29138 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/IInlineCompletionService.java @@ -0,0 +1,60 @@ +package com.tabbyml.tabby4eclipse.inlineCompletion; + +public interface IInlineCompletionService { + /** + * Returns whether the completion item ghost text is currently visible in the + * active text editor. + */ + public boolean isCompletionItemVisible(); + + /** + * Returns whether the completion request is running. + */ + public boolean isLoading(); + + /** + * Validate the current completion context is still valid for the current caret + * position in the active text editor. + */ + public boolean isValid(); + + /** + * Trigger an inline completion request at the current caret position of the + * active text editor. + * + * @param isManualTrigger Whether to trigger manually or automatically. + * Automatic trigger will only generate one choice, and + * manual trigger will generate multiple choices. + */ + public void trigger(boolean isManualTrigger); + + /** + * Cycle to the next choice if it exists. If the current inline completion is + * auto-triggered, a manual trigger will be called to attempt to generate + * multiple choices. + */ + public void next(); + + /** + * Cycle to the previous choice if it exists. If the current inline completion + * is auto-triggered, a manual trigger will be called to attempt to generate + * multiple choices. + */ + public void previous(); + + enum AcceptType { + FULL_COMPLETION, NEXT_LINE, NEXT_WORD, + } + + /** + * Accept the current completion item ghost text. + * + * @param type the type of completion to accept. + */ + public void accept(AcceptType type); + + /** + * Dismiss the current completion item ghost text. + */ + public void dismiss(); +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/InlineCompletionItem.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/InlineCompletionItem.java new file mode 100644 index 000000000000..31610be70a3d --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/InlineCompletionItem.java @@ -0,0 +1,46 @@ +package com.tabbyml.tabby4eclipse.inlineCompletion; + +import com.tabbyml.tabby4eclipse.lsp.protocol.CompletionEventId; + +public class InlineCompletionItem { + + public static class ReplaceRange { + private int prefixLength; + private int suffixLength; + + public ReplaceRange(int prefix, int suffix) { + this.prefixLength = prefix; + this.suffixLength = suffix; + } + + public int getPrefixLength() { + return prefixLength; + } + + public int getSuffixLength() { + return suffixLength; + } + } + + private String insertText; + private ReplaceRange replaceRange; + private CompletionEventId eventId; + + public InlineCompletionItem(String insertText, ReplaceRange replaceRange, CompletionEventId eventId) { + this.insertText = insertText; + this.replaceRange = replaceRange; + this.eventId = eventId; + } + + public String getInsertText() { + return insertText; + } + + public ReplaceRange getReplaceRange() { + return replaceRange; + } + + public CompletionEventId getEventId() { + return eventId; + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/InlineCompletionList.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/InlineCompletionList.java new file mode 100644 index 000000000000..cf968439d9fb --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/InlineCompletionList.java @@ -0,0 +1,21 @@ +package com.tabbyml.tabby4eclipse.inlineCompletion; + +import java.util.List; + +public class InlineCompletionList { + private boolean isIncomplete; + private List items; + + public InlineCompletionList(boolean isIncomplete, List items) { + this.isIncomplete = isIncomplete; + this.items = items; + } + + public boolean isIncomplete() { + return isIncomplete; + } + + public List getItems() { + return items; + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/InlineCompletionService.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/InlineCompletionService.java new file mode 100644 index 000000000000..de0feff533b6 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/InlineCompletionService.java @@ -0,0 +1,422 @@ +package com.tabbyml.tabby4eclipse.inlineCompletion; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.ITextSelection; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.jface.text.TextSelection; +import org.eclipse.lsp4e.LSPEclipseUtils; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.ui.contexts.IContextActivation; +import org.eclipse.ui.contexts.IContextService; +import org.eclipse.ui.texteditor.ITextEditor; + +import com.tabbyml.tabby4eclipse.Logger; +import com.tabbyml.tabby4eclipse.editor.EditorUtils; +import com.tabbyml.tabby4eclipse.inlineCompletion.renderer.InlineCompletionRendererSupport; +import com.tabbyml.tabby4eclipse.lsp.LanguageServerService; +import com.tabbyml.tabby4eclipse.lsp.protocol.CompletionEventId; +import com.tabbyml.tabby4eclipse.lsp.protocol.EventParams; +import com.tabbyml.tabby4eclipse.lsp.protocol.ILanguageServer; +import com.tabbyml.tabby4eclipse.lsp.protocol.ITelemetryService; +import com.tabbyml.tabby4eclipse.lsp.protocol.ITextDocumentServiceExt; +import com.tabbyml.tabby4eclipse.lsp.protocol.InlineCompletionParams; +import com.tabbyml.tabby4eclipse.preferences.PreferencesService; + +public class InlineCompletionService implements IInlineCompletionService { + public static IInlineCompletionService getInstance() { + return LazyHolder.INSTANCE; + } + + private static class LazyHolder { + private static final IInlineCompletionService INSTANCE = new InlineCompletionService(); + } + + private static final String INLINE_COMPLETION_VISIBLE_CONTEXT_ID = "com.tabbyml.tabby4eclipse.inlineCompletionVisible"; + + private Logger logger = new Logger("InlineCompletionService"); + private InlineCompletionRendererSupport renderer = new InlineCompletionRendererSupport(); + private InlineCompletionContext current; + private IContextActivation inlineCompletionVisibleContext; + + @Override + public boolean isCompletionItemVisible() { + ITextEditor textEditor = EditorUtils.getActiveTextEditor(); + ITextViewer textViewer = EditorUtils.getTextViewer(textEditor); + boolean matched = current != null && current.request != null && current.request.textEditor == textEditor + && current.response != null && textViewer != null && textViewer == renderer.getCurrentTextViewer() + && renderer.getCurrentCompletionItem() != null + && renderer.getCurrentCompletionItem() == current.response.getActiveCompletionItem(); + logger.debug("isCompletionItemVisible: " + matched); + return matched; + } + + @Override + public boolean isLoading() { + return current != null && current.job != null && !current.job.isDone(); + } + + @Override + public boolean isValid() { + ITextEditor textEditor = EditorUtils.getActiveTextEditor(); + int offset = EditorUtils.getCurrentOffsetInDocument(textEditor); + long modificationStamp = EditorUtils.getDocumentModificationStamp(textEditor); + return current != null && current.request != null && current.request.textEditor == textEditor + && current.request.offset == offset && current.request.modificationStamp == modificationStamp; + } + + @Override + public void trigger(boolean isManualTrigger) { + boolean autoTriggerEnabled = PreferencesService.getInstance().getInlineCompletionTriggerAuto(); + if (!autoTriggerEnabled && !isManualTrigger) { + return; + } + ITextEditor textEditor = EditorUtils.getActiveTextEditor(); + int offset = EditorUtils.getCurrentOffsetInDocument(textEditor); + long modificationStamp = EditorUtils.getDocumentModificationStamp(textEditor); + logger.info("Provide inline completion for TextEditor " + textEditor.getTitle() + " at offset " + offset + + " with modification stamp " + modificationStamp); + renderer.hide(); + deactivateInlineCompletionVisibleContext(textEditor); + if (current != null) { + if (current.job != null && !current.job.isDone()) { + logger.info("Cancel the current job due to new request."); + current.job.cancel(true); + } + current = null; + } + + ITextViewer textViewer = EditorUtils.getTextViewer(textEditor); + InlineCompletionContext.Request request = new InlineCompletionContext.Request(textEditor, offset, + modificationStamp, isManualTrigger); + InlineCompletionParams params = request.toInlineCompletionParams(); + if (params == null) { + return; + } + CompletableFuture job = LanguageServerService + .getInstance().getServer().execute((server) -> { + ITextDocumentServiceExt textDocumentService = ((ILanguageServer) server) + .getTextDocumentServiceExt(); + return textDocumentService.inlineCompletion(params); + }); + job.thenAccept((completionList) -> { + if (completionList == null || request != current.request) { + return; + } + try { + InlineCompletionList list = request.convertInlineCompletionList(completionList); + current.response = new InlineCompletionContext.Response(list); + renderer.show(textViewer, current.response.getActiveCompletionItem()); + activateInlineCompletionVisibleContext(textEditor); + EventParams eventParams = buildTelemetryEventParams(EventParams.Type.VIEW); + postTelemetryEvent(eventParams); + } catch (BadLocationException e) { + logger.error("Failed to show inline completion.", e); + } + }); + InlineCompletionContext context = new InlineCompletionContext(request, job, null); + current = context; + } + + @Override + public void next() { + cycle(1); + } + + @Override + public void previous() { + cycle(-1); + } + + private void cycle(int step) { + ITextEditor textEditor = EditorUtils.getActiveTextEditor(); + ITextViewer textViewer = EditorUtils.getTextViewer(textEditor); + + logger.info("Cycle inline completion choices, step: " + step); + if (current == null || current.request == null || current.response == null) { + return; + } + if (current.response.completionList.isIncomplete()) { + int index = current.response.getItemIndex(); + int offset = EditorUtils.getCurrentOffsetInDocument(textEditor); + long modificationStamp = EditorUtils.getDocumentModificationStamp(textEditor); + InlineCompletionContext.Request request = new InlineCompletionContext.Request(textEditor, offset, + modificationStamp, true); + InlineCompletionParams params = request.toInlineCompletionParams(); + if (params == null) { + return; + } + CompletableFuture job = LanguageServerService + .getInstance().getServer().execute((server) -> { + ITextDocumentServiceExt textDocumentService = ((ILanguageServer) server) + .getTextDocumentServiceExt(); + return textDocumentService.inlineCompletion(params); + }); + job.thenAccept((completionList) -> { + if (completionList == null || request != current.request) { + return; + } + try { + InlineCompletionList list = request.convertInlineCompletionList(completionList); + int cycleIndex = calcCycleIndex(index, list.getItems().size(), step); + current.response = new InlineCompletionContext.Response(list, cycleIndex); + renderer.show(textViewer, current.response.getActiveCompletionItem()); + EventParams eventParams = buildTelemetryEventParams(EventParams.Type.VIEW); + postTelemetryEvent(eventParams); + } catch (BadLocationException e) { + logger.error("Failed to show inline completion.", e); + } + }); + InlineCompletionContext context = new InlineCompletionContext(request, job, current.response); + current = context; + } else { + int cycleIndex = calcCycleIndex(current.response.getItemIndex(), + current.response.completionList.getItems().size(), step); + current.response.setItemIndex(cycleIndex); + renderer.show(textViewer, current.response.getActiveCompletionItem()); + EventParams eventParams = buildTelemetryEventParams(EventParams.Type.VIEW); + postTelemetryEvent(eventParams); + } + } + + private int calcCycleIndex(int index, int listSize, int step) { + if (listSize <= 1) { + return index; + } + int cycleIndex = index + step; + while (cycleIndex >= listSize) { + cycleIndex -= listSize; + } + if (cycleIndex < 0) { + cycleIndex += listSize; + } + return cycleIndex; + } + + @Override + public void accept(AcceptType acceptType) { + ITextEditor textEditor = EditorUtils.getActiveTextEditor(); + logger.info("Accept inline completion in TextEditor " + textEditor.toString()); + if (current == null || current.request == null || current.response == null) { + return; + } + int offset = current.request.offset; + InlineCompletionItem item = current.response.getActiveCompletionItem(); + EventParams eventParams = buildTelemetryEventParams(EventParams.Type.SELECT, + acceptType == AcceptType.FULL_COMPLETION ? null : "line"); + + renderer.hide(); + deactivateInlineCompletionVisibleContext(textEditor); + current = null; + + int prefixReplaceLength = item.getReplaceRange().getPrefixLength(); + int suffixReplaceLength = item.getReplaceRange().getSuffixLength(); + String text = item.getInsertText().substring(prefixReplaceLength); + String textToInsert; + + if (acceptType == AcceptType.NEXT_WORD) { + Pattern pattern = Pattern.compile("\\w+|\\W+"); + Matcher matcher = pattern.matcher(text); + if (matcher.find()) { + textToInsert = matcher.group(); + } else { + textToInsert = text; + } + } else if (acceptType == AcceptType.NEXT_LINE) { + List lines = List.of(text.split("\n")); + String line = lines.get(0); + if (text.isEmpty() && lines.size() > 1) { + line += "\n"; + line += lines.get(1); + } + textToInsert = line; + } else { + textToInsert = text; + } + + if (textToInsert.isEmpty()) { + return; + } + + IDocument document = EditorUtils.getDocument(textEditor); + EditorUtils.syncExec(textEditor, () -> { + try { + document.replace(offset, suffixReplaceLength, textToInsert); + ITextSelection selection = new TextSelection(offset + textToInsert.length(), 0); + textEditor.getSelectionProvider().setSelection(selection); + postTelemetryEvent(eventParams); + } catch (BadLocationException e) { + logger.error("Failed to accept inline completion.", e); + } + }); + } + + @Override + public void dismiss() { + if (renderer.getCurrentCompletionItem() != null) { + logger.info("Dismiss inline completion."); + EventParams eventParams = buildTelemetryEventParams(EventParams.Type.DISMISS); + renderer.hide(); + ITextEditor textEditor = EditorUtils.getActiveTextEditor(); + deactivateInlineCompletionVisibleContext(textEditor); + postTelemetryEvent(eventParams); + } + if (current != null) { + if (current.job != null && !current.job.isDone()) { + logger.info("Cancel the current job due to dismissed."); + current.job.cancel(true); + } + current = null; + } + } + + private EventParams buildTelemetryEventParams(String type) { + return buildTelemetryEventParams(type, null); + } + + private EventParams buildTelemetryEventParams(String type, String selectKind) { + InlineCompletionItem item = this.renderer.getCurrentCompletionItem(); + if (item != null && item == current.response.getActiveCompletionItem()) { + EventParams params = new EventParams(); + params.setType(type); + params.setSelectKind(selectKind); + params.setCompletionEventId(item.getEventId()); + params.setViewId(this.renderer.getCurrentViewId()); + params.setElapsed(this.renderer.getCurrentDisplayedTime()); + return params; + } + return null; + } + + private void postTelemetryEvent(EventParams params) { + if (params != null) { + LanguageServerService.getInstance().getServer().execute((server) -> { + ITelemetryService telemetryService = ((ILanguageServer) server).getTelemetryService(); + telemetryService.event(params); + return null; + }); + } + } + + private void activateInlineCompletionVisibleContext(ITextEditor editor) { + IContextService contextService = EditorUtils.getContextService(editor); + if (contextService != null) { + logger.debug("Activating inline completion visible context."); + inlineCompletionVisibleContext = contextService.activateContext(INLINE_COMPLETION_VISIBLE_CONTEXT_ID); + } + } + + private void deactivateInlineCompletionVisibleContext(ITextEditor editor) { + IContextService contextService = EditorUtils.getContextService(editor); + if (contextService != null && inlineCompletionVisibleContext != null) { + logger.debug("Deactivating inline completion visible context."); + contextService.deactivateContext(inlineCompletionVisibleContext); + } + } + + private class InlineCompletionContext { + private static class Request { + private Logger logger = new Logger("InlineCompletionContext.Request"); + + private ITextEditor textEditor; + private IDocument document; + private int offset; + private long modificationStamp; + private boolean manually; + + public Request(ITextEditor textEditor, int offset, long modificationStamp, boolean manually) { + this.textEditor = textEditor; + this.document = EditorUtils.getDocument(textEditor); + this.offset = offset; + this.modificationStamp = modificationStamp; + this.manually = manually; + } + + public InlineCompletionParams toInlineCompletionParams() { + try { + InlineCompletionParams.InlineCompletionContext context = new InlineCompletionParams.InlineCompletionContext( + manually ? InlineCompletionParams.InlineCompletionTriggerKind.Invoked + : InlineCompletionParams.InlineCompletionTriggerKind.Automatic, + null); + TextDocumentIdentifier documentIdentifier = LSPEclipseUtils.toTextDocumentIdentifier(document); + Position position = LSPEclipseUtils.toPosition(offset, document); + InlineCompletionParams params = new InlineCompletionParams(context, documentIdentifier, position); + return params; + } catch (BadLocationException e) { + logger.error("Failed to create InlineCompletionParams.", e); + return null; + } + } + + public InlineCompletionList convertInlineCompletionList( + com.tabbyml.tabby4eclipse.lsp.protocol.InlineCompletionList list) throws BadLocationException { + boolean isIncomplete = list.isIncomplete(); + List items = new ArrayList<>(); + for (int i = 0; i < list.getItems().size(); i++) { + com.tabbyml.tabby4eclipse.lsp.protocol.InlineCompletionItem item = list.getItems().get(i); + String insertText = item.getInsertText(); + int prefixReplaceLength = offset - LSPEclipseUtils.toOffset(item.getRange().getStart(), document); + int suffixReplaceLength = LSPEclipseUtils.toOffset(item.getRange().getEnd(), document) - offset; + InlineCompletionItem.ReplaceRange replaceRange = new InlineCompletionItem.ReplaceRange( + prefixReplaceLength, suffixReplaceLength); + CompletionEventId eventId = null; + if (item.getData() != null) { + eventId = item.getData().getEventId(); + } + items.add(new InlineCompletionItem(insertText, replaceRange, eventId)); + logger.debug("Converted InlineCompletionItem " + i + ": " + insertText + "\n replace range: " + + replaceRange.getPrefixLength() + ", " + replaceRange.getSuffixLength()); + } + return new InlineCompletionList(isIncomplete, items); + } + } + + private static class Response { + private InlineCompletionList completionList; + private int itemIndex; + + public Response(InlineCompletionList completionList) { + this.completionList = completionList; + this.itemIndex = 0; + } + + public Response(InlineCompletionList completionList, int itemIndex) { + this.completionList = completionList; + this.itemIndex = itemIndex; + } + + public InlineCompletionItem getActiveCompletionItem() { + if (itemIndex >= 0 && itemIndex < completionList.getItems().size()) { + return completionList.getItems().get(itemIndex); + } + return null; + } + + public int getItemIndex() { + return itemIndex; + } + + public void setItemIndex(int itemIndex) { + this.itemIndex = itemIndex; + } + } + + private Request request; + private CompletableFuture job; + private Response response; + + public InlineCompletionContext(Request request, + CompletableFuture job, Response response) { + this.request = request; + this.job = job; + this.response = response; + } + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/renderer/IInlineCompletionItemRenderer.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/renderer/IInlineCompletionItemRenderer.java new file mode 100644 index 000000000000..6f857207a64e --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/renderer/IInlineCompletionItemRenderer.java @@ -0,0 +1,16 @@ +package com.tabbyml.tabby4eclipse.inlineCompletion.renderer; + +import org.eclipse.jface.text.ITextViewer; + +import com.tabbyml.tabby4eclipse.inlineCompletion.InlineCompletionItem; + +public interface IInlineCompletionItemRenderer { + /** + * Update the inline completion item in the viewer at the current caret + * position. + * + * @param textViewer The viewer to show the inline completion item in. + * @param item The inline completion item to show. + */ + public abstract void updateInlineCompletionItem(ITextViewer textViewer, InlineCompletionItem item); +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/renderer/InlineCompletionItemRenderer.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/renderer/InlineCompletionItemRenderer.java new file mode 100644 index 000000000000..97834c169e20 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/renderer/InlineCompletionItemRenderer.java @@ -0,0 +1,31 @@ +package com.tabbyml.tabby4eclipse.inlineCompletion.renderer; + +public class InlineCompletionItemRenderer { + public static IInlineCompletionItemRenderer getInstance() { + return LazyHolder.INSTANCE; + } + + private static class LazyHolder { + private static final IInlineCompletionItemRenderer INSTANCE = createInstance(); + } + + private static String TABBY4ECLIPSE_EXPERIMENTAL_RENDERER_TEXTPAINTER = System + .getenv("TABBY4ECLIPSE_EXPERIMENTAL_RENDERER_TEXTPAINTER"); + private static boolean EXPERIMENTAL_RENDERER_TEXTPAINTER = TABBY4ECLIPSE_EXPERIMENTAL_RENDERER_TEXTPAINTER != null + && !TABBY4ECLIPSE_EXPERIMENTAL_RENDERER_TEXTPAINTER.isEmpty(); + + private static String TABBY4ECLIPSE_EXPERIMENTAL_RENDERER_TEXTPAINTER2 = System + .getenv("TABBY4ECLIPSE_EXPERIMENTAL_RENDERER_TEXTPAINTER2"); + private static boolean EXPERIMENTAL_RENDERER_TEXTPAINTER2 = TABBY4ECLIPSE_EXPERIMENTAL_RENDERER_TEXTPAINTER2 != null + && !TABBY4ECLIPSE_EXPERIMENTAL_RENDERER_TEXTPAINTER2.isEmpty(); + + public static IInlineCompletionItemRenderer createInstance() { + if (EXPERIMENTAL_RENDERER_TEXTPAINTER) { + return new InlineCompletionItemTextPainter(); + } + if (EXPERIMENTAL_RENDERER_TEXTPAINTER2) { + return new InlineCompletionItemTextPainter2(); + } + return new InlineCompletionItemTextPainter2(); + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/renderer/InlineCompletionItemTextPainter.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/renderer/InlineCompletionItemTextPainter.java new file mode 100644 index 000000000000..c1b14e0f22e5 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/renderer/InlineCompletionItemTextPainter.java @@ -0,0 +1,420 @@ +package com.tabbyml.tabby4eclipse.inlineCompletion.renderer; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import org.eclipse.jface.text.IPaintPositionManager; +import org.eclipse.jface.text.IPainter; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.jface.text.ITextViewerExtension2; +import org.eclipse.jface.text.Position; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.StyleRange; +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.events.PaintEvent; +import org.eclipse.swt.events.PaintListener; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.graphics.FontMetrics; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.GlyphMetrics; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.widgets.Display; + +import com.tabbyml.tabby4eclipse.Logger; +import com.tabbyml.tabby4eclipse.StringUtils; +import com.tabbyml.tabby4eclipse.StringUtils.TextWithTabs; +import com.tabbyml.tabby4eclipse.inlineCompletion.InlineCompletionItem; + +public class InlineCompletionItemTextPainter implements IInlineCompletionItemRenderer { + private Logger logger = new Logger("InlineCompletionRenderer.InlineCompletionItemTextPainter"); + private Map painters = new HashMap<>(); + + @Override + public void updateInlineCompletionItem(ITextViewer textViewer, InlineCompletionItem item) { + getPainter(textViewer).update(item); + } + + private GhostTextPainter getPainter(ITextViewer viewer) { + GhostTextPainter painter = painters.get(viewer); + if (painter == null) { + painter = new GhostTextPainter(viewer); + painters.put(viewer, painter); + } + return painter; + } + + private class GhostTextPainter implements IPainter, PaintListener { + private ITextViewer viewer; + + private InlineCompletionItem item; + private int offset; + + private IPaintPositionManager positionManager; + private Font font; + private List modifiedLinesVerticalIndent = new ArrayList<>(); + private List modifiedGlyphMetrics = new ArrayList<>(); + private List> paintFunctions = new ArrayList<>(); + + public GhostTextPainter(ITextViewer viewer) { + this.viewer = viewer; + getDisplay().syncExec(() -> { + ((ITextViewerExtension2) this.viewer).addPainter(this); + getWidget().addPaintListener(this); + }); + } + + public void update(InlineCompletionItem item) { + if (this.item != item) { + this.item = item; + getDisplay().syncExec(() -> { + this.offset = getWidget().getCaretOffset(); + cleanup(); + setupPainting(); + getWidget().redraw(); + }); + } + } + + @Override + public void paintControl(PaintEvent event) { + paintFunctions.forEach((fn) -> { + fn.accept(event.gc); + }); + } + + @Override + public void paint(int reason) { + } + + @Override + public void deactivate(boolean redraw) { + } + + @Override + public void dispose() { + logger.debug("Painter dispose called."); + getWidget().removePaintListener(this); + if (font != null) { + font.dispose(); + font = null; + } + } + + @Override + public void setPositionManager(IPaintPositionManager manager) { + this.positionManager = manager; + } + + private StyledText getWidget() { + return viewer.getTextWidget(); + } + + private Display getDisplay() { + return getWidget().getDisplay(); + } + + private void cleanup() { + try { + paintFunctions.clear(); + + StyledText widget = getWidget(); + modifiedLinesVerticalIndent.forEach((modifiedLineVerticalIndent) -> { + Position position = modifiedLineVerticalIndent.position; + int line = widget.getLineAtOffset(position.getOffset()); + positionManager.unmanagePosition(position); + int indent = modifiedLineVerticalIndent.indent; + int modifiedIndent = modifiedLineVerticalIndent.modifiedIndent; + // Find the line to restore the indent + int lineToRestore = -1; + int delta = 0; + while (delta < widget.getLineCount()) { + lineToRestore = line + delta; + if (lineToRestore >= 0 && lineToRestore < widget.getLineCount() + && widget.getLineVerticalIndent(lineToRestore) == modifiedIndent) { + break; + } + lineToRestore = line - delta; + if (lineToRestore >= 0 && lineToRestore < widget.getLineCount() + && widget.getLineVerticalIndent(lineToRestore) == modifiedIndent) { + break; + } + delta++; + } + if (lineToRestore >= 0 && lineToRestore < widget.getLineCount()) { + widget.setLineVerticalIndent(lineToRestore, indent); + logger.debug("Restore LineVerticalIndent: " + lineToRestore + " -> " + indent); + } + }); + modifiedLinesVerticalIndent.clear(); + + StyleRange[] styleRanges = getWidget().getStyleRanges(); + for (StyleRange styleRange : styleRanges) { + if (modifiedGlyphMetrics.contains(styleRange.metrics)) { + styleRange.metrics = null; + getWidget().setStyleRange(styleRange); + logger.debug("Restore StyleRange:" + styleRange.start + " -> " + styleRange.metrics); + } + } + modifiedGlyphMetrics.clear(); + } catch (Exception e) { + logger.error("Failed to cleanup renderer.", e); + } + } + + private void setupPainting() { + if (item == null) { + return; + } + StyledText widget = getWidget(); + + int prefixReplaceLength = item.getReplaceRange().getPrefixLength(); + int suffixReplaceLength = item.getReplaceRange().getSuffixLength(); + String text = item.getInsertText().substring(prefixReplaceLength); + if (text.isEmpty()) { + return; + } + logger.debug("Begin setupPainting..."); + + int currentLineEndOffset; + int nextLineNumber = widget.getLineAtOffset(offset) + 1; + if (nextLineNumber < widget.getLineCount()) { + currentLineEndOffset = widget.getOffsetAtLine(nextLineNumber) - 1; + } else { + currentLineEndOffset = widget.getCharCount() - 1; + } + String currentLineSuffix = ""; + if (offset < widget.getCharCount() && offset < currentLineEndOffset) { + currentLineSuffix = widget.getText(offset, currentLineEndOffset); + } + + String textCurrentLine; + String textSuffixLines; + int firstLineBreakIndex = text.indexOf("\n"); + if (firstLineBreakIndex == -1) { + textCurrentLine = text; + textSuffixLines = ""; + } else { + textCurrentLine = text.substring(0, firstLineBreakIndex); + textSuffixLines = text.substring(firstLineBreakIndex + 1); + } + + if (suffixReplaceLength == 0 || currentLineSuffix.isEmpty()) { + // No replace range to handle + if (textSuffixLines.isEmpty()) { + drawInsertPartText(offset, textCurrentLine); + } else { + if (!textCurrentLine.isEmpty()) { + drawOverwriteText(offset, textCurrentLine); + } + drawSuffixLines(offset, textSuffixLines + currentLineSuffix); + } + } else if (suffixReplaceLength == 1) { + // Replace range contains one char + char replaceChar = currentLineSuffix.charAt(0); + int replaceCharIndex = textCurrentLine.indexOf(replaceChar); + if (replaceCharIndex > 0) { + // If textCurrentLine contain the replaceChar + // InsertPart is substring of textCurrentLine that before the replaceChar + // AppendPart is substring of textCurrentLine that after the replaceChar + String insertPart = textCurrentLine.substring(0, replaceCharIndex); + String appendPart = textCurrentLine.substring(replaceCharIndex + 1); + if (!insertPart.isEmpty()) { + drawInsertPartText(offset, insertPart); + } + if (!appendPart.isEmpty()) { + if (textSuffixLines.isEmpty()) { + drawInsertPartText(offset + 1, appendPart); + } else { + drawOverwriteText(offset + 1, appendPart); + } + } + } else { + drawReplacePartText(offset, textCurrentLine, currentLineSuffix.substring(0, 1)); + } + if (!textSuffixLines.isEmpty()) { + drawSuffixLines(offset, textSuffixLines + currentLineSuffix.substring(1)); + } + } else { + // Replace range contains multiple chars + if (textSuffixLines.isEmpty()) { + drawReplacePartText(offset, textCurrentLine, currentLineSuffix.substring(0, suffixReplaceLength)); + } else { + if (!textCurrentLine.isEmpty()) { + drawOverwriteText(offset, textCurrentLine); + } + drawSuffixLines(offset, textSuffixLines + currentLineSuffix.substring(suffixReplaceLength)); + } + } + logger.debug("End setupPainting."); + } + + private void drawOverwriteText(int offset, String text) { + logger.debug("drawCurrentLineText:" + offset + ":" + text); + StyledText widget = getWidget(); + TextWithTabs textWithTabs = StringUtils.splitLeadingTabs(text); + + paintFunctions.add((gc) -> { + // Draw ghost text + setStyleToGhostText(gc); + int spaceWidth = gc.textExtent(" ").x; + int tabWidth = textWithTabs.getTabs() * widget.getTabs() * spaceWidth; + Point location = widget.getLocationAtOffset(offset); + gc.drawString(textWithTabs.getText(), location.x + tabWidth, location.y); + }); + } + + private void drawInsertPartText(int offset, String text) { + drawReplacePartText(offset, text, ""); + } + + private void drawReplacePartText(int offset, String text, String replacedText) { + logger.debug("drawReplacePartText:" + offset + ":" + text + ":" + replacedText); + StyledText widget = getWidget(); + TextWithTabs textWithTabs = StringUtils.splitLeadingTabs(text); + + int targetOffset = offset + replacedText.length(); + if (targetOffset >= widget.getCharCount()) { + // End of document, draw the ghost text only + paintFunctions.add((gc) -> { + // Draw ghost text + setStyleToGhostText(gc); + int spaceWidth = gc.textExtent(" ").x; + int tabWidth = textWithTabs.getTabs() * widget.getTabs() * spaceWidth; + Point location = widget.getLocationAtOffset(offset); + gc.drawString(textWithTabs.getText(), location.x + tabWidth, location.y); + }); + + } else { + // otherwise, draw the ghost text, and move target char after the ghost text + String targetChar = widget.getText(targetOffset, targetOffset); + StyleRange originStyleRange; + if (widget.getStyleRangeAtOffset(targetOffset) != null) { + originStyleRange = widget.getStyleRangeAtOffset(targetOffset); + logger.debug( + "Find origin StyleRange:" + originStyleRange.start + " -> " + originStyleRange.metrics); + } else { + originStyleRange = new StyleRange(); + originStyleRange.start = targetOffset; + originStyleRange.length = 1; + logger.debug("Create StyleRange:" + originStyleRange.start + " -> " + originStyleRange.metrics); + } + + paintFunctions.add((gc) -> { + // Draw ghost text + setStyleToGhostText(gc); + int spaceWidth = gc.textExtent(" ").x; + int tabWidth = textWithTabs.getTabs() * widget.getTabs() * spaceWidth; + int ghostTextWidth = tabWidth + gc.stringExtent(textWithTabs.getText()).x; + Point location = widget.getLocationAtOffset(offset); + gc.drawString(textWithTabs.getText(), location.x + tabWidth, location.y); + + // Leave the space for the ghost text + setStyle(gc, originStyleRange); + int shiftWidth = ghostTextWidth - gc.stringExtent(replacedText).x; + int targetCharWidth = gc.stringExtent(targetChar).x; + + StyleRange currentStyleRange = widget.getStyleRangeAtOffset(targetOffset); + if (currentStyleRange != null && currentStyleRange.metrics != null + && currentStyleRange.metrics.width == shiftWidth + targetCharWidth) { + // nothing to do + } else { + StyleRange styleRange = (StyleRange) originStyleRange.clone(); + styleRange.start = targetOffset; + styleRange.length = 1; + FontMetrics fontMetrics = gc.getFontMetrics(); + GlyphMetrics glyphMetrics = new GlyphMetrics(fontMetrics.getAscent(), fontMetrics.getDescent(), + shiftWidth + targetCharWidth); + modifiedGlyphMetrics.add(glyphMetrics); + styleRange.metrics = glyphMetrics; + widget.setStyleRange(styleRange); + logger.debug("Set StyleRange:" + styleRange.start + " -> " + styleRange.metrics); + } + + // Draw the moved char + Point targetCharLocation = widget.getLocationAtOffset(targetOffset); + gc.drawString(targetChar, targetCharLocation.x + shiftWidth, targetCharLocation.y, true); + }); + } + } + + private void drawSuffixLines(int offset, String text) { + logger.debug("drawSuffixLines:" + offset + ":" + text); + StyledText widget = getWidget(); + int lineHeight = widget.getLineHeight(); + List lines = text.lines().toList(); + + // Leave the space for the ghost text + int nextLine = widget.getLineAtOffset(offset) + 1; + if (nextLine < widget.getLineCount()) { + int lineCount = lines.size(); + int originVerticalIndent = widget.getLineVerticalIndent(nextLine); + Position position = new Position(widget.getOffsetAtLine(nextLine), 0); + positionManager.managePosition(position); + int modifiedVerticalIndent = originVerticalIndent + lineCount * lineHeight; + modifiedLinesVerticalIndent + .add(new ModifiedLineVerticalIndent(position, originVerticalIndent, modifiedVerticalIndent)); + widget.setLineVerticalIndent(nextLine, modifiedVerticalIndent); + logger.debug("Set LineVerticalIndent:" + nextLine + " -> " + modifiedVerticalIndent); + } + + List linesTextWithTab = new ArrayList<>(); + for (String line : lines) { + linesTextWithTab.add(StringUtils.splitLeadingTabs(line)); + } + + paintFunctions.add((gc) -> { + // Draw ghost text + setStyleToGhostText(gc); + int spaceWidth = gc.textExtent(" ").x; + Point location = widget.getLocationAtOffset(offset); + int y = location.y; + for (TextWithTabs textWithTabs : linesTextWithTab) { + int x = widget.getLeftMargin() + textWithTabs.getTabs() * widget.getTabs() * spaceWidth; + y += lineHeight; + gc.drawString(textWithTabs.getText(), x, y, true); + } + }); + } + + private void setStyle(GC gc, StyleRange styleRange) { + if (styleRange.foreground != null) { + gc.setForeground(styleRange.foreground); + } else { + gc.setForeground(getWidget().getForeground()); + } + if (styleRange.font != null) { + gc.setFont(styleRange.font); + } else { + gc.setFont(getWidget().getFont()); + } + } + + private void setStyleToGhostText(GC gc) { + gc.setForeground(getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY)); + if (font == null || font.isDisposed()) { + FontData[] fontData = getWidget().getFont().getFontData(); + for (int i = 0; i < fontData.length; ++i) { + fontData[i].setStyle(fontData[i].getStyle() | SWT.ITALIC); + } + font = new Font(getDisplay(), fontData); + } + gc.setFont(font); + } + + private static class ModifiedLineVerticalIndent { + private Position position; + private int indent; + private int modifiedIndent; + + public ModifiedLineVerticalIndent(Position position, int indent, int modifiedIndent) { + this.position = position; + this.indent = indent; + this.modifiedIndent = modifiedIndent; + } + } + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/renderer/InlineCompletionItemTextPainter2.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/renderer/InlineCompletionItemTextPainter2.java new file mode 100644 index 000000000000..09f09c019b50 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/renderer/InlineCompletionItemTextPainter2.java @@ -0,0 +1,389 @@ +package com.tabbyml.tabby4eclipse.inlineCompletion.renderer; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import org.eclipse.jface.text.IPaintPositionManager; +import org.eclipse.jface.text.IPainter; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.jface.text.ITextViewerExtension2; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.StyleRange; +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.custom.StyledTextLineSpacingProvider; +import org.eclipse.swt.events.PaintEvent; +import org.eclipse.swt.events.PaintListener; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.graphics.FontMetrics; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.GlyphMetrics; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.widgets.Display; + +import com.tabbyml.tabby4eclipse.Logger; +import com.tabbyml.tabby4eclipse.StringUtils; +import com.tabbyml.tabby4eclipse.StringUtils.TextWithTabs; +import com.tabbyml.tabby4eclipse.inlineCompletion.InlineCompletionItem; + +public class InlineCompletionItemTextPainter2 implements IInlineCompletionItemRenderer { + private Logger logger = new Logger("InlineCompletionRenderer.InlineCompletionItemTextPainter"); + private Map painters = new HashMap<>(); + + @Override + public void updateInlineCompletionItem(ITextViewer textViewer, InlineCompletionItem item) { + getPainter(textViewer).update(item); + } + + private GhostTextPainter getPainter(ITextViewer viewer) { + GhostTextPainter painter = painters.get(viewer); + if (painter == null) { + painter = new GhostTextPainter(viewer); + painters.put(viewer, painter); + } + return painter; + } + + private class GhostTextPainter implements IPainter, PaintListener, StyledTextLineSpacingProvider { + private ITextViewer viewer; + + private InlineCompletionItem item; + private int offset; + private int lineIndex; + private int lineSpacing; + + private Font font; + private List modifiedGlyphMetrics = new ArrayList<>(); + private List> paintFunctions = new ArrayList<>(); + + public GhostTextPainter(ITextViewer viewer) { + this.viewer = viewer; + getDisplay().syncExec(() -> { + ((ITextViewerExtension2) this.viewer).addPainter(this); + getWidget().addPaintListener(this); + getWidget().setLineSpacingProvider(this); + }); + } + + public void update(InlineCompletionItem item) { + if (this.item != item) { + this.item = item; + getDisplay().syncExec(() -> { + cleanup(); + this.offset = getWidget().getCaretOffset(); + this.lineIndex = getWidget().getLineAtOffset(offset); + setupPainting(); + getWidget().redraw(); + }); + } + } + + @Override + public void paintControl(PaintEvent event) { + paintFunctions.forEach((fn) -> { + fn.accept(event.gc); + }); + } + + @Override + public void paint(int reason) { + } + + @Override + public void deactivate(boolean redraw) { + } + + @Override + public void dispose() { + logger.debug("Painter dispose called."); + getWidget().removePaintListener(this); + if (font != null) { + font.dispose(); + font = null; + } + } + + @Override + public void setPositionManager(IPaintPositionManager manager) { + } + + @Override + public Integer getLineSpacing(int lineIndex) { + if (this.lineIndex == lineIndex) { + return lineSpacing; + } + return null; + } + + private StyledText getWidget() { + return viewer.getTextWidget(); + } + + private Display getDisplay() { + return getWidget().getDisplay(); + } + + private void cleanup() { + try { + paintFunctions.clear(); + lineSpacing = 0; + + StyleRange[] styleRanges = getWidget().getStyleRanges(); + for (StyleRange styleRange : styleRanges) { + if (modifiedGlyphMetrics.contains(styleRange.metrics)) { + styleRange.metrics = null; + getWidget().setStyleRange(styleRange); + logger.debug("Restore StyleRange: " + styleRange.start + ": " + styleRange.metrics); + } + } + modifiedGlyphMetrics.clear(); + } catch (Exception e) { + logger.error("Failed to cleanup renderer.", e); + } + } + + private void setupPainting() { + if (item == null) { + return; + } + StyledText widget = getWidget(); + + int prefixReplaceLength = item.getReplaceRange().getPrefixLength(); + int suffixReplaceLength = item.getReplaceRange().getSuffixLength(); + String text = item.getInsertText().substring(prefixReplaceLength); + if (text.isEmpty()) { + return; + } + logger.debug("Begin setupPainting..."); + + int currentLineEndOffset; + int nextLineNumber = widget.getLineAtOffset(offset) + 1; + if (nextLineNumber < widget.getLineCount()) { + currentLineEndOffset = widget.getOffsetAtLine(nextLineNumber) - 1; + } else { + currentLineEndOffset = widget.getCharCount() - 1; + } + String currentLineSuffix = ""; + if (offset < widget.getCharCount() && offset < currentLineEndOffset) { + currentLineSuffix = widget.getText(offset, currentLineEndOffset); + } + + String textCurrentLine; + String textSuffixLines; + int firstLineBreakIndex = text.indexOf("\n"); + if (firstLineBreakIndex == -1) { + textCurrentLine = text; + textSuffixLines = ""; + } else { + textCurrentLine = text.substring(0, firstLineBreakIndex); + textSuffixLines = text.substring(firstLineBreakIndex + 1); + } + + if (suffixReplaceLength == 0 || currentLineSuffix.isEmpty()) { + // No replace range to handle + if (textSuffixLines.isEmpty()) { + drawInsertPartText(offset, textCurrentLine); + } else { + if (!textCurrentLine.isEmpty()) { + drawOverwriteText(offset, textCurrentLine); + } + drawSuffixLines(offset, textSuffixLines + currentLineSuffix); + } + } else if (suffixReplaceLength == 1) { + // Replace range contains one char + char replaceChar = currentLineSuffix.charAt(0); + int replaceCharIndex = textCurrentLine.indexOf(replaceChar); + if (replaceCharIndex > 0) { + // If textCurrentLine contain the replaceChar + // InsertPart is substring of textCurrentLine that before the replaceChar + // AppendPart is substring of textCurrentLine that after the replaceChar + String insertPart = textCurrentLine.substring(0, replaceCharIndex); + String appendPart = textCurrentLine.substring(replaceCharIndex + 1); + if (!insertPart.isEmpty()) { + drawInsertPartText(offset, insertPart); + } + if (!appendPart.isEmpty()) { + if (textSuffixLines.isEmpty()) { + drawInsertPartText(offset + 1, appendPart); + } else { + drawOverwriteText(offset + 1, appendPart); + } + } + } else { + drawReplacePartText(offset, textCurrentLine, currentLineSuffix.substring(0, 1)); + } + if (!textSuffixLines.isEmpty()) { + drawSuffixLines(offset, textSuffixLines + currentLineSuffix.substring(1)); + } + } else { + // Replace range contains multiple chars + if (textSuffixLines.isEmpty()) { + drawReplacePartText(offset, textCurrentLine, currentLineSuffix.substring(0, suffixReplaceLength)); + } else { + if (!textCurrentLine.isEmpty()) { + drawOverwriteText(offset, textCurrentLine); + } + drawSuffixLines(offset, textSuffixLines + currentLineSuffix.substring(suffixReplaceLength)); + } + } + logger.debug("End setupPainting."); + } + + private void drawOverwriteText(int offset, String text) { + logger.debug("drawCurrentLineText:" + offset + ":" + text); + StyledText widget = getWidget(); + TextWithTabs textWithTabs = StringUtils.splitLeadingTabs(text); + + paintFunctions.add((gc) -> { + if (offset > widget.getCharCount()) { + return; + } + // Draw ghost text + setStyleToGhostText(gc); + int spaceWidth = gc.textExtent(" ").x; + int tabWidth = textWithTabs.getTabs() * widget.getTabs() * spaceWidth; + Point location = widget.getLocationAtOffset(offset); + gc.drawString(textWithTabs.getText(), location.x + tabWidth, location.y); + }); + } + + private void drawInsertPartText(int offset, String text) { + drawReplacePartText(offset, text, ""); + } + + private void drawReplacePartText(int offset, String text, String replacedText) { + logger.debug("drawReplacePartText:" + offset + ":" + text + ":" + replacedText); + StyledText widget = getWidget(); + TextWithTabs textWithTabs = StringUtils.splitLeadingTabs(text); + + int targetOffset = offset + replacedText.length(); + if (targetOffset >= widget.getCharCount()) { + // End of document, draw the ghost text only + paintFunctions.add((gc) -> { + if (offset > widget.getCharCount()) { + return; + } + // Draw ghost text + setStyleToGhostText(gc); + int spaceWidth = gc.textExtent(" ").x; + int tabWidth = textWithTabs.getTabs() * widget.getTabs() * spaceWidth; + Point location = widget.getLocationAtOffset(offset); + gc.drawString(textWithTabs.getText(), location.x + tabWidth, location.y); + }); + + } else { + // otherwise, draw the ghost text, and move target char after the ghost text + String targetChar = widget.getText(targetOffset, targetOffset); + StyleRange originStyleRange; + if (widget.getStyleRangeAtOffset(targetOffset) != null) { + originStyleRange = widget.getStyleRangeAtOffset(targetOffset); + logger.debug("Find origin StyleRange: " + originStyleRange.start + ": " + originStyleRange.metrics); + } else { + originStyleRange = new StyleRange(); + originStyleRange.start = targetOffset; + originStyleRange.length = 1; + logger.debug("Create StyleRange: " + originStyleRange.start + ": " + originStyleRange.metrics); + } + + paintFunctions.add((gc) -> { + if (offset > widget.getCharCount()) { + return; + } + // Draw ghost text + setStyleToGhostText(gc); + int spaceWidth = gc.textExtent(" ").x; + int tabWidth = textWithTabs.getTabs() * widget.getTabs() * spaceWidth; + int ghostTextWidth = tabWidth + gc.stringExtent(textWithTabs.getText()).x; + Point location = widget.getLocationAtOffset(offset); + gc.drawString(textWithTabs.getText(), location.x + tabWidth, location.y); + + // Leave the space for the ghost text + setStyle(gc, originStyleRange); + int shiftWidth = ghostTextWidth - gc.stringExtent(replacedText).x; + int targetCharWidth = gc.stringExtent(targetChar).x; + + StyleRange currentStyleRange = widget.getStyleRangeAtOffset(targetOffset); + if (currentStyleRange != null && currentStyleRange.metrics != null + && currentStyleRange.metrics.width == shiftWidth + targetCharWidth) { + // nothing to do + } else { + StyleRange styleRange = (StyleRange) originStyleRange.clone(); + styleRange.start = targetOffset; + styleRange.length = 1; + FontMetrics fontMetrics = gc.getFontMetrics(); + GlyphMetrics glyphMetrics = new GlyphMetrics(fontMetrics.getAscent(), fontMetrics.getDescent(), + shiftWidth + targetCharWidth); + modifiedGlyphMetrics.add(glyphMetrics); + styleRange.metrics = glyphMetrics; + widget.setStyleRange(styleRange); + logger.debug("Set StyleRange: " + styleRange.start + ": " + styleRange.metrics); + } + + // Draw the moved char + Point targetCharLocation = widget.getLocationAtOffset(targetOffset); + gc.drawString(targetChar, targetCharLocation.x + shiftWidth, targetCharLocation.y, true); + }); + } + } + + private void drawSuffixLines(int offset, String text) { + logger.debug("drawSuffixLines:" + offset + ":" + text); + StyledText widget = getWidget(); + int lineHeight = widget.getLineHeight(); + List lines = text.lines().toList(); + + // Leave the space for the ghost text + int lineCount = lines.size(); + this.lineSpacing = widget.getLineSpacing() + lineCount * lineHeight; + + List linesTextWithTab = new ArrayList<>(); + for (String line : lines) { + linesTextWithTab.add(StringUtils.splitLeadingTabs(line)); + } + + paintFunctions.add((gc) -> { + if (offset > widget.getCharCount()) { + return; + } + // Draw ghost text + setStyleToGhostText(gc); + int spaceWidth = gc.textExtent(" ").x; + Point location = widget.getLocationAtOffset(offset); + int y = location.y; + for (TextWithTabs textWithTabs : linesTextWithTab) { + int x = widget.getLeftMargin() + textWithTabs.getTabs() * widget.getTabs() * spaceWidth; + y += lineHeight; + gc.drawString(textWithTabs.getText(), x, y); + } + }); + } + + private void setStyle(GC gc, StyleRange styleRange) { + if (styleRange.foreground != null) { + gc.setForeground(styleRange.foreground); + } else { + gc.setForeground(getWidget().getForeground()); + } + if (styleRange.font != null) { + gc.setFont(styleRange.font); + } else { + gc.setFont(getWidget().getFont()); + } + } + + private void setStyleToGhostText(GC gc) { + gc.setForeground(getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY)); + if (font == null || font.isDisposed()) { + FontData[] fontData = getWidget().getFont().getFontData(); + for (int i = 0; i < fontData.length; ++i) { + fontData[i].setStyle(fontData[i].getStyle() | SWT.ITALIC); + } + font = new Font(getDisplay(), fontData); + } + gc.setFont(font); + } + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/renderer/InlineCompletionRendererSupport.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/renderer/InlineCompletionRendererSupport.java new file mode 100644 index 000000000000..95176d85bff7 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/renderer/InlineCompletionRendererSupport.java @@ -0,0 +1,59 @@ +package com.tabbyml.tabby4eclipse.inlineCompletion.renderer; + +import org.eclipse.jface.text.ITextViewer; + +import com.tabbyml.tabby4eclipse.inlineCompletion.InlineCompletionItem; + +public class InlineCompletionRendererSupport { + private IInlineCompletionItemRenderer renderer = InlineCompletionItemRenderer.getInstance(); + private ITextViewer currentTextViewer = null; + private InlineCompletionItem currentCompletionItem = null; + private String currentViewId = null; + private Long currentDisplayAt = null; + + public void show(ITextViewer viewer, InlineCompletionItem item) { + if (currentTextViewer != null) { + renderer.updateInlineCompletionItem(currentTextViewer, null); + } + currentTextViewer = viewer; + currentCompletionItem = item; + renderer.updateInlineCompletionItem(viewer, item); + + currentDisplayAt = System.currentTimeMillis(); + + String completionId = "no-cmpl-id"; + if (item.getEventId() != null && item.getEventId().getCompletionId() != null) { + completionId = item.getEventId().getCompletionId().replace("cmpl-", ""); + } + currentViewId = String.format("view-%s-at-%d", completionId, currentDisplayAt); + } + + public void hide() { + if (currentTextViewer != null) { + renderer.updateInlineCompletionItem(currentTextViewer, null); + currentTextViewer = null; + } + currentCompletionItem = null; + currentViewId = null; + currentDisplayAt = null; + } + + public InlineCompletionItem getCurrentCompletionItem() { + return currentCompletionItem; + } + + public ITextViewer getCurrentTextViewer() { + return currentTextViewer; + } + + public String getCurrentViewId() { + return currentViewId; + } + + public Long getCurrentDisplayedTime() { + if (currentDisplayAt != null) { + return System.currentTimeMillis() - currentDisplayAt; + } + return null; + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/trigger/BasicInputEventTrigger.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/trigger/BasicInputEventTrigger.java new file mode 100644 index 000000000000..02f143aa64b2 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/trigger/BasicInputEventTrigger.java @@ -0,0 +1,119 @@ +package com.tabbyml.tabby4eclipse.inlineCompletion.trigger; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.events.KeyEvent; +import org.eclipse.swt.events.KeyListener; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.events.MouseListener; +import org.eclipse.ui.texteditor.ITextEditor; + +import com.tabbyml.tabby4eclipse.Logger; +import com.tabbyml.tabby4eclipse.editor.EditorUtils; +import com.tabbyml.tabby4eclipse.inlineCompletion.IInlineCompletionService; +import com.tabbyml.tabby4eclipse.inlineCompletion.InlineCompletionService; + +/** + * This DocumentBasedTrigger listens to keyboard and mouse events to determine + * when to trigger inline completion. When a keyboard or mouse key up event is + * received, check if the cursor is moved, then check if document has changes, + * and trigger inline completion if so, otherwise dismiss inline completion. + */ +public class BasicInputEventTrigger implements IInlineCompletionTrigger { + private Logger logger = new Logger("InlineCompletionTrigger.BasicInputEventTrigger"); + + private IInlineCompletionService inlineCompletionService = InlineCompletionService.getInstance(); + + private Map keyListeners = new HashMap<>(); + private Map mouseListeners = new HashMap<>(); + + private int lastOffset = -1; + private long lastModificationStamp = -1; + + @Override + public void register(ITextEditor textEditor) { + StyledText widget = EditorUtils.getStyledTextWidget(textEditor); + widget.getDisplay().syncExec(() -> { + KeyListener keyListener = new KeyListener() { + @Override + public void keyPressed(KeyEvent e) { + } + + @Override + public void keyReleased(KeyEvent e) { + handleEvent(); + } + }; + widget.addKeyListener(keyListener); + keyListeners.put(textEditor, keyListener); + logger.debug("Created key listener for TextEditor " + textEditor.toString()); + + MouseListener mouseListener = new MouseListener() { + @Override + public void mouseDoubleClick(MouseEvent e) { + } + + @Override + public void mouseDown(MouseEvent e) { + } + + @Override + public void mouseUp(MouseEvent e) { + handleEvent(); + } + }; + widget.addMouseListener(mouseListener); + mouseListeners.put(textEditor, mouseListener); + logger.debug("Created mouse listener for TextEditor " + textEditor.toString()); + }); + } + + @Override + public void unregister(ITextEditor textEditor) { + StyledText widget = EditorUtils.getStyledTextWidget(textEditor); + widget.getDisplay().syncExec(() -> { + KeyListener keyListener = keyListeners.get(textEditor); + if (keyListener != null) { + widget.removeKeyListener(keyListener); + keyListeners.remove(textEditor); + logger.debug("Removed key listener for TextEditor " + textEditor.toString()); + } + MouseListener mouseListener = mouseListeners.get(textEditor); + if (mouseListener != null) { + widget.removeMouseListener(mouseListener); + mouseListeners.remove(textEditor); + logger.debug("Removed mouse listener for TextEditor " + textEditor.toString()); + } + }); + } + + private void handleEvent() { + logger.debug("handle input event."); + ITextEditor textEditor = EditorUtils.getActiveTextEditor(); + if (textEditor == null) { + return; + } + int offset = EditorUtils.getCurrentOffsetInDocument(textEditor); + long modificationStamp = EditorUtils.getDocumentModificationStamp(textEditor); + if (lastOffset != -1 && lastModificationStamp != -1) { + if (offset != lastOffset) { + logger.debug("offset cahnged, check next..."); + if (modificationStamp != lastModificationStamp) { + logger.debug("modificationStamp changed, trigger inline completion."); + inlineCompletionService.trigger(false); + } else { + logger.debug("modificationStamp not changed, dismiss inline completion."); + inlineCompletionService.dismiss(); + } + } else { + logger.debug("offset not changed, ignore event."); + } + } else { + logger.debug("init offset and modificationStamp."); + } + lastOffset = offset; + lastModificationStamp = modificationStamp; + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/trigger/DebouncedDocumentEventTrigger.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/trigger/DebouncedDocumentEventTrigger.java new file mode 100644 index 000000000000..a2b4c1c50d19 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/trigger/DebouncedDocumentEventTrigger.java @@ -0,0 +1,132 @@ +package com.tabbyml.tabby4eclipse.inlineCompletion.trigger; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jface.text.DocumentEvent; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IDocumentListener; +import org.eclipse.swt.custom.CaretEvent; +import org.eclipse.swt.custom.CaretListener; +import org.eclipse.swt.custom.StyledText; +import org.eclipse.ui.texteditor.ITextEditor; + +import com.tabbyml.tabby4eclipse.DebouncedRunnable; +import com.tabbyml.tabby4eclipse.Logger; +import com.tabbyml.tabby4eclipse.editor.EditorUtils; +import com.tabbyml.tabby4eclipse.inlineCompletion.IInlineCompletionService; +import com.tabbyml.tabby4eclipse.inlineCompletion.InlineCompletionService; + +/** + * This DocumentBasedTrigger listens to document and caret events to determine + * when to trigger inline completion. When a document changed event is received, + * it waits for a debouncing interval, then triggers inline completion. When a + * caret event is received, it waits for a debouncing interval, then checks if + * the current completion context is valid, and dismisses completion if not + * valid. + */ +public class DebouncedDocumentEventTrigger implements IInlineCompletionTrigger { + private final static int DOCUMENT_CHANGED_DEBOUNCE_INTERVAL = 3; // ms + private final static int CARET_MOVED_DEBOUNCE_INTERVAL = 16; // ms + + private Logger logger = new Logger("InlineCompletionTrigger.DebouncedDocumentEventTrigger"); + + private IInlineCompletionService inlineCompletionService = InlineCompletionService.getInstance(); + + private Map caretListeners = new HashMap<>(); + private Map documentListeners = new HashMap<>(); + + @Override + public void register(ITextEditor textEditor) { + StyledText widget = EditorUtils.getStyledTextWidget(textEditor); + widget.getDisplay().syncExec(() -> { + CaretListener caretListener = new CaretListener() { + @Override + public void caretMoved(CaretEvent event) { + handleCaretMoved(textEditor, event); + } + }; + widget.addCaretListener(caretListener); + caretListeners.put(textEditor, caretListener); + logger.debug("Created caret listener for TextEditor " + textEditor.toString()); + }); + + IDocument document = EditorUtils.getDocument(textEditor); + IDocumentListener documentListener = new IDocumentListener() { + @Override + public void documentAboutToBeChanged(DocumentEvent event) { + } + + @Override + public void documentChanged(DocumentEvent event) { + handleDocumentChanged(textEditor, event); + } + }; + document.addDocumentListener(documentListener); + documentListeners.put(textEditor, documentListener); + logger.debug("Created document listener for TextEditor " + textEditor.toString()); + } + + @Override + public void unregister(ITextEditor textEditor) { + StyledText widget = EditorUtils.getStyledTextWidget(textEditor); + widget.getDisplay().syncExec(() -> { + CaretListener caretListener = caretListeners.get(textEditor); + if (caretListener != null) { + widget.removeCaretListener(caretListener); + caretListeners.remove(textEditor); + logger.debug("Removed caret listener for TextEditor " + textEditor.toString()); + } + }); + + IDocument document = EditorUtils.getDocument(textEditor); + IDocumentListener documentListener = documentListeners.get(textEditor); + if (documentListener != null) { + document.removeDocumentListener(documentListener); + documentListeners.remove(textEditor); + logger.debug("Removed document listener for TextEditor " + textEditor.toString()); + } + } + + private DebouncedRunnable documentChangedRunnable = new DebouncedRunnable(() -> { + try { + EditorUtils.syncExec(() -> { + logger.debug("Trigger inline completion after debouncing."); + inlineCompletionService.trigger(false); + }); + } catch (Exception e) { + logger.error("Failed to handle documentChangedRunnable after debouncing.", e); + } + }, DOCUMENT_CHANGED_DEBOUNCE_INTERVAL); + + private DebouncedRunnable caretMovedRunnable = new DebouncedRunnable(() -> { + try { + EditorUtils.syncExec(() -> { + if (!inlineCompletionService.isValid()) { + logger.debug("Dismiss invalid inline completion after debouncing."); + inlineCompletionService.dismiss(); + } else { + logger.debug("Keep still valid inline completion after debouncing."); + } + }); + } catch (Exception e) { + logger.error("Failed to handle caretMovedRunnable after debouncing.", e); + } + }, CARET_MOVED_DEBOUNCE_INTERVAL); + + private void handleCaretMoved(ITextEditor textEditor, CaretEvent event) { + if (!EditorUtils.isActiveEditor(textEditor)) { + return; + } + logger.debug("handleCaretMoved: " + event.toString() + " offset: " + event.caretOffset); + caretMovedRunnable.call(); + } + + private void handleDocumentChanged(ITextEditor textEditor, DocumentEvent event) { + if (!EditorUtils.isActiveEditor(textEditor)) { + return; + } + logger.debug("handleDocumentChanged: " + event.toString()); + documentChangedRunnable.call(); + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/trigger/IInlineCompletionTrigger.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/trigger/IInlineCompletionTrigger.java new file mode 100644 index 000000000000..3953c4f4559b --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/trigger/IInlineCompletionTrigger.java @@ -0,0 +1,15 @@ +package com.tabbyml.tabby4eclipse.inlineCompletion.trigger; + +import org.eclipse.ui.texteditor.ITextEditor; + +public interface IInlineCompletionTrigger { + /** + * Register a text editor to be monitored for inline completion triggers. + */ + public void register(ITextEditor textEditor); + + /** + * Unregister a text editor from being monitored for inline completion triggers. + */ + public void unregister(ITextEditor textEditor); +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/trigger/InlineCompletionTrigger.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/trigger/InlineCompletionTrigger.java new file mode 100644 index 000000000000..3dbb8299cde8 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/trigger/InlineCompletionTrigger.java @@ -0,0 +1,39 @@ +package com.tabbyml.tabby4eclipse.inlineCompletion.trigger; + +public class InlineCompletionTrigger { + public static IInlineCompletionTrigger getInstance() { + return LazyHolder.INSTANCE; + } + + private static class LazyHolder { + private static final IInlineCompletionTrigger INSTANCE = createInstance(); + } + + private static String TABBY4ECLIPSE_EXPERIMENTAL_TRIGGER_BASICINPUT = System + .getenv("TABBY4ECLIPSE_EXPERIMENTAL_TRIGGER_BASICINPUT"); + private static boolean EXPERIMENTAL_TRIGGER_BASICINPUT = TABBY4ECLIPSE_EXPERIMENTAL_TRIGGER_BASICINPUT != null + && !TABBY4ECLIPSE_EXPERIMENTAL_TRIGGER_BASICINPUT.isEmpty(); + + private static String TABBY4ECLIPSE_EXPERIMENTAL_TRIGGER_DEBOUNCEDDOCUMENT = System + .getenv("TABBY4ECLIPSE_EXPERIMENTAL_TRIGGER_DEBOUNCEDDOCUMENT"); + private static boolean EXPERIMENTAL_TRIGGER_DEBOUNCEDDOCUMENT = TABBY4ECLIPSE_EXPERIMENTAL_TRIGGER_DEBOUNCEDDOCUMENT != null + && !TABBY4ECLIPSE_EXPERIMENTAL_TRIGGER_DEBOUNCEDDOCUMENT.isEmpty(); + + private static String TABBY4ECLIPSE_EXPERIMENTAL_TRIGGER_PAIREDDOCUMENT = System + .getenv("TABBY4ECLIPSE_EXPERIMENTAL_TRIGGER_PAIREDDOCUMENT"); + private static boolean EXPERIMENTAL_TRIGGER_PAIREDDOCUMENT = TABBY4ECLIPSE_EXPERIMENTAL_TRIGGER_PAIREDDOCUMENT != null + && !TABBY4ECLIPSE_EXPERIMENTAL_TRIGGER_PAIREDDOCUMENT.isEmpty(); + + public static IInlineCompletionTrigger createInstance() { + if (EXPERIMENTAL_TRIGGER_BASICINPUT) { + return new BasicInputEventTrigger(); + } + if (EXPERIMENTAL_TRIGGER_DEBOUNCEDDOCUMENT) { + return new DebouncedDocumentEventTrigger(); + } + if (EXPERIMENTAL_TRIGGER_PAIREDDOCUMENT) { + return new PairedDocumentEventTrigger(); + } + return new DebouncedDocumentEventTrigger(); + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/trigger/PairedDocumentEventTrigger.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/trigger/PairedDocumentEventTrigger.java new file mode 100644 index 000000000000..471b9238491a --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/trigger/PairedDocumentEventTrigger.java @@ -0,0 +1,145 @@ +package com.tabbyml.tabby4eclipse.inlineCompletion.trigger; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jface.text.DocumentEvent; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IDocumentListener; +import org.eclipse.swt.custom.CaretEvent; +import org.eclipse.swt.custom.CaretListener; +import org.eclipse.swt.custom.StyledText; +import org.eclipse.ui.texteditor.ITextEditor; + +import com.tabbyml.tabby4eclipse.Logger; +import com.tabbyml.tabby4eclipse.editor.EditorUtils; +import com.tabbyml.tabby4eclipse.inlineCompletion.IInlineCompletionService; +import com.tabbyml.tabby4eclipse.inlineCompletion.InlineCompletionService; + +/** + * This DocumentBasedTrigger listens to document and caret events to determine + * when to trigger inline completion. When a pair of document and caret events + * are received, which means user is typing, it triggers inline completion. When + * a single event is received, which means user is moving cursor, it dismisses + * the current inline completion. + */ +public class PairedDocumentEventTrigger implements IInlineCompletionTrigger { + private Logger logger = new Logger("InlineCompletionTrigger.PairedDocumentEventTrigger"); + + private IInlineCompletionService inlineCompletionService = InlineCompletionService.getInstance(); + + private Map caretListeners = new HashMap<>(); + private Map documentListeners = new HashMap<>(); + private TriggerEvent pendingEvent; + + private class TriggerEvent { + private ITextEditor textEditor; + private long modificationStamp; + private DocumentEvent documentEvent; + private CaretEvent caretEvent; + } + + @Override + public void register(ITextEditor textEditor) { + StyledText widget = EditorUtils.getStyledTextWidget(textEditor); + widget.getDisplay().syncExec(() -> { + CaretListener caretListener = new CaretListener() { + @Override + public void caretMoved(CaretEvent event) { + handleCaretMoved(textEditor, event); + } + }; + widget.addCaretListener(caretListener); + caretListeners.put(textEditor, caretListener); + logger.debug("Created caret listener for TextEditor " + textEditor.toString()); + }); + + IDocument document = EditorUtils.getDocument(textEditor); + IDocumentListener documentListener = new IDocumentListener() { + @Override + public void documentAboutToBeChanged(DocumentEvent event) { + handleDocumentAboutToBeChanged(textEditor, event); + } + + @Override + public void documentChanged(DocumentEvent event) { + handleDocumentChanged(textEditor, event); + } + }; + document.addDocumentListener(documentListener); + documentListeners.put(textEditor, documentListener); + logger.debug("Created document listener for TextEditor " + textEditor.toString()); + } + + @Override + public void unregister(ITextEditor textEditor) { + StyledText widget = EditorUtils.getStyledTextWidget(textEditor); + widget.getDisplay().syncExec(() -> { + CaretListener caretListener = caretListeners.get(textEditor); + if (caretListener != null) { + widget.removeCaretListener(caretListener); + caretListeners.remove(textEditor); + logger.debug("Removed caret listener for TextEditor " + textEditor.toString()); + } + }); + + IDocument document = EditorUtils.getDocument(textEditor); + IDocumentListener documentListener = documentListeners.get(textEditor); + if (documentListener != null) { + document.removeDocumentListener(documentListener); + documentListeners.remove(textEditor); + logger.debug("Removed document listener for TextEditor " + textEditor.toString()); + } + } + + private void handleCaretMoved(ITextEditor textEditor, CaretEvent event) { + if (!EditorUtils.isActiveEditor(textEditor)) { + return; + } + logger.debug("handleCaretMoved: " + event.toString() + " offset: " + event.caretOffset); + long modificationStamp = EditorUtils.getDocumentModificationStamp(textEditor); + if (pendingEvent != null && pendingEvent.textEditor == textEditor) { + if (pendingEvent.documentEvent != null && pendingEvent.modificationStamp == modificationStamp) { + logger.debug("Received caretEvent with paired documentEvent, trigger inline completion."); + inlineCompletionService.trigger(false); + pendingEvent = null; + } else { + logger.debug("Received caretEvent, waiting for paired documentEvent."); + pendingEvent.caretEvent = event; + pendingEvent.modificationStamp = modificationStamp; + } + } else { + logger.debug("Received caretEvent without document changes, dismiss inline completion."); + inlineCompletionService.dismiss(); + } + } + + private void handleDocumentAboutToBeChanged(ITextEditor textEditor, DocumentEvent event) { + if (!EditorUtils.isActiveEditor(textEditor)) { + return; + } + logger.debug("handleDocumentAboutToBeChanged: " + event.toString()); + pendingEvent = new TriggerEvent(); + pendingEvent.textEditor = textEditor; + } + + private void handleDocumentChanged(ITextEditor textEditor, DocumentEvent event) { + if (!EditorUtils.isActiveEditor(textEditor)) { + return; + } + logger.debug("handleDocumentChanged: " + event.toString()); + long modificationStamp = EditorUtils.getDocumentModificationStamp(textEditor); + if (pendingEvent != null && pendingEvent.textEditor == textEditor) { + if (pendingEvent.caretEvent != null && pendingEvent.modificationStamp == modificationStamp) { + logger.debug("Received documentEvent with paired caretEvent, trigger inline completion."); + inlineCompletionService.trigger(false); + pendingEvent = null; + } else { + logger.debug("Received documentEvent, waiting for paired caretEvent."); + pendingEvent.documentEvent = event; + pendingEvent.modificationStamp = modificationStamp; + } + } + } + +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/ConnectionProvider.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/ConnectionProvider.java new file mode 100644 index 000000000000..e3e4623cd779 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/ConnectionProvider.java @@ -0,0 +1,140 @@ +package com.tabbyml.tabby4eclipse.lsp; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.util.List; + +import org.eclipse.core.runtime.FileLocator; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.Platform; +import org.eclipse.lsp4e.server.ProcessStreamConnectionProvider; +import org.osgi.framework.Bundle; + +import com.tabbyml.tabby4eclipse.Activator; +import com.tabbyml.tabby4eclipse.Logger; +import com.tabbyml.tabby4eclipse.Utils; +import com.tabbyml.tabby4eclipse.git.GitProvider; +import com.tabbyml.tabby4eclipse.lsp.protocol.ClientCapabilities; +import com.tabbyml.tabby4eclipse.lsp.protocol.ClientCapabilities.TabbyClientCapabilities; +import com.tabbyml.tabby4eclipse.lsp.protocol.ClientCapabilities.TextDocumentClientCapabilities; +import com.tabbyml.tabby4eclipse.lsp.protocol.ClientInfo; +import com.tabbyml.tabby4eclipse.lsp.protocol.ClientInfo.TabbyPluginInfo; +import com.tabbyml.tabby4eclipse.lsp.protocol.ClientProvidedConfig; +import com.tabbyml.tabby4eclipse.lsp.protocol.InitializationOptions; +import com.tabbyml.tabby4eclipse.preferences.PreferencesService; + +public class ConnectionProvider extends ProcessStreamConnectionProvider { + private Logger logger = new Logger("ConnectionProvider"); + + public ConnectionProvider() { + try { + // Find node executable + File nodeExecutableFile = null; + + String nodePathFromPreference = PreferencesService.getInstance().getNodeBinaryPath(); + if (nodePathFromPreference != null && !nodePathFromPreference.isEmpty()) { + String nodePath = nodePathFromPreference.replaceFirst("~", System.getProperty("user.home")); + logger.info("Node executable path from preference: " + nodePath); + File file = new File(nodePath); + if (file.exists() && file.canExecute()) { + nodeExecutableFile = file; + } else { + logger.info("Cannot find node executable in: " + nodePath); + } + } + + if (nodeExecutableFile == null) { + String systemPath = System.getenv("PATH"); + logger.info("Finding node executable in system paths: " + systemPath); + if (systemPath != null) { + String[] paths = systemPath.split(File.pathSeparator); + for (String p : paths) { + File file = new File(p, Utils.isWindows() ? "node.exe" : "node"); + if (file.exists() && file.canExecute()) { + nodeExecutableFile = file; + break; + } + } + } + } + + if (nodeExecutableFile == null) { + StatusInfoHolder.getInstance().setConnectionFailed(true); + logger.error("Cannot find node executable."); + return; + } else { + logger.info("Using node executable: " + nodeExecutableFile.getAbsolutePath()); + } + + // Find tabby-agent script + Bundle bundle = Platform.getBundle(Activator.PLUGIN_ID); + URL agentScriptUrl = FileLocator.find(bundle, new Path("tabby-agent/dist/node/index.js")); + if (agentScriptUrl == null) { + StatusInfoHolder.getInstance().setConnectionFailed(true); + logger.error("Cannot find tabby-agent script."); + return; + } + File agentScriptFile = new File(FileLocator.toFileURL(agentScriptUrl).getPath()); + // Setup command to start tabby-agent + List commands = List.of(nodeExecutableFile.getAbsolutePath(), agentScriptFile.getAbsolutePath(), + "--stdio"); + logger.info("Command to start Tabby language server: " + commands.toString()); + this.setCommands(commands); + } catch (IOException e) { + StatusInfoHolder.getInstance().setConnectionFailed(true); + logger.error("Failed to setup command to start Tabby language server.", e); + } + } + + @Override + public Object getInitializationOptions(URI rootUri) { + return new InitializationOptions(getProvidedConfig(), getClientInfo(), getClientCapabilities()); + } + + @Override + public void start() throws IOException { + super.start(); + logger.info("Tabby language server started."); + } + + @Override + public void stop() { + super.stop(); + logger.info("Tabby language server stopped."); + } + + private ClientProvidedConfig getProvidedConfig() { + return PreferencesService.getInstance().buildClientProvidedConfig(); + } + + private ClientInfo getClientInfo() { + TabbyPluginInfo tabbyPluginInfo = new TabbyPluginInfo(); + Bundle bundle = Platform.getBundle(Activator.PLUGIN_ID); + tabbyPluginInfo.setName(Activator.PLUGIN_ID); + tabbyPluginInfo.setVersion(bundle.getVersion().toString()); + + ClientInfo clientInfo = new ClientInfo(); + clientInfo.setTabbyPlugin(tabbyPluginInfo); + return clientInfo; + } + + private ClientCapabilities getClientCapabilities() { + TextDocumentClientCapabilities textDocumentClientCapabilities = new TextDocumentClientCapabilities(); + textDocumentClientCapabilities.setCompletion(false); + textDocumentClientCapabilities.setInlineCompletion(true); + + TabbyClientCapabilities tabbyClientCapabilities = new TabbyClientCapabilities(); + tabbyClientCapabilities.setConfigDidChangeListener(true); + tabbyClientCapabilities.setStatusDidChangeListener(true); + tabbyClientCapabilities.setWorkspaceFileSystem(true); + tabbyClientCapabilities.setGitProvider(GitProvider.getInstance().isAvailable()); + tabbyClientCapabilities.setLanguageSupport(true); + + ClientCapabilities clientCapabilities = new ClientCapabilities(); + clientCapabilities.setTextDocument(textDocumentClientCapabilities); + clientCapabilities.setTabby(tabbyClientCapabilities); + return clientCapabilities; + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/LanguageClientImpl.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/LanguageClientImpl.java new file mode 100644 index 000000000000..edd453e6ecbe --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/LanguageClientImpl.java @@ -0,0 +1,104 @@ +package com.tabbyml.tabby4eclipse.lsp; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.jface.text.IDocument; +import org.eclipse.lsp4e.LSPEclipseUtils; +import org.eclipse.lsp4j.ConfigurationParams; +import org.eclipse.lsp4j.DeclarationParams; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.LocationLink; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.SemanticTokensRangeParams; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.eclipse.lsp4j.jsonrpc.services.JsonNotification; +import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; + +import com.tabbyml.tabby4eclipse.git.GitProvider; +import com.tabbyml.tabby4eclipse.lsp.protocol.Config; +import com.tabbyml.tabby4eclipse.lsp.protocol.GitDiffParams; +import com.tabbyml.tabby4eclipse.lsp.protocol.GitDiffResult; +import com.tabbyml.tabby4eclipse.lsp.protocol.GitRepository; +import com.tabbyml.tabby4eclipse.lsp.protocol.GitRepositoryParams; +import com.tabbyml.tabby4eclipse.lsp.protocol.ReadFileParams; +import com.tabbyml.tabby4eclipse.lsp.protocol.ReadFileResult; +import com.tabbyml.tabby4eclipse.lsp.protocol.SemanticTokensRangeResult; +import com.tabbyml.tabby4eclipse.lsp.protocol.StatusInfo; +import com.tabbyml.tabby4eclipse.preferences.PreferencesService; + +public class LanguageClientImpl extends org.eclipse.lsp4e.LanguageClientImpl { + @Override + public CompletableFuture> configuration(ConfigurationParams configurationParams) { + + return CompletableFuture.completedFuture(new ArrayList<>() { + { + add(PreferencesService.getInstance().buildClientProvidedConfig()); + } + }); + } + + @JsonNotification("tabby/status/didChange") + void statusDidChange(StatusInfo params) { + StatusInfoHolder.getInstance().setStatusInfo(params); + } + + @JsonNotification("tabby/config/didChange") + void statusDidChange(Config params) { + ServerConfigHolder.getInstance().setConfig(params); + } + + @JsonRequest("tabby/workspaceFileSystem/readFile") + CompletableFuture workspaceReadFile(ReadFileParams params) { + if (params.getFormat().equals("text")) { + try { + URI uri = new URI(params.getUri()); + IDocument document = LSPEclipseUtils.getDocument(uri); + Range range = params.getRange(); + + ReadFileResult result = new ReadFileResult(); + if (range != null) { + int start = LSPEclipseUtils.toOffset(range.getStart(), document); + int end = LSPEclipseUtils.toOffset(range.getEnd(), document); + result.setText(document.get(start, end - start)); + } else { + result.setText(document.get()); + } + return CompletableFuture.completedFuture(result); + } catch (Exception e) { + return CompletableFuture.completedFuture(null); + } + } else { + return CompletableFuture.completedFuture(null); + } + } + + @JsonRequest("tabby/git/repository") + CompletableFuture gitRepository(GitRepositoryParams params) { + CompletableFuture future = new CompletableFuture<>(); + GitRepository result = GitProvider.getInstance().getRepository(params); + future.complete(result); + return future; + } + + @JsonRequest("tabby/git/diff") + CompletableFuture gitDiff(GitDiffParams params) { + CompletableFuture future = new CompletableFuture<>(); + GitDiffResult result = GitProvider.getInstance().getDiff(params); + future.complete(result); + return future; + } + + @JsonRequest("tabby/languageSupport/textDocument/declaration") + CompletableFuture, List>> languageSupportDeclaration( + DeclarationParams params) { + return LanguageSupportProvider.getInstance().languageSupportDeclaration(params); + } + + @JsonRequest("tabby/languageSupport/textDocument/semanticTokens/range") + CompletableFuture languageSupportSemanticTokensRange(SemanticTokensRangeParams params) { + return LanguageSupportProvider.getInstance().languageSupportSemanticTokensRange(params); + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/LanguageServerService.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/LanguageServerService.java new file mode 100644 index 000000000000..b1351d38eb3d --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/LanguageServerService.java @@ -0,0 +1,43 @@ +package com.tabbyml.tabby4eclipse.lsp; + +import org.eclipse.lsp4e.LanguageServerWrapper; +import org.eclipse.lsp4e.LanguageServersRegistry; +import org.eclipse.lsp4e.LanguageServersRegistry.LanguageServerDefinition; +import org.eclipse.lsp4e.LanguageServiceAccessor; + +import com.tabbyml.tabby4eclipse.Logger; + +public class LanguageServerService { + public static final String LANGUAGE_SERVER_ID = "com.tabbyml.tabby4eclipse.languageServer"; + + public static LanguageServerService getInstance() { + return LazyHolder.INSTANCE; + } + + private static class LazyHolder { + private static final LanguageServerService INSTANCE = new LanguageServerService(); + } + + public static LanguageServerDefinition getLanguageServerDefinition() { + LanguageServerDefinition def = LanguageServersRegistry.getInstance().getDefinition(LANGUAGE_SERVER_ID); + return def; + } + + private Logger logger = new Logger("LanguageServerService"); + private LanguageServerWrapper serverWrapper; + + public LanguageServerService() { + } + + public void init() { + try { + serverWrapper = LanguageServiceAccessor.getLSWrapper(null, getLanguageServerDefinition()); + } catch (Exception e) { + logger.error("Failed to start Tabby language server.", e); + } + } + + public LanguageServerWrapper getServer() { + return serverWrapper; + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/LanguageSupportProvider.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/LanguageSupportProvider.java new file mode 100644 index 000000000000..dd4436fe90a5 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/LanguageSupportProvider.java @@ -0,0 +1,91 @@ +package com.tabbyml.tabby4eclipse.lsp; + +import java.net.URI; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.jface.text.IDocument; +import org.eclipse.lsp4e.LSPEclipseUtils; +import org.eclipse.lsp4e.LanguageServers; +import org.eclipse.lsp4j.DeclarationParams; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.LocationLink; +import org.eclipse.lsp4j.SemanticTokensLegend; +import org.eclipse.lsp4j.SemanticTokensRangeParams; +import org.eclipse.lsp4j.SemanticTokensWithRegistrationOptions; +import org.eclipse.lsp4j.jsonrpc.messages.Either; + +import com.tabbyml.tabby4eclipse.Logger; +import com.tabbyml.tabby4eclipse.lsp.protocol.SemanticTokensRangeResult; + +public class LanguageSupportProvider { + public static LanguageSupportProvider getInstance() { + return LazyHolder.INSTANCE; + } + + private static class LazyHolder { + private static final LanguageSupportProvider INSTANCE = new LanguageSupportProvider(); + } + + private Logger logger = new Logger("LanguageSupportProvider"); + + CompletableFuture, List>> languageSupportDeclaration( + DeclarationParams params) { + try { + URI uri = new URI(params.getTextDocument().getUri()); + IDocument document = LSPEclipseUtils.getDocument(uri); + + CompletableFuture, List>> future = LanguageServers + .forDocument(document).withFilter((serverCapabilities) -> { + return serverCapabilities.getDeclarationProvider() != null; + }).computeFirst((wrapper, server) -> { + return server.getTextDocumentService().declaration(params); + }).thenApply((result) -> { + if (result.isPresent()) { + return result.get(); + } else { + return null; + } + }); + return future; + } catch (Exception e) { + logger.error("Failed to handle request languageSupport/declaration.", e); + return CompletableFuture.completedFuture(null); + } + } + + CompletableFuture languageSupportSemanticTokensRange(SemanticTokensRangeParams params) { + try { + URI uri = new URI(params.getTextDocument().getUri()); + IDocument document = LSPEclipseUtils.getDocument(uri); + + CompletableFuture future = LanguageServers.forDocument(document) + .withFilter((serverCapabilities) -> { + final SemanticTokensWithRegistrationOptions semanticTokensProvider = serverCapabilities + .getSemanticTokensProvider(); + return semanticTokensProvider != null + && Boolean.TRUE.equals(semanticTokensProvider.getRange().get()); + }).computeFirst((wrapper, server) -> { + return server.getTextDocumentService().semanticTokensRange(params).thenApply((tokens) -> { + SemanticTokensRangeResult result = new SemanticTokensRangeResult(); + SemanticTokensLegend legend = wrapper.getServerCapabilities().getSemanticTokensProvider() + .getLegend(); + result.setLegend(legend); + result.setTokens(tokens); + return result; + }); + }).thenApply((result) -> { + if (result.isPresent()) { + return result.get(); + } else { + return null; + } + }); + return future; + } catch (Exception e) { + logger.error("Failed to handle request languageSupport/semanticTokens/range.", e); + return CompletableFuture.completedFuture(null); + } + } + +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/ServerConfigHolder.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/ServerConfigHolder.java new file mode 100644 index 000000000000..19aa9971a6c5 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/ServerConfigHolder.java @@ -0,0 +1,38 @@ +package com.tabbyml.tabby4eclipse.lsp; + +import java.util.ArrayList; +import java.util.List; + +import com.tabbyml.tabby4eclipse.lsp.protocol.Config; + +public class ServerConfigHolder { + public static ServerConfigHolder getInstance() { + return LazyHolder.INSTANCE; + } + + private static class LazyHolder { + private static final ServerConfigHolder INSTANCE = new ServerConfigHolder(); + } + + private Config config = new Config(); + private List listeners = new ArrayList<>(); + + public Config getConfig() { + return config; + } + + public void setConfig(Config config) { + this.config = config; + for (ConfigDidChangeListener listener : listeners) { + listener.configDidChange(); + } + } + + public void addConfigDidChangeListener(ConfigDidChangeListener listener) { + listeners.add(listener); + } + + public interface ConfigDidChangeListener { + void configDidChange(); + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/StatusInfoHolder.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/StatusInfoHolder.java new file mode 100644 index 000000000000..1a100439de5c --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/StatusInfoHolder.java @@ -0,0 +1,53 @@ +package com.tabbyml.tabby4eclipse.lsp; + +import java.util.ArrayList; +import java.util.List; + +import com.tabbyml.tabby4eclipse.lsp.protocol.StatusInfo; + +public class StatusInfoHolder { + public static StatusInfoHolder getInstance() { + return LazyHolder.INSTANCE; + } + + private static class LazyHolder { + private static final StatusInfoHolder INSTANCE = new StatusInfoHolder(); + } + + private boolean isConnectionFailed = false; + private StatusInfo statusInfo = new StatusInfo(); + private List listeners = new ArrayList<>(); + + public boolean isConnectionFailed() { + return isConnectionFailed; + } + + public void setConnectionFailed(boolean isConnectionFailed) { + if (this.isConnectionFailed == isConnectionFailed) { + return; + } + this.isConnectionFailed = isConnectionFailed; + for (StatusDidChangeListener listener : listeners) { + listener.statusDidChange(); + } + } + + public StatusInfo getStatusInfo() { + return statusInfo; + } + + public void setStatusInfo(StatusInfo statusInfo) { + this.statusInfo = statusInfo; + for (StatusDidChangeListener listener : listeners) { + listener.statusDidChange(); + } + } + + public void addStatusDidChangeListener(StatusDidChangeListener listener) { + listeners.add(listener); + } + + public interface StatusDidChangeListener { + void statusDidChange(); + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/ClientCapabilities.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/ClientCapabilities.java new file mode 100644 index 000000000000..8c6563a1dc0d --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/ClientCapabilities.java @@ -0,0 +1,127 @@ +package com.tabbyml.tabby4eclipse.lsp.protocol; + +public class ClientCapabilities { + private TextDocumentClientCapabilities textDocument; + private TabbyClientCapabilities tabby; + + public ClientCapabilities() { + } + + public TextDocumentClientCapabilities getTextDocument() { + return textDocument; + } + + public void setTextDocument(TextDocumentClientCapabilities textDocument) { + this.textDocument = textDocument; + } + + public TabbyClientCapabilities getTabby() { + return tabby; + } + + public void setTabby(TabbyClientCapabilities tabby) { + this.tabby = tabby; + } + + public static class TextDocumentClientCapabilities { + private boolean completion; + private boolean inlineCompletion; + + public TextDocumentClientCapabilities() { + this.completion = false; + this.inlineCompletion = false; + } + + public boolean getCompletion() { + return completion; + } + + public void setCompletion(boolean completion) { + this.completion = completion; + } + + public boolean getInlineCompletion() { + return inlineCompletion; + } + + public void setInlineCompletion(boolean inlineCompletion) { + this.inlineCompletion = inlineCompletion; + } + } + + public static class TabbyClientCapabilities { + private boolean configDidChangeListener; + private boolean statusDidChangeListener; + private boolean workspaceFileSystem; + private boolean dataStore; + private boolean languageSupport; + private boolean gitProvider; + private boolean editorOptions; + + public TabbyClientCapabilities() { + this.configDidChangeListener = false; + this.statusDidChangeListener = false; + this.workspaceFileSystem = false; + this.dataStore = false; + this.languageSupport = false; + this.gitProvider = false; + this.editorOptions = false; + } + + public boolean getConfigDidChangeListener() { + return configDidChangeListener; + } + + public void setConfigDidChangeListener(boolean configDidChangeListener) { + this.configDidChangeListener = configDidChangeListener; + } + + public boolean getStatusDidChangeListener() { + return statusDidChangeListener; + } + + public void setStatusDidChangeListener(boolean statusDidChangeListener) { + this.statusDidChangeListener = statusDidChangeListener; + } + + public boolean getWorkspaceFileSystem() { + return workspaceFileSystem; + } + + public void setWorkspaceFileSystem(boolean workspaceFileSystem) { + this.workspaceFileSystem = workspaceFileSystem; + } + + public boolean getDataStore() { + return dataStore; + } + + public void setDateStore(boolean dataStore) { + this.dataStore = dataStore; + } + + public boolean getLanguageSupport() { + return languageSupport; + } + + public void setLanguageSupport(boolean languageSupport) { + this.languageSupport = languageSupport; + } + + public boolean getGitProvider() { + return gitProvider; + } + + public void setGitProvider(boolean gitProvider) { + this.gitProvider = gitProvider; + } + + public boolean getEditorOptions() { + return editorOptions; + } + + public void setEditorOptions(boolean editorOptions) { + this.editorOptions = editorOptions; + } + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/ClientInfo.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/ClientInfo.java new file mode 100644 index 000000000000..4c257b9b90a5 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/ClientInfo.java @@ -0,0 +1,55 @@ +package com.tabbyml.tabby4eclipse.lsp.protocol; + +public class ClientInfo { + private String name; + private String version; + private TabbyPluginInfo tabbyPlugin; + + public ClientInfo() { + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public TabbyPluginInfo getTabbyPlugin() { + return tabbyPlugin; + } + + public void setTabbyPlugin(TabbyPluginInfo tabbyPlugin) { + this.tabbyPlugin = tabbyPlugin; + } + + public static class TabbyPluginInfo { + private String name; + private String version; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + } +} \ No newline at end of file diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/ClientProvidedConfig.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/ClientProvidedConfig.java new file mode 100644 index 000000000000..df8cde1e6087 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/ClientProvidedConfig.java @@ -0,0 +1,112 @@ +package com.tabbyml.tabby4eclipse.lsp.protocol; + +public class ClientProvidedConfig { + private ServerConfig server; + private InlineCompletionConfig inlineCompletion; + private String keybindings; + private AnonymousUsageTrackingConfig anonymousUsageTracking; + + public ClientProvidedConfig() { + } + + public ServerConfig getServer() { + return server; + } + + public void setServer(ServerConfig server) { + this.server = server; + } + + public InlineCompletionConfig getInlineCompletion() { + return inlineCompletion; + } + + public void setInlineCompletion(InlineCompletionConfig inlineCompletion) { + this.inlineCompletion = inlineCompletion; + } + + public String getKeybindings() { + return keybindings; + } + + public void setKeybindings(String keybindings) { + this.keybindings = keybindings; + } + + public AnonymousUsageTrackingConfig getAnonymousUsageTracking() { + return anonymousUsageTracking; + } + + public void setAnonymousUsageTracking(AnonymousUsageTrackingConfig anonymousUsageTracking) { + this.anonymousUsageTracking = anonymousUsageTracking; + } + + public static class ServerConfig { + private String endpoint; + private String token; + + public ServerConfig(String endpoint, String token) { + this.endpoint = endpoint; + this.token = token; + } + + public String getEndpoint() { + return endpoint; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + } + + public static class InlineCompletionConfig { + private String triggerMode; + + public InlineCompletionConfig(String triggerMode) { + this.triggerMode = triggerMode; + } + + public String getTriggerMode() { + return triggerMode; + } + + public void setTriggerMode(String triggerMode) { + this.triggerMode = triggerMode; + } + + public static class TriggerMode { + public static final String AUTO = "auto"; + public static final String MANUAL = "manual"; + } + } + + public static class Keybindings { + public static final String DEFAULT = "default"; + public static final String TABBY_STYLE = "tabby-style"; + public static final String CUSTOMIZE = "customize"; + } + + public static class AnonymousUsageTrackingConfig { + private boolean disable; + + public AnonymousUsageTrackingConfig(boolean disable) { + this.disable = disable; + } + + public boolean getDisable() { + return disable; + } + + public void setDisable(boolean disable) { + this.disable = disable; + } + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/CompletionEventId.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/CompletionEventId.java new file mode 100644 index 000000000000..9fe9d400fff8 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/CompletionEventId.java @@ -0,0 +1,27 @@ +package com.tabbyml.tabby4eclipse.lsp.protocol; + +public class CompletionEventId { + private String completionId; + private int choiceIndex; + + public CompletionEventId(String completionId, int choiceIndex) { + this.completionId = completionId; + this.choiceIndex = choiceIndex; + } + + public String getCompletionId() { + return completionId; + } + + public void setCompletionId(String completionId) { + this.completionId = completionId; + } + + public int getChoiceIndex() { + return choiceIndex; + } + + public void setChoiceIndex(int choiceIndex) { + this.choiceIndex = choiceIndex; + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/Config.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/Config.java new file mode 100644 index 000000000000..14436b37dbc3 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/Config.java @@ -0,0 +1,51 @@ +package com.tabbyml.tabby4eclipse.lsp.protocol; + +import java.util.Map; + +public class Config { + private ServerConfig server; + + public Config() { + } + + public ServerConfig getServer() { + return server; + } + + public void setServer(ServerConfig server) { + this.server = server; + } + + public static class ServerConfig { + private String endpoint; + private String token; + private Map requestHeaders; + + public ServerConfig() { + } + + public String getEndpoint() { + return endpoint; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public Map getRequestHeaders() { + return requestHeaders; + } + + public void setRequestHeaders(Map requestHeaders) { + this.requestHeaders = requestHeaders; + } + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/EventParams.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/EventParams.java new file mode 100644 index 000000000000..170b2b102be1 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/EventParams.java @@ -0,0 +1,62 @@ +package com.tabbyml.tabby4eclipse.lsp.protocol; + +public class EventParams { + private String type; + private String selectKind; + private CompletionEventId eventId; + private String viewId; + private Long elapsed; + + public EventParams() { + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getSelectKind() { + return selectKind; + } + + public void setSelectKind(String selectKind) { + this.selectKind = selectKind; + } + + public CompletionEventId getCompletionEventId() { + return eventId; + } + + public void setCompletionEventId(CompletionEventId completionEventId) { + this.eventId = completionEventId; + } + + public String getViewId() { + return viewId; + } + + public void setViewId(String viewId) { + this.viewId = viewId; + } + + public Long getElapsed() { + return elapsed; + } + + public void setElapsed(Long elapsed) { + this.elapsed = elapsed; + } + + public static class Type { + public static final String VIEW = "view"; + public static final String SELECT = "select"; + public static final String DISMISS = "dismiss"; + } + + public static class SelectKind { + public static final String LINE = "line"; + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/GitDiffParams.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/GitDiffParams.java new file mode 100644 index 000000000000..7c116bd3391b --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/GitDiffParams.java @@ -0,0 +1,35 @@ +package com.tabbyml.tabby4eclipse.lsp.protocol; + +public class GitDiffParams { + private String repository; + private boolean cached; + + public GitDiffParams() { + } + + public GitDiffParams(String repository) { + this.repository = repository; + this.cached = false; + } + + public GitDiffParams(String repository, boolean cached) { + this.repository = repository; + this.cached = cached; + } + + public String getRepository() { + return repository; + } + + public void setRepository(String repository) { + this.repository = repository; + } + + public boolean getCached() { + return cached; + } + + public void setCached(boolean cached) { + this.cached = cached; + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/GitDiffResult.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/GitDiffResult.java new file mode 100644 index 000000000000..1de2a9d4202e --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/GitDiffResult.java @@ -0,0 +1,20 @@ +package com.tabbyml.tabby4eclipse.lsp.protocol; + +public class GitDiffResult { + private String diff; + + public GitDiffResult() { + } + + public GitDiffResult(String diff) { + this.diff = diff; + } + + public String getDiff() { + return diff; + } + + public void setDiff(String diff) { + this.diff = diff; + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/GitRepository.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/GitRepository.java new file mode 100644 index 000000000000..2735e7fed513 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/GitRepository.java @@ -0,0 +1,25 @@ +package com.tabbyml.tabby4eclipse.lsp.protocol; + +public class GitRepository { + private String root; + private String remoteUrl; + + public GitRepository() { + } + + public String getRoot() { + return root; + } + + public void setRoot(String root) { + this.root = root; + } + + public String getRemoteUrl() { + return remoteUrl; + } + + public void setRemoteUrl(String remoteUrl) { + this.remoteUrl = remoteUrl; + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/GitRepositoryParams.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/GitRepositoryParams.java new file mode 100644 index 000000000000..806c8c60f745 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/GitRepositoryParams.java @@ -0,0 +1,17 @@ +package com.tabbyml.tabby4eclipse.lsp.protocol; + +public class GitRepositoryParams { + private String uri; + + public GitRepositoryParams(String uri) { + this.uri = uri; + } + + public String getUri() { + return uri; + } + + public void setUri(String uri) { + this.uri = uri; + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/IConfigService.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/IConfigService.java new file mode 100644 index 000000000000..b44182397f6f --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/IConfigService.java @@ -0,0 +1,12 @@ +package com.tabbyml.tabby4eclipse.lsp.protocol; + +import java.util.concurrent.CompletableFuture; + +import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; +import org.eclipse.lsp4j.jsonrpc.services.JsonSegment; + +@JsonSegment("tabby") +public interface IConfigService { + @JsonRequest("config") + CompletableFuture getConfig(Object params); +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/ILanguageServer.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/ILanguageServer.java new file mode 100644 index 000000000000..a97e758ccfc6 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/ILanguageServer.java @@ -0,0 +1,15 @@ +package com.tabbyml.tabby4eclipse.lsp.protocol; + +import org.eclipse.lsp4j.jsonrpc.services.JsonDelegate; +import org.eclipse.lsp4j.services.LanguageServer; + +public interface ILanguageServer extends LanguageServer { + @JsonDelegate + ITextDocumentServiceExt getTextDocumentServiceExt(); + + @JsonDelegate + IStatusService getStatusService(); + + @JsonDelegate + ITelemetryService getTelemetryService(); +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/IStatusService.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/IStatusService.java new file mode 100644 index 000000000000..6f4fdf9c986d --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/IStatusService.java @@ -0,0 +1,18 @@ +package com.tabbyml.tabby4eclipse.lsp.protocol; + +import java.util.concurrent.CompletableFuture; + +import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; +import org.eclipse.lsp4j.jsonrpc.services.JsonSegment; + +@JsonSegment("tabby") +public interface IStatusService { + @JsonRequest("status") + CompletableFuture getStatus(StatusRequestParams params); + + @JsonRequest("status/showHelpMessage") + CompletableFuture showHelpMessage(Object params); + + @JsonRequest("status/ignoredIssues/edit") + CompletableFuture editIngoredIssues(StatusIgnoredIssuesEditParams params); +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/ITelemetryService.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/ITelemetryService.java new file mode 100644 index 000000000000..c1bcff3a06d6 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/ITelemetryService.java @@ -0,0 +1,10 @@ +package com.tabbyml.tabby4eclipse.lsp.protocol; + +import org.eclipse.lsp4j.jsonrpc.services.JsonNotification; +import org.eclipse.lsp4j.jsonrpc.services.JsonSegment; + +@JsonSegment("tabby/telemetry") +public interface ITelemetryService { + @JsonNotification + void event(EventParams params); +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/ITextDocumentServiceExt.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/ITextDocumentServiceExt.java new file mode 100644 index 000000000000..ed564645a725 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/ITextDocumentServiceExt.java @@ -0,0 +1,12 @@ +package com.tabbyml.tabby4eclipse.lsp.protocol; + +import java.util.concurrent.CompletableFuture; + +import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; +import org.eclipse.lsp4j.jsonrpc.services.JsonSegment; + +@JsonSegment("textDocument") +public interface ITextDocumentServiceExt { + @JsonRequest + CompletableFuture inlineCompletion(InlineCompletionParams params); +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/InitializationOptions.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/InitializationOptions.java new file mode 100644 index 000000000000..eefc075ec36c --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/InitializationOptions.java @@ -0,0 +1,41 @@ +package com.tabbyml.tabby4eclipse.lsp.protocol; + +public class InitializationOptions { + private ClientProvidedConfig config; + private ClientInfo clientInfo; + private ClientCapabilities clientCapabilities; + + public InitializationOptions() { + } + + public InitializationOptions(ClientProvidedConfig config, ClientInfo clientInfo, + ClientCapabilities clientCapabilities) { + this.config = config; + this.clientInfo = clientInfo; + this.clientCapabilities = clientCapabilities; + } + + public ClientProvidedConfig getConfig() { + return config; + } + + public void setConfig(ClientProvidedConfig config) { + this.config = config; + } + + public ClientInfo getClientInfo() { + return clientInfo; + } + + public void setClientInfo(ClientInfo clientInfo) { + this.clientInfo = clientInfo; + } + + public ClientCapabilities getClientCapabilities() { + return clientCapabilities; + } + + public void setClientCapabilities(ClientCapabilities clientCapabilities) { + this.clientCapabilities = clientCapabilities; + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/InlineCompletionItem.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/InlineCompletionItem.java new file mode 100644 index 000000000000..55795c3eb942 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/InlineCompletionItem.java @@ -0,0 +1,76 @@ +package com.tabbyml.tabby4eclipse.lsp.protocol; + +import org.eclipse.lsp4j.Command; +import org.eclipse.lsp4j.Range; + +public class InlineCompletionItem { + private String insertText; + private String filterText; + private Range range; + private Command command; + private Data data; + + public InlineCompletionItem(String insertText, String filterText, Range range, Command command, Data data) { + this.insertText = insertText; + this.filterText = filterText; + this.range = range; + this.command = command; + this.data = data; + } + + public String getInsertText() { + return insertText; + } + + public void setInsertText(String insertText) { + this.insertText = insertText; + } + + public String getFilterText() { + return filterText; + } + + public void setFilterText(String filterText) { + this.filterText = filterText; + } + + public Range getRange() { + return range; + } + + public void setRange(Range range) { + this.range = range; + } + + public Command getCommand() { + return command; + } + + public void setCommand(Command command) { + this.command = command; + } + + public Data getData() { + return data; + } + + public void setData(Data data) { + this.data = data; + } + + public static class Data { + private CompletionEventId eventId; + + public Data(CompletionEventId eventId) { + this.eventId = eventId; + } + + public CompletionEventId getEventId() { + return eventId; + } + + public void setEventId(CompletionEventId eventId) { + this.eventId = eventId; + } + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/InlineCompletionList.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/InlineCompletionList.java new file mode 100644 index 000000000000..f96c5dd45dbf --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/InlineCompletionList.java @@ -0,0 +1,29 @@ +package com.tabbyml.tabby4eclipse.lsp.protocol; + +import java.util.List; + +public class InlineCompletionList { + private boolean isIncomplete; + private List items; + + public InlineCompletionList(boolean isIncomplete, List items) { + this.isIncomplete = isIncomplete; + this.items = items; + } + + public boolean isIncomplete() { + return isIncomplete; + } + + public void setIncomplete(boolean isIncomplete) { + this.isIncomplete = isIncomplete; + } + + public List getItems() { + return items; + } + + public void setItems(List items) { + this.items = items; + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/InlineCompletionParams.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/InlineCompletionParams.java new file mode 100644 index 000000000000..a24775d011db --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/InlineCompletionParams.java @@ -0,0 +1,109 @@ +package com.tabbyml.tabby4eclipse.lsp.protocol; + +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.TextDocumentIdentifier; + +public class InlineCompletionParams { + private InlineCompletionContext context; + private TextDocumentIdentifier textDocument; + private Position position; + + public InlineCompletionParams(InlineCompletionContext context, TextDocumentIdentifier textDocument, + Position position) { + this.context = context; + this.textDocument = textDocument; + this.position = position; + } + + public InlineCompletionContext getContext() { + return context; + } + + public void setContext(InlineCompletionContext context) { + this.context = context; + } + + public TextDocumentIdentifier getTextDocument() { + return textDocument; + } + + public void setTextDocument(TextDocumentIdentifier textDocument) { + this.textDocument = textDocument; + } + + public Position getPosition() { + return position; + } + + public void setPosition(Position position) { + this.position = position; + } + + public static class InlineCompletionContext { + private InlineCompletionTriggerKind triggerKind; + private SelectedCompletionInfo selectedCompletionInfo; + + public InlineCompletionContext(InlineCompletionTriggerKind triggerKind, + SelectedCompletionInfo selectedCompletionInfo) { + this.triggerKind = triggerKind; + this.selectedCompletionInfo = selectedCompletionInfo; + } + + public InlineCompletionTriggerKind getTriggerKind() { + return triggerKind; + } + + public void setTriggerKind(InlineCompletionTriggerKind triggerKind) { + this.triggerKind = triggerKind; + } + + public SelectedCompletionInfo getSelectedCompletionInfo() { + return selectedCompletionInfo; + } + + public void setSelectedCompletionInfo(SelectedCompletionInfo selectedCompletionInfo) { + this.selectedCompletionInfo = selectedCompletionInfo; + } + } + + public enum InlineCompletionTriggerKind { + Invoked(0), Automatic(1); + + private final int value; + + InlineCompletionTriggerKind(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + } + + public static class SelectedCompletionInfo { + private String text; + private Range range; + + public SelectedCompletionInfo(String text, Range range) { + this.text = text; + this.range = range; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public Range getRange() { + return range; + } + + public void setRange(Range range) { + this.range = range; + } + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/ReadFileParams.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/ReadFileParams.java new file mode 100644 index 000000000000..ec2f82f8151e --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/ReadFileParams.java @@ -0,0 +1,37 @@ +package com.tabbyml.tabby4eclipse.lsp.protocol; + +import org.eclipse.lsp4j.Range; + +public class ReadFileParams { + private String uri; + private String format; + private Range range; + + public ReadFileParams(String uri) { + this.uri = uri; + } + + public String getUri() { + return uri; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public String getFormat() { + return format; + } + + public void setFormat(String format) { + this.format = format; + } + + public Range getRange() { + return range; + } + + public void setRange(Range range) { + this.range = range; + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/ReadFileResult.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/ReadFileResult.java new file mode 100644 index 000000000000..586e40768b48 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/ReadFileResult.java @@ -0,0 +1,20 @@ +package com.tabbyml.tabby4eclipse.lsp.protocol; + +public class ReadFileResult { + private String text; + + public ReadFileResult() { + } + + public ReadFileResult(String text) { + this.text = text; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/SemanticTokensRangeResult.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/SemanticTokensRangeResult.java new file mode 100644 index 000000000000..7a4d302d4a0d --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/SemanticTokensRangeResult.java @@ -0,0 +1,28 @@ +package com.tabbyml.tabby4eclipse.lsp.protocol; + +import org.eclipse.lsp4j.SemanticTokens; +import org.eclipse.lsp4j.SemanticTokensLegend; + +public class SemanticTokensRangeResult { + private SemanticTokensLegend legend; + private SemanticTokens tokens; + + public SemanticTokensRangeResult() { + } + + public SemanticTokensLegend getLegend() { + return legend; + } + + public void setLegend(SemanticTokensLegend legend) { + this.legend = legend; + } + + public SemanticTokens getTokens() { + return tokens; + } + + public void setTokens(SemanticTokens tokens) { + this.tokens = tokens; + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/StatusIgnoredIssuesEditParams.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/StatusIgnoredIssuesEditParams.java new file mode 100644 index 000000000000..53572627b3c3 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/StatusIgnoredIssuesEditParams.java @@ -0,0 +1,35 @@ +package com.tabbyml.tabby4eclipse.lsp.protocol; + +public class StatusIgnoredIssuesEditParams { + private String operation; + private String[] issues; + + public StatusIgnoredIssuesEditParams() { + } + + public String getOperation() { + return operation; + } + + public void setOperation(String operation) { + this.operation = operation; + } + + public String[] getIssues() { + return issues; + } + + public void setIssues(String[] issues) { + this.issues = issues; + } + + public static class Operation { + public static final String ADD = "add"; + public static final String REMOVE = "remove"; + public static final String REMOVE_ALL = "removeAll"; + } + + public static class StatusIssuesName { + public static final String COMPLETION_RESPONSE_SLOW = "completionResponseSlow"; + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/StatusInfo.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/StatusInfo.java new file mode 100644 index 000000000000..7c790dffa91e --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/StatusInfo.java @@ -0,0 +1,59 @@ +package com.tabbyml.tabby4eclipse.lsp.protocol; + +import java.util.Map; + +import org.eclipse.lsp4j.Command; + +public class StatusInfo { + private String status; + private String tooltip; + private Map serverHealth; + private Command command; + + public StatusInfo() { + this.status = Status.DISCONNECTED; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getTooltip() { + return tooltip; + } + + public void setTooltip(String tooltip) { + this.tooltip = tooltip; + } + + public Map getServerHealth() { + return serverHealth; + } + + public void setServerHealth(Map serverHealth) { + this.serverHealth = serverHealth; + } + + public Command getCommand() { + return command; + } + + public void setCommand(Command command) { + this.command = command; + } + + public static class Status { + public static final String CONNECTING = "connecting"; + public static final String UNAUTHORIZED = "unauthorized"; + public static final String DISCONNECTED = "disconnected"; + public static final String READY = "ready"; + public static final String READY_FOR_AUTO_TRIGGER = "readyForAutoTrigger"; + public static final String READY_FOR_MANUAL_TRIGGER = "readyForManualTrigger"; + public static final String FETCHING = "fetching"; + public static final String COMPLETION_RESPONSE_SLOW = "completionResponseSlow"; + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/StatusRequestParams.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/StatusRequestParams.java new file mode 100644 index 000000000000..d0f08866f567 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/lsp/protocol/StatusRequestParams.java @@ -0,0 +1,16 @@ +package com.tabbyml.tabby4eclipse.lsp.protocol; + +public class StatusRequestParams { + private boolean recheckConnection; + + public StatusRequestParams() { + } + + public boolean getRecheckConnection() { + return recheckConnection; + } + + public void setRecheckConnection(boolean recheckConnection) { + this.recheckConnection = recheckConnection; + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/preferences/MainPreferencesPage.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/preferences/MainPreferencesPage.java new file mode 100644 index 000000000000..e87fdc96d31a --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/preferences/MainPreferencesPage.java @@ -0,0 +1,125 @@ +package com.tabbyml.tabby4eclipse.preferences; + +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.jface.preference.BooleanFieldEditor; +import org.eclipse.jface.preference.FieldEditorPreferencePage; +import org.eclipse.jface.preference.PreferenceDialog; +import org.eclipse.jface.preference.StringFieldEditor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPreferencePage; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.dialogs.PreferencesUtil; + +import com.tabbyml.tabby4eclipse.Activator; + +public class MainPreferencesPage extends FieldEditorPreferencePage implements IWorkbenchPreferencePage { + public static final String ID = "com.tabbyml.tabby4eclipse.preferences.main"; + + public static void openPreferences() { + PreferenceDialog dialog = PreferencesUtil.createPreferenceDialogOn( + PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(), MainPreferencesPage.ID, + new String[] { MainPreferencesPage.ID, "org.eclipse.lsp4e.preferences", + "org.eclipse.lsp4e.logging.preferences", "org.eclipse.ui.preferencePages.Keys" }, + (Object) null); + if (dialog != null) { + dialog.open(); + } + } + + public MainPreferencesPage() { + super(GRID); + setPreferenceStore(Activator.getDefault().getPreferenceStore()); + } + + @Override + public void createFieldEditors() { + Composite parent = getFieldEditorParent(); + createServerGroup(parent); + createCompletionGroup(parent); + createEnvironmentGroup(parent); + createTelemetryGroup(parent); + } + + private void createServerGroup(Composite parent) { + Group group = new Group(parent, SWT.NONE); + GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).grab(true, false).span(2, 1).applyTo(group); + group.setText("Server"); + group.setLayout(new GridLayout()); + + Composite grid = new Composite(group, SWT.NONE); + grid.setLayout(new GridLayout(2, false)); + + StringFieldEditor endpointInput = new StringFieldEditor(PreferencesService.KEY_SERVER_ENDPOINT, "Endpoint", + grid); + addField(endpointInput); + StringFieldEditor tokenInput = new StringFieldEditor(PreferencesService.KEY_SERVER_TOKEN, "Token", grid); + tokenInput.getTextControl(grid).setEchoChar('*'); + addField(tokenInput); + + Label tip = new Label(grid, SWT.WRAP); + tip.setText( + "Note: If left empty, the server endpoint config in `~/.tabby-client/agent/config.toml` will be used."); + GridDataFactory.fillDefaults().indent(10, 2).span(2, 1).applyTo(tip); + } + + private void createCompletionGroup(Composite parent) { + Group group = new Group(parent, SWT.NONE); + GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).grab(true, false).span(2, 1).applyTo(group); + group.setText("Completion"); + group.setLayout(new GridLayout()); + + Composite grid = new Composite(group, SWT.NONE); + grid.setLayout(new GridLayout(2, false)); + + BooleanFieldEditor autoTriggerCheckbox = new BooleanFieldEditor( + PreferencesService.KEY_INLINE_COMPLETION_TRIGGER_AUTO, "Automatically trigger inline completion", grid); + addField(autoTriggerCheckbox); + } + + private void createEnvironmentGroup(Composite parent) { + Group group = new Group(parent, SWT.NONE); + GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).grab(true, false).span(2, 1).applyTo(group); + group.setText("Environment"); + group.setLayout(new GridLayout()); + + Composite grid = new Composite(group, SWT.NONE); + grid.setLayout(new GridLayout(2, false)); + + StringFieldEditor nodeBinaryPathInput = new StringFieldEditor(PreferencesService.KEY_NODE_BINARY_PATH, + "Node.js binary path", grid); + addField(nodeBinaryPathInput); + + Label tip = new Label(grid, SWT.WRAP); + tip.setText( + "Tabby will attempt to find the Node binary in the `PATH` environment variable for running tabby-agent.\nYou can specify the path to the Node.js binary here if required. Restart IDE to take effect."); + GridDataFactory.fillDefaults().indent(10, 2).span(2, 1).applyTo(tip); + } + + private void createTelemetryGroup(Composite parent) { + Group group = new Group(parent, SWT.NONE); + GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).grab(true, false).span(2, 1).applyTo(group); + group.setText("Telemetry"); + group.setLayout(new GridLayout()); + + Composite grid = new Composite(group, SWT.NONE); + grid.setLayout(new GridLayout(2, false)); + + BooleanFieldEditor disableAnonymousUsageTrackingCheckbox = new BooleanFieldEditor( + PreferencesService.KEY_ANONYMOUS_USAGE_TRACKING_DISABLED, "Disable anonymous usage tracking", grid); + addField(disableAnonymousUsageTrackingCheckbox); + + Label tip = new Label(grid, SWT.WRAP); + tip.setText( + "Tabby collects aggregated anonymous usage data and sends it to the Tabby team to help improve our products.\nYour code, generated completions, or any identifying information is never tracked or transmitted.\nFor more details on data collection, please check our online documentation."); + GridDataFactory.fillDefaults().indent(10, 2).span(2, 1).applyTo(tip); + } + + @Override + public void init(IWorkbench workbench) { + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/preferences/PreferencesService.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/preferences/PreferencesService.java new file mode 100644 index 000000000000..2b9705403384 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/preferences/PreferencesService.java @@ -0,0 +1,92 @@ +package com.tabbyml.tabby4eclipse.preferences; + +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.util.IPropertyChangeListener; +import org.eclipse.jface.util.PropertyChangeEvent; +import org.eclipse.lsp4j.DidChangeConfigurationParams; +import org.eclipse.lsp4j.services.WorkspaceService; + +import com.tabbyml.tabby4eclipse.Activator; +import com.tabbyml.tabby4eclipse.Logger; +import com.tabbyml.tabby4eclipse.lsp.LanguageServerService; +import com.tabbyml.tabby4eclipse.lsp.protocol.ClientProvidedConfig; +import com.tabbyml.tabby4eclipse.lsp.protocol.ILanguageServer; + +public class PreferencesService { + public static final String KEY_SERVER_ENDPOINT = "SERVER_ENDPOINT"; + public static final String KEY_SERVER_TOKEN = "SERVER_TOKEN"; + public static final String KEY_INLINE_COMPLETION_TRIGGER_AUTO = "INLINE_COMPLETION_TRIGGER_AUTO"; + public static final String KEY_NODE_BINARY_PATH = "NODE_BINARY_PATH"; + public static final String KEY_ANONYMOUS_USAGE_TRACKING_DISABLED = "ANONYMOUS_USAGE_TRACKING_DISABLED"; + + public static PreferencesService getInstance() { + return LazyHolder.INSTANCE; + } + + private static class LazyHolder { + private static final PreferencesService INSTANCE = new PreferencesService(); + } + + private Logger logger = new Logger("PreferencesService"); + + public void init() { + getPreferenceStore().setDefault(KEY_INLINE_COMPLETION_TRIGGER_AUTO, true); + + getPreferenceStore().addPropertyChangeListener(new IPropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent event) { + logger.info("Syncing configuration."); + LanguageServerService.getInstance().getServer().execute((server) -> { + WorkspaceService workspaceService = ((ILanguageServer) server).getWorkspaceService(); + DidChangeConfigurationParams params = new DidChangeConfigurationParams(); + params.setSettings(buildClientProvidedConfig()); + workspaceService.didChangeConfiguration(params); + return null; + }); + } + }); + } + + public IPreferenceStore getPreferenceStore() { + return Activator.getDefault().getPreferenceStore(); + } + + public ClientProvidedConfig buildClientProvidedConfig() { + ClientProvidedConfig config = new ClientProvidedConfig(); + + ClientProvidedConfig.ServerConfig serverConfig = new ClientProvidedConfig.ServerConfig(getServerEndpoint(), + getServerToken()); + config.setServer(serverConfig); + + ClientProvidedConfig.InlineCompletionConfig inlineCompletionConfig = new ClientProvidedConfig.InlineCompletionConfig( + getInlineCompletionTriggerAuto() ? ClientProvidedConfig.InlineCompletionConfig.TriggerMode.AUTO + : ClientProvidedConfig.InlineCompletionConfig.TriggerMode.MANUAL); + config.setInlineCompletion(inlineCompletionConfig); + + ClientProvidedConfig.AnonymousUsageTrackingConfig anonymousUsageTrackingConfig = new ClientProvidedConfig.AnonymousUsageTrackingConfig( + getAnonymousUsageTrackingDisabled()); + config.setAnonymousUsageTracking(anonymousUsageTrackingConfig); + + return config; + } + + public String getServerEndpoint() { + return getPreferenceStore().getString(KEY_SERVER_ENDPOINT); + } + + public String getServerToken() { + return getPreferenceStore().getString(KEY_SERVER_TOKEN); + } + + public boolean getInlineCompletionTriggerAuto() { + return getPreferenceStore().getBoolean(KEY_INLINE_COMPLETION_TRIGGER_AUTO); + } + + public String getNodeBinaryPath() { + return getPreferenceStore().getString(KEY_NODE_BINARY_PATH); + } + + public boolean getAnonymousUsageTrackingDisabled() { + return getPreferenceStore().getBoolean(KEY_ANONYMOUS_USAGE_TRACKING_DISABLED); + } +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/statusbar/StatusbarContribution.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/statusbar/StatusbarContribution.java new file mode 100644 index 000000000000..046a28e92086 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/statusbar/StatusbarContribution.java @@ -0,0 +1,174 @@ +package com.tabbyml.tabby4eclipse.statusbar; + +import org.eclipse.lsp4j.Command; +import org.eclipse.lsp4j.ExecuteCommandParams; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.CLabel; +import org.eclipse.swt.events.MouseAdapter; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Menu; +import org.eclipse.swt.widgets.MenuItem; +import org.eclipse.ui.menus.WorkbenchWindowControlContribution; + +import com.tabbyml.tabby4eclipse.Images; +import com.tabbyml.tabby4eclipse.chat.ChatViewUtils; +import com.tabbyml.tabby4eclipse.lsp.LanguageServerService; +import com.tabbyml.tabby4eclipse.lsp.StatusInfoHolder; +import com.tabbyml.tabby4eclipse.lsp.protocol.StatusInfo; +import com.tabbyml.tabby4eclipse.preferences.MainPreferencesPage; + +public class StatusbarContribution extends WorkbenchWindowControlContribution { + private static final String TOOLTIP_INITIALIZATION_FAILED = "Tabby: Initialization Failed"; + + private StatusInfoHolder statusInfoHolder = StatusInfoHolder.getInstance(); + + @Override + protected Control createControl(Composite parent) { + CLabel label = new CLabel(parent, SWT.NONE); + label.setText("Tabby"); + label.addMouseListener(new MouseAdapter() { + @Override + public void mouseUp(MouseEvent e) { + Menu menu = createMenu(label); + menu.setVisible(true); + } + }); + + updateLabel(label); + + StatusInfoHolder.getInstance().addStatusDidChangeListener(() -> { + label.getDisplay().asyncExec(() -> { + updateLabel(label); + }); + }); + + return label; + } + + private void updateLabel(CLabel label) { + if (statusInfoHolder.isConnectionFailed()) { + label.setImage(Images.getIcon(Images.ICON_ERROR)); + label.setToolTipText(TOOLTIP_INITIALIZATION_FAILED); + } + StatusInfo statusInfo = statusInfoHolder.getStatusInfo(); + if (statusInfo.getTooltip() != null) { + label.setToolTipText(statusInfo.getTooltip()); + } else { + label.setToolTipText("Tabby: " + statusInfo.getStatus()); + } + switch (statusInfo.getStatus()) { + case StatusInfo.Status.CONNECTING: + label.setImage(Images.getIcon(Images.ICON_LOADING)); + break; + case StatusInfo.Status.UNAUTHORIZED: + label.setImage(Images.getIcon(Images.ICON_WARN)); + break; + case StatusInfo.Status.DISCONNECTED: + label.setImage(Images.getIcon(Images.ICON_ERROR)); + break; + case StatusInfo.Status.READY: + label.setImage(Images.getIcon(Images.ICON_CHECK)); + break; + case StatusInfo.Status.READY_FOR_AUTO_TRIGGER: + label.setImage(Images.getIcon(Images.ICON_CHECK)); + break; + case StatusInfo.Status.READY_FOR_MANUAL_TRIGGER: + label.setImage(Images.getIcon(Images.ICON_CHECK)); + break; + case StatusInfo.Status.FETCHING: + label.setImage(Images.getIcon(Images.ICON_LOADING)); + break; + case StatusInfo.Status.COMPLETION_RESPONSE_SLOW: + label.setImage(Images.getIcon(Images.ICON_WARN)); + break; + default: + break; + } + } + + private Menu createMenu(CLabel label) { + Menu menu = new Menu(label); + + MenuItem statusItem = new MenuItem(menu, SWT.NONE); + if (statusInfoHolder.isConnectionFailed()) { + statusItem.setImage(Images.getIcon(Images.ICON_ERROR)); + statusItem.setText(TOOLTIP_INITIALIZATION_FAILED); + } + + StatusInfo statusInfo = statusInfoHolder.getStatusInfo(); + if (statusInfo.getTooltip() != null) { + statusItem.setText(statusInfo.getTooltip()); + } else { + statusItem.setText("Tabby: " + statusInfo.getStatus()); + } + Command commmand = statusInfo.getCommand(); + if (commmand != null) { + statusItem.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + LanguageServerService.getInstance().getServer().execute((server) -> { + ExecuteCommandParams params = new ExecuteCommandParams(); + params.setCommand(commmand.getCommand()); + params.setArguments(commmand.getArguments()); + server.getWorkspaceService().executeCommand(params); + return null; + }); + } + }); + } + switch (statusInfo.getStatus()) { + case StatusInfo.Status.CONNECTING: + statusItem.setImage(Images.getIcon(Images.ICON_LOADING)); + break; + case StatusInfo.Status.UNAUTHORIZED: + statusItem.setImage(Images.getIcon(Images.ICON_WARN)); + break; + case StatusInfo.Status.DISCONNECTED: + statusItem.setImage(Images.getIcon(Images.ICON_ERROR)); + break; + case StatusInfo.Status.READY: + statusItem.setImage(Images.getIcon(Images.ICON_CHECK)); + break; + case StatusInfo.Status.READY_FOR_AUTO_TRIGGER: + statusItem.setImage(Images.getIcon(Images.ICON_CHECK)); + break; + case StatusInfo.Status.READY_FOR_MANUAL_TRIGGER: + statusItem.setImage(Images.getIcon(Images.ICON_CHECK)); + break; + case StatusInfo.Status.FETCHING: + statusItem.setImage(Images.getIcon(Images.ICON_LOADING)); + break; + case StatusInfo.Status.COMPLETION_RESPONSE_SLOW: + statusItem.setImage(Images.getIcon(Images.ICON_WARN)); + break; + default: + break; + } + + MenuItem openChatViewItem = new MenuItem(menu, SWT.NONE); + openChatViewItem.setImage(Images.getIcon(Images.ICON_CHAT)); + openChatViewItem.setText("Open Tabby Chat"); + openChatViewItem.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + ChatViewUtils.openChatView(); + } + }); + + MenuItem openPreferencesItem = new MenuItem(menu, SWT.NONE); + openPreferencesItem.setImage(Images.getIcon(Images.ICON_SETTINGS)); + openPreferencesItem.setText("Open Settings"); + openPreferencesItem.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + MainPreferencesPage.openPreferences(); + } + }); + + return menu; + } +} diff --git a/clients/eclipse/scripts/copy-dependencies.js b/clients/eclipse/scripts/copy-dependencies.js new file mode 100644 index 000000000000..0662eddc6a61 --- /dev/null +++ b/clients/eclipse/scripts/copy-dependencies.js @@ -0,0 +1,34 @@ +#!/usr/bin/env node + +const fs = require('fs-extra'); +const path = require('path'); + +const cwd = process.cwd(); + +async function copyTabbyAgentScript() { + const sourceDir = path.join(cwd, 'node_modules', 'tabby-agent', 'dist', 'node'); + const targetDir = path.join(cwd, 'plugin', 'tabby-agent', 'dist', 'node'); + try { + await fs.emptyDir(targetDir); + await fs.copy(sourceDir, targetDir, { + filter: (src) => !src.endsWith('.js.map') + }); + console.log(`✅ Files copied: ${sourceDir} -> ${targetDir}`); + } catch (err) { + console.error('❌ Error copying files:', err); + } +} + +async function copyTabbyChatPanelScript() { + const sourceFile = path.join(cwd, 'node_modules', 'tabby-chat-panel', 'dist', 'iife', 'tabby-chat-panel.min.js'); + const targetFile = path.join(cwd, 'plugin', 'chat-panel', 'tabby-chat-panel.min.js'); + try { + await fs.copy(sourceFile, targetFile); + console.log(`✅ Files copied: ${sourceFile} -> ${targetFile}`); + } catch (err) { + console.error('❌ Error copying files:', err); + } +} + +copyTabbyAgentScript(); +copyTabbyChatPanelScript(); diff --git a/clients/example-vscode-lsp/.gitattributes b/clients/example-vscode-lsp/.gitattributes deleted file mode 100644 index c091529f367f..000000000000 --- a/clients/example-vscode-lsp/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -*.png filter=lfs diff=lfs merge=lfs -text \ No newline at end of file diff --git a/clients/example-vscode-lsp/.gitignore b/clients/example-vscode-lsp/.gitignore deleted file mode 100644 index d2703e4fe658..000000000000 --- a/clients/example-vscode-lsp/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -dist -*.vsix diff --git a/clients/example-vscode-lsp/README.md b/clients/example-vscode-lsp/README.md index 4573d6e41545..a89eaaf66e23 100644 --- a/clients/example-vscode-lsp/README.md +++ b/clients/example-vscode-lsp/README.md @@ -1,9 +1,6 @@ -# Example VSCode client for Tabby agent (LSP) +This example has been outdated and removed. -This is an example of a VSCode extension for the Tabby agent. It runs the Tabby agent as a language server and creates a client connecting to it. - -![Demo](./demo.png) - -For more information about Tabby agent, please refer to [Tabby agent](https://github.com/TabbyML/tabby/tree/main/clients/tabby-agent). - -For more information about developing a VSCode LSP extension, please refer to [VSCode Language Extensions Guide](https://code.visualstudio.com/api/language-extensions/overview). +As of tabby-agent v1.7, the Language Server Protocol (LSP) has become the only supported method to connect to tabby-agent. +For examples of creating IDE extensions that connect with tabby-agent using LSP, please refer to the source code of our IDE extensions: +- [VSCode Extension](https://github.com/TabbyML/tabby/tree/main/clients/vscode) +- [IntelliJ Platform Plugin](https://github.com/TabbyML/tabby/tree/main/clients/intellij) diff --git a/clients/example-vscode-lsp/demo.png b/clients/example-vscode-lsp/demo.png deleted file mode 100644 index 2bec013800ca..000000000000 --- a/clients/example-vscode-lsp/demo.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:542a3e013ee260fc455eeae21c374f98fa2471186f737c6b535f147eb10ffe6f -size 66601 diff --git a/clients/example-vscode-lsp/package.json b/clients/example-vscode-lsp/package.json deleted file mode 100644 index 3b53d63eb61a..000000000000 --- a/clients/example-vscode-lsp/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "example-vscode-tabby-lsp-client", - "description": "Example VSCode client for Tabby agent (LSP).", - "publisher": "TabbyML", - "version": "0.0.1", - "engines": { - "vscode": "^1.82.0" - }, - "main": "./dist/extension.js", - "activationEvents": [ - "onLanguage" - ], - "scripts": { - "build": "tsup --minify", - "watch": "tsup --watch ./ --ignore-watch ./dist" - }, - "devDependencies": { - "@types/vscode": "^1.82.0", - "esbuild-plugin-copy": "^2.1.1", - "tabby-agent": "1.3.3" - }, - "dependencies": { - "vscode-languageclient": "^9.0.1" - } -} diff --git a/clients/example-vscode-lsp/src/extension.ts b/clients/example-vscode-lsp/src/extension.ts deleted file mode 100644 index 675cbee3ee0c..000000000000 --- a/clients/example-vscode-lsp/src/extension.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { ExtensionContext } from "vscode"; -import { LanguageClient, ServerOptions, TransportKind } from "vscode-languageclient/node"; - -let client: LanguageClient; - -export async function activate(context: ExtensionContext): Promise { - console.debug("Tabby LSP Example: activate"); - - const serverModulePath = context.asAbsolutePath("dist/server/tabby-agent.js"); - const serverOptions: ServerOptions = { - run: { - module: serverModulePath, - args: ["--lsp"], - transport: TransportKind.ipc, - }, - debug: { - module: serverModulePath, - args: ["--lsp"], - transport: TransportKind.ipc, - }, - }; - const clientOptions = { - documentSelector: [{ pattern: "**/*" }], - }; - if (!client) { - client = new LanguageClient("Tabby LSP Example", serverOptions, clientOptions); - } - return await client.start(); -} - -export async function deactivate(): Promise { - console.debug("Tabby LSP Example: deactivate"); - - if (client) { - return await client.stop(); - } -} diff --git a/clients/example-vscode-lsp/tsconfig.json b/clients/example-vscode-lsp/tsconfig.json deleted file mode 100644 index 954a4cc3f267..000000000000 --- a/clients/example-vscode-lsp/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "target": "ES2020", - "lib": ["ES2020"], - "sourceMap": true, - "strict": true - }, - "include": ["./src"] -} diff --git a/clients/example-vscode-lsp/tsup.config.ts b/clients/example-vscode-lsp/tsup.config.ts deleted file mode 100644 index 1efa0a2b4ecb..000000000000 --- a/clients/example-vscode-lsp/tsup.config.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { defineConfig } from "tsup"; -import { copy } from "esbuild-plugin-copy"; -import { dependencies } from "./package.json"; - -export default () => [ - defineConfig({ - name: "node", - entry: ["src/extension.ts"], - outDir: "dist", - platform: "node", - target: "node18", - external: ["vscode"], - noExternal: Object.keys(dependencies), - clean: true, - esbuildPlugins: [ - copy({ - assets: [ - { - from: "../tabby-agent/dist/cli.js", - to: "./server/tabby-agent.js", - }, - { - from: "../tabby-agent/dist/wasm/*", - to: "./server/wasm", - }, - ], - }), - ], - }), -]; diff --git a/clients/intellij/.gitignore b/clients/intellij/.gitignore index b63da4551b2e..8146b1544afe 100644 --- a/clients/intellij/.gitignore +++ b/clients/intellij/.gitignore @@ -1,4 +1,5 @@ .gradle +.intellijPlatform build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ @@ -15,6 +16,7 @@ build/ out/ !**/src/main/**/out/ !**/src/test/**/out/ +*.log ### Eclipse ### .apt_generated diff --git a/clients/intellij/.idea/kotlinc.xml b/clients/intellij/.idea/kotlinc.xml index 217e5c51fbf1..4cb745724991 100644 --- a/clients/intellij/.idea/kotlinc.xml +++ b/clients/intellij/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/clients/intellij/.idea/vcs.xml b/clients/intellij/.idea/vcs.xml index 8fe5bdbdc5ac..623660f4d5a5 100644 --- a/clients/intellij/.idea/vcs.xml +++ b/clients/intellij/.idea/vcs.xml @@ -3,5 +3,7 @@ + + \ No newline at end of file diff --git a/clients/intellij/.run/Run IDE with Plugin.run.xml b/clients/intellij/.run/Run IDE with Plugin.run.xml deleted file mode 100644 index 7747a294009c..000000000000 --- a/clients/intellij/.run/Run IDE with Plugin.run.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - true - true - false - - - \ No newline at end of file diff --git a/clients/intellij/.run/Run Plugin Verifier.run.xml b/clients/intellij/.run/Run Plugin Verifier.run.xml new file mode 100644 index 000000000000..a4fffc7b1220 --- /dev/null +++ b/clients/intellij/.run/Run Plugin Verifier.run.xml @@ -0,0 +1,24 @@ + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/clients/intellij/.run/Run Plugin.run.xml b/clients/intellij/.run/Run Plugin.run.xml new file mode 100644 index 000000000000..7d6ef8b10578 --- /dev/null +++ b/clients/intellij/.run/Run Plugin.run.xml @@ -0,0 +1,25 @@ + + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/clients/intellij/CHANGELOG.md b/clients/intellij/CHANGELOG.md index f109473218c7..f62623995160 100644 --- a/clients/intellij/CHANGELOG.md +++ b/clients/intellij/CHANGELOG.md @@ -1,3 +1,149 @@ +## 1.13.0 + +### Features + +- **Chat**: + - Requires Tabby Server 0.27.0 or later. + - You can now select code in the editor and use the `Code Review` option from the context menu to review your code and add comments using Tabby Chat. + +## 1.12.0 + +### Features + +- **Chat**: + - Requires connecting to a Tabby server version 0.26.0 or later. + - Added support to view recent chat history in the chat panel. +- Added an action `Generate Commit Message` in the `Git` menu to generate a commit message based on the current changes. + +## 1.11.0 + +### Features + +- **Chat**: + - Requires connecting to a Tabby server version 0.25.0 or later. + - Now uses the active editor as context by default. + +### Fixes & Improvements + +- Optimized the display of file paths in the chat panel. + +## 1.10.1 + +### Fixes & Improvements + +- Reduced the use of read locks to prevent UI freezing issues when collecting declaration code snippets for code completion. + +## 1.10.0 + +### Features + +- **Chat**: + - Added support to explicitly select a configured Git repository as the context for chat conversations. + - Added support to use the active editor selection as the context for chat conversations. + - **Note**: Requires updating the Tabby server to version 0.23.0 or later. + +## 1.9.1 + +### Fixes & Improvements + +- Updated the chat panel to be compatible with Tabby server versions 0.21.2, 0.22.0, and later. + +## 1.9.0 + +### Features + +- Added a list of actions in the editor's right-click context menu to interact with the Tabby chat panel. + +### Fixes & Improvements + +- Added support for collecting declaration code snippets to improve the code completion context. +- Fixed the "Test Connection" button in the settings page to wait for the response correctly. +- Fixed the bug where changing the completion trigger mode did not take effect immediately. +- Fixed the chat panel theme syncing issue when switching between light and dark themes. +- Added a help message when failing to create the chat panel. + +## 1.8.6 + +### Fixes & Improvements + +- Fixed unhandled exception for requests when the completion API is not available on the server. +- Added support for the latest IntelliJ Platform IDE versions. + +## 1.8.4 + +### Fixes & Improvements + +- Fixed an issue where the chat panel failed to display when the endpoint configuration ended with a trailing slash. + +## 1.8.3 + +### Fixes & Improvements + +- Fixed a bug that caused the Tabby plugin to not initialize when TLS certificates failed to load. (https://github.com/TabbyML/tabby/issues/3248) + +## 1.8.2 + +### Fixes & Improvements + +- Fix DataStore initialization that prevented Tabby from starting on a fresh installation. (https://github.com/TabbyML/tabby/issues/3234) + +## 1.8.1 + +### Features + +- Updated the chat panel to compatible with Tabby server v0.18.0 or later. + +## 1.7.1 + +### Features + +- Introduced a new chat view feature that allows users to engage in conversations with their AI assistant. +- Added support for HTTP proxy configuration. Users can now set up an HTTP proxy either through environment variables or in the config file. + +### Fixes & Improvements + +- Fixed a bug where the inline completion service created too many jobs when receiving multiple document change events at the same time, such as during a reformat code action. + +## 1.6.3 + +### Fixes & Improvements + +- Fixed a bug that caused the Tabby plugin to get stuck in initialization when an editor has no related virtual file. + +## 1.6.2 + +### Breaking Changes + +- The minimum required IDE version has been increased to >= 2023.1. + +### Features + +- Added support for multiple choices in inline completion. Completion choices can be cycled by shortcuts `Alt + [` and `Alt +]`. +- Added support to collect workspace info and git context to enhance inline completion. Credits to Vladimir (#2044). + +### Fixes & Improvements + +- Updated the underlay protocol to connect to tabby-agent to use LSP. +- Improved interaction when partially accepting a completion. + +## 1.4.1 + +### Fixes: + +- Added support for IntelliJ Platform IDEs version 2024.1. + +## 1.4.0 + +### Features + +- Added support for loading system-wide CA certificates. Previously, only Node.js bundled CA certificates were used. +- Added support for loading configurations from Tabby server, including `Disabling Client-side Telemetry`. +- Removed the notification when disconnected from Tabby server, keep only status bar icon. + +### Fixes + +- Fixed the unexpected behaviors that occur when a closed project is reopened within the same IDE process. + ## 1.3.2 ### Fixes: diff --git a/clients/intellij/README.md b/clients/intellij/README.md index a3460331fac0..0bd991776748 100644 --- a/clients/intellij/README.md +++ b/clients/intellij/README.md @@ -5,7 +5,7 @@ Tabby is an AI coding assistant that can suggest multi-line code or full functions in real-time. -Tabby IntelliJ Platform plugin works with all [IntelliJ Platform IDEs](https://plugins.jetbrains.com/docs/intellij/intellij-platform.html#ides-based-on-the-intellij-platform) that have build 2022.2.5 or later versions, such as [IDEA](https://www.jetbrains.com/idea/), [PyCharm](https://www.jetbrains.com/pycharm/), [GoLand](https://www.jetbrains.com/go/), [Android Studio](https://developer.android.com/studio), and [more](https://plugins.jetbrains.com/docs/intellij/intellij-platform.html#ides-based-on-the-intellij-platform). +Tabby IntelliJ Platform plugin works with all [IntelliJ Platform IDEs](https://plugins.jetbrains.com/docs/intellij/intellij-platform.html#ides-based-on-the-intellij-platform) that have build 2023.1 or later versions, such as [IDEA](https://www.jetbrains.com/idea/), [PyCharm](https://www.jetbrains.com/pycharm/), [GoLand](https://www.jetbrains.com/go/), [Android Studio](https://developer.android.com/studio), and [more](https://plugins.jetbrains.com/docs/intellij/intellij-platform.html#ides-based-on-the-intellij-platform). ## Getting Started diff --git a/clients/intellij/build.gradle.kts b/clients/intellij/build.gradle.kts index 054cb456566d..6ebe27880465 100644 --- a/clients/intellij/build.gradle.kts +++ b/clients/intellij/build.gradle.kts @@ -1,24 +1,35 @@ +// Configure Gradle IntelliJ Plugin +// Read more: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html + plugins { id("java") - id("org.jetbrains.kotlin.jvm") version "1.8.21" - id("org.jetbrains.intellij") version "1.13.3" + id("org.jetbrains.kotlin.jvm") version "1.9.25" + id("org.jetbrains.intellij.platform") version "2.0.0" id("org.jetbrains.changelog") version "2.2.0" } -group = "com.tabbyml" -version = "1.3.2" - repositories { mavenCentral() + intellijPlatform { + defaultRepositories() + } } -// Configure Gradle IntelliJ Plugin -// Read more: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html -intellij { - version.set("2022.2.5") - type.set("IC") // Target IDE Platform - - plugins.set(listOf(/* Plugin Dependencies */)) +dependencies { + intellijPlatform { + intellijIdeaCommunity("2023.1") + bundledPlugins( + listOf( + "Git4Idea", + "org.jetbrains.kotlin", + ) + ) + pluginVerifier() + zipSigner() + instrumentationTools() + } + implementation("org.eclipse.lsp4j:org.eclipse.lsp4j:0.23.1") + implementation("io.github.z4kn4fein:semver:2.0.0") } tasks { @@ -31,39 +42,61 @@ tasks { kotlinOptions.jvmTarget = "17" } - patchPluginXml { - sinceBuild.set("222") - untilBuild.set("233.*") - changeNotes.set(provider { - changelog.renderItem( - changelog.getLatest(), - org.jetbrains.changelog.Changelog.OutputType.HTML - ) - }) + intellijPlatform { + pluginConfiguration { + version.set("1.14.0-dev") + changeNotes.set(provider { + changelog.renderItem( + changelog.getLatest(), + org.jetbrains.changelog.Changelog.OutputType.HTML + ) + }) + ideaVersion { + sinceBuild.set("231") + untilBuild.set(provider { null }) + } + } + pluginVerification { + ides { + recommended() + } + } + signing { + certificateChain.set(System.getenv("CERTIFICATE_CHAIN")) + privateKey.set(System.getenv("PRIVATE_KEY")) + } + publishing { + token.set(System.getenv("PUBLISH_TOKEN")) + channels.set(listOf(System.getenv("PUBLISH_CHANNEL"))) + } } - val copyNodeScripts by register("copyNodeScripts") { - dependsOn(prepareSandbox) - from("node_scripts") - into("build/idea-sandbox/plugins/intellij-tabby/node_scripts") + register("buildDependencies") { + exec { + commandLine("pnpm", "turbo", "build") + } } - buildSearchableOptions { - dependsOn(copyNodeScripts) - } + prepareSandbox { + dependsOn("buildDependencies") - runIde { - dependsOn(copyNodeScripts) - } - - signPlugin { - certificateChain.set(System.getenv("CERTIFICATE_CHAIN")) - privateKey.set(System.getenv("PRIVATE_KEY")) - password.set(System.getenv("PRIVATE_KEY_PASSWORD")) - } + // Copy the tabby-agent to the sandbox + from( + fileTree("node_modules/tabby-agent/dist/") { + include("node/**/*") + exclude("**/*.js.map") + } + ) { + into("intellij-tabby/tabby-agent/") + } - publishPlugin { - token.set(System.getenv("PUBLISH_TOKEN")) - channels.set(listOf("alpha")) + // Copy the tabby-chat-panel to the sandbox + from( + fileTree("node_modules/tabby-chat-panel/dist/") { + include("iife/tabby-chat-panel.min.js") + } + ) { + into("intellij-tabby/tabby-chat-panel/") + } } } diff --git a/clients/intellij/gradle.properties b/clients/intellij/gradle.properties index 2bca45cc813a..0d6aa7b61fbc 100644 --- a/clients/intellij/gradle.properties +++ b/clients/intellij/gradle.properties @@ -1,3 +1 @@ kotlin.stdlib.default.dependency=false -# TODO temporary workaround for Kotlin 1.8.20+ (https://jb.gg/intellij-platform-kotlin-oom) -kotlin.incremental.useClasspathSnapshot=false diff --git a/clients/intellij/gradle/wrapper/gradle-wrapper.jar b/clients/intellij/gradle/wrapper/gradle-wrapper.jar index 249e5832f090..c1962a79e29d 100644 Binary files a/clients/intellij/gradle/wrapper/gradle-wrapper.jar and b/clients/intellij/gradle/wrapper/gradle-wrapper.jar differ diff --git a/clients/intellij/gradle/wrapper/gradle-wrapper.properties b/clients/intellij/gradle/wrapper/gradle-wrapper.properties index fae08049a6f0..171d8761b27a 100644 --- a/clients/intellij/gradle/wrapper/gradle-wrapper.properties +++ b/clients/intellij/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/clients/intellij/gradlew b/clients/intellij/gradlew index 1b6c787337ff..aeb74cbb43e3 100755 --- a/clients/intellij/gradlew +++ b/clients/intellij/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,13 +80,10 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -143,12 +140,16 @@ fi if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -193,6 +194,10 @@ if "$cygwin" || "$msys" ; then done fi + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + # Collect all arguments for the java command; # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # shell script including quotes and variable substitutions, so put them in @@ -205,6 +210,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/clients/intellij/gradlew.bat b/clients/intellij/gradlew.bat index ac1b06f93825..6689b85beecd 100644 --- a/clients/intellij/gradlew.bat +++ b/clients/intellij/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/clients/intellij/node_scripts/tabby-agent.js b/clients/intellij/node_scripts/tabby-agent.js deleted file mode 100755 index 4afacab3bc35..000000000000 --- a/clients/intellij/node_scripts/tabby-agent.js +++ /dev/null @@ -1,269 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -var events = require('events'); -var DO = require('crypto'); -var Vx = require('path'); -var oT = require('os'); -var m3 = require('readline'); - -function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } - -var DO__default = /*#__PURE__*/_interopDefault(DO); -var Vx__default = /*#__PURE__*/_interopDefault(Vx); -var oT__default = /*#__PURE__*/_interopDefault(oT); -var m3__default = /*#__PURE__*/_interopDefault(m3); - -/** - * Tabby Agent - * https://github.com/tabbyml/tabby/tree/main/clients/tabby-agent - * Copyright (c) 2023-2024 TabbyML, Inc. - * Licensed under the Apache License 2.0. - */ -var SO=Object.create;var Bo=Object.defineProperty;var EO=Object.getOwnPropertyDescriptor;var RO=Object.getOwnPropertyNames;var CO=Object.getPrototypeOf,xO=Object.prototype.hasOwnProperty;var oe=(t=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(t,{get:(e,r)=>(typeof require<"u"?require:e)[r]}):t)(function(t){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+t+'" is not supported')});var TO=(t,e)=>()=>(t&&(e=t(t=0)),e);var A=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports),AO=(t,e)=>{for(var r in e)Bo(t,r,{get:e[r],enumerable:!0});},Gg=(t,e,r,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of RO(e))!xO.call(t,i)&&i!==r&&Bo(t,i,{get:()=>e[i],enumerable:!(n=EO(e,i))||n.enumerable});return t};var $t=(t,e,r)=>(r=t!=null?SO(CO(t)):{},Gg(e||!t||!t.__esModule?Bo(r,"default",{value:t,enumerable:!0}):r,t)),PO=t=>Gg(Bo({},"__esModule",{value:!0}),t);var vl=A((Jz,Jg)=>{var Zg=Object.prototype.toString;Jg.exports=function(e){var r=Zg.call(e),n=r==="[object Arguments]";return n||(n=r!=="[object Array]"&&e!==null&&typeof e=="object"&&typeof e.length=="number"&&e.length>=0&&Zg.call(e.callee)==="[object Function]"),n};});var s_=A((Yz,i_)=>{var n_;Object.keys||(Ts=Object.prototype.hasOwnProperty,wl=Object.prototype.toString,Yg=vl(),Sl=Object.prototype.propertyIsEnumerable,Xg=!Sl.call({toString:null},"toString"),Qg=Sl.call(function(){},"prototype"),As=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],Vo=function(t){var e=t.constructor;return e&&e.prototype===t},e_={$applicationCache:!0,$console:!0,$external:!0,$frame:!0,$frameElement:!0,$frames:!0,$innerHeight:!0,$innerWidth:!0,$onmozfullscreenchange:!0,$onmozfullscreenerror:!0,$outerHeight:!0,$outerWidth:!0,$pageXOffset:!0,$pageYOffset:!0,$parent:!0,$scrollLeft:!0,$scrollTop:!0,$scrollX:!0,$scrollY:!0,$self:!0,$webkitIndexedDB:!0,$webkitStorageInfo:!0,$window:!0},t_=function(){if(typeof window>"u")return !1;for(var t in window)try{if(!e_["$"+t]&&Ts.call(window,t)&&window[t]!==null&&typeof window[t]=="object")try{Vo(window[t]);}catch{return !0}}catch{return !0}return !1}(),r_=function(t){if(typeof window>"u"||!t_)return Vo(t);try{return Vo(t)}catch{return !1}},n_=function(e){var r=e!==null&&typeof e=="object",n=wl.call(e)==="[object Function]",i=Yg(e),s=r&&wl.call(e)==="[object String]",o=[];if(!r&&!n&&!i)throw new TypeError("Object.keys called on a non-object");var a=Qg&&n;if(s&&e.length>0&&!Ts.call(e,0))for(var l=0;l0)for(var d=0;d{var kO=Array.prototype.slice,MO=vl(),o_=Object.keys,Go=o_?function(e){return o_(e)}:s_(),a_=Object.keys;Go.shim=function(){if(Object.keys){var e=function(){var r=Object.keys(arguments);return r&&r.length===arguments.length}(1,2);e||(Object.keys=function(n){return MO(n)?a_(kO.call(n)):a_(n)});}else Object.keys=Go;return Object.keys||Go};u_.exports=Go;});var Zo=A((Qz,c_)=>{c_.exports=function(){if(typeof Symbol!="function"||typeof Object.getOwnPropertySymbols!="function")return !1;if(typeof Symbol.iterator=="symbol")return !0;var e={},r=Symbol("test"),n=Object(r);if(typeof r=="string"||Object.prototype.toString.call(r)!=="[object Symbol]"||Object.prototype.toString.call(n)!=="[object Symbol]")return !1;var i=42;e[r]=i;for(r in e)return !1;if(typeof Object.keys=="function"&&Object.keys(e).length!==0||typeof Object.getOwnPropertyNames=="function"&&Object.getOwnPropertyNames(e).length!==0)return !1;var s=Object.getOwnPropertySymbols(e);if(s.length!==1||s[0]!==r||!Object.prototype.propertyIsEnumerable.call(e,r))return !1;if(typeof Object.getOwnPropertyDescriptor=="function"){var o=Object.getOwnPropertyDescriptor(e,r);if(o.value!==i||o.enumerable!==!0)return !1}return !0};});var El=A((e6,f_)=>{var l_=typeof Symbol<"u"&&Symbol,FO=Zo();f_.exports=function(){return typeof l_!="function"||typeof Symbol!="function"||typeof l_("foo")!="symbol"||typeof Symbol("bar")!="symbol"?!1:FO()};});var p_=A((t6,h_)=>{var d_={foo:{}},NO=Object;h_.exports=function(){return {__proto__:d_}.foo===d_.foo&&!({__proto__:null}instanceof NO)};});var __=A((r6,g_)=>{var qO="Function.prototype.bind called on incompatible ",LO=Object.prototype.toString,$O=Math.max,jO="[object Function]",m_=function(e,r){for(var n=[],i=0;i{var BO=__();y_.exports=Function.prototype.bind||BO;});var v_=A((i6,b_)=>{var UO=Function.prototype.call,zO=Object.prototype.hasOwnProperty,VO=Jo();b_.exports=VO.call(UO,zO);});var Ar=A((s6,C_)=>{var De,xi=SyntaxError,R_=Function,Ci=TypeError,Rl=function(t){try{return R_('"use strict"; return ('+t+").constructor;")()}catch{}},Vn=Object.getOwnPropertyDescriptor;var Cl=function(){throw new Ci},GO=Vn?function(){try{return Cl}catch{try{return Vn(arguments,"callee").get}catch{return Cl}}}():Cl,Ei=El()(),KO=p_()(),gt=Object.getPrototypeOf||(KO?function(t){return t.__proto__}:null),Ri={},ZO=typeof Uint8Array>"u"||!gt?De:gt(Uint8Array),Gn={"%AggregateError%":typeof AggregateError>"u"?De:AggregateError,"%Array%":Array,"%ArrayBuffer%":typeof ArrayBuffer>"u"?De:ArrayBuffer,"%ArrayIteratorPrototype%":Ei&>?gt([][Symbol.iterator]()):De,"%AsyncFromSyncIteratorPrototype%":De,"%AsyncFunction%":Ri,"%AsyncGenerator%":Ri,"%AsyncGeneratorFunction%":Ri,"%AsyncIteratorPrototype%":Ri,"%Atomics%":typeof Atomics>"u"?De:Atomics,"%BigInt%":typeof BigInt>"u"?De:BigInt,"%BigInt64Array%":typeof BigInt64Array>"u"?De:BigInt64Array,"%BigUint64Array%":typeof BigUint64Array>"u"?De:BigUint64Array,"%Boolean%":Boolean,"%DataView%":typeof DataView>"u"?De:DataView,"%Date%":Date,"%decodeURI%":decodeURI,"%decodeURIComponent%":decodeURIComponent,"%encodeURI%":encodeURI,"%encodeURIComponent%":encodeURIComponent,"%Error%":Error,"%eval%":eval,"%EvalError%":EvalError,"%Float32Array%":typeof Float32Array>"u"?De:Float32Array,"%Float64Array%":typeof Float64Array>"u"?De:Float64Array,"%FinalizationRegistry%":typeof FinalizationRegistry>"u"?De:FinalizationRegistry,"%Function%":R_,"%GeneratorFunction%":Ri,"%Int8Array%":typeof Int8Array>"u"?De:Int8Array,"%Int16Array%":typeof Int16Array>"u"?De:Int16Array,"%Int32Array%":typeof Int32Array>"u"?De:Int32Array,"%isFinite%":isFinite,"%isNaN%":isNaN,"%IteratorPrototype%":Ei&>?gt(gt([][Symbol.iterator]())):De,"%JSON%":typeof JSON=="object"?JSON:De,"%Map%":typeof Map>"u"?De:Map,"%MapIteratorPrototype%":typeof Map>"u"||!Ei||!gt?De:gt(new Map()[Symbol.iterator]()),"%Math%":Math,"%Number%":Number,"%Object%":Object,"%parseFloat%":parseFloat,"%parseInt%":parseInt,"%Promise%":typeof Promise>"u"?De:Promise,"%Proxy%":typeof Proxy>"u"?De:Proxy,"%RangeError%":RangeError,"%ReferenceError%":ReferenceError,"%Reflect%":typeof Reflect>"u"?De:Reflect,"%RegExp%":RegExp,"%Set%":typeof Set>"u"?De:Set,"%SetIteratorPrototype%":typeof Set>"u"||!Ei||!gt?De:gt(new Set()[Symbol.iterator]()),"%SharedArrayBuffer%":typeof SharedArrayBuffer>"u"?De:SharedArrayBuffer,"%String%":String,"%StringIteratorPrototype%":Ei&>?gt(""[Symbol.iterator]()):De,"%Symbol%":Ei?Symbol:De,"%SyntaxError%":xi,"%ThrowTypeError%":GO,"%TypedArray%":ZO,"%TypeError%":Ci,"%Uint8Array%":typeof Uint8Array>"u"?De:Uint8Array,"%Uint8ClampedArray%":typeof Uint8ClampedArray>"u"?De:Uint8ClampedArray,"%Uint16Array%":typeof Uint16Array>"u"?De:Uint16Array,"%Uint32Array%":typeof Uint32Array>"u"?De:Uint32Array,"%URIError%":URIError,"%WeakMap%":typeof WeakMap>"u"?De:WeakMap,"%WeakRef%":typeof WeakRef>"u"?De:WeakRef,"%WeakSet%":typeof WeakSet>"u"?De:WeakSet};var JO=function t(e){var r;if(e==="%AsyncFunction%")r=Rl("async function () {}");else if(e==="%GeneratorFunction%")r=Rl("function* () {}");else if(e==="%AsyncGeneratorFunction%")r=Rl("async function* () {}");else if(e==="%AsyncGenerator%"){var n=t("%AsyncGeneratorFunction%");n&&(r=n.prototype);}else if(e==="%AsyncIteratorPrototype%"){var i=t("%AsyncGenerator%");i&>&&(r=gt(i.prototype));}return Gn[e]=r,r},S_={"%ArrayBufferPrototype%":["ArrayBuffer","prototype"],"%ArrayPrototype%":["Array","prototype"],"%ArrayProto_entries%":["Array","prototype","entries"],"%ArrayProto_forEach%":["Array","prototype","forEach"],"%ArrayProto_keys%":["Array","prototype","keys"],"%ArrayProto_values%":["Array","prototype","values"],"%AsyncFunctionPrototype%":["AsyncFunction","prototype"],"%AsyncGenerator%":["AsyncGeneratorFunction","prototype"],"%AsyncGeneratorPrototype%":["AsyncGeneratorFunction","prototype","prototype"],"%BooleanPrototype%":["Boolean","prototype"],"%DataViewPrototype%":["DataView","prototype"],"%DatePrototype%":["Date","prototype"],"%ErrorPrototype%":["Error","prototype"],"%EvalErrorPrototype%":["EvalError","prototype"],"%Float32ArrayPrototype%":["Float32Array","prototype"],"%Float64ArrayPrototype%":["Float64Array","prototype"],"%FunctionPrototype%":["Function","prototype"],"%Generator%":["GeneratorFunction","prototype"],"%GeneratorPrototype%":["GeneratorFunction","prototype","prototype"],"%Int8ArrayPrototype%":["Int8Array","prototype"],"%Int16ArrayPrototype%":["Int16Array","prototype"],"%Int32ArrayPrototype%":["Int32Array","prototype"],"%JSONParse%":["JSON","parse"],"%JSONStringify%":["JSON","stringify"],"%MapPrototype%":["Map","prototype"],"%NumberPrototype%":["Number","prototype"],"%ObjectPrototype%":["Object","prototype"],"%ObjProto_toString%":["Object","prototype","toString"],"%ObjProto_valueOf%":["Object","prototype","valueOf"],"%PromisePrototype%":["Promise","prototype"],"%PromiseProto_then%":["Promise","prototype","then"],"%Promise_all%":["Promise","all"],"%Promise_reject%":["Promise","reject"],"%Promise_resolve%":["Promise","resolve"],"%RangeErrorPrototype%":["RangeError","prototype"],"%ReferenceErrorPrototype%":["ReferenceError","prototype"],"%RegExpPrototype%":["RegExp","prototype"],"%SetPrototype%":["Set","prototype"],"%SharedArrayBufferPrototype%":["SharedArrayBuffer","prototype"],"%StringPrototype%":["String","prototype"],"%SymbolPrototype%":["Symbol","prototype"],"%SyntaxErrorPrototype%":["SyntaxError","prototype"],"%TypedArrayPrototype%":["TypedArray","prototype"],"%TypeErrorPrototype%":["TypeError","prototype"],"%Uint8ArrayPrototype%":["Uint8Array","prototype"],"%Uint8ClampedArrayPrototype%":["Uint8ClampedArray","prototype"],"%Uint16ArrayPrototype%":["Uint16Array","prototype"],"%Uint32ArrayPrototype%":["Uint32Array","prototype"],"%URIErrorPrototype%":["URIError","prototype"],"%WeakMapPrototype%":["WeakMap","prototype"],"%WeakSetPrototype%":["WeakSet","prototype"]},Ps=Jo(),Yo=v_(),YO=Ps.call(Function.call,Array.prototype.concat),XO=Ps.call(Function.apply,Array.prototype.splice),E_=Ps.call(Function.call,String.prototype.replace),Xo=Ps.call(Function.call,String.prototype.slice),QO=Ps.call(Function.call,RegExp.prototype.exec),eI=/[^%.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|%$))/g,tI=/\\(\\)?/g,rI=function(e){var r=Xo(e,0,1),n=Xo(e,-1);if(r==="%"&&n!=="%")throw new xi("invalid intrinsic syntax, expected closing `%`");if(n==="%"&&r!=="%")throw new xi("invalid intrinsic syntax, expected opening `%`");var i=[];return E_(e,eI,function(s,o,a,l){i[i.length]=a?E_(l,tI,"$1"):o||s;}),i},nI=function(e,r){var n=e,i;if(Yo(S_,n)&&(i=S_[n],n="%"+i[0]+"%"),Yo(Gn,n)){var s=Gn[n];if(s===Ri&&(s=JO(n)),typeof s>"u"&&!r)throw new Ci("intrinsic "+e+" exists, but is not available. Please file an issue!");return {alias:i,name:n,value:s}}throw new xi("intrinsic "+e+" does not exist!")};C_.exports=function(e,r){if(typeof e!="string"||e.length===0)throw new Ci("intrinsic name must be a non-empty string");if(arguments.length>1&&typeof r!="boolean")throw new Ci('"allowMissing" argument must be a boolean');if(QO(/^%?[^%]*%?$/,e)===null)throw new xi("`%` may not be present anywhere but at the beginning and end of the intrinsic name");var n=rI(e),i=n.length>0?n[0]:"",s=nI("%"+i+"%",r),o=s.name,a=s.value,l=!1,d=s.alias;d&&(i=d[0],XO(n,YO([0,1],d)));for(var c=1,p=!0;c=n.length){var E=Vn(a,m);p=!!E,p&&"get"in E&&!("originalValue"in E.get)?a=E.get:a=a[m];}else p=Yo(a,m),a=a[m];p&&!l&&(Gn[o]=a);}}return a};});var Ds=A((o6,x_)=>{var iI=Ar(),xl=iI("%Object.defineProperty%",!0),Tl=function(){if(xl)try{return xl({},"a",{value:1}),!0}catch{return !1}return !1};Tl.hasArrayLengthDefineBug=function(){if(!Tl())return null;try{return xl([],"length",{value:1}).length!==1}catch{return !0}};x_.exports=Tl;});var ea=A((a6,T_)=>{var sI=Ar(),Qo=sI("%Object.getOwnPropertyDescriptor%",!0);if(Qo)try{Qo([],"length");}catch{Qo=null;}T_.exports=Qo;});var ta=A((u6,P_)=>{var oI=Ds()(),Al=Ar(),Os=oI&&Al("%Object.defineProperty%",!0);if(Os)try{Os({},"a",{value:1});}catch{Os=!1;}var aI=Al("%SyntaxError%"),Ti=Al("%TypeError%"),A_=ea();P_.exports=function(e,r,n){if(!e||typeof e!="object"&&typeof e!="function")throw new Ti("`obj` must be an object or a function`");if(typeof r!="string"&&typeof r!="symbol")throw new Ti("`property` must be a string or a symbol`");if(arguments.length>3&&typeof arguments[3]!="boolean"&&arguments[3]!==null)throw new Ti("`nonEnumerable`, if provided, must be a boolean or null");if(arguments.length>4&&typeof arguments[4]!="boolean"&&arguments[4]!==null)throw new Ti("`nonWritable`, if provided, must be a boolean or null");if(arguments.length>5&&typeof arguments[5]!="boolean"&&arguments[5]!==null)throw new Ti("`nonConfigurable`, if provided, must be a boolean or null");if(arguments.length>6&&typeof arguments[6]!="boolean")throw new Ti("`loose`, if provided, must be a boolean");var i=arguments.length>3?arguments[3]:null,s=arguments.length>4?arguments[4]:null,o=arguments.length>5?arguments[5]:null,a=arguments.length>6?arguments[6]:!1,l=!!A_&&A_(e,r);if(Os)Os(e,r,{configurable:o===null&&l?l.configurable:!o,enumerable:i===null&&l?l.enumerable:!i,value:n,writable:s===null&&l?l.writable:!s});else if(a||!i&&!s&&!o)e[r]=n;else throw new aI("This environment does not support defining a property as non-configurable, non-writable, or non-enumerable.")};});var gn=A((c6,k_)=>{var uI=Ko(),cI=typeof Symbol=="function"&&typeof Symbol("foo")=="symbol",lI=Object.prototype.toString,fI=Array.prototype.concat,D_=ta(),dI=function(t){return typeof t=="function"&&lI.call(t)==="[object Function]"},O_=Ds()(),hI=function(t,e,r,n){if(e in t){if(n===!0){if(t[e]===r)return}else if(!dI(n)||!n())return}O_?D_(t,e,r,!0):D_(t,e,r);},I_=function(t,e){var r=arguments.length>2?arguments[2]:{},n=uI(e);cI&&(n=fI.call(n,Object.getOwnPropertySymbols(e)));for(var i=0;i{var q_=Ar(),M_=ta(),pI=Ds()(),F_=ea(),N_=q_("%TypeError%"),mI=q_("%Math.floor%");L_.exports=function(e,r){if(typeof e!="function")throw new N_("`fn` is not a function");if(typeof r!="number"||r<0||r>4294967295||mI(r)!==r)throw new N_("`length` must be a positive 32-bit integer");var n=arguments.length>2&&!!arguments[2],i=!0,s=!0;if("length"in e&&F_){var o=F_(e,"length");o&&!o.configurable&&(i=!1),o&&!o.writable&&(s=!1);}return (i||s||!n)&&(pI?M_(e,"length",r,!0,!0):M_(e,"length",r)),e};});var Kn=A((f6,ra)=>{var Pl=Jo(),Ai=Ar(),gI=$_(),_I=Ai("%TypeError%"),H_=Ai("%Function.prototype.apply%"),W_=Ai("%Function.prototype.call%"),B_=Ai("%Reflect.apply%",!0)||Pl.call(W_,H_),Is=Ai("%Object.defineProperty%",!0),yI=Ai("%Math.max%");if(Is)try{Is({},"a",{value:1});}catch{Is=null;}ra.exports=function(e){if(typeof e!="function")throw new _I("a function is required");var r=B_(Pl,W_,arguments);return gI(r,1+yI(0,e.length-(arguments.length-1)),!0)};var j_=function(){return B_(Pl,H_,arguments)};Is?Is(ra.exports,"apply",{value:j_}):ra.exports.apply=j_;});var mr=A((d6,V_)=>{var U_=Ar(),z_=Kn(),bI=z_(U_("String.prototype.indexOf"));V_.exports=function(e,r){var n=U_(e,!!r);return typeof n=="function"&&bI(e,".prototype.")>-1?z_(n):n};});var Dl=A((h6,Y_)=>{var vI=Ko(),Z_=Zo()(),J_=mr(),G_=Object,wI=J_("Array.prototype.push"),K_=J_("Object.prototype.propertyIsEnumerable"),SI=Z_?Object.getOwnPropertySymbols:null;Y_.exports=function(e,r){if(e==null)throw new TypeError("target must be an object");var n=G_(e);if(arguments.length===1)return n;for(var i=1;i{var Ol=Dl(),EI=function(){if(!Object.assign)return !1;for(var t="abcdefghijklmnopqrst",e=t.split(""),r={},n=0;n{var CI=gn(),xI=Il();Q_.exports=function(){var e=xI();return CI(Object,{assign:e},{assign:function(){return Object.assign!==e}}),e};});var iy=A((g6,ny)=>{var TI=gn(),AI=Kn(),PI=Dl(),ty=Il(),DI=ey(),OI=AI.apply(ty()),ry=function(e,r){return OI(Object,arguments)};TI(ry,{getPolyfill:ty,implementation:PI,shim:DI});ny.exports=ry;});var oy=A((_6,sy)=>{var Ms=function(){return typeof function(){}.name=="string"},ks=Object.getOwnPropertyDescriptor;Ms.functionsHaveConfigurableNames=function(){if(!Ms()||!ks)return !1;var e=ks(function(){},"name");return !!e&&!!e.configurable};var II=Function.prototype.bind;Ms.boundFunctionsHaveNames=function(){return Ms()&&typeof II=="function"&&function(){}.bind().name!==""};sy.exports=Ms;});var cy=A((y6,uy)=>{var ay=ta(),kI=Ds()(),MI=oy().functionsHaveConfigurableNames(),FI=TypeError;uy.exports=function(e,r){if(typeof e!="function")throw new FI("`fn` is not a function");var n=arguments.length>2&&!!arguments[2];return (!n||MI)&&(kI?ay(e,"name",r,!0,!0):ay(e,"name",r)),e};});var kl=A((b6,ly)=>{var NI=cy(),qI=Object,LI=TypeError;ly.exports=NI(function(){if(this!=null&&this!==qI(this))throw new LI("RegExp.prototype.flags getter called on non-object");var e="";return this.hasIndices&&(e+="d"),this.global&&(e+="g"),this.ignoreCase&&(e+="i"),this.multiline&&(e+="m"),this.dotAll&&(e+="s"),this.unicode&&(e+="u"),this.unicodeSets&&(e+="v"),this.sticky&&(e+="y"),e},"get flags",!0);});var Ml=A((v6,fy)=>{var $I=kl(),jI=gn().supportsDescriptors,HI=Object.getOwnPropertyDescriptor;fy.exports=function(){if(jI&&/a/mig.flags==="gim"){var e=HI(RegExp.prototype,"flags");if(e&&typeof e.get=="function"&&typeof RegExp.prototype.dotAll=="boolean"&&typeof RegExp.prototype.hasIndices=="boolean"){var r="",n={};if(Object.defineProperty(n,"hasIndices",{get:function(){r+="d";}}),Object.defineProperty(n,"sticky",{get:function(){r+="y";}}),r==="dy")return e.get}}return $I};});var py=A((w6,hy)=>{var WI=gn().supportsDescriptors,BI=Ml(),UI=Object.getOwnPropertyDescriptor,zI=Object.defineProperty,VI=TypeError,dy=Object.getPrototypeOf,GI=/a/;hy.exports=function(){if(!WI||!dy)throw new VI("RegExp.prototype.flags requires a true ES5 environment that supports property descriptors");var e=BI(),r=dy(GI),n=UI(r,"flags");return (!n||n.get!==e)&&zI(r,"flags",{configurable:!0,enumerable:!1,get:e}),e};});var yy=A((S6,_y)=>{var KI=gn(),ZI=Kn(),JI=kl(),my=Ml(),YI=py(),gy=ZI(my());KI(gy,{getPolyfill:my,implementation:JI,shim:YI});_y.exports=gy;});var wy=A((E6,vy)=>{var by=Symbol.iterator;vy.exports=function(e){if(e!=null&&typeof e[by]<"u")return e[by]()};});var Ey=A((R6,Sy)=>{Sy.exports=oe("util").inspect;});var By=A((C6,Wy)=>{var Ul=typeof Map=="function"&&Map.prototype,Fl=Object.getOwnPropertyDescriptor&&Ul?Object.getOwnPropertyDescriptor(Map.prototype,"size"):null,ia=Ul&&Fl&&typeof Fl.get=="function"?Fl.get:null,Ry=Ul&&Map.prototype.forEach,zl=typeof Set=="function"&&Set.prototype,Nl=Object.getOwnPropertyDescriptor&&zl?Object.getOwnPropertyDescriptor(Set.prototype,"size"):null,sa=zl&&Nl&&typeof Nl.get=="function"?Nl.get:null,Cy=zl&&Set.prototype.forEach,XI=typeof WeakMap=="function"&&WeakMap.prototype,Ns=XI?WeakMap.prototype.has:null,QI=typeof WeakSet=="function"&&WeakSet.prototype,qs=QI?WeakSet.prototype.has:null,ek=typeof WeakRef=="function"&&WeakRef.prototype,xy=ek?WeakRef.prototype.deref:null,tk=Boolean.prototype.valueOf,rk=Object.prototype.toString,nk=Function.prototype.toString,ik=String.prototype.match,Vl=String.prototype.slice,yn=String.prototype.replace,sk=String.prototype.toUpperCase,Ty=String.prototype.toLowerCase,Ny=RegExp.prototype.test,Ay=Array.prototype.concat,$r=Array.prototype.join,ok=Array.prototype.slice,Py=Math.floor,$l=typeof BigInt=="function"?BigInt.prototype.valueOf:null,ql=Object.getOwnPropertySymbols,jl=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?Symbol.prototype.toString:null,Pi=typeof Symbol=="function"&&typeof Symbol.iterator=="object",Dt=typeof Symbol=="function"&&Symbol.toStringTag&&(typeof Symbol.toStringTag===Pi||!0)?Symbol.toStringTag:null,qy=Object.prototype.propertyIsEnumerable,Dy=(typeof Reflect=="function"?Reflect.getPrototypeOf:Object.getPrototypeOf)||([].__proto__===Array.prototype?function(t){return t.__proto__}:null);function Oy(t,e){if(t===1/0||t===-1/0||t!==t||t&&t>-1e3&&t<1e3||Ny.call(/e/,e))return e;var r=/[0-9](?=(?:[0-9]{3})+(?![0-9]))/g;if(typeof t=="number"){var n=t<0?-Py(-t):Py(t);if(n!==t){var i=String(n),s=Vl.call(e,i.length+1);return yn.call(i,r,"$&_")+"."+yn.call(yn.call(s,/([0-9]{3})/g,"$&_"),/_$/,"")}}return yn.call(e,r,"$&_")}var Hl=Ey(),Iy=Hl.custom,ky=$y(Iy)?Iy:null;Wy.exports=function t(e,r,n,i){var s=r||{};if(_n(s,"quoteStyle")&&s.quoteStyle!=="single"&&s.quoteStyle!=="double")throw new TypeError('option "quoteStyle" must be "single" or "double"');if(_n(s,"maxStringLength")&&(typeof s.maxStringLength=="number"?s.maxStringLength<0&&s.maxStringLength!==1/0:s.maxStringLength!==null))throw new TypeError('option "maxStringLength", if provided, must be a positive integer, Infinity, or `null`');var o=_n(s,"customInspect")?s.customInspect:!0;if(typeof o!="boolean"&&o!=="symbol")throw new TypeError("option \"customInspect\", if provided, must be `true`, `false`, or `'symbol'`");if(_n(s,"indent")&&s.indent!==null&&s.indent!==" "&&!(parseInt(s.indent,10)===s.indent&&s.indent>0))throw new TypeError('option "indent" must be "\\t", an integer > 0, or `null`');if(_n(s,"numericSeparator")&&typeof s.numericSeparator!="boolean")throw new TypeError('option "numericSeparator", if provided, must be `true` or `false`');var a=s.numericSeparator;if(typeof e>"u")return "undefined";if(e===null)return "null";if(typeof e=="boolean")return e?"true":"false";if(typeof e=="string")return Hy(e,s);if(typeof e=="number"){if(e===0)return 1/0/e>0?"0":"-0";var l=String(e);return a?Oy(e,l):l}if(typeof e=="bigint"){var d=String(e)+"n";return a?Oy(e,d):d}var c=typeof s.depth>"u"?5:s.depth;if(typeof n>"u"&&(n=0),n>=c&&c>0&&typeof e=="object")return Wl(e)?"[Array]":"[Object]";var p=Rk(s,n);if(typeof i>"u")i=[];else if(jy(i,e)>=0)return "[Circular]";function m(ae,ye,q){if(ye&&(i=ok.call(i),i.push(ye)),q){var W={depth:s.depth};return _n(s,"quoteStyle")&&(W.quoteStyle=s.quoteStyle),t(ae,W,n+1,i)}return t(ae,s,n+1,i)}if(typeof e=="function"&&!My(e)){var _=mk(e),w=na(e,m);return "[Function"+(_?": "+_:" (anonymous)")+"]"+(w.length>0?" { "+$r.call(w,", ")+" }":"")}if($y(e)){var E=Pi?yn.call(String(e),/^(Symbol\(.*\))_[^)]*$/,"$1"):jl.call(e);return typeof e=="object"&&!Pi?Fs(E):E}if(wk(e)){for(var P="<"+Ty.call(String(e.nodeName)),D=e.attributes||[],g=0;g",P}if(Wl(e)){if(e.length===0)return "[]";var S=na(e,m);return p&&!Ek(S)?"["+Bl(S,p)+"]":"[ "+$r.call(S,", ")+" ]"}if(ck(e)){var I=na(e,m);return !("cause"in Error.prototype)&&"cause"in e&&!qy.call(e,"cause")?"{ ["+String(e)+"] "+$r.call(Ay.call("[cause]: "+m(e.cause),I),", ")+" }":I.length===0?"["+String(e)+"]":"{ ["+String(e)+"] "+$r.call(I,", ")+" }"}if(typeof e=="object"&&o){if(ky&&typeof e[ky]=="function"&&Hl)return Hl(e,{depth:c-n});if(o!=="symbol"&&typeof e.inspect=="function")return e.inspect()}if(gk(e)){var L=[];return Ry&&Ry.call(e,function(ae,ye){L.push(m(ye,e,!0)+" => "+m(ae,e));}),Fy("Map",ia.call(e),L,p)}if(bk(e)){var Z=[];return Cy&&Cy.call(e,function(ae){Z.push(m(ae,e));}),Fy("Set",sa.call(e),Z,p)}if(_k(e))return Ll("WeakMap");if(vk(e))return Ll("WeakSet");if(yk(e))return Ll("WeakRef");if(fk(e))return Fs(m(Number(e)));if(hk(e))return Fs(m($l.call(e)));if(dk(e))return Fs(tk.call(e));if(lk(e))return Fs(m(String(e)));if(typeof window<"u"&&e===window)return "{ [object Window] }";if(e===global)return "{ [object globalThis] }";if(!uk(e)&&!My(e)){var K=na(e,m),U=Dy?Dy(e)===Object.prototype:e instanceof Object||e.constructor===Object,B=e instanceof Object?"":"null prototype",Q=!U&&Dt&&Object(e)===e&&Dt in e?Vl.call(bn(e),8,-1):B?"Object":"",N=U||typeof e.constructor!="function"?"":e.constructor.name?e.constructor.name+" ":"",te=N+(Q||B?"["+$r.call(Ay.call([],Q||[],B||[]),": ")+"] ":"");return K.length===0?te+"{}":p?te+"{"+Bl(K,p)+"}":te+"{ "+$r.call(K,", ")+" }"}return String(e)};function Ly(t,e,r){var n=(r.quoteStyle||e)==="double"?'"':"'";return n+t+n}function ak(t){return yn.call(String(t),/"/g,""")}function Wl(t){return bn(t)==="[object Array]"&&(!Dt||!(typeof t=="object"&&Dt in t))}function uk(t){return bn(t)==="[object Date]"&&(!Dt||!(typeof t=="object"&&Dt in t))}function My(t){return bn(t)==="[object RegExp]"&&(!Dt||!(typeof t=="object"&&Dt in t))}function ck(t){return bn(t)==="[object Error]"&&(!Dt||!(typeof t=="object"&&Dt in t))}function lk(t){return bn(t)==="[object String]"&&(!Dt||!(typeof t=="object"&&Dt in t))}function fk(t){return bn(t)==="[object Number]"&&(!Dt||!(typeof t=="object"&&Dt in t))}function dk(t){return bn(t)==="[object Boolean]"&&(!Dt||!(typeof t=="object"&&Dt in t))}function $y(t){if(Pi)return t&&typeof t=="object"&&t instanceof Symbol;if(typeof t=="symbol")return !0;if(!t||typeof t!="object"||!jl)return !1;try{return jl.call(t),!0}catch{}return !1}function hk(t){if(!t||typeof t!="object"||!$l)return !1;try{return $l.call(t),!0}catch{}return !1}var pk=Object.prototype.hasOwnProperty||function(t){return t in this};function _n(t,e){return pk.call(t,e)}function bn(t){return rk.call(t)}function mk(t){if(t.name)return t.name;var e=ik.call(nk.call(t),/^function\s*([\w$]+)/);return e?e[1]:null}function jy(t,e){if(t.indexOf)return t.indexOf(e);for(var r=0,n=t.length;re.maxStringLength){var r=t.length-e.maxStringLength,n="... "+r+" more character"+(r>1?"s":"");return Hy(Vl.call(t,0,e.maxStringLength),e)+n}var i=yn.call(yn.call(t,/(['\\])/g,"\\$1"),/[\x00-\x1f]/g,Sk);return Ly(i,"single",e)}function Sk(t){var e=t.charCodeAt(0),r={8:"b",9:"t",10:"n",12:"f",13:"r"}[e];return r?"\\"+r:"\\x"+(e<16?"0":"")+sk.call(e.toString(16))}function Fs(t){return "Object("+t+")"}function Ll(t){return t+" { ? }"}function Fy(t,e,r,n){var i=n?Bl(r,n):$r.call(r,", ");return t+" ("+e+") {"+i+"}"}function Ek(t){for(var e=0;e=0)return !1;return !0}function Rk(t,e){var r;if(t.indent===" ")r=" ";else if(typeof t.indent=="number"&&t.indent>0)r=$r.call(Array(t.indent+1)," ");else return null;return {base:r,prev:$r.call(Array(e+1),r)}}function Bl(t,e){if(t.length===0)return "";var r=` -`+e.prev+e.base;return r+$r.call(t,","+r)+` -`+e.prev}function na(t,e){var r=Wl(t),n=[];if(r){n.length=t.length;for(var i=0;i{var Gl=Ar(),Di=mr(),Ck=By(),xk=Gl("%TypeError%"),oa=Gl("%WeakMap%",!0),aa=Gl("%Map%",!0),Tk=Di("WeakMap.prototype.get",!0),Ak=Di("WeakMap.prototype.set",!0),Pk=Di("WeakMap.prototype.has",!0),Dk=Di("Map.prototype.get",!0),Ok=Di("Map.prototype.set",!0),Ik=Di("Map.prototype.has",!0),Kl=function(t,e){for(var r=t,n;(n=r.next)!==null;r=n)if(n.key===e)return r.next=n.next,n.next=t.next,t.next=n,n},kk=function(t,e){var r=Kl(t,e);return r&&r.value},Mk=function(t,e,r){var n=Kl(t,e);n?n.value=r:t.next={key:e,next:t.next,value:r};},Fk=function(t,e){return !!Kl(t,e)};Uy.exports=function(){var e,r,n,i={assert:function(s){if(!i.has(s))throw new xk("Side channel does not contain "+Ck(s))},get:function(s){if(oa&&s&&(typeof s=="object"||typeof s=="function")){if(e)return Tk(e,s)}else if(aa){if(r)return Dk(r,s)}else if(n)return kk(n,s)},has:function(s){if(oa&&s&&(typeof s=="object"||typeof s=="function")){if(e)return Pk(e,s)}else if(aa){if(r)return Ik(r,s)}else if(n)return Fk(n,s);return !1},set:function(s,o){oa&&s&&(typeof s=="object"||typeof s=="function")?(e||(e=new oa),Ak(e,s,o)):aa?(r||(r=new aa),Ok(r,s,o)):(n||(n={key:{},next:null}),Mk(n,s,o));}};return i};});var Zl=A((T6,Gy)=>{var Vy=function(t){return t!==t};Gy.exports=function(e,r){return e===0&&r===0?1/e===1/r:!!(e===r||Vy(e)&&Vy(r))};});var Jl=A((A6,Ky)=>{var Nk=Zl();Ky.exports=function(){return typeof Object.is=="function"?Object.is:Nk};});var Jy=A((P6,Zy)=>{var qk=Jl(),Lk=gn();Zy.exports=function(){var e=qk();return Lk(Object,{is:e},{is:function(){return Object.is!==e}}),e};});var eb=A((D6,Qy)=>{var $k=gn(),jk=Kn(),Hk=Zl(),Yy=Jl(),Wk=Jy(),Xy=jk(Yy(),Object);$k(Xy,{getPolyfill:Yy,implementation:Hk,shim:Wk});Qy.exports=Xy;});var vn=A((O6,tb)=>{var Bk=Zo();tb.exports=function(){return Bk()&&!!Symbol.toStringTag};});var ib=A((I6,nb)=>{var Uk=vn()(),zk=mr(),Yl=zk("Object.prototype.toString"),ua=function(e){return Uk&&e&&typeof e=="object"&&Symbol.toStringTag in e?!1:Yl(e)==="[object Arguments]"},rb=function(e){return ua(e)?!0:e!==null&&typeof e=="object"&&typeof e.length=="number"&&e.length>=0&&Yl(e)!=="[object Array]"&&Yl(e.callee)==="[object Function]"},Vk=function(){return ua(arguments)}();ua.isLegacyArguments=rb;nb.exports=Vk?ua:rb;});var ob=A((k6,sb)=>{var Gk={}.toString;sb.exports=Array.isArray||function(t){return Gk.call(t)=="[object Array]"};});var lb=A((M6,cb)=>{var ub=Function.prototype.toString,Oi=typeof Reflect=="object"&&Reflect!==null&&Reflect.apply,Ql,ca;if(typeof Oi=="function"&&typeof Object.defineProperty=="function")try{Ql=Object.defineProperty({},"length",{get:function(){throw ca}}),ca={},Oi(function(){throw 42},null,Ql);}catch(t){t!==ca&&(Oi=null);}else Oi=null;var Kk=/^\s*class\b/,ef=function(e){try{var r=ub.call(e);return Kk.test(r)}catch{return !1}},Xl=function(e){try{return ef(e)?!1:(ub.call(e),!0)}catch{return !1}},la=Object.prototype.toString,Zk="[object Object]",Jk="[object Function]",Yk="[object GeneratorFunction]",Xk="[object HTMLAllCollection]",Qk="[object HTML document.all class]",eM="[object HTMLCollection]",tM=typeof Symbol=="function"&&!!Symbol.toStringTag,rM=!(0 in[,]),tf=function(){return !1};typeof document=="object"&&(ab=document.all,la.call(ab)===la.call(document.all)&&(tf=function(e){if((rM||!e)&&(typeof e>"u"||typeof e=="object"))try{var r=la.call(e);return (r===Xk||r===Qk||r===eM||r===Zk)&&e("")==null}catch{}return !1}));var ab;cb.exports=Oi?function(e){if(tf(e))return !0;if(!e||typeof e!="function"&&typeof e!="object")return !1;try{Oi(e,null,Ql);}catch(r){if(r!==ca)return !1}return !ef(e)&&Xl(e)}:function(e){if(tf(e))return !0;if(!e||typeof e!="function"&&typeof e!="object")return !1;if(tM)return Xl(e);if(ef(e))return !1;var r=la.call(e);return r!==Jk&&r!==Yk&&!/^\[object HTML/.test(r)?!1:Xl(e)};});var hb=A((F6,db)=>{var nM=lb(),iM=Object.prototype.toString,fb=Object.prototype.hasOwnProperty,sM=function(e,r,n){for(var i=0,s=e.length;i=3&&(i=n),iM.call(e)==="[object Array]"?sM(e,r,i):typeof e=="string"?oM(e,r,i):aM(e,r,i);};db.exports=uM;});var mb=A((N6,pb)=>{var rf=["BigInt64Array","BigUint64Array","Float32Array","Float64Array","Int16Array","Int32Array","Int8Array","Uint16Array","Uint32Array","Uint8Array","Uint8ClampedArray"],cM=typeof globalThis>"u"?global:globalThis;pb.exports=function(){for(var e=[],r=0;r{var da=hb(),lM=mb(),gb=Kn(),of=mr(),fa=ea(),fM=of("Object.prototype.toString"),yb=vn()(),_b=typeof globalThis>"u"?global:globalThis,sf=lM(),af=of("String.prototype.slice"),nf=Object.getPrototypeOf,dM=of("Array.prototype.indexOf",!0)||function(e,r){for(var n=0;n-1?r:r!=="Object"?!1:pM(e)}return fa?hM(e):null};});var wb=A((L6,vb)=>{var mM=uf();vb.exports=function(e){return !!mM(e)};});var cf=A(($6,Tb)=>{var gM=Kn(),_M=mr(),xb=Ar(),yM=wb(),Sb=xb("ArrayBuffer",!0),Eb=xb("Float32Array",!0),pa=_M("ArrayBuffer.prototype.byteLength",!0),Rb=Sb&&!pa&&new Sb().slice,Cb=Rb&&gM(Rb);Tb.exports=pa||Cb?function(e){if(!e||typeof e!="object")return !1;try{return pa?pa(e):Cb(e,0),!0}catch{return !1}}:Eb?function(e){try{return new Eb(e).buffer===e&&!yM(e)}catch(r){return typeof e=="object"&&r.name==="RangeError"}}:function(e){return !1};});var Pb=A((j6,Ab)=>{var bM=Date.prototype.getDay,vM=function(e){try{return bM.call(e),!0}catch{return !1}},wM=Object.prototype.toString,SM="[object Date]",EM=vn()();Ab.exports=function(e){return typeof e!="object"||e===null?!1:EM?vM(e):wM.call(e)===SM};});var Mb=A((H6,kb)=>{var lf=mr(),Db=vn()(),Ob,Ib,ff,df;Db&&(Ob=lf("Object.prototype.hasOwnProperty"),Ib=lf("RegExp.prototype.exec"),ff={},ma=function(){throw ff},df={toString:ma,valueOf:ma},typeof Symbol.toPrimitive=="symbol"&&(df[Symbol.toPrimitive]=ma));var ma,RM=lf("Object.prototype.toString"),CM=Object.getOwnPropertyDescriptor,xM="[object RegExp]";kb.exports=Db?function(e){if(!e||typeof e!="object")return !1;var r=CM(e,"lastIndex"),n=r&&Ob(r,"value");if(!n)return !1;try{Ib(e,df);}catch(i){return i===ff}}:function(e){return !e||typeof e!="object"&&typeof e!="function"?!1:RM(e)===xM};});var qb=A((W6,Nb)=>{var TM=mr(),Fb=TM("SharedArrayBuffer.prototype.byteLength",!0);Nb.exports=Fb?function(e){if(!e||typeof e!="object")return !1;try{return Fb(e),!0}catch{return !1}}:function(e){return !1};});var $b=A((B6,Lb)=>{var AM=String.prototype.valueOf,PM=function(e){try{return AM.call(e),!0}catch{return !1}},DM=Object.prototype.toString,OM="[object String]",IM=vn()();Lb.exports=function(e){return typeof e=="string"?!0:typeof e!="object"?!1:IM?PM(e):DM.call(e)===OM};});var Hb=A((U6,jb)=>{var kM=Number.prototype.toString,MM=function(e){try{return kM.call(e),!0}catch{return !1}},FM=Object.prototype.toString,NM="[object Number]",qM=vn()();jb.exports=function(e){return typeof e=="number"?!0:typeof e!="object"?!1:qM?MM(e):FM.call(e)===NM};});var Ub=A((z6,Bb)=>{var Wb=mr(),LM=Wb("Boolean.prototype.toString"),$M=Wb("Object.prototype.toString"),jM=function(e){try{return LM(e),!0}catch{return !1}},HM="[object Boolean]",WM=vn()();Bb.exports=function(e){return typeof e=="boolean"?!0:e===null||typeof e!="object"?!1:WM&&Symbol.toStringTag in e?jM(e):$M(e)===HM};});var Kb=A((V6,hf)=>{var BM=Object.prototype.toString,UM=El()();UM?(zb=Symbol.prototype.toString,Vb=/^Symbol\(.*\)$/,Gb=function(e){return typeof e.valueOf()!="symbol"?!1:Vb.test(zb.call(e))},hf.exports=function(e){if(typeof e=="symbol")return !0;if(BM.call(e)!=="[object Symbol]")return !1;try{return Gb(e)}catch{return !1}}):hf.exports=function(e){return !1};var zb,Vb,Gb;});var Yb=A((G6,Jb)=>{var Zb=typeof BigInt<"u"&&BigInt;Jb.exports=function(){return typeof Zb=="function"&&typeof BigInt=="function"&&typeof Zb(42)=="bigint"&&typeof BigInt(42)=="bigint"};});var ev=A((K6,pf)=>{var zM=Yb()();zM?(Xb=BigInt.prototype.valueOf,Qb=function(e){try{return Xb.call(e),!0}catch{}return !1},pf.exports=function(e){return e===null||typeof e>"u"||typeof e=="boolean"||typeof e=="string"||typeof e=="number"||typeof e=="symbol"||typeof e=="function"?!1:typeof e=="bigint"?!0:Qb(e)}):pf.exports=function(e){return !1};var Xb,Qb;});var rv=A((Z6,tv)=>{var VM=$b(),GM=Hb(),KM=Ub(),ZM=Kb(),JM=ev();tv.exports=function(e){if(e==null||typeof e!="object"&&typeof e!="function")return null;if(VM(e))return "String";if(GM(e))return "Number";if(KM(e))return "Boolean";if(ZM(e))return "Symbol";if(JM(e))return "BigInt"};});var ov=A((J6,sv)=>{var mf=typeof Map=="function"&&Map.prototype?Map:null,YM=typeof Set=="function"&&Set.prototype?Set:null,ga;mf||(ga=function(e){return !1});var iv=mf?Map.prototype.has:null,nv=YM?Set.prototype.has:null;!ga&&!iv&&(ga=function(e){return !1});sv.exports=ga||function(e){if(!e||typeof e!="object")return !1;try{if(iv.call(e),nv)try{nv.call(e);}catch{return !0}return e instanceof mf}catch{}return !1};});var lv=A((Y6,cv)=>{var XM=typeof Map=="function"&&Map.prototype?Map:null,gf=typeof Set=="function"&&Set.prototype?Set:null,_a;gf||(_a=function(e){return !1});var av=XM?Map.prototype.has:null,uv=gf?Set.prototype.has:null;!_a&&!uv&&(_a=function(e){return !1});cv.exports=_a||function(e){if(!e||typeof e!="object")return !1;try{if(uv.call(e),av)try{av.call(e);}catch{return !0}return e instanceof gf}catch{}return !1};});var hv=A((X6,dv)=>{var ya=typeof WeakMap=="function"&&WeakMap.prototype?WeakMap:null,fv=typeof WeakSet=="function"&&WeakSet.prototype?WeakSet:null,ba;ya||(ba=function(e){return !1});var yf=ya?ya.prototype.has:null,_f=fv?fv.prototype.has:null;!ba&&!yf&&(ba=function(e){return !1});dv.exports=ba||function(e){if(!e||typeof e!="object")return !1;try{if(yf.call(e,yf),_f)try{_f.call(e,_f);}catch{return !0}return e instanceof ya}catch{}return !1};});var mv=A((Q6,vf)=>{var QM=Ar(),pv=mr(),eF=QM("%WeakSet%",!0),bf=pv("WeakSet.prototype.has",!0);bf?(va=pv("WeakMap.prototype.has",!0),vf.exports=function(e){if(!e||typeof e!="object")return !1;try{if(bf(e,bf),va)try{va(e,va);}catch{return !0}return e instanceof eF}catch{}return !1}):vf.exports=function(e){return !1};var va;});var _v=A((e9,gv)=>{var tF=ov(),rF=lv(),nF=hv(),iF=mv();gv.exports=function(e){if(e&&typeof e=="object"){if(tF(e))return "Map";if(rF(e))return "Set";if(nF(e))return "WeakMap";if(iF(e))return "WeakSet"}return !1};});var vv=A((t9,bv)=>{var sF=mr(),yv=sF("ArrayBuffer.prototype.byteLength",!0),oF=cf();bv.exports=function(e){return oF(e)?yv?yv(e):e.byteLength:NaN};});var xa=A((r9,Uv)=>{var Hv=iy(),jr=mr(),wv=yy(),aF=Ar(),Ii=wy(),uF=zy(),Sv=eb(),Ev=ib(),Rv=ob(),Cv=cf(),xv=Pb(),Tv=Mb(),Av=qb(),Pv=Ko(),Dv=rv(),Ov=_v(),Iv=uf(),kv=vv(),Mv=jr("SharedArrayBuffer.prototype.byteLength",!0),Fv=jr("Date.prototype.getTime"),wf=Object.getPrototypeOf,Nv=jr("Object.prototype.toString"),Sa=aF("%Set%",!0),Sf=jr("Map.prototype.has",!0),Ea=jr("Map.prototype.get",!0),qv=jr("Map.prototype.size",!0),Ra=jr("Set.prototype.add",!0),Wv=jr("Set.prototype.delete",!0),Ca=jr("Set.prototype.has",!0),wa=jr("Set.prototype.size",!0);function Lv(t,e,r,n){for(var i=Ii(t),s;(s=i.next())&&!s.done;)if(Pr(e,s.value,r,n))return Wv(t,s.value),!0;return !1}function Bv(t){if(typeof t>"u")return null;if(typeof t!="object")return typeof t=="symbol"?!1:typeof t=="string"||typeof t=="number"?+t==+t:!0}function cF(t,e,r,n,i,s){var o=Bv(r);if(o!=null)return o;var a=Ea(e,o),l=Hv({},i,{strict:!1});return typeof a>"u"&&!Sf(e,o)||!Pr(n,a,l,s)?!1:!Sf(t,o)&&Pr(n,a,l,s)}function lF(t,e,r){var n=Bv(r);return n??(Ca(e,n)&&!Ca(t,n))}function $v(t,e,r,n,i,s){for(var o=Ii(t),a,l;(a=o.next())&&!a.done;)if(l=a.value,Pr(r,l,i,s)&&Pr(n,Ea(e,l),i,s))return Wv(t,l),!0;return !1}function Pr(t,e,r,n){var i=r||{};if(i.strict?Sv(t,e):t===e)return !0;var s=Dv(t),o=Dv(e);if(s!==o)return !1;if(!t||!e||typeof t!="object"&&typeof e!="object")return i.strict?Sv(t,e):t==e;var a=n.has(t),l=n.has(e),d;if(a&&l){if(n.get(t)===n.get(e))return !0}else d={};return a||n.set(t,d),l||n.set(e,d),hF(t,e,i,n)}function jv(t){return !t||typeof t!="object"||typeof t.length!="number"||typeof t.copy!="function"||typeof t.slice!="function"||t.length>0&&typeof t[0]!="number"?!1:!!(t.constructor&&t.constructor.isBuffer&&t.constructor.isBuffer(t))}function fF(t,e,r,n){if(wa(t)!==wa(e))return !1;for(var i=Ii(t),s=Ii(e),o,a,l;(o=i.next())&&!o.done;)if(o.value&&typeof o.value=="object")l||(l=new Sa),Ra(l,o.value);else if(!Ca(e,o.value)){if(r.strict||!lF(t,e,o.value))return !1;l||(l=new Sa),Ra(l,o.value);}if(l){for(;(a=s.next())&&!a.done;)if(a.value&&typeof a.value=="object"){if(!Lv(l,a.value,r.strict,n))return !1}else if(!r.strict&&!Ca(t,a.value)&&!Lv(l,a.value,r.strict,n))return !1;return wa(l)===0}return !0}function dF(t,e,r,n){if(qv(t)!==qv(e))return !1;for(var i=Ii(t),s=Ii(e),o,a,l,d,c,p;(o=i.next())&&!o.done;)if(d=o.value[0],c=o.value[1],d&&typeof d=="object")l||(l=new Sa),Ra(l,d);else if(p=Ea(e,d),typeof p>"u"&&!Sf(e,d)||!Pr(c,p,r,n)){if(r.strict||!cF(t,e,d,c,r,n))return !1;l||(l=new Sa),Ra(l,d);}if(l){for(;(a=s.next())&&!a.done;)if(d=a.value[0],p=a.value[1],d&&typeof d=="object"){if(!$v(l,t,d,p,r,n))return !1}else if(!r.strict&&(!t.has(d)||!Pr(Ea(t,d),p,r,n))&&!$v(l,t,d,p,Hv({},r,{strict:!1}),n))return !1;return wa(l)===0}return !0}function hF(t,e,r,n){var i,s;if(typeof t!=typeof e||t==null||e==null||Nv(t)!==Nv(e)||Ev(t)!==Ev(e))return !1;var o=Rv(t),a=Rv(e);if(o!==a)return !1;var l=t instanceof Error,d=e instanceof Error;if(l!==d||(l||d)&&(t.name!==e.name||t.message!==e.message))return !1;var c=Tv(t),p=Tv(e);if(c!==p||(c||p)&&(t.source!==e.source||wv(t)!==wv(e)))return !1;var m=xv(t),_=xv(e);if(m!==_||(m||_)&&Fv(t)!==Fv(e)||r.strict&&wf&&wf(t)!==wf(e))return !1;var w=Iv(t),E=Iv(e);if(w!==E)return !1;if(w||E){if(t.length!==e.length)return !1;for(i=0;i=0;i--)if(Z[i]!=K[i])return !1;for(i=Z.length-1;i>=0;i--)if(s=Z[i],!Pr(t[s],e[s],r,n))return !1;var U=Ov(t),B=Ov(e);return U!==B?!1:U==="Set"||B==="Set"?fF(t,e,r,n):U==="Map"?dF(t,e,r,n):!0}Uv.exports=function(e,r,n){return Pr(e,r,n,uF())};});var ew={};AO(ew,{closest:()=>NF,distance:()=>Qv});var Qr,MF,FF,Qv,NF,tw=TO(()=>{Qr=new Uint32Array(65536),MF=(t,e)=>{let r=t.length,n=e.length,i=1<{let r=e.length,n=t.length,i=[],s=[],o=Math.ceil(r/32),a=Math.ceil(n/32);for(let w=0;w>>g&1,L=i[g/32|0]>>>g&1,Z=S|w,K=((S|L)&E)+E^E|S|L,U=w|~(K|E),B=E&K;U>>>31^I&&(s[g/32|0]^=1<>>31^L&&(i[g/32|0]^=1<>>w&1,D=i[w/32|0]>>>w&1,g=E|d,S=((E|D)&c)+c^c|E|D,I=d|~(S|c),L=c&S;_+=I>>>n-1&1,_-=L>>>n-1&1,I>>>31^P&&(s[w/32|0]^=1<>>31^D&&(i[w/32|0]^=1<{if(t.length{let r=1/0,n=0;for(let i=0;i{(function(){var t;try{t=typeof Intl<"u"&&typeof Intl.Collator<"u"?Intl.Collator("generic",{sensitivity:"base"}):null;}catch{console.log("Collator could not be initialized and wouldn't be used");}var e=(tw(),PO(ew)),r=[],n=[],i={get:function(s,o,a){var l=a&&t&&a.useCollator;if(l){var d=s.length,c=o.length;if(d===0)return c;if(c===0)return d;var p,m,_,w,E;for(_=0;_E&&(m=E),E=r[w+1]+1,m>E&&(m=E),r[w]=p;r[w]=m;}return m}return e.distance(s,o)}};typeof define<"u"&&define!==null&&define.amd?define(function(){return i}):typeof $s<"u"&&$s!==null&&typeof Tf<"u"&&$s.exports===Tf?$s.exports=i:typeof self<"u"&&typeof self.postMessage=="function"&&typeof self.importScripts=="function"?self.Levenshtein=i:typeof window<"u"&&window!==null&&(window.Levenshtein=i);})();});var _t=A(If=>{If.fromCallback=function(t){return Object.defineProperty(function(...e){if(typeof e[e.length-1]=="function")t.apply(this,e);else return new Promise((r,n)=>{e.push((i,s)=>i!=null?n(i):r(s)),t.apply(this,e);})},"name",{value:t.name})};If.fromPromise=function(t){return Object.defineProperty(function(...e){let r=e[e.length-1];if(typeof r!="function")return t.apply(this,e);e.pop(),t.apply(this,e).then(n=>r(null,n),r);},"name",{value:t.name})};});var aw=A((l9,ow)=>{var Sn=oe("constants"),$F=process.cwd,ka=null,jF=process.env.GRACEFUL_FS_PLATFORM||process.platform;process.cwd=function(){return ka||(ka=$F.call(process)),ka};try{process.cwd();}catch{}typeof process.chdir=="function"&&(kf=process.chdir,process.chdir=function(t){ka=null,kf.call(process,t);},Object.setPrototypeOf&&Object.setPrototypeOf(process.chdir,kf));var kf;ow.exports=HF;function HF(t){Sn.hasOwnProperty("O_SYMLINK")&&process.version.match(/^v0\.6\.[0-2]|^v0\.5\./)&&e(t),t.lutimes||r(t),t.chown=s(t.chown),t.fchown=s(t.fchown),t.lchown=s(t.lchown),t.chmod=n(t.chmod),t.fchmod=n(t.fchmod),t.lchmod=n(t.lchmod),t.chownSync=o(t.chownSync),t.fchownSync=o(t.fchownSync),t.lchownSync=o(t.lchownSync),t.chmodSync=i(t.chmodSync),t.fchmodSync=i(t.fchmodSync),t.lchmodSync=i(t.lchmodSync),t.stat=a(t.stat),t.fstat=a(t.fstat),t.lstat=a(t.lstat),t.statSync=l(t.statSync),t.fstatSync=l(t.fstatSync),t.lstatSync=l(t.lstatSync),t.chmod&&!t.lchmod&&(t.lchmod=function(c,p,m){m&&process.nextTick(m);},t.lchmodSync=function(){}),t.chown&&!t.lchown&&(t.lchown=function(c,p,m,_){_&&process.nextTick(_);},t.lchownSync=function(){}),jF==="win32"&&(t.rename=typeof t.rename!="function"?t.rename:function(c){function p(m,_,w){var E=Date.now(),P=0;c(m,_,function D(g){if(g&&(g.code==="EACCES"||g.code==="EPERM"||g.code==="EBUSY")&&Date.now()-E<6e4){setTimeout(function(){t.stat(_,function(S,I){S&&S.code==="ENOENT"?c(m,_,D):w(g);});},P),P<100&&(P+=10);return}w&&w(g);});}return Object.setPrototypeOf&&Object.setPrototypeOf(p,c),p}(t.rename)),t.read=typeof t.read!="function"?t.read:function(c){function p(m,_,w,E,P,D){var g;if(D&&typeof D=="function"){var S=0;g=function(I,L,Z){if(I&&I.code==="EAGAIN"&&S<10)return S++,c.call(t,m,_,w,E,P,g);D.apply(this,arguments);};}return c.call(t,m,_,w,E,P,g)}return Object.setPrototypeOf&&Object.setPrototypeOf(p,c),p}(t.read),t.readSync=typeof t.readSync!="function"?t.readSync:function(c){return function(p,m,_,w,E){for(var P=0;;)try{return c.call(t,p,m,_,w,E)}catch(D){if(D.code==="EAGAIN"&&P<10){P++;continue}throw D}}}(t.readSync);function e(c){c.lchmod=function(p,m,_){c.open(p,Sn.O_WRONLY|Sn.O_SYMLINK,m,function(w,E){if(w){_&&_(w);return}c.fchmod(E,m,function(P){c.close(E,function(D){_&&_(P||D);});});});},c.lchmodSync=function(p,m){var _=c.openSync(p,Sn.O_WRONLY|Sn.O_SYMLINK,m),w=!0,E;try{E=c.fchmodSync(_,m),w=!1;}finally{if(w)try{c.closeSync(_);}catch{}else c.closeSync(_);}return E};}function r(c){Sn.hasOwnProperty("O_SYMLINK")&&c.futimes?(c.lutimes=function(p,m,_,w){c.open(p,Sn.O_SYMLINK,function(E,P){if(E){w&&w(E);return}c.futimes(P,m,_,function(D){c.close(P,function(g){w&&w(D||g);});});});},c.lutimesSync=function(p,m,_){var w=c.openSync(p,Sn.O_SYMLINK),E,P=!0;try{E=c.futimesSync(w,m,_),P=!1;}finally{if(P)try{c.closeSync(w);}catch{}else c.closeSync(w);}return E}):c.futimes&&(c.lutimes=function(p,m,_,w){w&&process.nextTick(w);},c.lutimesSync=function(){});}function n(c){return c&&function(p,m,_){return c.call(t,p,m,function(w){d(w)&&(w=null),_&&_.apply(this,arguments);})}}function i(c){return c&&function(p,m){try{return c.call(t,p,m)}catch(_){if(!d(_))throw _}}}function s(c){return c&&function(p,m,_,w){return c.call(t,p,m,_,function(E){d(E)&&(E=null),w&&w.apply(this,arguments);})}}function o(c){return c&&function(p,m,_){try{return c.call(t,p,m,_)}catch(w){if(!d(w))throw w}}}function a(c){return c&&function(p,m,_){typeof m=="function"&&(_=m,m=null);function w(E,P){P&&(P.uid<0&&(P.uid+=4294967296),P.gid<0&&(P.gid+=4294967296)),_&&_.apply(this,arguments);}return m?c.call(t,p,m,w):c.call(t,p,w)}}function l(c){return c&&function(p,m){var _=m?c.call(t,p,m):c.call(t,p);return _&&(_.uid<0&&(_.uid+=4294967296),_.gid<0&&(_.gid+=4294967296)),_}}function d(c){if(!c||c.code==="ENOSYS")return !0;var p=!process.getuid||process.getuid()!==0;return !!(p&&(c.code==="EINVAL"||c.code==="EPERM"))}}});var lw=A((f9,cw)=>{var uw=oe("stream").Stream;cw.exports=WF;function WF(t){return {ReadStream:e,WriteStream:r};function e(n,i){if(!(this instanceof e))return new e(n,i);uw.call(this);var s=this;this.path=n,this.fd=null,this.readable=!0,this.paused=!1,this.flags="r",this.mode=438,this.bufferSize=64*1024,i=i||{};for(var o=Object.keys(i),a=0,l=o.length;athis.end)throw new Error("start must be <= end");this.pos=this.start;}if(this.fd!==null){process.nextTick(function(){s._read();});return}t.open(this.path,this.flags,this.mode,function(c,p){if(c){s.emit("error",c),s.readable=!1;return}s.fd=p,s.emit("open",p),s._read();});}function r(n,i){if(!(this instanceof r))return new r(n,i);uw.call(this),this.path=n,this.fd=null,this.writable=!0,this.flags="w",this.encoding="binary",this.mode=438,this.bytesWritten=0,i=i||{};for(var s=Object.keys(i),o=0,a=s.length;o= zero");this.pos=this.start;}this.busy=!1,this._queue=[],this.fd===null&&(this._open=t.open,this._queue.push([this._open,this.path,this.flags,this.mode,void 0]),this.flush());}}});var dw=A((d9,fw)=>{fw.exports=UF;var BF=Object.getPrototypeOf||function(t){return t.__proto__};function UF(t){if(t===null||typeof t!="object")return t;if(t instanceof Object)var e={__proto__:BF(t)};else var e=Object.create(null);return Object.getOwnPropertyNames(t).forEach(function(r){Object.defineProperty(e,r,Object.getOwnPropertyDescriptor(t,r));}),e}});var Fi=A((h9,Nf)=>{var it=oe("fs"),zF=aw(),VF=lw(),GF=dw(),Ma=oe("util"),Tt,Na;typeof Symbol=="function"&&typeof Symbol.for=="function"?(Tt=Symbol.for("graceful-fs.queue"),Na=Symbol.for("graceful-fs.previous")):(Tt="___graceful-fs.queue",Na="___graceful-fs.previous");function KF(){}function mw(t,e){Object.defineProperty(t,Tt,{get:function(){return e}});}var Yn=KF;Ma.debuglog?Yn=Ma.debuglog("gfs4"):/\bgfs4\b/i.test(process.env.NODE_DEBUG||"")&&(Yn=function(){var t=Ma.format.apply(Ma,arguments);t="GFS4: "+t.split(/\n/).join(` -GFS4: `),console.error(t);});it[Tt]||(hw=global[Tt]||[],mw(it,hw),it.close=function(t){function e(r,n){return t.call(it,r,function(i){i||pw(),typeof n=="function"&&n.apply(this,arguments);})}return Object.defineProperty(e,Na,{value:t}),e}(it.close),it.closeSync=function(t){function e(r){t.apply(it,arguments),pw();}return Object.defineProperty(e,Na,{value:t}),e}(it.closeSync),/\bgfs4\b/i.test(process.env.NODE_DEBUG||"")&&process.on("exit",function(){Yn(it[Tt]),oe("assert").equal(it[Tt].length,0);}));var hw;global[Tt]||mw(global,it[Tt]);Nf.exports=Mf(GF(it));process.env.TEST_GRACEFUL_FS_GLOBAL_PATCH&&!it.__patched&&(Nf.exports=Mf(it),it.__patched=!0);function Mf(t){zF(t),t.gracefulify=Mf,t.createReadStream=L,t.createWriteStream=Z;var e=t.readFile;t.readFile=r;function r(B,Q,N){return typeof Q=="function"&&(N=Q,Q=null),te(B,Q,N);function te(ae,ye,q,W){return e(ae,ye,function(me){me&&(me.code==="EMFILE"||me.code==="ENFILE")?Mi([te,[ae,ye,q],me,W||Date.now(),Date.now()]):typeof q=="function"&&q.apply(this,arguments);})}}var n=t.writeFile;t.writeFile=i;function i(B,Q,N,te){return typeof N=="function"&&(te=N,N=null),ae(B,Q,N,te);function ae(ye,q,W,me,pe){return n(ye,q,W,function(we){we&&(we.code==="EMFILE"||we.code==="ENFILE")?Mi([ae,[ye,q,W,me],we,pe||Date.now(),Date.now()]):typeof me=="function"&&me.apply(this,arguments);})}}var s=t.appendFile;s&&(t.appendFile=o);function o(B,Q,N,te){return typeof N=="function"&&(te=N,N=null),ae(B,Q,N,te);function ae(ye,q,W,me,pe){return s(ye,q,W,function(we){we&&(we.code==="EMFILE"||we.code==="ENFILE")?Mi([ae,[ye,q,W,me],we,pe||Date.now(),Date.now()]):typeof me=="function"&&me.apply(this,arguments);})}}var a=t.copyFile;a&&(t.copyFile=l);function l(B,Q,N,te){return typeof N=="function"&&(te=N,N=0),ae(B,Q,N,te);function ae(ye,q,W,me,pe){return a(ye,q,W,function(we){we&&(we.code==="EMFILE"||we.code==="ENFILE")?Mi([ae,[ye,q,W,me],we,pe||Date.now(),Date.now()]):typeof me=="function"&&me.apply(this,arguments);})}}var d=t.readdir;t.readdir=p;var c=/^v[0-5]\./;function p(B,Q,N){typeof Q=="function"&&(N=Q,Q=null);var te=c.test(process.version)?function(q,W,me,pe){return d(q,ae(q,W,me,pe))}:function(q,W,me,pe){return d(q,W,ae(q,W,me,pe))};return te(B,Q,N);function ae(ye,q,W,me){return function(pe,we){pe&&(pe.code==="EMFILE"||pe.code==="ENFILE")?Mi([te,[ye,q,W],pe,me||Date.now(),Date.now()]):(we&&we.sort&&we.sort(),typeof W=="function"&&W.call(this,pe,we));}}}if(process.version.substr(0,4)==="v0.8"){var m=VF(t);D=m.ReadStream,S=m.WriteStream;}var _=t.ReadStream;_&&(D.prototype=Object.create(_.prototype),D.prototype.open=g);var w=t.WriteStream;w&&(S.prototype=Object.create(w.prototype),S.prototype.open=I),Object.defineProperty(t,"ReadStream",{get:function(){return D},set:function(B){D=B;},enumerable:!0,configurable:!0}),Object.defineProperty(t,"WriteStream",{get:function(){return S},set:function(B){S=B;},enumerable:!0,configurable:!0});var E=D;Object.defineProperty(t,"FileReadStream",{get:function(){return E},set:function(B){E=B;},enumerable:!0,configurable:!0});var P=S;Object.defineProperty(t,"FileWriteStream",{get:function(){return P},set:function(B){P=B;},enumerable:!0,configurable:!0});function D(B,Q){return this instanceof D?(_.apply(this,arguments),this):D.apply(Object.create(D.prototype),arguments)}function g(){var B=this;U(B.path,B.flags,B.mode,function(Q,N){Q?(B.autoClose&&B.destroy(),B.emit("error",Q)):(B.fd=N,B.emit("open",N),B.read());});}function S(B,Q){return this instanceof S?(w.apply(this,arguments),this):S.apply(Object.create(S.prototype),arguments)}function I(){var B=this;U(B.path,B.flags,B.mode,function(Q,N){Q?(B.destroy(),B.emit("error",Q)):(B.fd=N,B.emit("open",N));});}function L(B,Q){return new t.ReadStream(B,Q)}function Z(B,Q){return new t.WriteStream(B,Q)}var K=t.open;t.open=U;function U(B,Q,N,te){return typeof N=="function"&&(te=N,N=null),ae(B,Q,N,te);function ae(ye,q,W,me,pe){return K(ye,q,W,function(we,Xe){we&&(we.code==="EMFILE"||we.code==="ENFILE")?Mi([ae,[ye,q,W,me],we,pe||Date.now(),Date.now()]):typeof me=="function"&&me.apply(this,arguments);})}}return t}function Mi(t){Yn("ENQUEUE",t[0].name,t[1]),it[Tt].push(t),Ff();}var Fa;function pw(){for(var t=Date.now(),e=0;e2&&(it[Tt][e][3]=t,it[Tt][e][4]=t);Ff();}function Ff(){if(clearTimeout(Fa),Fa=void 0,it[Tt].length!==0){var t=it[Tt].shift(),e=t[0],r=t[1],n=t[2],i=t[3],s=t[4];if(i===void 0)Yn("RETRY",e.name,r),e.apply(null,r);else if(Date.now()-i>=6e4){Yn("TIMEOUT",e.name,r);var o=r.pop();typeof o=="function"&&o.call(null,n);}else {var a=Date.now()-s,l=Math.max(s-i,1),d=Math.min(l*1.2,100);a>=d?(Yn("RETRY",e.name,r),e.apply(null,r.concat([i]))):it[Tt].push(t);}Fa===void 0&&(Fa=setTimeout(Ff,0));}}});var Wt=A(tn=>{var gw=_t().fromCallback,Ht=Fi(),ZF=["access","appendFile","chmod","chown","close","copyFile","fchmod","fchown","fdatasync","fstat","fsync","ftruncate","futimes","lchmod","lchown","link","lstat","mkdir","mkdtemp","open","opendir","readdir","readFile","readlink","realpath","rename","rm","rmdir","stat","symlink","truncate","unlink","utimes","writeFile"].filter(t=>typeof Ht[t]=="function");Object.assign(tn,Ht);ZF.forEach(t=>{tn[t]=gw(Ht[t]);});tn.exists=function(t,e){return typeof e=="function"?Ht.exists(t,e):new Promise(r=>Ht.exists(t,r))};tn.read=function(t,e,r,n,i,s){return typeof s=="function"?Ht.read(t,e,r,n,i,s):new Promise((o,a)=>{Ht.read(t,e,r,n,i,(l,d,c)=>{if(l)return a(l);o({bytesRead:d,buffer:c});});})};tn.write=function(t,e,...r){return typeof r[r.length-1]=="function"?Ht.write(t,e,...r):new Promise((n,i)=>{Ht.write(t,e,...r,(s,o,a)=>{if(s)return i(s);n({bytesWritten:o,buffer:a});});})};tn.readv=function(t,e,...r){return typeof r[r.length-1]=="function"?Ht.readv(t,e,...r):new Promise((n,i)=>{Ht.readv(t,e,...r,(s,o,a)=>{if(s)return i(s);n({bytesRead:o,buffers:a});});})};tn.writev=function(t,e,...r){return typeof r[r.length-1]=="function"?Ht.writev(t,e,...r):new Promise((n,i)=>{Ht.writev(t,e,...r,(s,o,a)=>{if(s)return i(s);n({bytesWritten:o,buffers:a});});})};typeof Ht.realpath.native=="function"?tn.realpath.native=gw(Ht.realpath.native):process.emitWarning("fs.realpath.native is not a function. Is fs being monkey-patched?","Warning","fs-extra-WARN0003");});var yw=A((m9,_w)=>{var JF=oe("path");_w.exports.checkPath=function(e){if(process.platform==="win32"&&/[<>:"|?*]/.test(e.replace(JF.parse(e).root,""))){let n=new Error(`Path contains invalid characters: ${e}`);throw n.code="EINVAL",n}};});var Sw=A((g9,qf)=>{var bw=Wt(),{checkPath:vw}=yw(),ww=t=>{let e={mode:511};return typeof t=="number"?t:{...e,...t}.mode};qf.exports.makeDir=async(t,e)=>(vw(t),bw.mkdir(t,{mode:ww(e),recursive:!0}));qf.exports.makeDirSync=(t,e)=>(vw(t),bw.mkdirSync(t,{mode:ww(e),recursive:!0}));});var Dr=A((_9,Ew)=>{var YF=_t().fromPromise,{makeDir:XF,makeDirSync:Lf}=Sw(),$f=YF(XF);Ew.exports={mkdirs:$f,mkdirsSync:Lf,mkdirp:$f,mkdirpSync:Lf,ensureDir:$f,ensureDirSync:Lf};});var En=A((y9,Cw)=>{var QF=_t().fromPromise,Rw=Wt();function eN(t){return Rw.access(t).then(()=>!0).catch(()=>!1)}Cw.exports={pathExists:QF(eN),pathExistsSync:Rw.existsSync};});var jf=A((b9,xw)=>{var Ni=Wt(),tN=_t().fromPromise;async function rN(t,e,r){let n=await Ni.open(t,"r+"),i=null;try{await Ni.futimes(n,e,r);}finally{try{await Ni.close(n);}catch(s){i=s;}}if(i)throw i}function nN(t,e,r){let n=Ni.openSync(t,"r+");return Ni.futimesSync(n,e,r),Ni.closeSync(n)}xw.exports={utimesMillis:tN(rN),utimesMillisSync:nN};});var Xn=A((v9,Dw)=>{var qi=Wt(),yt=oe("path"),Tw=_t().fromPromise;function iN(t,e,r){let n=r.dereference?i=>qi.stat(i,{bigint:!0}):i=>qi.lstat(i,{bigint:!0});return Promise.all([n(t),n(e).catch(i=>{if(i.code==="ENOENT")return null;throw i})]).then(([i,s])=>({srcStat:i,destStat:s}))}function sN(t,e,r){let n,i=r.dereference?o=>qi.statSync(o,{bigint:!0}):o=>qi.lstatSync(o,{bigint:!0}),s=i(t);try{n=i(e);}catch(o){if(o.code==="ENOENT")return {srcStat:s,destStat:null};throw o}return {srcStat:s,destStat:n}}async function oN(t,e,r,n){let{srcStat:i,destStat:s}=await iN(t,e,n);if(s){if(Ws(i,s)){let o=yt.basename(t),a=yt.basename(e);if(r==="move"&&o!==a&&o.toLowerCase()===a.toLowerCase())return {srcStat:i,destStat:s,isChangingCase:!0};throw new Error("Source and destination must not be the same.")}if(i.isDirectory()&&!s.isDirectory())throw new Error(`Cannot overwrite non-directory '${e}' with directory '${t}'.`);if(!i.isDirectory()&&s.isDirectory())throw new Error(`Cannot overwrite directory '${e}' with non-directory '${t}'.`)}if(i.isDirectory()&&Hf(t,e))throw new Error(qa(t,e,r));return {srcStat:i,destStat:s}}function aN(t,e,r,n){let{srcStat:i,destStat:s}=sN(t,e,n);if(s){if(Ws(i,s)){let o=yt.basename(t),a=yt.basename(e);if(r==="move"&&o!==a&&o.toLowerCase()===a.toLowerCase())return {srcStat:i,destStat:s,isChangingCase:!0};throw new Error("Source and destination must not be the same.")}if(i.isDirectory()&&!s.isDirectory())throw new Error(`Cannot overwrite non-directory '${e}' with directory '${t}'.`);if(!i.isDirectory()&&s.isDirectory())throw new Error(`Cannot overwrite directory '${e}' with non-directory '${t}'.`)}if(i.isDirectory()&&Hf(t,e))throw new Error(qa(t,e,r));return {srcStat:i,destStat:s}}async function Aw(t,e,r,n){let i=yt.resolve(yt.dirname(t)),s=yt.resolve(yt.dirname(r));if(s===i||s===yt.parse(s).root)return;let o;try{o=await qi.stat(s,{bigint:!0});}catch(a){if(a.code==="ENOENT")return;throw a}if(Ws(e,o))throw new Error(qa(t,r,n));return Aw(t,e,s,n)}function Pw(t,e,r,n){let i=yt.resolve(yt.dirname(t)),s=yt.resolve(yt.dirname(r));if(s===i||s===yt.parse(s).root)return;let o;try{o=qi.statSync(s,{bigint:!0});}catch(a){if(a.code==="ENOENT")return;throw a}if(Ws(e,o))throw new Error(qa(t,r,n));return Pw(t,e,s,n)}function Ws(t,e){return e.ino&&e.dev&&e.ino===t.ino&&e.dev===t.dev}function Hf(t,e){let r=yt.resolve(t).split(yt.sep).filter(i=>i),n=yt.resolve(e).split(yt.sep).filter(i=>i);return r.every((i,s)=>n[s]===i)}function qa(t,e,r){return `Cannot ${r} '${t}' to a subdirectory of itself, '${e}'.`}Dw.exports={checkPaths:Tw(oN),checkPathsSync:aN,checkParentPaths:Tw(Aw),checkParentPathsSync:Pw,isSrcSubdir:Hf,areIdentical:Ws};});var Fw=A((w9,Mw)=>{var Ot=Wt(),Bs=oe("path"),{mkdirs:uN}=Dr(),{pathExists:cN}=En(),{utimesMillis:lN}=jf(),Us=Xn();async function fN(t,e,r={}){typeof r=="function"&&(r={filter:r}),r.clobber="clobber"in r?!!r.clobber:!0,r.overwrite="overwrite"in r?!!r.overwrite:r.clobber,r.preserveTimestamps&&process.arch==="ia32"&&process.emitWarning(`Using the preserveTimestamps option in 32-bit node is not recommended; - - see https://github.com/jprichardson/node-fs-extra/issues/269`,"Warning","fs-extra-WARN0001");let{srcStat:n,destStat:i}=await Us.checkPaths(t,e,"copy",r);if(await Us.checkParentPaths(t,n,e,"copy"),!await Iw(t,e,r))return;let o=Bs.dirname(e);await cN(o)||await uN(o),await kw(i,t,e,r);}async function Iw(t,e,r){return r.filter?r.filter(t,e):!0}async function kw(t,e,r,n){let s=await(n.dereference?Ot.stat:Ot.lstat)(e);if(s.isDirectory())return mN(s,t,e,r,n);if(s.isFile()||s.isCharacterDevice()||s.isBlockDevice())return dN(s,t,e,r,n);if(s.isSymbolicLink())return gN(t,e,r,n);throw s.isSocket()?new Error(`Cannot copy a socket file: ${e}`):s.isFIFO()?new Error(`Cannot copy a FIFO pipe: ${e}`):new Error(`Unknown file: ${e}`)}async function dN(t,e,r,n,i){if(!e)return Ow(t,r,n,i);if(i.overwrite)return await Ot.unlink(n),Ow(t,r,n,i);if(i.errorOnExist)throw new Error(`'${n}' already exists`)}async function Ow(t,e,r,n){if(await Ot.copyFile(e,r),n.preserveTimestamps){hN(t.mode)&&await pN(r,t.mode);let i=await Ot.stat(e);await lN(r,i.atime,i.mtime);}return Ot.chmod(r,t.mode)}function hN(t){return (t&128)===0}function pN(t,e){return Ot.chmod(t,e|128)}async function mN(t,e,r,n,i){e||await Ot.mkdir(n);let s=await Ot.readdir(r);await Promise.all(s.map(async o=>{let a=Bs.join(r,o),l=Bs.join(n,o);if(!await Iw(a,l,i))return;let{destStat:c}=await Us.checkPaths(a,l,"copy",i);return kw(c,a,l,i)})),e||await Ot.chmod(n,t.mode);}async function gN(t,e,r,n){let i=await Ot.readlink(e);if(n.dereference&&(i=Bs.resolve(process.cwd(),i)),!t)return Ot.symlink(i,r);let s=null;try{s=await Ot.readlink(r);}catch(o){if(o.code==="EINVAL"||o.code==="UNKNOWN")return Ot.symlink(i,r);throw o}if(n.dereference&&(s=Bs.resolve(process.cwd(),s)),Us.isSrcSubdir(i,s))throw new Error(`Cannot copy '${i}' to a subdirectory of itself, '${s}'.`);if(Us.isSrcSubdir(s,i))throw new Error(`Cannot overwrite '${s}' with '${i}'.`);return await Ot.unlink(r),Ot.symlink(i,r)}Mw.exports=fN;});var jw=A((S9,$w)=>{var Bt=Fi(),zs=oe("path"),_N=Dr().mkdirsSync,yN=jf().utimesMillisSync,Vs=Xn();function bN(t,e,r){typeof r=="function"&&(r={filter:r}),r=r||{},r.clobber="clobber"in r?!!r.clobber:!0,r.overwrite="overwrite"in r?!!r.overwrite:r.clobber,r.preserveTimestamps&&process.arch==="ia32"&&process.emitWarning(`Using the preserveTimestamps option in 32-bit node is not recommended; - - see https://github.com/jprichardson/node-fs-extra/issues/269`,"Warning","fs-extra-WARN0002");let{srcStat:n,destStat:i}=Vs.checkPathsSync(t,e,"copy",r);if(Vs.checkParentPathsSync(t,n,e,"copy"),r.filter&&!r.filter(t,e))return;let s=zs.dirname(e);return Bt.existsSync(s)||_N(s),Nw(i,t,e,r)}function Nw(t,e,r,n){let s=(n.dereference?Bt.statSync:Bt.lstatSync)(e);if(s.isDirectory())return xN(s,t,e,r,n);if(s.isFile()||s.isCharacterDevice()||s.isBlockDevice())return vN(s,t,e,r,n);if(s.isSymbolicLink())return PN(t,e,r,n);throw s.isSocket()?new Error(`Cannot copy a socket file: ${e}`):s.isFIFO()?new Error(`Cannot copy a FIFO pipe: ${e}`):new Error(`Unknown file: ${e}`)}function vN(t,e,r,n,i){return e?wN(t,r,n,i):qw(t,r,n,i)}function wN(t,e,r,n){if(n.overwrite)return Bt.unlinkSync(r),qw(t,e,r,n);if(n.errorOnExist)throw new Error(`'${r}' already exists`)}function qw(t,e,r,n){return Bt.copyFileSync(e,r),n.preserveTimestamps&&SN(t.mode,e,r),Wf(r,t.mode)}function SN(t,e,r){return EN(t)&&RN(r,t),CN(e,r)}function EN(t){return (t&128)===0}function RN(t,e){return Wf(t,e|128)}function Wf(t,e){return Bt.chmodSync(t,e)}function CN(t,e){let r=Bt.statSync(t);return yN(e,r.atime,r.mtime)}function xN(t,e,r,n,i){return e?Lw(r,n,i):TN(t.mode,r,n,i)}function TN(t,e,r,n){return Bt.mkdirSync(r),Lw(e,r,n),Wf(r,t)}function Lw(t,e,r){Bt.readdirSync(t).forEach(n=>AN(n,t,e,r));}function AN(t,e,r,n){let i=zs.join(e,t),s=zs.join(r,t);if(n.filter&&!n.filter(i,s))return;let{destStat:o}=Vs.checkPathsSync(i,s,"copy",n);return Nw(o,i,s,n)}function PN(t,e,r,n){let i=Bt.readlinkSync(e);if(n.dereference&&(i=zs.resolve(process.cwd(),i)),t){let s;try{s=Bt.readlinkSync(r);}catch(o){if(o.code==="EINVAL"||o.code==="UNKNOWN")return Bt.symlinkSync(i,r);throw o}if(n.dereference&&(s=zs.resolve(process.cwd(),s)),Vs.isSrcSubdir(i,s))throw new Error(`Cannot copy '${i}' to a subdirectory of itself, '${s}'.`);if(Vs.isSrcSubdir(s,i))throw new Error(`Cannot overwrite '${s}' with '${i}'.`);return DN(i,r)}else return Bt.symlinkSync(i,r)}function DN(t,e){return Bt.unlinkSync(e),Bt.symlinkSync(t,e)}$w.exports=bN;});var La=A((E9,Hw)=>{var ON=_t().fromPromise;Hw.exports={copy:ON(Fw()),copySync:jw()};});var Gs=A((R9,Bw)=>{var Ww=Fi(),IN=_t().fromCallback;function kN(t,e){Ww.rm(t,{recursive:!0,force:!0},e);}function MN(t){Ww.rmSync(t,{recursive:!0,force:!0});}Bw.exports={remove:IN(kN),removeSync:MN};});var Yw=A((C9,Jw)=>{var FN=_t().fromPromise,Vw=Wt(),Gw=oe("path"),Kw=Dr(),Zw=Gs(),Uw=FN(async function(e){let r;try{r=await Vw.readdir(e);}catch{return Kw.mkdirs(e)}return Promise.all(r.map(n=>Zw.remove(Gw.join(e,n))))});function zw(t){let e;try{e=Vw.readdirSync(t);}catch{return Kw.mkdirsSync(t)}e.forEach(r=>{r=Gw.join(t,r),Zw.removeSync(r);});}Jw.exports={emptyDirSync:zw,emptydirSync:zw,emptyDir:Uw,emptydir:Uw};});var tS=A((x9,eS)=>{var NN=_t().fromPromise,Xw=oe("path"),rn=Wt(),Qw=Dr();async function qN(t){let e;try{e=await rn.stat(t);}catch{}if(e&&e.isFile())return;let r=Xw.dirname(t),n=null;try{n=await rn.stat(r);}catch(i){if(i.code==="ENOENT"){await Qw.mkdirs(r),await rn.writeFile(t,"");return}else throw i}n.isDirectory()?await rn.writeFile(t,""):await rn.readdir(r);}function LN(t){let e;try{e=rn.statSync(t);}catch{}if(e&&e.isFile())return;let r=Xw.dirname(t);try{rn.statSync(r).isDirectory()||rn.readdirSync(r);}catch(n){if(n&&n.code==="ENOENT")Qw.mkdirsSync(r);else throw n}rn.writeFileSync(t,"");}eS.exports={createFile:NN(qN),createFileSync:LN};});var oS=A((T9,sS)=>{var $N=_t().fromPromise,rS=oe("path"),Rn=Wt(),nS=Dr(),{pathExists:jN}=En(),{areIdentical:iS}=Xn();async function HN(t,e){let r;try{r=await Rn.lstat(e);}catch{}let n;try{n=await Rn.lstat(t);}catch(o){throw o.message=o.message.replace("lstat","ensureLink"),o}if(r&&iS(n,r))return;let i=rS.dirname(e);await jN(i)||await nS.mkdirs(i),await Rn.link(t,e);}function WN(t,e){let r;try{r=Rn.lstatSync(e);}catch{}try{let s=Rn.lstatSync(t);if(r&&iS(s,r))return}catch(s){throw s.message=s.message.replace("lstat","ensureLink"),s}let n=rS.dirname(e);return Rn.existsSync(n)||nS.mkdirsSync(n),Rn.linkSync(t,e)}sS.exports={createLink:$N(HN),createLinkSync:WN};});var uS=A((A9,aS)=>{var Cn=oe("path"),Ks=Wt(),{pathExists:BN}=En(),UN=_t().fromPromise;async function zN(t,e){if(Cn.isAbsolute(t)){try{await Ks.lstat(t);}catch(s){throw s.message=s.message.replace("lstat","ensureSymlink"),s}return {toCwd:t,toDst:t}}let r=Cn.dirname(e),n=Cn.join(r,t);if(await BN(n))return {toCwd:n,toDst:t};try{await Ks.lstat(t);}catch(s){throw s.message=s.message.replace("lstat","ensureSymlink"),s}return {toCwd:t,toDst:Cn.relative(r,t)}}function VN(t,e){if(Cn.isAbsolute(t)){if(!Ks.existsSync(t))throw new Error("absolute srcpath does not exist");return {toCwd:t,toDst:t}}let r=Cn.dirname(e),n=Cn.join(r,t);if(Ks.existsSync(n))return {toCwd:n,toDst:t};if(!Ks.existsSync(t))throw new Error("relative srcpath does not exist");return {toCwd:t,toDst:Cn.relative(r,t)}}aS.exports={symlinkPaths:UN(zN),symlinkPathsSync:VN};});var fS=A((P9,lS)=>{var cS=Wt(),GN=_t().fromPromise;async function KN(t,e){if(e)return e;let r;try{r=await cS.lstat(t);}catch{return "file"}return r&&r.isDirectory()?"dir":"file"}function ZN(t,e){if(e)return e;let r;try{r=cS.lstatSync(t);}catch{return "file"}return r&&r.isDirectory()?"dir":"file"}lS.exports={symlinkType:GN(KN),symlinkTypeSync:ZN};});var mS=A((D9,pS)=>{var JN=_t().fromPromise,dS=oe("path"),Wr=Wt(),{mkdirs:YN,mkdirsSync:XN}=Dr(),{symlinkPaths:QN,symlinkPathsSync:eq}=uS(),{symlinkType:tq,symlinkTypeSync:rq}=fS(),{pathExists:nq}=En(),{areIdentical:hS}=Xn();async function iq(t,e,r){let n;try{n=await Wr.lstat(e);}catch{}if(n&&n.isSymbolicLink()){let[a,l]=await Promise.all([Wr.stat(t),Wr.stat(e)]);if(hS(a,l))return}let i=await QN(t,e);t=i.toDst;let s=await tq(i.toCwd,r),o=dS.dirname(e);return await nq(o)||await YN(o),Wr.symlink(t,e,s)}function sq(t,e,r){let n;try{n=Wr.lstatSync(e);}catch{}if(n&&n.isSymbolicLink()){let a=Wr.statSync(t),l=Wr.statSync(e);if(hS(a,l))return}let i=eq(t,e);t=i.toDst,r=rq(i.toCwd,r);let s=dS.dirname(e);return Wr.existsSync(s)||XN(s),Wr.symlinkSync(t,e,r)}pS.exports={createSymlink:JN(iq),createSymlinkSync:sq};});var ES=A((O9,SS)=>{var{createFile:gS,createFileSync:_S}=tS(),{createLink:yS,createLinkSync:bS}=oS(),{createSymlink:vS,createSymlinkSync:wS}=mS();SS.exports={createFile:gS,createFileSync:_S,ensureFile:gS,ensureFileSync:_S,createLink:yS,createLinkSync:bS,ensureLink:yS,ensureLinkSync:bS,createSymlink:vS,createSymlinkSync:wS,ensureSymlink:vS,ensureSymlinkSync:wS};});var $a=A((I9,RS)=>{function oq(t,{EOL:e=` -`,finalEOL:r=!0,replacer:n=null,spaces:i}={}){let s=r?e:"";return JSON.stringify(t,n,i).replace(/\n/g,e)+s}function aq(t){return Buffer.isBuffer(t)&&(t=t.toString("utf8")),t.replace(/^\uFEFF/,"")}RS.exports={stringify:oq,stripBom:aq};});var AS=A((k9,TS)=>{var Li;try{Li=Fi();}catch{Li=oe("fs");}var ja=_t(),{stringify:CS,stripBom:xS}=$a();async function uq(t,e={}){typeof e=="string"&&(e={encoding:e});let r=e.fs||Li,n="throws"in e?e.throws:!0,i=await ja.fromCallback(r.readFile)(t,e);i=xS(i);let s;try{s=JSON.parse(i,e?e.reviver:null);}catch(o){if(n)throw o.message=`${t}: ${o.message}`,o;return null}return s}var cq=ja.fromPromise(uq);function lq(t,e={}){typeof e=="string"&&(e={encoding:e});let r=e.fs||Li,n="throws"in e?e.throws:!0;try{let i=r.readFileSync(t,e);return i=xS(i),JSON.parse(i,e.reviver)}catch(i){if(n)throw i.message=`${t}: ${i.message}`,i;return null}}async function fq(t,e,r={}){let n=r.fs||Li,i=CS(e,r);await ja.fromCallback(n.writeFile)(t,i,r);}var dq=ja.fromPromise(fq);function hq(t,e,r={}){let n=r.fs||Li,i=CS(e,r);return n.writeFileSync(t,i,r)}var pq={readFile:cq,readFileSync:lq,writeFile:dq,writeFileSync:hq};TS.exports=pq;});var DS=A((M9,PS)=>{var Ha=AS();PS.exports={readJson:Ha.readFile,readJsonSync:Ha.readFileSync,writeJson:Ha.writeFile,writeJsonSync:Ha.writeFileSync};});var Wa=A((F9,kS)=>{var mq=_t().fromPromise,Bf=Wt(),OS=oe("path"),IS=Dr(),gq=En().pathExists;async function _q(t,e,r="utf-8"){let n=OS.dirname(t);return await gq(n)||await IS.mkdirs(n),Bf.writeFile(t,e,r)}function yq(t,...e){let r=OS.dirname(t);Bf.existsSync(r)||IS.mkdirsSync(r),Bf.writeFileSync(t,...e);}kS.exports={outputFile:mq(_q),outputFileSync:yq};});var NS=A((N9,MS)=>{var{stringify:bq}=$a(),{outputFile:vq}=Wa();async function wq(t,e,r={}){let n=bq(e,r);await vq(t,n,r);}MS.exports=wq;});var LS=A((q9,qS)=>{var{stringify:Sq}=$a(),{outputFileSync:Eq}=Wa();function Rq(t,e,r){let n=Sq(e,r);Eq(t,n,r);}qS.exports=Rq;});var jS=A((L9,$S)=>{var Cq=_t().fromPromise,Ut=DS();Ut.outputJson=Cq(NS());Ut.outputJsonSync=LS();Ut.outputJSON=Ut.outputJson;Ut.outputJSONSync=Ut.outputJsonSync;Ut.writeJSON=Ut.writeJson;Ut.writeJSONSync=Ut.writeJsonSync;Ut.readJSON=Ut.readJson;Ut.readJSONSync=Ut.readJsonSync;$S.exports=Ut;});var zS=A(($9,US)=>{var xq=Wt(),HS=oe("path"),{copy:Tq}=La(),{remove:BS}=Gs(),{mkdirp:Aq}=Dr(),{pathExists:Pq}=En(),WS=Xn();async function Dq(t,e,r={}){let n=r.overwrite||r.clobber||!1,{srcStat:i,isChangingCase:s=!1}=await WS.checkPaths(t,e,"move",r);await WS.checkParentPaths(t,i,e,"move");let o=HS.dirname(e);return HS.parse(o).root!==o&&await Aq(o),Oq(t,e,n,s)}async function Oq(t,e,r,n){if(!n){if(r)await BS(e);else if(await Pq(e))throw new Error("dest already exists.")}try{await xq.rename(t,e);}catch(i){if(i.code!=="EXDEV")throw i;await Iq(t,e,r);}}async function Iq(t,e,r){return await Tq(t,e,{overwrite:r,errorOnExist:!0,preserveTimestamps:!0}),BS(t)}US.exports=Dq;});var JS=A((j9,ZS)=>{var GS=Fi(),zf=oe("path"),kq=La().copySync,KS=Gs().removeSync,Mq=Dr().mkdirpSync,VS=Xn();function Fq(t,e,r){r=r||{};let n=r.overwrite||r.clobber||!1,{srcStat:i,isChangingCase:s=!1}=VS.checkPathsSync(t,e,"move",r);return VS.checkParentPathsSync(t,i,e,"move"),Nq(e)||Mq(zf.dirname(e)),qq(t,e,n,s)}function Nq(t){let e=zf.dirname(t);return zf.parse(e).root===e}function qq(t,e,r,n){if(n)return Uf(t,e,r);if(r)return KS(e),Uf(t,e,r);if(GS.existsSync(e))throw new Error("dest already exists.");return Uf(t,e,r)}function Uf(t,e,r){try{GS.renameSync(t,e);}catch(n){if(n.code!=="EXDEV")throw n;return Lq(t,e,r)}}function Lq(t,e,r){return kq(t,e,{overwrite:r,errorOnExist:!0,preserveTimestamps:!0}),KS(t)}ZS.exports=Fq;});var XS=A((H9,YS)=>{var $q=_t().fromPromise;YS.exports={move:$q(zS()),moveSync:JS()};});var Vf=A((W9,QS)=>{QS.exports={...Wt(),...La(),...Yw(),...ES(),...jS(),...Dr(),...XS(),...Wa(),...En(),...Gs()};});var Zs=A((B9,iE)=>{var jq=oe("path"),Br="\\\\/",eE=`[^${Br}]`,nn="\\.",Hq="\\+",Wq="\\?",Ba="\\/",Bq="(?=.)",tE="[^/]",Gf=`(?:${Ba}|$)`,rE=`(?:^|${Ba})`,Kf=`${nn}{1,2}${Gf}`,Uq=`(?!${nn})`,zq=`(?!${rE}${Kf})`,Vq=`(?!${nn}{0,1}${Gf})`,Gq=`(?!${Kf})`,Kq=`[^.${Ba}]`,Zq=`${tE}*?`,nE={DOT_LITERAL:nn,PLUS_LITERAL:Hq,QMARK_LITERAL:Wq,SLASH_LITERAL:Ba,ONE_CHAR:Bq,QMARK:tE,END_ANCHOR:Gf,DOTS_SLASH:Kf,NO_DOT:Uq,NO_DOTS:zq,NO_DOT_SLASH:Vq,NO_DOTS_SLASH:Gq,QMARK_NO_DOT:Kq,STAR:Zq,START_ANCHOR:rE},Jq={...nE,SLASH_LITERAL:`[${Br}]`,QMARK:eE,STAR:`${eE}*?`,DOTS_SLASH:`${nn}{1,2}(?:[${Br}]|$)`,NO_DOT:`(?!${nn})`,NO_DOTS:`(?!(?:^|[${Br}])${nn}{1,2}(?:[${Br}]|$))`,NO_DOT_SLASH:`(?!${nn}{0,1}(?:[${Br}]|$))`,NO_DOTS_SLASH:`(?!${nn}{1,2}(?:[${Br}]|$))`,QMARK_NO_DOT:`[^.${Br}]`,START_ANCHOR:`(?:^|[${Br}])`,END_ANCHOR:`(?:[${Br}]|$)`},Yq={alnum:"a-zA-Z0-9",alpha:"a-zA-Z",ascii:"\\x00-\\x7F",blank:" \\t",cntrl:"\\x00-\\x1F\\x7F",digit:"0-9",graph:"\\x21-\\x7E",lower:"a-z",print:"\\x20-\\x7E ",punct:"\\-!\"#$%&'()\\*+,./:;<=>?@[\\]^_`{|}~",space:" \\t\\r\\n\\v\\f",upper:"A-Z",word:"A-Za-z0-9_",xdigit:"A-Fa-f0-9"};iE.exports={MAX_LENGTH:1024*64,POSIX_REGEX_SOURCE:Yq,REGEX_BACKSLASH:/\\(?![*+?^${}(|)[\]])/g,REGEX_NON_SPECIAL_CHARS:/^[^@![\].,$*+?^{}()|\\/]+/,REGEX_SPECIAL_CHARS:/[-*+?.^${}(|)[\]]/,REGEX_SPECIAL_CHARS_BACKREF:/(\\?)((\W)(\3*))/g,REGEX_SPECIAL_CHARS_GLOBAL:/([-*+?.^${}(|)[\]])/g,REGEX_REMOVE_BACKSLASH:/(?:\[.*?[^\\]\]|\\(?=.))/g,REPLACEMENTS:{"***":"*","**/**":"**","**/**/**":"**"},CHAR_0:48,CHAR_9:57,CHAR_UPPERCASE_A:65,CHAR_LOWERCASE_A:97,CHAR_UPPERCASE_Z:90,CHAR_LOWERCASE_Z:122,CHAR_LEFT_PARENTHESES:40,CHAR_RIGHT_PARENTHESES:41,CHAR_ASTERISK:42,CHAR_AMPERSAND:38,CHAR_AT:64,CHAR_BACKWARD_SLASH:92,CHAR_CARRIAGE_RETURN:13,CHAR_CIRCUMFLEX_ACCENT:94,CHAR_COLON:58,CHAR_COMMA:44,CHAR_DOT:46,CHAR_DOUBLE_QUOTE:34,CHAR_EQUAL:61,CHAR_EXCLAMATION_MARK:33,CHAR_FORM_FEED:12,CHAR_FORWARD_SLASH:47,CHAR_GRAVE_ACCENT:96,CHAR_HASH:35,CHAR_HYPHEN_MINUS:45,CHAR_LEFT_ANGLE_BRACKET:60,CHAR_LEFT_CURLY_BRACE:123,CHAR_LEFT_SQUARE_BRACKET:91,CHAR_LINE_FEED:10,CHAR_NO_BREAK_SPACE:160,CHAR_PERCENT:37,CHAR_PLUS:43,CHAR_QUESTION_MARK:63,CHAR_RIGHT_ANGLE_BRACKET:62,CHAR_RIGHT_CURLY_BRACE:125,CHAR_RIGHT_SQUARE_BRACKET:93,CHAR_SEMICOLON:59,CHAR_SINGLE_QUOTE:39,CHAR_SPACE:32,CHAR_TAB:9,CHAR_UNDERSCORE:95,CHAR_VERTICAL_LINE:124,CHAR_ZERO_WIDTH_NOBREAK_SPACE:65279,SEP:jq.sep,extglobChars(t){return {"!":{type:"negate",open:"(?:(?!(?:",close:`))${t.STAR})`},"?":{type:"qmark",open:"(?:",close:")?"},"+":{type:"plus",open:"(?:",close:")+"},"*":{type:"star",open:"(?:",close:")*"},"@":{type:"at",open:"(?:",close:")"}}},globChars(t){return t===!0?Jq:nE}};});var Ua=A(Gt=>{var Xq=oe("path"),Qq=process.platform==="win32",{REGEX_BACKSLASH:eL,REGEX_REMOVE_BACKSLASH:tL,REGEX_SPECIAL_CHARS:rL,REGEX_SPECIAL_CHARS_GLOBAL:nL}=Zs();Gt.isObject=t=>t!==null&&typeof t=="object"&&!Array.isArray(t);Gt.hasRegexChars=t=>rL.test(t);Gt.isRegexChar=t=>t.length===1&&Gt.hasRegexChars(t);Gt.escapeRegex=t=>t.replace(nL,"\\$1");Gt.toPosixSlashes=t=>t.replace(eL,"/");Gt.removeBackslashes=t=>t.replace(tL,e=>e==="\\"?"":e);Gt.supportsLookbehinds=()=>{let t=process.version.slice(1).split(".").map(Number);return t.length===3&&t[0]>=9||t[0]===8&&t[1]>=10};Gt.isWindows=t=>t&&typeof t.windows=="boolean"?t.windows:Qq===!0||Xq.sep==="\\";Gt.escapeLast=(t,e,r)=>{let n=t.lastIndexOf(e,r);return n===-1?t:t[n-1]==="\\"?Gt.escapeLast(t,e,n-1):`${t.slice(0,n)}\\${t.slice(n)}`};Gt.removePrefix=(t,e={})=>{let r=t;return r.startsWith("./")&&(r=r.slice(2),e.prefix="./"),r};Gt.wrapOutput=(t,e={},r={})=>{let n=r.contains?"":"^",i=r.contains?"":"$",s=`${n}(?:${t})${i}`;return e.negated===!0&&(s=`(?:^(?!${s}).*$)`),s};});var dE=A((z9,fE)=>{var sE=Ua(),{CHAR_ASTERISK:Zf,CHAR_AT:iL,CHAR_BACKWARD_SLASH:Js,CHAR_COMMA:sL,CHAR_DOT:Jf,CHAR_EXCLAMATION_MARK:Yf,CHAR_FORWARD_SLASH:lE,CHAR_LEFT_CURLY_BRACE:Xf,CHAR_LEFT_PARENTHESES:Qf,CHAR_LEFT_SQUARE_BRACKET:oL,CHAR_PLUS:aL,CHAR_QUESTION_MARK:oE,CHAR_RIGHT_CURLY_BRACE:uL,CHAR_RIGHT_PARENTHESES:aE,CHAR_RIGHT_SQUARE_BRACKET:cL}=Zs(),uE=t=>t===lE||t===Js,cE=t=>{t.isPrefix!==!0&&(t.depth=t.isGlobstar?1/0:1);},lL=(t,e)=>{let r=e||{},n=t.length-1,i=r.parts===!0||r.scanToEnd===!0,s=[],o=[],a=[],l=t,d=-1,c=0,p=0,m=!1,_=!1,w=!1,E=!1,P=!1,D=!1,g=!1,S=!1,I=!1,L=!1,Z=0,K,U,B={value:"",depth:0,isGlob:!1},Q=()=>d>=n,N=()=>l.charCodeAt(d+1),te=()=>(K=U,l.charCodeAt(++d));for(;d0&&(ye=l.slice(0,c),l=l.slice(c),p-=c),ae&&w===!0&&p>0?(ae=l.slice(0,p),q=l.slice(p)):w===!0?(ae="",q=l):ae=l,ae&&ae!==""&&ae!=="/"&&ae!==l&&uE(ae.charCodeAt(ae.length-1))&&(ae=ae.slice(0,-1)),r.unescape===!0&&(q&&(q=sE.removeBackslashes(q)),ae&&g===!0&&(ae=sE.removeBackslashes(ae)));let W={prefix:ye,input:t,start:c,base:ae,glob:q,isBrace:m,isBracket:_,isGlob:w,isExtglob:E,isGlobstar:P,negated:S,negatedExtglob:I};if(r.tokens===!0&&(W.maxDepth=0,uE(U)||o.push(B),W.tokens=o),r.parts===!0||r.tokens===!0){let me;for(let pe=0;pe{var za=Zs(),or=Ua(),{MAX_LENGTH:Va,POSIX_REGEX_SOURCE:fL,REGEX_NON_SPECIAL_CHARS:dL,REGEX_SPECIAL_CHARS_BACKREF:hL,REPLACEMENTS:hE}=za,pL=(t,e)=>{if(typeof e.expandRange=="function")return e.expandRange(...t,e);t.sort();let r=`[${t.join("-")}]`;return r},$i=(t,e)=>`Missing ${t}: "${e}" - use "\\\\${e}" to match literal characters`,ed=(t,e)=>{if(typeof t!="string")throw new TypeError("Expected a string");t=hE[t]||t;let r={...e},n=typeof r.maxLength=="number"?Math.min(Va,r.maxLength):Va,i=t.length;if(i>n)throw new SyntaxError(`Input length: ${i}, exceeds maximum allowed length: ${n}`);let s={type:"bos",value:"",output:r.prepend||""},o=[s],a=r.capture?"":"?:",l=or.isWindows(e),d=za.globChars(l),c=za.extglobChars(d),{DOT_LITERAL:p,PLUS_LITERAL:m,SLASH_LITERAL:_,ONE_CHAR:w,DOTS_SLASH:E,NO_DOT:P,NO_DOT_SLASH:D,NO_DOTS_SLASH:g,QMARK:S,QMARK_NO_DOT:I,STAR:L,START_ANCHOR:Z}=d,K=ce=>`(${a}(?:(?!${Z}${ce.dot?E:p}).)*?)`,U=r.dot?"":P,B=r.dot?S:I,Q=r.bash===!0?K(r):L;r.capture&&(Q=`(${Q})`),typeof r.noext=="boolean"&&(r.noextglob=r.noext);let N={input:t,index:-1,start:0,dot:r.dot===!0,consumed:"",output:"",prefix:"",backtrack:!1,negated:!1,brackets:0,braces:0,parens:0,quotes:0,globstar:!1,tokens:o};t=or.removePrefix(t,N),i=t.length;let te=[],ae=[],ye=[],q=s,W,me=()=>N.index===i-1,pe=N.peek=(ce=1)=>t[N.index+ce],we=N.advance=()=>t[++N.index]||"",Xe=()=>t.slice(N.index+1),Ke=(ce="",Me=0)=>{N.consumed+=ce,N.index+=Me;},Pt=ce=>{N.output+=ce.output!=null?ce.output:ce.value,Ke(ce.value);},Lr=()=>{let ce=1;for(;pe()==="!"&&(pe(2)!=="("||pe(3)==="?");)we(),N.start++,ce++;return ce%2===0?!1:(N.negated=!0,N.start++,!0)},zt=ce=>{N[ce]++,ye.push(ce);},St=ce=>{N[ce]--,ye.pop();},Ce=ce=>{if(q.type==="globstar"){let Me=N.braces>0&&(ce.type==="comma"||ce.type==="brace"),ie=ce.extglob===!0||te.length&&(ce.type==="pipe"||ce.type==="paren");ce.type!=="slash"&&ce.type!=="paren"&&!Me&&!ie&&(N.output=N.output.slice(0,-q.output.length),q.type="star",q.value="*",q.output=Q,N.output+=q.output);}if(te.length&&ce.type!=="paren"&&(te[te.length-1].inner+=ce.value),(ce.value||ce.output)&&Pt(ce),q&&q.type==="text"&&ce.type==="text"){q.value+=ce.value,q.output=(q.output||"")+ce.value;return}ce.prev=q,o.push(ce),q=ce;},Cr=(ce,Me)=>{let ie={...c[Me],conditions:1,inner:""};ie.prev=q,ie.parens=N.parens,ie.output=N.output;let be=(r.capture?"(":"")+ie.open;zt("parens"),Ce({type:ce,value:Me,output:N.output?"":w}),Ce({type:"paren",extglob:!0,value:we(),output:be}),te.push(ie);},fn=ce=>{let Me=ce.close+(r.capture?")":""),ie;if(ce.type==="negate"){let be=Q;if(ce.inner&&ce.inner.length>1&&ce.inner.includes("/")&&(be=K(r)),(be!==Q||me()||/^\)+$/.test(Xe()))&&(Me=ce.close=`)$))${be}`),ce.inner.includes("*")&&(ie=Xe())&&/^\.[^\\/.]+$/.test(ie)){let $e=ed(ie,{...e,fastpaths:!1}).output;Me=ce.close=`)${$e})${be})`;}ce.prev.type==="bos"&&(N.negatedExtglob=!0);}Ce({type:"paren",extglob:!0,value:W,output:Me}),St("parens");};if(r.fastpaths!==!1&&!/(^[*!]|[/()[\]{}"])/.test(t)){let ce=!1,Me=t.replace(hL,(ie,be,$e,ot,He,fr)=>ot==="\\"?(ce=!0,ie):ot==="?"?be?be+ot+(He?S.repeat(He.length):""):fr===0?B+(He?S.repeat(He.length):""):S.repeat($e.length):ot==="."?p.repeat($e.length):ot==="*"?be?be+ot+(He?Q:""):Q:be?ie:`\\${ie}`);return ce===!0&&(r.unescape===!0?Me=Me.replace(/\\/g,""):Me=Me.replace(/\\+/g,ie=>ie.length%2===0?"\\\\":ie?"\\":"")),Me===t&&r.contains===!0?(N.output=t,N):(N.output=or.wrapOutput(Me,N,e),N)}for(;!me();){if(W=we(),W==="\0")continue;if(W==="\\"){let ie=pe();if(ie==="/"&&r.bash!==!0||ie==="."||ie===";")continue;if(!ie){W+="\\",Ce({type:"text",value:W});continue}let be=/^\\+/.exec(Xe()),$e=0;if(be&&be[0].length>2&&($e=be[0].length,N.index+=$e,$e%2!==0&&(W+="\\")),r.unescape===!0?W=we():W+=we(),N.brackets===0){Ce({type:"text",value:W});continue}}if(N.brackets>0&&(W!=="]"||q.value==="["||q.value==="[^")){if(r.posix!==!1&&W===":"){let ie=q.value.slice(1);if(ie.includes("[")&&(q.posix=!0,ie.includes(":"))){let be=q.value.lastIndexOf("["),$e=q.value.slice(0,be),ot=q.value.slice(be+2),He=fL[ot];if(He){q.value=$e+He,N.backtrack=!0,we(),!s.output&&o.indexOf(q)===1&&(s.output=w);continue}}}(W==="["&&pe()!==":"||W==="-"&&pe()==="]")&&(W=`\\${W}`),W==="]"&&(q.value==="["||q.value==="[^")&&(W=`\\${W}`),r.posix===!0&&W==="!"&&q.value==="["&&(W="^"),q.value+=W,Pt({value:W});continue}if(N.quotes===1&&W!=='"'){W=or.escapeRegex(W),q.value+=W,Pt({value:W});continue}if(W==='"'){N.quotes=N.quotes===1?0:1,r.keepQuotes===!0&&Ce({type:"text",value:W});continue}if(W==="("){zt("parens"),Ce({type:"paren",value:W});continue}if(W===")"){if(N.parens===0&&r.strictBrackets===!0)throw new SyntaxError($i("opening","("));let ie=te[te.length-1];if(ie&&N.parens===ie.parens+1){fn(te.pop());continue}Ce({type:"paren",value:W,output:N.parens?")":"\\)"}),St("parens");continue}if(W==="["){if(r.nobracket===!0||!Xe().includes("]")){if(r.nobracket!==!0&&r.strictBrackets===!0)throw new SyntaxError($i("closing","]"));W=`\\${W}`;}else zt("brackets");Ce({type:"bracket",value:W});continue}if(W==="]"){if(r.nobracket===!0||q&&q.type==="bracket"&&q.value.length===1){Ce({type:"text",value:W,output:`\\${W}`});continue}if(N.brackets===0){if(r.strictBrackets===!0)throw new SyntaxError($i("opening","["));Ce({type:"text",value:W,output:`\\${W}`});continue}St("brackets");let ie=q.value.slice(1);if(q.posix!==!0&&ie[0]==="^"&&!ie.includes("/")&&(W=`/${W}`),q.value+=W,Pt({value:W}),r.literalBrackets===!1||or.hasRegexChars(ie))continue;let be=or.escapeRegex(q.value);if(N.output=N.output.slice(0,-q.value.length),r.literalBrackets===!0){N.output+=be,q.value=be;continue}q.value=`(${a}${be}|${q.value})`,N.output+=q.value;continue}if(W==="{"&&r.nobrace!==!0){zt("braces");let ie={type:"brace",value:W,output:"(",outputIndex:N.output.length,tokensIndex:N.tokens.length};ae.push(ie),Ce(ie);continue}if(W==="}"){let ie=ae[ae.length-1];if(r.nobrace===!0||!ie){Ce({type:"text",value:W,output:W});continue}let be=")";if(ie.dots===!0){let $e=o.slice(),ot=[];for(let He=$e.length-1;He>=0&&(o.pop(),$e[He].type!=="brace");He--)$e[He].type!=="dots"&&ot.unshift($e[He].value);be=pL(ot,r),N.backtrack=!0;}if(ie.comma!==!0&&ie.dots!==!0){let $e=N.output.slice(0,ie.outputIndex),ot=N.tokens.slice(ie.tokensIndex);ie.value=ie.output="\\{",W=be="\\}",N.output=$e;for(let He of ot)N.output+=He.output||He.value;}Ce({type:"brace",value:W,output:be}),St("braces"),ae.pop();continue}if(W==="|"){te.length>0&&te[te.length-1].conditions++,Ce({type:"text",value:W});continue}if(W===","){let ie=W,be=ae[ae.length-1];be&&ye[ye.length-1]==="braces"&&(be.comma=!0,ie="|"),Ce({type:"comma",value:W,output:ie});continue}if(W==="/"){if(q.type==="dot"&&N.index===N.start+1){N.start=N.index+1,N.consumed="",N.output="",o.pop(),q=s;continue}Ce({type:"slash",value:W,output:_});continue}if(W==="."){if(N.braces>0&&q.type==="dot"){q.value==="."&&(q.output=p);let ie=ae[ae.length-1];q.type="dots",q.output+=W,q.value+=W,ie.dots=!0;continue}if(N.braces+N.parens===0&&q.type!=="bos"&&q.type!=="slash"){Ce({type:"text",value:W,output:p});continue}Ce({type:"dot",value:W,output:p});continue}if(W==="?"){if(!(q&&q.value==="(")&&r.noextglob!==!0&&pe()==="("&&pe(2)!=="?"){Cr("qmark",W);continue}if(q&&q.type==="paren"){let be=pe(),$e=W;if(be==="<"&&!or.supportsLookbehinds())throw new Error("Node.js v10 or higher is required for regex lookbehinds");(q.value==="("&&!/[!=<:]/.test(be)||be==="<"&&!/<([!=]|\w+>)/.test(Xe()))&&($e=`\\${W}`),Ce({type:"text",value:W,output:$e});continue}if(r.dot!==!0&&(q.type==="slash"||q.type==="bos")){Ce({type:"qmark",value:W,output:I});continue}Ce({type:"qmark",value:W,output:S});continue}if(W==="!"){if(r.noextglob!==!0&&pe()==="("&&(pe(2)!=="?"||!/[!=<:]/.test(pe(3)))){Cr("negate",W);continue}if(r.nonegate!==!0&&N.index===0){Lr();continue}}if(W==="+"){if(r.noextglob!==!0&&pe()==="("&&pe(2)!=="?"){Cr("plus",W);continue}if(q&&q.value==="("||r.regex===!1){Ce({type:"plus",value:W,output:m});continue}if(q&&(q.type==="bracket"||q.type==="paren"||q.type==="brace")||N.parens>0){Ce({type:"plus",value:W});continue}Ce({type:"plus",value:m});continue}if(W==="@"){if(r.noextglob!==!0&&pe()==="("&&pe(2)!=="?"){Ce({type:"at",extglob:!0,value:W,output:""});continue}Ce({type:"text",value:W});continue}if(W!=="*"){(W==="$"||W==="^")&&(W=`\\${W}`);let ie=dL.exec(Xe());ie&&(W+=ie[0],N.index+=ie[0].length),Ce({type:"text",value:W});continue}if(q&&(q.type==="globstar"||q.star===!0)){q.type="star",q.star=!0,q.value+=W,q.output=Q,N.backtrack=!0,N.globstar=!0,Ke(W);continue}let ce=Xe();if(r.noextglob!==!0&&/^\([^?]/.test(ce)){Cr("star",W);continue}if(q.type==="star"){if(r.noglobstar===!0){Ke(W);continue}let ie=q.prev,be=ie.prev,$e=ie.type==="slash"||ie.type==="bos",ot=be&&(be.type==="star"||be.type==="globstar");if(r.bash===!0&&(!$e||ce[0]&&ce[0]!=="/")){Ce({type:"star",value:W,output:""});continue}let He=N.braces>0&&(ie.type==="comma"||ie.type==="brace"),fr=te.length&&(ie.type==="pipe"||ie.type==="paren");if(!$e&&ie.type!=="paren"&&!He&&!fr){Ce({type:"star",value:W,output:""});continue}for(;ce.slice(0,3)==="/**";){let Nt=t[N.index+4];if(Nt&&Nt!=="/")break;ce=ce.slice(3),Ke("/**",3);}if(ie.type==="bos"&&me()){q.type="globstar",q.value+=W,q.output=K(r),N.output=q.output,N.globstar=!0,Ke(W);continue}if(ie.type==="slash"&&ie.prev.type!=="bos"&&!ot&&me()){N.output=N.output.slice(0,-(ie.output+q.output).length),ie.output=`(?:${ie.output}`,q.type="globstar",q.output=K(r)+(r.strictSlashes?")":"|$)"),q.value+=W,N.globstar=!0,N.output+=ie.output+q.output,Ke(W);continue}if(ie.type==="slash"&&ie.prev.type!=="bos"&&ce[0]==="/"){let Nt=ce[1]!==void 0?"|$":"";N.output=N.output.slice(0,-(ie.output+q.output).length),ie.output=`(?:${ie.output}`,q.type="globstar",q.output=`${K(r)}${_}|${_}${Nt})`,q.value+=W,N.output+=ie.output+q.output,N.globstar=!0,Ke(W+we()),Ce({type:"slash",value:"/",output:""});continue}if(ie.type==="bos"&&ce[0]==="/"){q.type="globstar",q.value+=W,q.output=`(?:^|${_}|${K(r)}${_})`,N.output=q.output,N.globstar=!0,Ke(W+we()),Ce({type:"slash",value:"/",output:""});continue}N.output=N.output.slice(0,-q.output.length),q.type="globstar",q.output=K(r),q.value+=W,N.output+=q.output,N.globstar=!0,Ke(W);continue}let Me={type:"star",value:W,output:Q};if(r.bash===!0){Me.output=".*?",(q.type==="bos"||q.type==="slash")&&(Me.output=U+Me.output),Ce(Me);continue}if(q&&(q.type==="bracket"||q.type==="paren")&&r.regex===!0){Me.output=W,Ce(Me);continue}(N.index===N.start||q.type==="slash"||q.type==="dot")&&(q.type==="dot"?(N.output+=D,q.output+=D):r.dot===!0?(N.output+=g,q.output+=g):(N.output+=U,q.output+=U),pe()!=="*"&&(N.output+=w,q.output+=w)),Ce(Me);}for(;N.brackets>0;){if(r.strictBrackets===!0)throw new SyntaxError($i("closing","]"));N.output=or.escapeLast(N.output,"["),St("brackets");}for(;N.parens>0;){if(r.strictBrackets===!0)throw new SyntaxError($i("closing",")"));N.output=or.escapeLast(N.output,"("),St("parens");}for(;N.braces>0;){if(r.strictBrackets===!0)throw new SyntaxError($i("closing","}"));N.output=or.escapeLast(N.output,"{"),St("braces");}if(r.strictSlashes!==!0&&(q.type==="star"||q.type==="bracket")&&Ce({type:"maybe_slash",value:"",output:`${_}?`}),N.backtrack===!0){N.output="";for(let ce of N.tokens)N.output+=ce.output!=null?ce.output:ce.value,ce.suffix&&(N.output+=ce.suffix);}return N};ed.fastpaths=(t,e)=>{let r={...e},n=typeof r.maxLength=="number"?Math.min(Va,r.maxLength):Va,i=t.length;if(i>n)throw new SyntaxError(`Input length: ${i}, exceeds maximum allowed length: ${n}`);t=hE[t]||t;let s=or.isWindows(e),{DOT_LITERAL:o,SLASH_LITERAL:a,ONE_CHAR:l,DOTS_SLASH:d,NO_DOT:c,NO_DOTS:p,NO_DOTS_SLASH:m,STAR:_,START_ANCHOR:w}=za.globChars(s),E=r.dot?p:c,P=r.dot?m:c,D=r.capture?"":"?:",g={negated:!1,prefix:""},S=r.bash===!0?".*?":_;r.capture&&(S=`(${S})`);let I=U=>U.noglobstar===!0?S:`(${D}(?:(?!${w}${U.dot?d:o}).)*?)`,L=U=>{switch(U){case"*":return `${E}${l}${S}`;case".*":return `${o}${l}${S}`;case"*.*":return `${E}${S}${o}${l}${S}`;case"*/*":return `${E}${S}${a}${l}${P}${S}`;case"**":return E+I(r);case"**/*":return `(?:${E}${I(r)}${a})?${P}${l}${S}`;case"**/*.*":return `(?:${E}${I(r)}${a})?${P}${S}${o}${l}${S}`;case"**/.*":return `(?:${E}${I(r)}${a})?${o}${l}${S}`;default:{let B=/^(.*?)\.(\w+)$/.exec(U);if(!B)return;let Q=L(B[1]);return Q?Q+o+B[2]:void 0}}},Z=or.removePrefix(t,g),K=L(Z);return K&&r.strictSlashes!==!0&&(K+=`${a}?`),K};pE.exports=ed;});var _E=A((G9,gE)=>{var mL=oe("path"),gL=dE(),td=mE(),rd=Ua(),_L=Zs(),yL=t=>t&&typeof t=="object"&&!Array.isArray(t),lt=(t,e,r=!1)=>{if(Array.isArray(t)){let c=t.map(m=>lt(m,e,r));return m=>{for(let _ of c){let w=_(m);if(w)return w}return !1}}let n=yL(t)&&t.tokens&&t.input;if(t===""||typeof t!="string"&&!n)throw new TypeError("Expected pattern to be a non-empty string");let i=e||{},s=rd.isWindows(e),o=n?lt.compileRe(t,e):lt.makeRe(t,e,!1,!0),a=o.state;delete o.state;let l=()=>!1;if(i.ignore){let c={...e,ignore:null,onMatch:null,onResult:null};l=lt(i.ignore,c,r);}let d=(c,p=!1)=>{let{isMatch:m,match:_,output:w}=lt.test(c,o,e,{glob:t,posix:s}),E={glob:t,state:a,regex:o,posix:s,input:c,output:w,match:_,isMatch:m};return typeof i.onResult=="function"&&i.onResult(E),m===!1?(E.isMatch=!1,p?E:!1):l(c)?(typeof i.onIgnore=="function"&&i.onIgnore(E),E.isMatch=!1,p?E:!1):(typeof i.onMatch=="function"&&i.onMatch(E),p?E:!0)};return r&&(d.state=a),d};lt.test=(t,e,r,{glob:n,posix:i}={})=>{if(typeof t!="string")throw new TypeError("Expected input to be a string");if(t==="")return {isMatch:!1,output:""};let s=r||{},o=s.format||(i?rd.toPosixSlashes:null),a=t===n,l=a&&o?o(t):t;return a===!1&&(l=o?o(t):t,a=l===n),(a===!1||s.capture===!0)&&(s.matchBase===!0||s.basename===!0?a=lt.matchBase(t,e,r,i):a=e.exec(l)),{isMatch:!!a,match:a,output:l}};lt.matchBase=(t,e,r,n=rd.isWindows(r))=>(e instanceof RegExp?e:lt.makeRe(e,r)).test(mL.basename(t));lt.isMatch=(t,e,r)=>lt(e,r)(t);lt.parse=(t,e)=>Array.isArray(t)?t.map(r=>lt.parse(r,e)):td(t,{...e,fastpaths:!1});lt.scan=(t,e)=>gL(t,e);lt.compileRe=(t,e,r=!1,n=!1)=>{if(r===!0)return t.output;let i=e||{},s=i.contains?"":"^",o=i.contains?"":"$",a=`${s}(?:${t.output})${o}`;t&&t.negated===!0&&(a=`^(?!${a}).*$`);let l=lt.toRegex(a,e);return n===!0&&(l.state=t),l};lt.makeRe=(t,e={},r=!1,n=!1)=>{if(!t||typeof t!="string")throw new TypeError("Expected a non-empty string");let i={negated:!1,fastpaths:!0};return e.fastpaths!==!1&&(t[0]==="."||t[0]==="*")&&(i.output=td.fastpaths(t,e)),i.output||(i=td(t,e)),lt.compileRe(i,e,r,n)};lt.toRegex=(t,e)=>{try{let r=e||{};return new RegExp(t,r.flags||(r.nocase?"i":""))}catch(r){if(e&&e.debug===!0)throw r;return /$^/}};lt.constants=_L;gE.exports=lt;});var nd=A((K9,yE)=>{yE.exports=_E();});var xE=A((Z9,CE)=>{var Xs=oe("fs"),{Readable:bL}=oe("stream"),Ys=oe("path"),{promisify:Ja}=oe("util"),id=nd(),vL=Ja(Xs.readdir),wL=Ja(Xs.stat),bE=Ja(Xs.lstat),SL=Ja(Xs.realpath),EL="!",EE="READDIRP_RECURSIVE_ERROR",RL=new Set(["ENOENT","EPERM","EACCES","ELOOP",EE]),sd="files",RE="directories",Ka="files_directories",Ga="all",vE=[sd,RE,Ka,Ga],CL=t=>RL.has(t.code),[wE,xL]=process.versions.node.split(".").slice(0,2).map(t=>Number.parseInt(t,10)),TL=process.platform==="win32"&&(wE>10||wE===10&&xL>=5),SE=t=>{if(t!==void 0){if(typeof t=="function")return t;if(typeof t=="string"){let e=id(t.trim());return r=>e(r.basename)}if(Array.isArray(t)){let e=[],r=[];for(let n of t){let i=n.trim();i.charAt(0)===EL?r.push(id(i.slice(1))):e.push(id(i));}return r.length>0?e.length>0?n=>e.some(i=>i(n.basename))&&!r.some(i=>i(n.basename)):n=>!r.some(i=>i(n.basename)):n=>e.some(i=>i(n.basename))}}},Za=class t extends bL{static get defaultOptions(){return {root:".",fileFilter:e=>!0,directoryFilter:e=>!0,type:sd,lstat:!1,depth:2147483648,alwaysStat:!1}}constructor(e={}){super({objectMode:!0,autoDestroy:!0,highWaterMark:e.highWaterMark||4096});let r={...t.defaultOptions,...e},{root:n,type:i}=r;this._fileFilter=SE(r.fileFilter),this._directoryFilter=SE(r.directoryFilter);let s=r.lstat?bE:wL;TL?this._stat=o=>s(o,{bigint:!0}):this._stat=s,this._maxDepth=r.depth,this._wantsDir=[RE,Ka,Ga].includes(i),this._wantsFile=[sd,Ka,Ga].includes(i),this._wantsEverything=i===Ga,this._root=Ys.resolve(n),this._isDirent="Dirent"in Xs&&!r.alwaysStat,this._statsProp=this._isDirent?"dirent":"stats",this._rdOptions={encoding:"utf8",withFileTypes:this._isDirent},this.parents=[this._exploreDir(n,1)],this.reading=!1,this.parent=void 0;}async _read(e){if(!this.reading){this.reading=!0;try{for(;!this.destroyed&&e>0;){let{path:r,depth:n,files:i=[]}=this.parent||{};if(i.length>0){let s=i.splice(0,e).map(o=>this._formatEntry(o,r));for(let o of await Promise.all(s)){if(this.destroyed)return;let a=await this._getEntryType(o);a==="directory"&&this._directoryFilter(o)?(n<=this._maxDepth&&this.parents.push(this._exploreDir(o.fullPath,n+1)),this._wantsDir&&(this.push(o),e--)):(a==="file"||this._includeAsFile(o))&&this._fileFilter(o)&&this._wantsFile&&(this.push(o),e--);}}else {let s=this.parents.pop();if(!s){this.push(null);break}if(this.parent=await s,this.destroyed)return}}}catch(r){this.destroy(r);}finally{this.reading=!1;}}}async _exploreDir(e,r){let n;try{n=await vL(e,this._rdOptions);}catch(i){this._onError(i);}return {files:n,depth:r,path:e}}async _formatEntry(e,r){let n;try{let i=this._isDirent?e.name:e,s=Ys.resolve(Ys.join(r,i));n={path:Ys.relative(this._root,s),fullPath:s,basename:i},n[this._statsProp]=this._isDirent?e:await this._stat(s);}catch(i){this._onError(i);}return n}_onError(e){CL(e)&&!this.destroyed?this.emit("warn",e):this.destroy(e);}async _getEntryType(e){let r=e&&e[this._statsProp];if(r){if(r.isFile())return "file";if(r.isDirectory())return "directory";if(r&&r.isSymbolicLink()){let n=e.fullPath;try{let i=await SL(n),s=await bE(i);if(s.isFile())return "file";if(s.isDirectory()){let o=i.length;if(n.startsWith(i)&&n.substr(o,1)===Ys.sep){let a=new Error(`Circular symlink detected: "${n}" points to "${i}"`);return a.code=EE,this._onError(a)}return "directory"}}catch(i){this._onError(i);}}}}_includeAsFile(e){let r=e&&e[this._statsProp];return r&&this._wantsEverything&&!r.isDirectory()}},ji=(t,e={})=>{let r=e.entryType||e.type;if(r==="both"&&(r=Ka),r&&(e.type=r),t){if(typeof t!="string")throw new TypeError("readdirp: root argument must be a string. Usage: readdirp(root, options)");if(r&&!vE.includes(r))throw new Error(`readdirp: Invalid type passed. Use one of ${vE.join(", ")}`)}else throw new Error("readdirp: root argument is required. Usage: readdirp(root, options)");return e.root=t,new Za(e)},AL=(t,e={})=>new Promise((r,n)=>{let i=[];ji(t,e).on("data",s=>i.push(s)).on("end",()=>r(i)).on("error",s=>n(s));});ji.promise=AL;ji.ReaddirpStream=Za;ji.default=ji;CE.exports=ji;});var od=A((J9,TE)=>{TE.exports=function(t,e){if(typeof t!="string")throw new TypeError("expected path to be a string");if(t==="\\"||t==="/")return "/";var r=t.length;if(r<=1)return t;var n="";if(r>4&&t[3]==="\\"){var i=t[2];(i==="?"||i===".")&&t.slice(0,2)==="\\\\"&&(t=t.slice(2),n="//");}var s=t.split(/[/\\]+/);return e!==!1&&s[s.length-1]===""&&s.pop(),n+s.join("/")};});var kE=A((OE,IE)=>{Object.defineProperty(OE,"__esModule",{value:!0});var DE=nd(),PL=od(),AE="!",DL={returnIndex:!1},OL=t=>Array.isArray(t)?t:[t],IL=(t,e)=>{if(typeof t=="function")return t;if(typeof t=="string"){let r=DE(t,e);return n=>t===n||r(n)}return t instanceof RegExp?r=>t.test(r):r=>!1},PE=(t,e,r,n)=>{let i=Array.isArray(r),s=i?r[0]:r;if(!i&&typeof s!="string")throw new TypeError("anymatch: second argument must be a string: got "+Object.prototype.toString.call(s));let o=PL(s,!1);for(let l=0;l{if(t==null)throw new TypeError("anymatch: specify first argument");let n=typeof r=="boolean"?{returnIndex:r}:r,i=n.returnIndex||!1,s=OL(t),o=s.filter(l=>typeof l=="string"&&l.charAt(0)===AE).map(l=>l.slice(1)).map(l=>DE(l,n)),a=s.filter(l=>typeof l!="string"||typeof l=="string"&&l.charAt(0)!==AE).map(l=>IL(l,n));return e==null?(l,d=!1)=>PE(a,o,l,typeof d=="boolean"?d:!1):PE(a,o,e,i)};ad.default=ad;IE.exports=ad;});var FE=A((Y9,ME)=>{ME.exports=function(e){if(typeof e!="string"||e==="")return !1;for(var r;r=/(\\).|([@?!+*]\(.*\))/g.exec(e);){if(r[2])return !0;e=e.slice(r.index+r[0].length);}return !1};});var ud=A((X9,qE)=>{var kL=FE(),NE={"{":"}","(":")","[":"]"},ML=function(t){if(t[0]==="!")return !0;for(var e=0,r=-2,n=-2,i=-2,s=-2,o=-2;ee&&(o===-1||o>n||(o=t.indexOf("\\",e),o===-1||o>n)))||i!==-1&&t[e]==="{"&&t[e+1]!=="}"&&(i=t.indexOf("}",e),i>e&&(o=t.indexOf("\\",e),o===-1||o>i))||s!==-1&&t[e]==="("&&t[e+1]==="?"&&/[:!=]/.test(t[e+2])&&t[e+3]!==")"&&(s=t.indexOf(")",e),s>e&&(o=t.indexOf("\\",e),o===-1||o>s))||r!==-1&&t[e]==="("&&t[e+1]!=="|"&&(rr&&(o=t.indexOf("\\",r),o===-1||o>s))))return !0;if(t[e]==="\\"){var a=t[e+1];e+=2;var l=NE[a];if(l){var d=t.indexOf(l,e);d!==-1&&(e=d+1);}if(t[e]==="!")return !0}else e++;}return !1},FL=function(t){if(t[0]==="!")return !0;for(var e=0;e{var NL=ud(),qL=oe("path").posix.dirname,LL=oe("os").platform()==="win32",cd="/",$L=/\\/g,jL=/[\{\[].*[\}\]]$/,HL=/(^|[^\\])([\{\[]|\([^\)]+$)/,WL=/\\([\!\*\?\|\[\]\(\)\{\}])/g;LE.exports=function(e,r){var n=Object.assign({flipBackslashes:!0},r);n.flipBackslashes&&LL&&e.indexOf(cd)<0&&(e=e.replace($L,cd)),jL.test(e)&&(e+=cd),e+="a";do e=qL(e);while(NL(e)||HL.test(e));return e.replace(WL,"$1")};});var Ya=A(gr=>{gr.isInteger=t=>typeof t=="number"?Number.isInteger(t):typeof t=="string"&&t.trim()!==""?Number.isInteger(Number(t)):!1;gr.find=(t,e)=>t.nodes.find(r=>r.type===e);gr.exceedsLimit=(t,e,r=1,n)=>n===!1||!gr.isInteger(t)||!gr.isInteger(e)?!1:(Number(e)-Number(t))/Number(r)>=n;gr.escapeNode=(t,e=0,r)=>{let n=t.nodes[e];n&&(r&&n.type===r||n.type==="open"||n.type==="close")&&n.escaped!==!0&&(n.value="\\"+n.value,n.escaped=!0);};gr.encloseBrace=t=>t.type!=="brace"||t.commas>>0+t.ranges>>0?!1:(t.invalid=!0,!0);gr.isInvalidBrace=t=>t.type!=="brace"?!1:t.invalid===!0||t.dollar?!0:!(t.commas>>0+t.ranges>>0)||t.open!==!0||t.close!==!0?(t.invalid=!0,!0):!1;gr.isOpenOrClose=t=>t.type==="open"||t.type==="close"?!0:t.open===!0||t.close===!0;gr.reduce=t=>t.reduce((e,r)=>(r.type==="text"&&e.push(r.value),r.type==="range"&&(r.type="text"),e),[]);gr.flatten=(...t)=>{let e=[],r=n=>{for(let i=0;i{var jE=Ya();HE.exports=(t,e={})=>{let r=(n,i={})=>{let s=e.escapeInvalid&&jE.isInvalidBrace(i),o=n.invalid===!0&&e.escapeInvalid===!0,a="";if(n.value)return (s||o)&&jE.isOpenOrClose(n)?"\\"+n.value:n.value;if(n.value)return n.value;if(n.nodes)for(let l of n.nodes)a+=r(l);return a};return r(t)};});var BE=A((rV,WE)=>{WE.exports=function(t){return typeof t=="number"?t-t===0:typeof t=="string"&&t.trim()!==""?Number.isFinite?Number.isFinite(+t):isFinite(+t):!1};});var XE=A((nV,YE)=>{var UE=BE(),Qn=(t,e,r)=>{if(UE(t)===!1)throw new TypeError("toRegexRange: expected the first argument to be a number");if(e===void 0||t===e)return String(t);if(UE(e)===!1)throw new TypeError("toRegexRange: expected the second argument to be a number.");let n={relaxZeros:!0,...r};typeof n.strictZeros=="boolean"&&(n.relaxZeros=n.strictZeros===!1);let i=String(n.relaxZeros),s=String(n.shorthand),o=String(n.capture),a=String(n.wrap),l=t+":"+e+"="+i+s+o+a;if(Qn.cache.hasOwnProperty(l))return Qn.cache[l].result;let d=Math.min(t,e),c=Math.max(t,e);if(Math.abs(d-c)===1){let E=t+"|"+e;return n.capture?`(${E})`:n.wrap===!1?E:`(?:${E})`}let p=JE(t)||JE(e),m={min:t,max:e,a:d,b:c},_=[],w=[];if(p&&(m.isPadded=p,m.maxLen=String(m.max).length),d<0){let E=c<0?Math.abs(c):1;w=zE(E,Math.abs(d),m,n),d=m.a=0;}return c>=0&&(_=zE(d,c,m,n)),m.negatives=w,m.positives=_,m.result=BL(w,_),n.capture===!0?m.result=`(${m.result})`:n.wrap!==!1&&_.length+w.length>1&&(m.result=`(?:${m.result})`),Qn.cache[l]=m,m.result};function BL(t,e,r){let n=ld(t,e,"-",!1)||[],i=ld(e,t,"",!1)||[],s=ld(t,e,"-?",!0)||[];return n.concat(s).concat(i).join("|")}function UL(t,e){let r=1,n=1,i=GE(t,r),s=new Set([e]);for(;t<=i&&i<=e;)s.add(i),r+=1,i=GE(t,r);for(i=KE(e+1,n)-1;t1&&a.count.pop(),a.count.push(c.count[0]),a.string=a.pattern+ZE(a.count),o=d+1;continue}r.isPadded&&(p=ZL(d,r,n)),c.string=p+c.pattern+ZE(c.count),s.push(c),o=d+1,a=c;}return s}function ld(t,e,r,n,i){let s=[];for(let o of t){let{string:a}=o;!n&&!VE(e,"string",a)&&s.push(r+a),n&&VE(e,"string",a)&&s.push(r+a);}return s}function VL(t,e){let r=[];for(let n=0;ne?1:e>t?-1:0}function VE(t,e,r){return t.some(n=>n[e]===r)}function GE(t,e){return Number(String(t).slice(0,-e)+"9".repeat(e))}function KE(t,e){return t-t%Math.pow(10,e)}function ZE(t){let[e=0,r=""]=t;return r||e>1?`{${e+(r?","+r:"")}}`:""}function KL(t,e,r){return `[${t}${e-t===1?"":"-"}${e}]`}function JE(t){return /^-?(0+)\d/.test(t)}function ZL(t,e,r){if(!e.isPadded)return t;let n=Math.abs(e.maxLen-String(t).length),i=r.relaxZeros!==!1;switch(n){case 0:return "";case 1:return i?"0?":"0";case 2:return i?"0{0,2}":"00";default:return i?`0{0,${n}}`:`0{${n}}`}}Qn.cache={};Qn.clearCache=()=>Qn.cache={};YE.exports=Qn;});var hd=A((iV,o0)=>{var JL=oe("util"),t0=XE(),QE=t=>t!==null&&typeof t=="object"&&!Array.isArray(t),YL=t=>e=>t===!0?Number(e):String(e),fd=t=>typeof t=="number"||typeof t=="string"&&t!=="",Qs=t=>Number.isInteger(+t),dd=t=>{let e=`${t}`,r=-1;if(e[0]==="-"&&(e=e.slice(1)),e==="0")return !1;for(;e[++r]==="0";);return r>0},XL=(t,e,r)=>typeof t=="string"||typeof e=="string"?!0:r.stringify===!0,QL=(t,e,r)=>{if(e>0){let n=t[0]==="-"?"-":"";n&&(t=t.slice(1)),t=n+t.padStart(n?e-1:e,"0");}return r===!1?String(t):t},e0=(t,e)=>{let r=t[0]==="-"?"-":"";for(r&&(t=t.slice(1),e--);t.length{t.negatives.sort((o,a)=>oa?1:0),t.positives.sort((o,a)=>oa?1:0);let r=e.capture?"":"?:",n="",i="",s;return t.positives.length&&(n=t.positives.join("|")),t.negatives.length&&(i=`-(${r}${t.negatives.join("|")})`),n&&i?s=`${n}|${i}`:s=n||i,e.wrap?`(${r}${s})`:s},r0=(t,e,r,n)=>{if(r)return t0(t,e,{wrap:!1,...n});let i=String.fromCharCode(t);if(t===e)return i;let s=String.fromCharCode(e);return `[${i}-${s}]`},n0=(t,e,r)=>{if(Array.isArray(t)){let n=r.wrap===!0,i=r.capture?"":"?:";return n?`(${i}${t.join("|")})`:t.join("|")}return t0(t,e,r)},i0=(...t)=>new RangeError("Invalid range arguments: "+JL.inspect(...t)),s0=(t,e,r)=>{if(r.strictRanges===!0)throw i0([t,e]);return []},t2=(t,e)=>{if(e.strictRanges===!0)throw new TypeError(`Expected step "${t}" to be a number`);return []},r2=(t,e,r=1,n={})=>{let i=Number(t),s=Number(e);if(!Number.isInteger(i)||!Number.isInteger(s)){if(n.strictRanges===!0)throw i0([t,e]);return []}i===0&&(i=0),s===0&&(s=0);let o=i>s,a=String(t),l=String(e),d=String(r);r=Math.max(Math.abs(r),1);let c=dd(a)||dd(l)||dd(d),p=c?Math.max(a.length,l.length,d.length):0,m=c===!1&&XL(t,e,n)===!1,_=n.transform||YL(m);if(n.toRegex&&r===1)return r0(e0(t,p),e0(e,p),!0,n);let w={negatives:[],positives:[]},E=g=>w[g<0?"negatives":"positives"].push(Math.abs(g)),P=[],D=0;for(;o?i>=s:i<=s;)n.toRegex===!0&&r>1?E(i):P.push(QL(_(i,D),p,m)),i=o?i-r:i+r,D++;return n.toRegex===!0?r>1?e2(w,n):n0(P,null,{wrap:!1,...n}):P},n2=(t,e,r=1,n={})=>{if(!Qs(t)&&t.length>1||!Qs(e)&&e.length>1)return s0(t,e,n);let i=n.transform||(m=>String.fromCharCode(m)),s=`${t}`.charCodeAt(0),o=`${e}`.charCodeAt(0),a=s>o,l=Math.min(s,o),d=Math.max(s,o);if(n.toRegex&&r===1)return r0(l,d,!1,n);let c=[],p=0;for(;a?s>=o:s<=o;)c.push(i(s,p)),s=a?s-r:s+r,p++;return n.toRegex===!0?n0(c,null,{wrap:!1,options:n}):c},Qa=(t,e,r,n={})=>{if(e==null&&fd(t))return [t];if(!fd(t)||!fd(e))return s0(t,e,n);if(typeof r=="function")return Qa(t,e,1,{transform:r});if(QE(r))return Qa(t,e,0,r);let i={...n};return i.capture===!0&&(i.wrap=!0),r=r||i.step||1,Qs(r)?Qs(t)&&Qs(e)?r2(t,e,r,i):n2(t,e,Math.max(Math.abs(r),1),i):r!=null&&!QE(r)?t2(r,i):Qa(t,e,1,r)};o0.exports=Qa;});var c0=A((sV,u0)=>{var i2=hd(),a0=Ya(),s2=(t,e={})=>{let r=(n,i={})=>{let s=a0.isInvalidBrace(i),o=n.invalid===!0&&e.escapeInvalid===!0,a=s===!0||o===!0,l=e.escapeInvalid===!0?"\\":"",d="";if(n.isOpen===!0||n.isClose===!0)return l+n.value;if(n.type==="open")return a?l+n.value:"(";if(n.type==="close")return a?l+n.value:")";if(n.type==="comma")return n.prev.type==="comma"?"":a?n.value:"|";if(n.value)return n.value;if(n.nodes&&n.ranges>0){let c=a0.reduce(n.nodes),p=i2(...c,{...e,wrap:!1,toRegex:!0});if(p.length!==0)return c.length>1&&p.length>1?`(${p})`:p}if(n.nodes)for(let c of n.nodes)d+=r(c,n);return d};return r(t)};u0.exports=s2;});var d0=A((oV,f0)=>{var o2=hd(),l0=Xa(),Hi=Ya(),ei=(t="",e="",r=!1)=>{let n=[];if(t=[].concat(t),e=[].concat(e),!e.length)return t;if(!t.length)return r?Hi.flatten(e).map(i=>`{${i}}`):e;for(let i of t)if(Array.isArray(i))for(let s of i)n.push(ei(s,e,r));else for(let s of e)r===!0&&typeof s=="string"&&(s=`{${s}}`),n.push(Array.isArray(s)?ei(i,s,r):i+s);return Hi.flatten(n)},a2=(t,e={})=>{let r=e.rangeLimit===void 0?1e3:e.rangeLimit,n=(i,s={})=>{i.queue=[];let o=s,a=s.queue;for(;o.type!=="brace"&&o.type!=="root"&&o.parent;)o=o.parent,a=o.queue;if(i.invalid||i.dollar){a.push(ei(a.pop(),l0(i,e)));return}if(i.type==="brace"&&i.invalid!==!0&&i.nodes.length===2){a.push(ei(a.pop(),["{}"]));return}if(i.nodes&&i.ranges>0){let p=Hi.reduce(i.nodes);if(Hi.exceedsLimit(...p,e.step,r))throw new RangeError("expanded array length exceeds range limit. Use options.rangeLimit to increase or disable the limit.");let m=o2(...p,e);m.length===0&&(m=l0(i,e)),a.push(ei(a.pop(),m)),i.nodes=[];return}let l=Hi.encloseBrace(i),d=i.queue,c=i;for(;c.type!=="brace"&&c.type!=="root"&&c.parent;)c=c.parent,d=c.queue;for(let p=0;p{h0.exports={MAX_LENGTH:1024*64,CHAR_0:"0",CHAR_9:"9",CHAR_UPPERCASE_A:"A",CHAR_LOWERCASE_A:"a",CHAR_UPPERCASE_Z:"Z",CHAR_LOWERCASE_Z:"z",CHAR_LEFT_PARENTHESES:"(",CHAR_RIGHT_PARENTHESES:")",CHAR_ASTERISK:"*",CHAR_AMPERSAND:"&",CHAR_AT:"@",CHAR_BACKSLASH:"\\",CHAR_BACKTICK:"`",CHAR_CARRIAGE_RETURN:"\r",CHAR_CIRCUMFLEX_ACCENT:"^",CHAR_COLON:":",CHAR_COMMA:",",CHAR_DOLLAR:"$",CHAR_DOT:".",CHAR_DOUBLE_QUOTE:'"',CHAR_EQUAL:"=",CHAR_EXCLAMATION_MARK:"!",CHAR_FORM_FEED:"\f",CHAR_FORWARD_SLASH:"/",CHAR_HASH:"#",CHAR_HYPHEN_MINUS:"-",CHAR_LEFT_ANGLE_BRACKET:"<",CHAR_LEFT_CURLY_BRACE:"{",CHAR_LEFT_SQUARE_BRACKET:"[",CHAR_LINE_FEED:` -`,CHAR_NO_BREAK_SPACE:"\xA0",CHAR_PERCENT:"%",CHAR_PLUS:"+",CHAR_QUESTION_MARK:"?",CHAR_RIGHT_ANGLE_BRACKET:">",CHAR_RIGHT_CURLY_BRACE:"}",CHAR_RIGHT_SQUARE_BRACKET:"]",CHAR_SEMICOLON:";",CHAR_SINGLE_QUOTE:"'",CHAR_SPACE:" ",CHAR_TAB:" ",CHAR_UNDERSCORE:"_",CHAR_VERTICAL_LINE:"|",CHAR_ZERO_WIDTH_NOBREAK_SPACE:"\uFEFF"};});var b0=A((uV,y0)=>{var u2=Xa(),{MAX_LENGTH:m0,CHAR_BACKSLASH:pd,CHAR_BACKTICK:c2,CHAR_COMMA:l2,CHAR_DOT:f2,CHAR_LEFT_PARENTHESES:d2,CHAR_RIGHT_PARENTHESES:h2,CHAR_LEFT_CURLY_BRACE:p2,CHAR_RIGHT_CURLY_BRACE:m2,CHAR_LEFT_SQUARE_BRACKET:g0,CHAR_RIGHT_SQUARE_BRACKET:_0,CHAR_DOUBLE_QUOTE:g2,CHAR_SINGLE_QUOTE:_2,CHAR_NO_BREAK_SPACE:y2,CHAR_ZERO_WIDTH_NOBREAK_SPACE:b2}=p0(),v2=(t,e={})=>{if(typeof t!="string")throw new TypeError("Expected a string");let r=e||{},n=typeof r.maxLength=="number"?Math.min(m0,r.maxLength):m0;if(t.length>n)throw new SyntaxError(`Input length (${t.length}), exceeds max characters (${n})`);let i={type:"root",input:t,nodes:[]},s=[i],o=i,a=i,l=0,d=t.length,c=0,p=0,m,w=()=>t[c++],E=P=>{if(P.type==="text"&&a.type==="dot"&&(a.type="text"),a&&a.type==="text"&&P.type==="text"){a.value+=P.value;return}return o.nodes.push(P),P.parent=o,P.prev=a,a=P,P};for(E({type:"bos"});c0){if(o.ranges>0){o.ranges=0;let P=o.nodes.shift();o.nodes=[P,{type:"text",value:u2(o)}];}E({type:"comma",value:m}),o.commas++;continue}if(m===f2&&p>0&&o.commas===0){let P=o.nodes;if(p===0||P.length===0){E({type:"text",value:m});continue}if(a.type==="dot"){if(o.range=[],a.value+=m,a.type="range",o.nodes.length!==3&&o.nodes.length!==5){o.invalid=!0,o.ranges=0,a.type="text";continue}o.ranges++,o.args=[];continue}if(a.type==="range"){P.pop();let D=P[P.length-1];D.value+=a.value+m,a=D,o.ranges--;continue}E({type:"dot",value:m});continue}E({type:"text",value:m});}do if(o=s.pop(),o.type!=="root"){o.nodes.forEach(g=>{g.nodes||(g.type==="open"&&(g.isOpen=!0),g.type==="close"&&(g.isClose=!0),g.nodes||(g.type="text"),g.invalid=!0);});let P=s[s.length-1],D=P.nodes.indexOf(o);P.nodes.splice(D,1,...o.nodes);}while(s.length>0);return E({type:"eos"}),i};y0.exports=v2;});var S0=A((cV,w0)=>{var v0=Xa(),w2=c0(),S2=d0(),E2=b0(),ar=(t,e={})=>{let r=[];if(Array.isArray(t))for(let n of t){let i=ar.create(n,e);Array.isArray(i)?r.push(...i):r.push(i);}else r=[].concat(ar.create(t,e));return e&&e.expand===!0&&e.nodupes===!0&&(r=[...new Set(r)]),r};ar.parse=(t,e={})=>E2(t,e);ar.stringify=(t,e={})=>v0(typeof t=="string"?ar.parse(t,e):t,e);ar.compile=(t,e={})=>(typeof t=="string"&&(t=ar.parse(t,e)),w2(t,e));ar.expand=(t,e={})=>{typeof t=="string"&&(t=ar.parse(t,e));let r=S2(t,e);return e.noempty===!0&&(r=r.filter(Boolean)),e.nodupes===!0&&(r=[...new Set(r)]),r};ar.create=(t,e={})=>t===""||t.length<3?[t]:e.expand!==!0?ar.compile(t,e):ar.expand(t,e);w0.exports=ar;});var E0=A((lV,R2)=>{R2.exports=["3dm","3ds","3g2","3gp","7z","a","aac","adp","ai","aif","aiff","alz","ape","apk","appimage","ar","arj","asf","au","avi","bak","baml","bh","bin","bk","bmp","btif","bz2","bzip2","cab","caf","cgm","class","cmx","cpio","cr2","cur","dat","dcm","deb","dex","djvu","dll","dmg","dng","doc","docm","docx","dot","dotm","dra","DS_Store","dsk","dts","dtshd","dvb","dwg","dxf","ecelp4800","ecelp7470","ecelp9600","egg","eol","eot","epub","exe","f4v","fbs","fh","fla","flac","flatpak","fli","flv","fpx","fst","fvt","g3","gh","gif","graffle","gz","gzip","h261","h263","h264","icns","ico","ief","img","ipa","iso","jar","jpeg","jpg","jpgv","jpm","jxr","key","ktx","lha","lib","lvp","lz","lzh","lzma","lzo","m3u","m4a","m4v","mar","mdi","mht","mid","midi","mj2","mka","mkv","mmr","mng","mobi","mov","movie","mp3","mp4","mp4a","mpeg","mpg","mpga","mxu","nef","npx","numbers","nupkg","o","odp","ods","odt","oga","ogg","ogv","otf","ott","pages","pbm","pcx","pdb","pdf","pea","pgm","pic","png","pnm","pot","potm","potx","ppa","ppam","ppm","pps","ppsm","ppsx","ppt","pptm","pptx","psd","pya","pyc","pyo","pyv","qt","rar","ras","raw","resources","rgb","rip","rlc","rmf","rmvb","rpm","rtf","rz","s3m","s7z","scpt","sgi","shar","snap","sil","sketch","slk","smv","snk","so","stl","suo","sub","swf","tar","tbz","tbz2","tga","tgz","thmx","tif","tiff","tlz","ttc","ttf","txz","udf","uvh","uvi","uvm","uvp","uvs","uvu","viv","vob","war","wav","wax","wbmp","wdp","weba","webm","webp","whl","wim","wm","wma","wmv","wmx","woff","woff2","wrm","wvx","xbm","xif","xla","xlam","xls","xlsb","xlsm","xlsx","xlt","xltm","xltx","xm","xmind","xpi","xpm","xwd","xz","z","zip","zipx"];});var C0=A((fV,R0)=>{R0.exports=E0();});var T0=A((dV,x0)=>{var C2=oe("path"),x2=C0(),T2=new Set(x2);x0.exports=t=>T2.has(C2.extname(t).slice(1).toLowerCase());});var eu=A(ge=>{var{sep:A2}=oe("path"),{platform:md}=process,P2=oe("os");ge.EV_ALL="all";ge.EV_READY="ready";ge.EV_ADD="add";ge.EV_CHANGE="change";ge.EV_ADD_DIR="addDir";ge.EV_UNLINK="unlink";ge.EV_UNLINK_DIR="unlinkDir";ge.EV_RAW="raw";ge.EV_ERROR="error";ge.STR_DATA="data";ge.STR_END="end";ge.STR_CLOSE="close";ge.FSEVENT_CREATED="created";ge.FSEVENT_MODIFIED="modified";ge.FSEVENT_DELETED="deleted";ge.FSEVENT_MOVED="moved";ge.FSEVENT_CLONED="cloned";ge.FSEVENT_UNKNOWN="unknown";ge.FSEVENT_TYPE_FILE="file";ge.FSEVENT_TYPE_DIRECTORY="directory";ge.FSEVENT_TYPE_SYMLINK="symlink";ge.KEY_LISTENERS="listeners";ge.KEY_ERR="errHandlers";ge.KEY_RAW="rawEmitters";ge.HANDLER_KEYS=[ge.KEY_LISTENERS,ge.KEY_ERR,ge.KEY_RAW];ge.DOT_SLASH=`.${A2}`;ge.BACK_SLASH_RE=/\\/g;ge.DOUBLE_SLASH_RE=/\/\//;ge.SLASH_OR_BACK_SLASH_RE=/[/\\]/;ge.DOT_RE=/\..*\.(sw[px])$|~$|\.subl.*\.tmp/;ge.REPLACER_RE=/^\.[/\\]/;ge.SLASH="/";ge.SLASH_SLASH="//";ge.BRACE_START="{";ge.BANG="!";ge.ONE_DOT=".";ge.TWO_DOTS="..";ge.STAR="*";ge.GLOBSTAR="**";ge.ROOT_GLOBSTAR="/**/*";ge.SLASH_GLOBSTAR="/**";ge.DIR_SUFFIX="Dir";ge.ANYMATCH_OPTS={dot:!0};ge.STRING_TYPE="string";ge.FUNCTION_TYPE="function";ge.EMPTY_STR="";ge.EMPTY_FN=()=>{};ge.IDENTITY_FN=t=>t;ge.isWindows=md==="win32";ge.isMacos=md==="darwin";ge.isLinux=md==="linux";ge.isIBMi=P2.type()==="OS400";});var k0=A((pV,I0)=>{var sn=oe("fs"),bt=oe("path"),{promisify:no}=oe("util"),D2=T0(),{isWindows:O2,isLinux:I2,EMPTY_FN:k2,EMPTY_STR:M2,KEY_LISTENERS:Wi,KEY_ERR:gd,KEY_RAW:eo,HANDLER_KEYS:F2,EV_CHANGE:ru,EV_ADD:tu,EV_ADD_DIR:N2,EV_ERROR:P0,STR_DATA:q2,STR_END:L2,BRACE_START:$2,STAR:j2}=eu(),H2="watch",W2=no(sn.open),D0=no(sn.stat),B2=no(sn.lstat),U2=no(sn.close),_d=no(sn.realpath),z2={lstat:B2,stat:D0},bd=(t,e)=>{t instanceof Set?t.forEach(e):e(t);},to=(t,e,r)=>{let n=t[e];n instanceof Set||(t[e]=n=new Set([n])),n.add(r);},V2=t=>e=>{let r=t[e];r instanceof Set?r.clear():delete t[e];},ro=(t,e,r)=>{let n=t[e];n instanceof Set?n.delete(r):n===r&&delete t[e];},O0=t=>t instanceof Set?t.size===0:!t,nu=new Map;function A0(t,e,r,n,i){let s=(o,a)=>{r(t),i(o,a,{watchedPath:t}),a&&t!==a&&iu(bt.resolve(t,a),Wi,bt.join(t,a));};try{return sn.watch(t,e,s)}catch(o){n(o);}}var iu=(t,e,r,n,i)=>{let s=nu.get(t);s&&bd(s[e],o=>{o(r,n,i);});},G2=(t,e,r,n)=>{let{listener:i,errHandler:s,rawEmitter:o}=n,a=nu.get(e),l;if(!r.persistent)return l=A0(t,r,i,s,o),l.close.bind(l);if(a)to(a,Wi,i),to(a,gd,s),to(a,eo,o);else {if(l=A0(t,r,iu.bind(null,e,Wi),s,iu.bind(null,e,eo)),!l)return;l.on(P0,async d=>{let c=iu.bind(null,e,gd);if(a.watcherUnusable=!0,O2&&d.code==="EPERM")try{let p=await W2(t,"r");await U2(p),c(d);}catch{}else c(d);}),a={listeners:i,errHandlers:s,rawEmitters:o,watcher:l},nu.set(e,a);}return ()=>{ro(a,Wi,i),ro(a,gd,s),ro(a,eo,o),O0(a.listeners)&&(a.watcher.close(),nu.delete(e),F2.forEach(V2(a)),a.watcher=void 0,Object.freeze(a));}},yd=new Map,K2=(t,e,r,n)=>{let {listener:i,rawEmitter:s}=n,o=yd.get(e),d=o&&o.options;return d&&(d.persistentr.interval)&&(sn.unwatchFile(e),o=void 0),o?(to(o,Wi,i),to(o,eo,s)):(o={listeners:i,rawEmitters:s,options:r,watcher:sn.watchFile(e,r,(c,p)=>{bd(o.rawEmitters,_=>{_(ru,e,{curr:c,prev:p});});let m=c.mtimeMs;(c.size!==p.size||m>p.mtimeMs||m===0)&&bd(o.listeners,_=>_(t,c));})},yd.set(e,o)),()=>{ro(o,Wi,i),ro(o,eo,s),O0(o.listeners)&&(yd.delete(e),sn.unwatchFile(e),o.options=o.watcher=void 0,Object.freeze(o));}},vd=class{constructor(e){this.fsw=e,this._boundHandleError=r=>e._handleError(r);}_watchWithNodeFs(e,r){let n=this.fsw.options,i=bt.dirname(e),s=bt.basename(e);this.fsw._getWatchedDir(i).add(s);let a=bt.resolve(e),l={persistent:n.persistent};r||(r=k2);let d;return n.usePolling?(l.interval=n.enableBinaryInterval&&D2(s)?n.binaryInterval:n.interval,d=K2(e,a,l,{listener:r,rawEmitter:this.fsw._emitRaw})):d=G2(e,a,l,{listener:r,errHandler:this._boundHandleError,rawEmitter:this.fsw._emitRaw}),d}_handleFile(e,r,n){if(this.fsw.closed)return;let i=bt.dirname(e),s=bt.basename(e),o=this.fsw._getWatchedDir(i),a=r;if(o.has(s))return;let l=async(c,p)=>{if(this.fsw._throttle(H2,e,5)){if(!p||p.mtimeMs===0)try{let m=await D0(e);if(this.fsw.closed)return;let _=m.atimeMs,w=m.mtimeMs;(!_||_<=w||w!==a.mtimeMs)&&this.fsw._emit(ru,e,m),I2&&a.ino!==m.ino?(this.fsw._closeFile(c),a=m,this.fsw._addPathCloser(c,this._watchWithNodeFs(e,l))):a=m;}catch{this.fsw._remove(i,s);}else if(o.has(s)){let m=p.atimeMs,_=p.mtimeMs;(!m||m<=_||_!==a.mtimeMs)&&this.fsw._emit(ru,e,p),a=p;}}},d=this._watchWithNodeFs(e,l);if(!(n&&this.fsw.options.ignoreInitial)&&this.fsw._isntIgnored(e)){if(!this.fsw._throttle(tu,e,0))return;this.fsw._emit(tu,e,r);}return d}async _handleSymlink(e,r,n,i){if(this.fsw.closed)return;let s=e.fullPath,o=this.fsw._getWatchedDir(r);if(!this.fsw.options.followSymlinks){this.fsw._incrReadyCount();let a;try{a=await _d(n);}catch{return this.fsw._emitReady(),!0}return this.fsw.closed?void 0:(o.has(i)?this.fsw._symlinkPaths.get(s)!==a&&(this.fsw._symlinkPaths.set(s,a),this.fsw._emit(ru,n,e.stats)):(o.add(i),this.fsw._symlinkPaths.set(s,a),this.fsw._emit(tu,n,e.stats)),this.fsw._emitReady(),!0)}if(this.fsw._symlinkPaths.has(s))return !0;this.fsw._symlinkPaths.set(s,!0);}_handleRead(e,r,n,i,s,o,a){if(e=bt.join(e,M2),!n.hasGlob&&(a=this.fsw._throttle("readdir",e,1e3),!a))return;let l=this.fsw._getWatchedDir(n.path),d=new Set,c=this.fsw._readdirp(e,{fileFilter:p=>n.filterPath(p),directoryFilter:p=>n.filterDir(p),depth:0}).on(q2,async p=>{if(this.fsw.closed){c=void 0;return}let m=p.path,_=bt.join(e,m);if(d.add(m),!(p.stats.isSymbolicLink()&&await this._handleSymlink(p,e,_,m))){if(this.fsw.closed){c=void 0;return}(m===i||!i&&!l.has(m))&&(this.fsw._incrReadyCount(),_=bt.join(s,bt.relative(s,_)),this._addToNodeFs(_,r,n,o+1));}}).on(P0,this._boundHandleError);return new Promise(p=>c.once(L2,()=>{if(this.fsw.closed){c=void 0;return}let m=a?a.clear():!1;p(),l.getChildren().filter(_=>_!==e&&!d.has(_)&&(!n.hasGlob||n.filterPath({fullPath:bt.resolve(e,_)}))).forEach(_=>{this.fsw._remove(e,_);}),c=void 0,m&&this._handleRead(e,!1,n,i,s,o,a);}))}async _handleDir(e,r,n,i,s,o,a){let l=this.fsw._getWatchedDir(bt.dirname(e)),d=l.has(bt.basename(e));!(n&&this.fsw.options.ignoreInitial)&&!s&&!d&&(!o.hasGlob||o.globFilter(e))&&this.fsw._emit(N2,e,r),l.add(bt.basename(e)),this.fsw._getWatchedDir(e);let c,p,m=this.fsw.options.depth;if((m==null||i<=m)&&!this.fsw._symlinkPaths.has(a)){if(!s&&(await this._handleRead(e,n,o,s,e,i,c),this.fsw.closed))return;p=this._watchWithNodeFs(e,(_,w)=>{w&&w.mtimeMs===0||this._handleRead(_,!1,o,s,e,i,c);});}return p}async _addToNodeFs(e,r,n,i,s){let o=this.fsw._emitReady;if(this.fsw._isIgnored(e)||this.fsw.closed)return o(),!1;let a=this.fsw._getWatchHelpers(e,i);!a.hasGlob&&n&&(a.hasGlob=n.hasGlob,a.globFilter=n.globFilter,a.filterPath=l=>n.filterPath(l),a.filterDir=l=>n.filterDir(l));try{let l=await z2[a.statMethod](a.watchPath);if(this.fsw.closed)return;if(this.fsw._isIgnored(a.watchPath,l))return o(),!1;let d=this.fsw.options.followSymlinks&&!e.includes(j2)&&!e.includes($2),c;if(l.isDirectory()){let p=bt.resolve(e),m=d?await _d(e):e;if(this.fsw.closed||(c=await this._handleDir(a.watchPath,l,r,i,s,a,m),this.fsw.closed))return;p!==m&&m!==void 0&&this.fsw._symlinkPaths.set(p,m);}else if(l.isSymbolicLink()){let p=d?await _d(e):e;if(this.fsw.closed)return;let m=bt.dirname(a.watchPath);if(this.fsw._getWatchedDir(m).add(a.watchPath),this.fsw._emit(tu,a.watchPath,l),c=await this._handleDir(m,l,r,i,e,a,p),this.fsw.closed)return;p!==void 0&&this.fsw._symlinkPaths.set(bt.resolve(e),p);}else c=this._handleFile(a.watchPath,l,r);return o(),this.fsw._addPathCloser(e,c),!1}catch(l){if(this.fsw._handleError(l))return o(),e}}};I0.exports=vd;});var j0=A((mV,Ad)=>{var xd=oe("fs"),vt=oe("path"),{promisify:Td}=oe("util"),Bi;try{Bi=oe("fsevents");}catch(t){process.env.CHOKIDAR_PRINT_FSEVENTS_REQUIRE_ERROR&&console.error(t);}if(Bi){let t=process.version.match(/v(\d+)\.(\d+)/);if(t&&t[1]&&t[2]){let e=Number.parseInt(t[1],10),r=Number.parseInt(t[2],10);e===8&&r<16&&(Bi=void 0);}}var{EV_ADD:wd,EV_CHANGE:Z2,EV_ADD_DIR:M0,EV_UNLINK:su,EV_ERROR:J2,STR_DATA:Y2,STR_END:X2,FSEVENT_CREATED:Q2,FSEVENT_MODIFIED:e$,FSEVENT_DELETED:t$,FSEVENT_MOVED:r$,FSEVENT_UNKNOWN:n$,FSEVENT_TYPE_FILE:i$,FSEVENT_TYPE_DIRECTORY:io,FSEVENT_TYPE_SYMLINK:$0,ROOT_GLOBSTAR:F0,DIR_SUFFIX:s$,DOT_SLASH:N0,FUNCTION_TYPE:Sd,EMPTY_FN:o$,IDENTITY_FN:a$}=eu(),u$=t=>isNaN(t)?{}:{depth:t},Rd=Td(xd.stat),c$=Td(xd.lstat),q0=Td(xd.realpath),l$={stat:Rd,lstat:c$},ti=new Map,f$=10,d$=new Set([69888,70400,71424,72704,73472,131328,131840,262912]),h$=(t,e)=>({stop:Bi.watch(t,e)});function p$(t,e,r,n){let i=vt.extname(e)?vt.dirname(e):e,s=vt.dirname(i),o=ti.get(i);m$(s)&&(i=s);let a=vt.resolve(t),l=a!==e,d=(p,m,_)=>{l&&(p=p.replace(e,a)),(p===a||!p.indexOf(a+vt.sep))&&r(p,m,_);},c=!1;for(let p of ti.keys())if(e.indexOf(vt.resolve(p)+vt.sep)===0){i=p,o=ti.get(i),c=!0;break}return o||c?o.listeners.add(d):(o={listeners:new Set([d]),rawEmitter:n,watcher:h$(i,(p,m)=>{if(!o.listeners.size)return;let _=Bi.getInfo(p,m);o.listeners.forEach(w=>{w(p,m,_);}),o.rawEmitter(_.event,p,_);})},ti.set(i,o)),()=>{let p=o.listeners;if(p.delete(d),!p.size&&(ti.delete(i),o.watcher))return o.watcher.stop().then(()=>{o.rawEmitter=o.watcher=void 0,Object.freeze(o);})}}var m$=t=>{let e=0;for(let r of ti.keys())if(r.indexOf(t)===0&&(e++,e>=f$))return !0;return !1},g$=()=>Bi&&ti.size<128,Ed=(t,e)=>{let r=0;for(;!t.indexOf(e)&&(t=vt.dirname(t))!==e;)r++;return r},L0=(t,e)=>t.type===io&&e.isDirectory()||t.type===$0&&e.isSymbolicLink()||t.type===i$&&e.isFile(),Cd=class{constructor(e){this.fsw=e;}checkIgnored(e,r){let n=this.fsw._ignoredPaths;if(this.fsw._isIgnored(e,r))return n.add(e),r&&r.isDirectory()&&n.add(e+F0),!0;n.delete(e),n.delete(e+F0);}addOrChange(e,r,n,i,s,o,a,l){let d=s.has(o)?Z2:wd;this.handleEvent(d,e,r,n,i,s,o,a,l);}async checkExists(e,r,n,i,s,o,a,l){try{let d=await Rd(e);if(this.fsw.closed)return;L0(a,d)?this.addOrChange(e,r,n,i,s,o,a,l):this.handleEvent(su,e,r,n,i,s,o,a,l);}catch(d){d.code==="EACCES"?this.addOrChange(e,r,n,i,s,o,a,l):this.handleEvent(su,e,r,n,i,s,o,a,l);}}handleEvent(e,r,n,i,s,o,a,l,d){if(!(this.fsw.closed||this.checkIgnored(r)))if(e===su){let c=l.type===io;(c||o.has(a))&&this.fsw._remove(s,a,c);}else {if(e===wd){if(l.type===io&&this.fsw._getWatchedDir(r),l.type===$0&&d.followSymlinks){let p=d.depth===void 0?void 0:Ed(n,i)+1;return this._addToFsEvents(r,!1,!0,p)}this.fsw._getWatchedDir(s).add(a);}let c=l.type===io?e+s$:e;this.fsw._emit(c,r),c===M0&&this._addToFsEvents(r,!1,!0);}}_watchWithFsEvents(e,r,n,i){if(this.fsw.closed||this.fsw._isIgnored(e))return;let s=this.fsw.options,a=p$(e,r,async(l,d,c)=>{if(this.fsw.closed||s.depth!==void 0&&Ed(l,r)>s.depth)return;let p=n(vt.join(e,vt.relative(e,l)));if(i&&!i(p))return;let m=vt.dirname(p),_=vt.basename(p),w=this.fsw._getWatchedDir(c.type===io?p:m);if(d$.has(d)||c.event===n$)if(typeof s.ignored===Sd){let E;try{E=await Rd(p);}catch{}if(this.fsw.closed||this.checkIgnored(p,E))return;L0(c,E)?this.addOrChange(p,l,r,m,w,_,c,s):this.handleEvent(su,p,l,r,m,w,_,c,s);}else this.checkExists(p,l,r,m,w,_,c,s);else switch(c.event){case Q2:case e$:return this.addOrChange(p,l,r,m,w,_,c,s);case t$:case r$:return this.checkExists(p,l,r,m,w,_,c,s)}},this.fsw._emitRaw);return this.fsw._emitReady(),a}async _handleFsEventsSymlink(e,r,n,i){if(!(this.fsw.closed||this.fsw._symlinkPaths.has(r))){this.fsw._symlinkPaths.set(r,!0),this.fsw._incrReadyCount();try{let s=await q0(e);if(this.fsw.closed)return;if(this.fsw._isIgnored(s))return this.fsw._emitReady();this.fsw._incrReadyCount(),this._addToFsEvents(s||e,o=>{let a=e;return s&&s!==N0?a=o.replace(s,e):o!==N0&&(a=vt.join(e,o)),n(a)},!1,i);}catch(s){if(this.fsw._handleError(s))return this.fsw._emitReady()}}}emitAdd(e,r,n,i,s){let o=n(e),a=r.isDirectory(),l=this.fsw._getWatchedDir(vt.dirname(o)),d=vt.basename(o);a&&this.fsw._getWatchedDir(o),!l.has(d)&&(l.add(d),(!i.ignoreInitial||s===!0)&&this.fsw._emit(a?M0:wd,o,r));}initWatch(e,r,n,i){if(this.fsw.closed)return;let s=this._watchWithFsEvents(n.watchPath,vt.resolve(e||n.watchPath),i,n.globFilter);this.fsw._addPathCloser(r,s);}async _addToFsEvents(e,r,n,i){if(this.fsw.closed)return;let s=this.fsw.options,o=typeof r===Sd?r:a$,a=this.fsw._getWatchHelpers(e);try{let l=await l$[a.statMethod](a.watchPath);if(this.fsw.closed)return;if(this.fsw._isIgnored(a.watchPath,l))throw null;if(l.isDirectory()){if(a.globFilter||this.emitAdd(o(e),l,o,s,n),i&&i>s.depth)return;this.fsw._readdirp(a.watchPath,{fileFilter:d=>a.filterPath(d),directoryFilter:d=>a.filterDir(d),...u$(s.depth-(i||0))}).on(Y2,d=>{if(this.fsw.closed||d.stats.isDirectory()&&!a.filterPath(d))return;let c=vt.join(a.watchPath,d.path),{fullPath:p}=d;if(a.followSymlinks&&d.stats.isSymbolicLink()){let m=s.depth===void 0?void 0:Ed(c,vt.resolve(a.watchPath))+1;this._handleFsEventsSymlink(c,p,o,m);}else this.emitAdd(c,d.stats,o,s,n);}).on(J2,o$).on(X2,()=>{this.fsw._emitReady();});}else this.emitAdd(a.watchPath,l,o,s,n),this.fsw._emitReady();}catch(l){(!l||this.fsw._handleError(l))&&(this.fsw._emitReady(),this.fsw._emitReady());}if(s.persistent&&n!==!0)if(typeof r===Sd)this.initWatch(void 0,e,a,o);else {let l;try{l=await q0(a.watchPath);}catch{}this.initWatch(l,e,a,o);}}};Ad.exports=Cd;Ad.exports.canUse=g$;});var Ud=A(Bd=>{var{EventEmitter:_$}=oe("events"),Hd=oe("fs"),Ue=oe("path"),{promisify:G0}=oe("util"),y$=xE(),Md=kE().default,b$=$E(),Pd=ud(),v$=S0(),w$=od(),S$=k0(),H0=j0(),{EV_ALL:Dd,EV_READY:E$,EV_ADD:ou,EV_CHANGE:so,EV_UNLINK:W0,EV_ADD_DIR:R$,EV_UNLINK_DIR:C$,EV_RAW:x$,EV_ERROR:Od,STR_CLOSE:T$,STR_END:A$,BACK_SLASH_RE:P$,DOUBLE_SLASH_RE:B0,SLASH_OR_BACK_SLASH_RE:D$,DOT_RE:O$,REPLACER_RE:I$,SLASH:Id,SLASH_SLASH:k$,BRACE_START:M$,BANG:Fd,ONE_DOT:K0,TWO_DOTS:F$,GLOBSTAR:N$,SLASH_GLOBSTAR:kd,ANYMATCH_OPTS:Nd,STRING_TYPE:Wd,FUNCTION_TYPE:q$,EMPTY_STR:qd,EMPTY_FN:L$,isWindows:$$,isMacos:j$,isIBMi:H$}=eu(),W$=G0(Hd.stat),B$=G0(Hd.readdir),Ld=(t=[])=>Array.isArray(t)?t:[t],Z0=(t,e=[])=>(t.forEach(r=>{Array.isArray(r)?Z0(r,e):e.push(r);}),e),U0=t=>{let e=Z0(Ld(t));if(!e.every(r=>typeof r===Wd))throw new TypeError(`Non-string provided as watch path: ${e}`);return e.map(J0)},z0=t=>{let e=t.replace(P$,Id),r=!1;for(e.startsWith(k$)&&(r=!0);e.match(B0);)e=e.replace(B0,Id);return r&&(e=Id+e),e},J0=t=>z0(Ue.normalize(z0(t))),V0=(t=qd)=>e=>typeof e!==Wd?e:J0(Ue.isAbsolute(e)?e:Ue.join(t,e)),U$=(t,e)=>Ue.isAbsolute(t)?t:t.startsWith(Fd)?Fd+Ue.join(e,t.slice(1)):Ue.join(e,t),Or=(t,e)=>t[e]===void 0,$d=class{constructor(e,r){this.path=e,this._removeWatcher=r,this.items=new Set;}add(e){let{items:r}=this;r&&e!==K0&&e!==F$&&r.add(e);}async remove(e){let{items:r}=this;if(!r||(r.delete(e),r.size>0))return;let n=this.path;try{await B$(n);}catch{this._removeWatcher&&this._removeWatcher(Ue.dirname(n),Ue.basename(n));}}has(e){let{items:r}=this;if(r)return r.has(e)}getChildren(){let{items:e}=this;if(e)return [...e.values()]}dispose(){this.items.clear(),delete this.path,delete this._removeWatcher,delete this.items,Object.freeze(this);}},z$="stat",V$="lstat",jd=class{constructor(e,r,n,i){this.fsw=i,this.path=e=e.replace(I$,qd),this.watchPath=r,this.fullWatchPath=Ue.resolve(r),this.hasGlob=r!==e,e===qd&&(this.hasGlob=!1),this.globSymlink=this.hasGlob&&n?void 0:!1,this.globFilter=this.hasGlob?Md(e,void 0,Nd):!1,this.dirParts=this.getDirParts(e),this.dirParts.forEach(s=>{s.length>1&&s.pop();}),this.followSymlinks=n,this.statMethod=n?z$:V$;}checkGlobSymlink(e){return this.globSymlink===void 0&&(this.globSymlink=e.fullParentDir===this.fullWatchPath?!1:{realPath:e.fullParentDir,linkPath:this.fullWatchPath}),this.globSymlink?e.fullPath.replace(this.globSymlink.realPath,this.globSymlink.linkPath):e.fullPath}entryPath(e){return Ue.join(this.watchPath,Ue.relative(this.watchPath,this.checkGlobSymlink(e)))}filterPath(e){let{stats:r}=e;if(r&&r.isSymbolicLink())return this.filterDir(e);let n=this.entryPath(e);return (this.hasGlob&&typeof this.globFilter===q$?this.globFilter(n):!0)&&this.fsw._isntIgnored(n,r)&&this.fsw._hasReadPermissions(r)}getDirParts(e){if(!this.hasGlob)return [];let r=[];return (e.includes(M$)?v$.expand(e):[e]).forEach(i=>{r.push(Ue.relative(this.watchPath,i).split(D$));}),r}filterDir(e){if(this.hasGlob){let r=this.getDirParts(this.checkGlobSymlink(e)),n=!1;this.unmatchedGlob=!this.dirParts.some(i=>i.every((s,o)=>(s===N$&&(n=!0),n||!r[0][o]||Md(s,r[0][o],Nd))));}return !this.unmatchedGlob&&this.fsw._isntIgnored(this.entryPath(e),e.stats)}},au=class extends _${constructor(e){super();let r={};e&&Object.assign(r,e),this._watched=new Map,this._closers=new Map,this._ignoredPaths=new Set,this._throttled=new Map,this._symlinkPaths=new Map,this._streams=new Set,this.closed=!1,Or(r,"persistent")&&(r.persistent=!0),Or(r,"ignoreInitial")&&(r.ignoreInitial=!1),Or(r,"ignorePermissionErrors")&&(r.ignorePermissionErrors=!1),Or(r,"interval")&&(r.interval=100),Or(r,"binaryInterval")&&(r.binaryInterval=300),Or(r,"disableGlobbing")&&(r.disableGlobbing=!1),r.enableBinaryInterval=r.binaryInterval!==r.interval,Or(r,"useFsEvents")&&(r.useFsEvents=!r.usePolling),H0.canUse()||(r.useFsEvents=!1),Or(r,"usePolling")&&!r.useFsEvents&&(r.usePolling=j$),H$&&(r.usePolling=!0);let i=process.env.CHOKIDAR_USEPOLLING;if(i!==void 0){let l=i.toLowerCase();l==="false"||l==="0"?r.usePolling=!1:l==="true"||l==="1"?r.usePolling=!0:r.usePolling=!!l;}let s=process.env.CHOKIDAR_INTERVAL;s&&(r.interval=Number.parseInt(s,10)),Or(r,"atomic")&&(r.atomic=!r.usePolling&&!r.useFsEvents),r.atomic&&(this._pendingUnlinks=new Map),Or(r,"followSymlinks")&&(r.followSymlinks=!0),Or(r,"awaitWriteFinish")&&(r.awaitWriteFinish=!1),r.awaitWriteFinish===!0&&(r.awaitWriteFinish={});let o=r.awaitWriteFinish;o&&(o.stabilityThreshold||(o.stabilityThreshold=2e3),o.pollInterval||(o.pollInterval=100),this._pendingWrites=new Map),r.ignored&&(r.ignored=Ld(r.ignored));let a=0;this._emitReady=()=>{a++,a>=this._readyCount&&(this._emitReady=L$,this._readyEmitted=!0,process.nextTick(()=>this.emit(E$)));},this._emitRaw=(...l)=>this.emit(x$,...l),this._readyEmitted=!1,this.options=r,r.useFsEvents?this._fsEventsHandler=new H0(this):this._nodeFsHandler=new S$(this),Object.freeze(r);}add(e,r,n){let{cwd:i,disableGlobbing:s}=this.options;this.closed=!1;let o=U0(e);return i&&(o=o.map(a=>{let l=U$(a,i);return s||!Pd(a)?l:w$(l)})),o=o.filter(a=>a.startsWith(Fd)?(this._ignoredPaths.add(a.slice(1)),!1):(this._ignoredPaths.delete(a),this._ignoredPaths.delete(a+kd),this._userIgnored=void 0,!0)),this.options.useFsEvents&&this._fsEventsHandler?(this._readyCount||(this._readyCount=o.length),this.options.persistent&&(this._readyCount*=2),o.forEach(a=>this._fsEventsHandler._addToFsEvents(a))):(this._readyCount||(this._readyCount=0),this._readyCount+=o.length,Promise.all(o.map(async a=>{let l=await this._nodeFsHandler._addToNodeFs(a,!n,0,0,r);return l&&this._emitReady(),l})).then(a=>{this.closed||a.filter(l=>l).forEach(l=>{this.add(Ue.dirname(l),Ue.basename(r||l));});})),this}unwatch(e){if(this.closed)return this;let r=U0(e),{cwd:n}=this.options;return r.forEach(i=>{!Ue.isAbsolute(i)&&!this._closers.has(i)&&(n&&(i=Ue.join(n,i)),i=Ue.resolve(i)),this._closePath(i),this._ignoredPaths.add(i),this._watched.has(i)&&this._ignoredPaths.add(i+kd),this._userIgnored=void 0;}),this}close(){if(this.closed)return this._closePromise;this.closed=!0,this.removeAllListeners();let e=[];return this._closers.forEach(r=>r.forEach(n=>{let i=n();i instanceof Promise&&e.push(i);})),this._streams.forEach(r=>r.destroy()),this._userIgnored=void 0,this._readyCount=0,this._readyEmitted=!1,this._watched.forEach(r=>r.dispose()),["closers","watched","streams","symlinkPaths","throttled"].forEach(r=>{this[`_${r}`].clear();}),this._closePromise=e.length?Promise.all(e).then(()=>{}):Promise.resolve(),this._closePromise}getWatched(){let e={};return this._watched.forEach((r,n)=>{let i=this.options.cwd?Ue.relative(this.options.cwd,n):n;e[i||K0]=r.getChildren().sort();}),e}emitWithAll(e,r){this.emit(...r),e!==Od&&this.emit(Dd,...r);}async _emit(e,r,n,i,s){if(this.closed)return;let o=this.options;$$&&(r=Ue.normalize(r)),o.cwd&&(r=Ue.relative(o.cwd,r));let a=[e,r];s!==void 0?a.push(n,i,s):i!==void 0?a.push(n,i):n!==void 0&&a.push(n);let l=o.awaitWriteFinish,d;if(l&&(d=this._pendingWrites.get(r)))return d.lastChange=new Date,this;if(o.atomic){if(e===W0)return this._pendingUnlinks.set(r,a),setTimeout(()=>{this._pendingUnlinks.forEach((c,p)=>{this.emit(...c),this.emit(Dd,...c),this._pendingUnlinks.delete(p);});},typeof o.atomic=="number"?o.atomic:100),this;e===ou&&this._pendingUnlinks.has(r)&&(e=a[0]=so,this._pendingUnlinks.delete(r));}if(l&&(e===ou||e===so)&&this._readyEmitted){let c=(p,m)=>{p?(e=a[0]=Od,a[1]=p,this.emitWithAll(e,a)):m&&(a.length>2?a[2]=m:a.push(m),this.emitWithAll(e,a));};return this._awaitWriteFinish(r,l.stabilityThreshold,e,c),this}if(e===so&&!this._throttle(so,r,50))return this;if(o.alwaysStat&&n===void 0&&(e===ou||e===R$||e===so)){let c=o.cwd?Ue.join(o.cwd,r):r,p;try{p=await W$(c);}catch{}if(!p||this.closed)return;a.push(p);}return this.emitWithAll(e,a),this}_handleError(e){let r=e&&e.code;return e&&r!=="ENOENT"&&r!=="ENOTDIR"&&(!this.options.ignorePermissionErrors||r!=="EPERM"&&r!=="EACCES")&&this.emit(Od,e),e||this.closed}_throttle(e,r,n){this._throttled.has(e)||this._throttled.set(e,new Map);let i=this._throttled.get(e),s=i.get(r);if(s)return s.count++,!1;let o,a=()=>{let d=i.get(r),c=d?d.count:0;return i.delete(r),clearTimeout(o),d&&clearTimeout(d.timeoutObject),c};o=setTimeout(a,n);let l={timeoutObject:o,clear:a,count:0};return i.set(r,l),l}_incrReadyCount(){return this._readyCount++}_awaitWriteFinish(e,r,n,i){let s,o=e;this.options.cwd&&!Ue.isAbsolute(e)&&(o=Ue.join(this.options.cwd,e));let a=new Date,l=d=>{Hd.stat(o,(c,p)=>{if(c||!this._pendingWrites.has(e)){c&&c.code!=="ENOENT"&&i(c);return}let m=Number(new Date);d&&p.size!==d.size&&(this._pendingWrites.get(e).lastChange=m);let _=this._pendingWrites.get(e);m-_.lastChange>=r?(this._pendingWrites.delete(e),i(void 0,p)):s=setTimeout(l,this.options.awaitWriteFinish.pollInterval,p);});};this._pendingWrites.has(e)||(this._pendingWrites.set(e,{lastChange:a,cancelWait:()=>(this._pendingWrites.delete(e),clearTimeout(s),n)}),s=setTimeout(l,this.options.awaitWriteFinish.pollInterval));}_getGlobIgnored(){return [...this._ignoredPaths.values()]}_isIgnored(e,r){if(this.options.atomic&&O$.test(e))return !0;if(!this._userIgnored){let{cwd:n}=this.options,i=this.options.ignored,s=i&&i.map(V0(n)),o=Ld(s).filter(l=>typeof l===Wd&&!Pd(l)).map(l=>l+kd),a=this._getGlobIgnored().map(V0(n)).concat(s,o);this._userIgnored=Md(a,void 0,Nd);}return this._userIgnored([e,r])}_isntIgnored(e,r){return !this._isIgnored(e,r)}_getWatchHelpers(e,r){let n=r||this.options.disableGlobbing||!Pd(e)?e:b$(e),i=this.options.followSymlinks;return new jd(e,n,i,this)}_getWatchedDir(e){this._boundRemove||(this._boundRemove=this._remove.bind(this));let r=Ue.resolve(e);return this._watched.has(r)||this._watched.set(r,new $d(r,this._boundRemove)),this._watched.get(r)}_hasReadPermissions(e){if(this.options.ignorePermissionErrors)return !0;let n=(e&&Number.parseInt(e.mode,10))&511;return !!(4&Number.parseInt(n.toString(8)[0],10))}_remove(e,r,n){let i=Ue.join(e,r),s=Ue.resolve(i);if(n=n??(this._watched.has(i)||this._watched.has(s)),!this._throttle("remove",i,100))return;!n&&!this.options.useFsEvents&&this._watched.size===1&&this.add(e,r,!0),this._getWatchedDir(i).getChildren().forEach(m=>this._remove(i,m));let l=this._getWatchedDir(e),d=l.has(r);l.remove(r),this._symlinkPaths.has(s)&&this._symlinkPaths.delete(s);let c=i;if(this.options.cwd&&(c=Ue.relative(this.options.cwd,i)),this.options.awaitWriteFinish&&this._pendingWrites.has(c)&&this._pendingWrites.get(c).cancelWait()===ou)return;this._watched.delete(i),this._watched.delete(s);let p=n?C$:W0;d&&!this._isIgnored(i)&&this._emit(p,i),this.options.useFsEvents||this._closePath(i);}_closePath(e){this._closeFile(e);let r=Ue.dirname(e);this._getWatchedDir(r).remove(Ue.basename(e));}_closeFile(e){let r=this._closers.get(e);r&&(r.forEach(n=>n()),this._closers.delete(e));}_addPathCloser(e,r){if(!r)return;let n=this._closers.get(e);n||(n=[],this._closers.set(e,n)),n.push(r);}_readdirp(e,r){if(this.closed)return;let n={type:Dd,alwaysStat:!0,lstat:!0,...r},i=y$(e,n);return this._streams.add(i),i.once(T$,()=>{i=void 0;}),i.once(A$,()=>{i&&(this._streams.delete(i),i=void 0);}),i}};Bd.FSWatcher=au;var G$=(t,e)=>{let r=new au(e);return r.add(t),r};Bd.watch=G$;});var Zd=A((EV,rR)=>{var oo=t=>t&&typeof t.message=="string",Kd=t=>{if(!t)return;let e=t.cause;if(typeof e=="function"){let r=t.cause();return oo(r)?r:void 0}else return oo(e)?e:void 0},eR=(t,e)=>{if(!oo(t))return "";let r=t.stack||"";if(e.has(t))return r+` -causes have become circular...`;let n=Kd(t);return n?(e.add(t),r+` -caused by: `+eR(n,e)):r},Y$=t=>eR(t,new Set),tR=(t,e,r)=>{if(!oo(t))return "";let n=r?"":t.message||"";if(e.has(t))return n+": ...";let i=Kd(t);if(i){e.add(t);let s=typeof t.cause=="function";return n+(s?"":": ")+tR(i,e,s)}else return n},X$=t=>tR(t,new Set);rR.exports={isErrorLike:oo,getErrorCause:Kd,stackWithCauses:Y$,messageWithCauses:X$};});var Jd=A((RV,iR)=>{var Q$=Symbol("circular-ref-tag"),uu=Symbol("pino-raw-err-ref"),nR=Object.create({},{type:{enumerable:!0,writable:!0,value:void 0},message:{enumerable:!0,writable:!0,value:void 0},stack:{enumerable:!0,writable:!0,value:void 0},aggregateErrors:{enumerable:!0,writable:!0,value:void 0},raw:{enumerable:!1,get:function(){return this[uu]},set:function(t){this[uu]=t;}}});Object.defineProperty(nR,uu,{writable:!0,value:{}});iR.exports={pinoErrProto:nR,pinoErrorSymbols:{seen:Q$,rawSymbol:uu}};});var aR=A((CV,oR)=>{oR.exports=Xd;var{messageWithCauses:ej,stackWithCauses:tj,isErrorLike:sR}=Zd(),{pinoErrProto:rj,pinoErrorSymbols:nj}=Jd(),{seen:Yd}=nj,{toString:ij}=Object.prototype;function Xd(t){if(!sR(t))return t;t[Yd]=void 0;let e=Object.create(rj);e.type=ij.call(t.constructor)==="[object Function]"?t.constructor.name:t.name,e.message=ej(t),e.stack=tj(t),Array.isArray(t.errors)&&(e.aggregateErrors=t.errors.map(r=>Xd(r)));for(let r in t)if(e[r]===void 0){let n=t[r];sR(n)?r!=="cause"&&!Object.prototype.hasOwnProperty.call(n,Yd)&&(e[r]=Xd(n)):e[r]=n;}return delete t[Yd],e.raw=t,e}});var cR=A((xV,uR)=>{uR.exports=lu;var{isErrorLike:Qd}=Zd(),{pinoErrProto:sj,pinoErrorSymbols:oj}=Jd(),{seen:cu}=oj,{toString:aj}=Object.prototype;function lu(t){if(!Qd(t))return t;t[cu]=void 0;let e=Object.create(sj);e.type=aj.call(t.constructor)==="[object Function]"?t.constructor.name:t.name,e.message=t.message,e.stack=t.stack,Array.isArray(t.errors)&&(e.aggregateErrors=t.errors.map(r=>lu(r))),Qd(t.cause)&&!Object.prototype.hasOwnProperty.call(t.cause,cu)&&(e.cause=lu(t.cause));for(let r in t)if(e[r]===void 0){let n=t[r];Qd(n)?Object.prototype.hasOwnProperty.call(n,cu)||(e[r]=lu(n)):e[r]=n;}return delete t[cu],e.raw=t,e}});var hR=A((TV,dR)=>{dR.exports={mapHttpRequest:uj,reqSerializer:fR};var eh=Symbol("pino-raw-req-ref"),lR=Object.create({},{id:{enumerable:!0,writable:!0,value:""},method:{enumerable:!0,writable:!0,value:""},url:{enumerable:!0,writable:!0,value:""},query:{enumerable:!0,writable:!0,value:""},params:{enumerable:!0,writable:!0,value:""},headers:{enumerable:!0,writable:!0,value:{}},remoteAddress:{enumerable:!0,writable:!0,value:""},remotePort:{enumerable:!0,writable:!0,value:""},raw:{enumerable:!1,get:function(){return this[eh]},set:function(t){this[eh]=t;}}});Object.defineProperty(lR,eh,{writable:!0,value:{}});function fR(t){let e=t.info||t.socket,r=Object.create(lR);if(r.id=typeof t.id=="function"?t.id():t.id||(t.info?t.info.id:void 0),r.method=t.method,t.originalUrl)r.url=t.originalUrl;else {let n=t.path;r.url=typeof n=="string"?n:t.url?t.url.path||t.url:void 0;}return t.query&&(r.query=t.query),t.params&&(r.params=t.params),r.headers=t.headers,r.remoteAddress=e&&e.remoteAddress,r.remotePort=e&&e.remotePort,r.raw=t.raw||t,r}function uj(t){return {req:fR(t)}}});var _R=A((AV,gR)=>{gR.exports={mapHttpResponse:cj,resSerializer:mR};var th=Symbol("pino-raw-res-ref"),pR=Object.create({},{statusCode:{enumerable:!0,writable:!0,value:0},headers:{enumerable:!0,writable:!0,value:""},raw:{enumerable:!1,get:function(){return this[th]},set:function(t){this[th]=t;}}});Object.defineProperty(pR,th,{writable:!0,value:{}});function mR(t){let e=Object.create(pR);return e.statusCode=t.headersSent?t.statusCode:null,e.headers=t.getHeaders?t.getHeaders():t._headers,e.raw=t,e}function cj(t){return {res:mR(t)}}});var nh=A((PV,yR)=>{var rh=aR(),lj=cR(),fu=hR(),du=_R();yR.exports={err:rh,errWithCause:lj,mapHttpRequest:fu.mapHttpRequest,mapHttpResponse:du.mapHttpResponse,req:fu.reqSerializer,res:du.resSerializer,wrapErrorSerializer:function(e){return e===rh?e:function(n){return e(rh(n))}},wrapRequestSerializer:function(e){return e===fu.reqSerializer?e:function(n){return e(fu.reqSerializer(n))}},wrapResponseSerializer:function(e){return e===du.resSerializer?e:function(n){return e(du.resSerializer(n))}}};});var ih=A((DV,bR)=>{function fj(t,e){return e}bR.exports=function(){let e=Error.prepareStackTrace;Error.prepareStackTrace=fj;let r=new Error().stack;if(Error.prepareStackTrace=e,!Array.isArray(r))return;let n=r.slice(2),i=[];for(let s of n)s&&i.push(s.getFileName());return i};});var wR=A((OV,vR)=>{vR.exports=dj;function dj(t={}){let{ERR_PATHS_MUST_BE_STRINGS:e=()=>"fast-redact - Paths must be (non-empty) strings",ERR_INVALID_PATH:r=n=>`fast-redact \u2013 Invalid path (${n})`}=t;return function({paths:i}){i.forEach(s=>{if(typeof s!="string")throw Error(e());try{if(/〇/.test(s))throw Error();let o=(s[0]==="["?"":".")+s.replace(/^\*/,"\u3007").replace(/\.\*/g,".\u3007").replace(/\[\*\]/g,"[\u3007]");if(/\n|\r|;/.test(o)||/\/\*/.test(o))throw Error();Function(` - 'use strict' - const o = new Proxy({}, { get: () => o, set: () => { throw Error() } }); - const \u3007 = null; - o${o} - if ([o${o}].length !== 1) throw Error()`)();}catch{throw Error(r(s))}});}}});var hu=A((IV,SR)=>{SR.exports=/[^.[\]]+|\[((?:.)*?)\]/g;});var RR=A((kV,ER)=>{var hj=hu();ER.exports=pj;function pj({paths:t}){let e=[];var r=0;let n=t.reduce(function(i,s,o){var a=s.match(hj).map(c=>c.replace(/'|"|`/g,""));let l=s[0]==="[";a=a.map(c=>c[0]==="["?c.substr(1,c.length-2):c);let d=a.indexOf("*");if(d>-1){let c=a.slice(0,d),p=c.join("."),m=a.slice(d+1,a.length),_=m.length>0;r++,e.push({before:c,beforeStr:p,after:m,nested:_});}else i[s]={path:a,val:void 0,precensored:!1,circle:"",escPath:JSON.stringify(s),leadingBracket:l};return i},{});return {wildcards:e,wcLen:r,secret:n}}});var xR=A((MV,CR)=>{var mj=hu();CR.exports=gj;function gj({secret:t,serialize:e,wcLen:r,strict:n,isCensorFct:i,censorFctTakesPath:s},o){let a=Function("o",` - if (typeof o !== 'object' || o == null) { - ${vj(n,e)} - } - const { censor, secret } = this - ${_j(t,i,s)} - this.compileRestore() - ${yj(r>0,i,s)} - ${bj(e)} - `).bind(o);return e===!1&&(a.restore=l=>o.restore(l)),a}function _j(t,e,r){return Object.keys(t).map(n=>{let{escPath:i,leadingBracket:s,path:o}=t[n],a=s?1:0,l=s?"":".",d=[];for(var c;(c=mj.exec(n))!==null;){let[,w]=c,{index:E,input:P}=c;E>a&&d.push(P.substring(0,E-(w?0:1)));}var p=d.map(w=>`o${l}${w}`).join(" && ");p.length===0?p+=`o${l}${n} != null`:p+=` && o${l}${n} != null`;let m=` - switch (true) { - ${d.reverse().map(w=>` - case o${l}${w} === censor: - secret[${i}].circle = ${JSON.stringify(w)} - break - `).join(` -`)} - } - `,_=r?`val, ${JSON.stringify(o)}`:"val";return ` - if (${p}) { - const val = o${l}${n} - if (val === censor) { - secret[${i}].precensored = true - } else { - secret[${i}].val = val - o${l}${n} = ${e?`censor(${_})`:"censor"} - ${m} - } - } - `}).join(` -`)}function yj(t,e,r){return t===!0?` - { - const { wildcards, wcLen, groupRedact, nestedRedact } = this - for (var i = 0; i < wcLen; i++) { - const { before, beforeStr, after, nested } = wildcards[i] - if (nested === true) { - secret[beforeStr] = secret[beforeStr] || [] - nestedRedact(secret[beforeStr], o, before, after, censor, ${e}, ${r}) - } else secret[beforeStr] = groupRedact(o, before, censor, ${e}, ${r}) - } - } - `:""}function bj(t){return t===!1?"return o":` - var s = this.serialize(o) - this.restore(o) - return s - `}function vj(t,e){return t===!0?"throw Error('fast-redact: primitives cannot be redacted')":e===!1?"return o":"return this.serialize(o)"}});var oh=A((FV,PR)=>{PR.exports={groupRedact:Sj,groupRestore:wj,nestedRedact:Rj,nestedRestore:Ej};function wj({keys:t,values:e,target:r}){if(r==null)return;let n=t.length;for(var i=0;i0;o--)s=s[n[o]];s[n[0]]=i;}}function Rj(t,e,r,n,i,s,o){let a=TR(e,r);if(a==null)return;let l=Object.keys(a),d=l.length;for(var c=0;c{var{groupRestore:Tj,nestedRestore:Aj}=oh();DR.exports=Pj;function Pj({secret:t,wcLen:e}){return function(){if(this.restore)return;let n=Object.keys(t),i=Dj(t,n),s=e>0,o=s?{secret:t,groupRestore:Tj,nestedRestore:Aj}:{secret:t};this.restore=Function("o",Oj(i,n,s)).bind(o);}}function Dj(t,e){return e.map(r=>{let{circle:n,escPath:i,leadingBracket:s}=t[r],a=n?`o.${n} = secret[${i}].val`:`o${s?"":"."}${r} = secret[${i}].val`,l=`secret[${i}].val = undefined`;return ` - if (secret[${i}].val !== undefined) { - try { ${a} } catch (e) {} - ${l} - } - `}).join("")}function Oj(t,e,r){return ` - const secret = this.secret - ${r===!0?` - const keys = Object.keys(secret) - const len = keys.length - for (var i = len - 1; i >= ${e.length}; i--) { - const k = keys[i] - const o = secret[k] - if (o.flat === true) this.groupRestore(o) - else this.nestedRestore(o) - secret[k] = null - } - `:""} - ${t} - return o - `}});var kR=A((qV,IR)=>{IR.exports=Ij;function Ij(t){let{secret:e,censor:r,compileRestore:n,serialize:i,groupRedact:s,nestedRedact:o,wildcards:a,wcLen:l}=t,d=[{secret:e,censor:r,compileRestore:n}];return i!==!1&&d.push({serialize:i}),l>0&&d.push({groupRedact:s,nestedRedact:o,wildcards:a,wcLen:l}),Object.assign(...d)}});var NR=A((LV,FR)=>{var MR=wR(),kj=RR(),Mj=xR(),Fj=OR(),{groupRedact:Nj,nestedRedact:qj}=oh(),Lj=kR(),$j=hu(),jj=MR(),ah=t=>t;ah.restore=ah;var Hj="[REDACTED]";uh.rx=$j;uh.validator=MR;FR.exports=uh;function uh(t={}){let e=Array.from(new Set(t.paths||[])),r="serialize"in t&&(t.serialize===!1||typeof t.serialize=="function")?t.serialize:JSON.stringify,n=t.remove;if(n===!0&&r!==JSON.stringify)throw Error("fast-redact \u2013 remove option may only be set when serializer is JSON.stringify");let i=n===!0?void 0:"censor"in t?t.censor:Hj,s=typeof i=="function",o=s&&i.length>1;if(e.length===0)return r||ah;jj({paths:e,serialize:r,censor:i});let{wildcards:a,wcLen:l,secret:d}=kj({paths:e,censor:i}),c=Fj({secret:d,wcLen:l}),p="strict"in t?t.strict:!0;return Mj({secret:d,wcLen:l,serialize:r,strict:p,isCensorFct:s,censorFctTakesPath:o},Lj({secret:d,censor:i,compileRestore:c,serialize:r,groupRedact:Nj,nestedRedact:qj,wildcards:a,wcLen:l}))}});var Ui=A(($V,qR)=>{var Wj=Symbol("pino.setLevel"),Bj=Symbol("pino.getLevel"),Uj=Symbol("pino.levelVal"),zj=Symbol("pino.useLevelLabels"),Vj=Symbol("pino.useOnlyCustomLevels"),Gj=Symbol("pino.mixin"),Kj=Symbol("pino.lsCache"),Zj=Symbol("pino.chindings"),Jj=Symbol("pino.asJson"),Yj=Symbol("pino.write"),Xj=Symbol("pino.redactFmt"),Qj=Symbol("pino.time"),eH=Symbol("pino.timeSliceIndex"),tH=Symbol("pino.stream"),rH=Symbol("pino.stringify"),nH=Symbol("pino.stringifySafe"),iH=Symbol("pino.stringifiers"),sH=Symbol("pino.end"),oH=Symbol("pino.formatOpts"),aH=Symbol("pino.messageKey"),uH=Symbol("pino.errorKey"),cH=Symbol("pino.nestedKey"),lH=Symbol("pino.nestedKeyStr"),fH=Symbol("pino.mixinMergeStrategy"),dH=Symbol("pino.msgPrefix"),hH=Symbol("pino.wildcardFirst"),pH=Symbol.for("pino.serializers"),mH=Symbol.for("pino.formatters"),gH=Symbol.for("pino.hooks"),_H=Symbol.for("pino.metadata");qR.exports={setLevelSym:Wj,getLevelSym:Bj,levelValSym:Uj,useLevelLabelsSym:zj,mixinSym:Gj,lsCacheSym:Kj,chindingsSym:Zj,asJsonSym:Jj,writeSym:Yj,serializersSym:pH,redactFmtSym:Xj,timeSym:Qj,timeSliceIndexSym:eH,streamSym:tH,stringifySym:rH,stringifySafeSym:nH,stringifiersSym:iH,endSym:sH,formatOptsSym:oH,messageKeySym:aH,errorKeySym:uH,nestedKeySym:cH,wildcardFirstSym:hH,needsMetadataGsym:_H,useOnlyCustomLevelsSym:Vj,formattersSym:mH,hooksSym:gH,nestedKeyStrSym:lH,mixinMergeStrategySym:fH,msgPrefixSym:dH};});var fh=A((jV,HR)=>{var lh=NR(),{redactFmtSym:yH,wildcardFirstSym:pu}=Ui(),{rx:ch,validator:bH}=lh,LR=bH({ERR_PATHS_MUST_BE_STRINGS:()=>"pino \u2013 redacted paths must be strings",ERR_INVALID_PATH:t=>`pino \u2013 redact paths array contains an invalid path (${t})`}),$R="[Redacted]",jR=!1;function vH(t,e){let{paths:r,censor:n}=wH(t),i=r.reduce((a,l)=>{ch.lastIndex=0;let d=ch.exec(l),c=ch.exec(l),p=d[1]!==void 0?d[1].replace(/^(?:"|'|`)(.*)(?:"|'|`)$/,"$1"):d[0];if(p==="*"&&(p=pu),c===null)return a[p]=null,a;if(a[p]===null)return a;let{index:m}=c,_=`${l.substr(m,l.length-1)}`;return a[p]=a[p]||[],p!==pu&&a[p].length===0&&a[p].push(...a[pu]||[]),p===pu&&Object.keys(a).forEach(function(w){a[w]&&a[w].push(_);}),a[p].push(_),a},{}),s={[yH]:lh({paths:r,censor:n,serialize:e,strict:jR})},o=(...a)=>e(typeof n=="function"?n(...a):n);return [...Object.keys(i),...Object.getOwnPropertySymbols(i)].reduce((a,l)=>{if(i[l]===null)a[l]=d=>o(d,[l]);else {let d=typeof n=="function"?(c,p)=>n(c,[l,...p]):n;a[l]=lh({paths:i[l],censor:d,serialize:e,strict:jR});}return a},s)}function wH(t){if(Array.isArray(t))return t={paths:t,censor:$R},LR(t),t;let{paths:e,censor:r=$R,remove:n}=t;if(Array.isArray(e)===!1)throw Error("pino \u2013 redact must contain an array of strings");return n===!0&&(r=void 0),LR({paths:e,censor:r}),{paths:e,censor:r}}HR.exports=vH;});var BR=A((HV,WR)=>{var SH=()=>"",EH=()=>`,"time":${Date.now()}`,RH=()=>`,"time":${Math.round(Date.now()/1e3)}`,CH=()=>`,"time":"${new Date(Date.now()).toISOString()}"`;WR.exports={nullTime:SH,epochTime:EH,unixTime:RH,isoTime:CH};});var zR=A((WV,UR)=>{function xH(t){try{return JSON.stringify(t)}catch{return '"[Circular]"'}}UR.exports=TH;function TH(t,e,r){var n=r&&r.stringify||xH,i=1;if(typeof t=="object"&&t!==null){var s=e.length+i;if(s===1)return t;var o=new Array(s);o[0]=n(t);for(var a=1;a-1?p:0,t.charCodeAt(_+1)){case 100:case 102:if(c>=l||e[c]==null)break;p<_&&(d+=t.slice(p,_)),d+=Number(e[c]),p=_+2,_++;break;case 105:if(c>=l||e[c]==null)break;p<_&&(d+=t.slice(p,_)),d+=Math.floor(Number(e[c])),p=_+2,_++;break;case 79:case 111:case 106:if(c>=l||e[c]===void 0)break;p<_&&(d+=t.slice(p,_));var w=typeof e[c];if(w==="string"){d+="'"+e[c]+"'",p=_+2,_++;break}if(w==="function"){d+=e[c].name||"",p=_+2,_++;break}d+=n(e[c]),p=_+2,_++;break;case 115:if(c>=l)break;p<_&&(d+=t.slice(p,_)),d+=String(e[c]),p=_+2,_++;break;case 37:p<_&&(d+=t.slice(p,_)),d+="%",p=_+2,_++,c--;break}++c;}++_;}return p===-1?t:(p{if(typeof SharedArrayBuffer<"u"&&typeof Atomics<"u"){let e=function(r){if((r>0&&r<1/0)===!1)throw typeof r!="number"&&typeof r!="bigint"?TypeError("sleep: ms must be a number"):RangeError("sleep: ms must be a number that is greater than 0 but less than Infinity");Atomics.wait(t,0,0,Number(r));},t=new Int32Array(new SharedArrayBuffer(4));dh.exports=e;}else {let t=function(e){if((e>0&&e<1/0)===!1)throw typeof e!="number"&&typeof e!="bigint"?TypeError("sleep: ms must be a number"):RangeError("sleep: ms must be a number that is greater than 0 but less than Infinity");};dh.exports=t;}});var QR=A((UV,XR)=>{var ft=oe("fs"),AH=oe("events"),PH=oe("util").inherits,VR=oe("path"),ph=hh(),mu=100,gu=Buffer.allocUnsafe(0),DH=16*1024,GR="buffer",KR="utf8";function ZR(t,e){e._opening=!0,e._writing=!0,e._asyncDrainScheduled=!1;function r(s,o){if(s){e._reopening=!1,e._writing=!1,e._opening=!1,e.sync?process.nextTick(()=>{e.listenerCount("error")>0&&e.emit("error",s);}):e.emit("error",s);return}e.fd=o,e.file=t,e._reopening=!1,e._opening=!1,e._writing=!1,e.sync?process.nextTick(()=>e.emit("ready")):e.emit("ready"),!(e._reopening||e.destroyed)&&(!e._writing&&e._len>e.minLength||e._flushPending)&&e._actualWrite();}let n=e.append?"a":"w",i=e.mode;if(e.sync)try{e.mkdir&&ft.mkdirSync(VR.dirname(t),{recursive:!0});let s=ft.openSync(t,n,i);r(null,s);}catch(s){throw r(s),s}else e.mkdir?ft.mkdir(VR.dirname(t),{recursive:!0},s=>{if(s)return r(s);ft.open(t,n,i,r);}):ft.open(t,n,i,r);}function Ir(t){if(!(this instanceof Ir))return new Ir(t);let{fd:e,dest:r,minLength:n,maxLength:i,maxWrite:s,sync:o,append:a=!0,mkdir:l,retryEAGAIN:d,fsync:c,contentMode:p,mode:m}=t||{};e=e||r,this._len=0,this.fd=-1,this._bufs=[],this._lens=[],this._writing=!1,this._ending=!1,this._reopening=!1,this._asyncDrainScheduled=!1,this._flushPending=!1,this._hwm=Math.max(n||0,16387),this.file=null,this.destroyed=!1,this.minLength=n||0,this.maxLength=i||0,this.maxWrite=s||DH,this.sync=o||!1,this.writable=!0,this._fsync=c||!1,this.append=a||!1,this.mode=m,this.retryEAGAIN=d||(()=>!0),this.mkdir=l||!1;let _,w;if(p===GR)this._writingBuf=gu,this.write=kH,this.flush=FH,this.flushSync=qH,this._actualWrite=$H,_=()=>ft.writeSync(this.fd,this._writingBuf),w=()=>ft.write(this.fd,this._writingBuf,this.release);else if(p===void 0||p===KR)this._writingBuf="",this.write=IH,this.flush=MH,this.flushSync=NH,this._actualWrite=LH,_=()=>ft.writeSync(this.fd,this._writingBuf,"utf8"),w=()=>ft.write(this.fd,this._writingBuf,"utf8",this.release);else throw new Error(`SonicBoom supports "${KR}" and "${GR}", but passed ${p}`);if(typeof e=="number")this.fd=e,process.nextTick(()=>this.emit("ready"));else if(typeof e=="string")ZR(e,this);else throw new Error("SonicBoom supports only file descriptors and files");if(this.minLength>=this.maxWrite)throw new Error(`minLength should be smaller than maxWrite (${this.maxWrite})`);this.release=(E,P)=>{if(E){if((E.code==="EAGAIN"||E.code==="EBUSY")&&this.retryEAGAIN(E,this._writingBuf.length,this._len-this._writingBuf.length))if(this.sync)try{ph(mu),this.release(void 0,0);}catch(g){this.release(g);}else setTimeout(w,mu);else this._writing=!1,this.emit("error",E);return}if(this.emit("write",P),this._len-=P,this._len<0&&(this._len=0),this._writingBuf=this._writingBuf.slice(P),this._writingBuf.length){if(!this.sync){w();return}try{do{let g=_();this._len-=g,this._writingBuf=this._writingBuf.slice(g);}while(this._writingBuf.length)}catch(g){this.release(g);return}}this._fsync&&ft.fsyncSync(this.fd);let D=this._len;this._reopening?(this._writing=!1,this._reopening=!1,this.reopen()):D>this.minLength?this._actualWrite():this._ending?D>0?this._actualWrite():(this._writing=!1,_u(this)):(this._writing=!1,this.sync?this._asyncDrainScheduled||(this._asyncDrainScheduled=!0,process.nextTick(OH,this)):this.emit("drain"));},this.on("newListener",function(E){E==="drain"&&(this._asyncDrainScheduled=!1);});}function OH(t){t.listenerCount("drain")>0&&(t._asyncDrainScheduled=!1,t.emit("drain"));}PH(Ir,AH);function JR(t,e){return t.length===0?gu:t.length===1?t[0]:Buffer.concat(t,e)}function IH(t){if(this.destroyed)throw new Error("SonicBoom destroyed");let e=this._len+t.length,r=this._bufs;return this.maxLength&&e>this.maxLength?(this.emit("drop",t),this._lenthis.maxWrite?r.push(""+t):r[r.length-1]+=t,this._len=e,!this._writing&&this._len>=this.minLength&&this._actualWrite(),this._lenthis.maxLength?(this.emit("drop",t),this._lenthis.maxWrite?(r.push([t]),n.push(t.length)):(r[r.length-1].push(t),n[n.length-1]+=t.length),this._len=e,!this._writing&&this._len>=this.minLength&&this._actualWrite(),this._len{this._fsync?(this._flushPending=!1,t()):ft.fsync(this.fd,n=>{this._flushPending=!1,t(n);}),this.off("error",r);},r=n=>{this._flushPending=!1,t(n),this.off("drain",e);};this.once("drain",e),this.once("error",r);}function MH(t){if(t!=null&&typeof t!="function")throw new Error("flush cb must be a function");if(this.destroyed){let e=new Error("SonicBoom destroyed");if(t){t(e);return}throw e}if(this.minLength<=0){t?.();return}t&&YR.call(this,t),!this._writing&&(this._bufs.length===0&&this._bufs.push(""),this._actualWrite());}function FH(t){if(t!=null&&typeof t!="function")throw new Error("flush cb must be a function");if(this.destroyed){let e=new Error("SonicBoom destroyed");if(t){t(e);return}throw e}if(this.minLength<=0){t?.();return}t&&YR.call(this,t),!this._writing&&(this._bufs.length===0&&(this._bufs.push([]),this._lens.push(0)),this._actualWrite());}Ir.prototype.reopen=function(t){if(this.destroyed)throw new Error("SonicBoom destroyed");if(this._opening){this.once("ready",()=>{this.reopen(t);});return}if(this._ending)return;if(!this.file)throw new Error("Unable to reopen a file descriptor, you must pass a file to SonicBoom");if(this._reopening=!0,this._writing)return;let e=this.fd;this.once("ready",()=>{e!==this.fd&&ft.close(e,r=>{if(r)return this.emit("error",r)});}),ZR(t||this.file,this);};Ir.prototype.end=function(){if(this.destroyed)throw new Error("SonicBoom destroyed");if(this._opening){this.once("ready",()=>{this.end();});return}this._ending||(this._ending=!0,!this._writing&&(this._len>0&&this.fd>=0?this._actualWrite():_u(this)));};function NH(){if(this.destroyed)throw new Error("SonicBoom destroyed");if(this.fd<0)throw new Error("sonic boom is not ready yet");!this._writing&&this._writingBuf.length>0&&(this._bufs.unshift(this._writingBuf),this._writingBuf="");let t="";for(;this._bufs.length||t;){t.length<=0&&(t=this._bufs[0]);try{let e=ft.writeSync(this.fd,t,"utf8");t=t.slice(e),this._len=Math.max(this._len-e,0),t.length<=0&&this._bufs.shift();}catch(e){if((e.code==="EAGAIN"||e.code==="EBUSY")&&!this.retryEAGAIN(e,t.length,this._len-t.length))throw e;ph(mu);}}try{ft.fsyncSync(this.fd);}catch{}}function qH(){if(this.destroyed)throw new Error("SonicBoom destroyed");if(this.fd<0)throw new Error("sonic boom is not ready yet");!this._writing&&this._writingBuf.length>0&&(this._bufs.unshift([this._writingBuf]),this._writingBuf=gu);let t=gu;for(;this._bufs.length||t.length;){t.length<=0&&(t=JR(this._bufs[0],this._lens[0]));try{let e=ft.writeSync(this.fd,t);t=t.subarray(e),this._len=Math.max(this._len-e,0),t.length<=0&&(this._bufs.shift(),this._lens.shift());}catch(e){if((e.code==="EAGAIN"||e.code==="EBUSY")&&!this.retryEAGAIN(e,t.length,this._len-t.length))throw e;ph(mu);}}}Ir.prototype.destroy=function(){this.destroyed||_u(this);};function LH(){let t=this.release;if(this._writing=!0,this._writingBuf=this._writingBuf||this._bufs.shift()||"",this.sync)try{let e=ft.writeSync(this.fd,this._writingBuf,"utf8");t(null,e);}catch(e){t(e);}else ft.write(this.fd,this._writingBuf,"utf8",t);}function $H(){let t=this.release;if(this._writing=!0,this._writingBuf=this._writingBuf.length?this._writingBuf:JR(this._bufs.shift(),this._lens.shift()),this.sync)try{let e=ft.writeSync(this.fd,this._writingBuf);t(null,e);}catch(e){t(e);}else ft.write(this.fd,this._writingBuf,t);}function _u(t){if(t.fd===-1){t.once("ready",_u.bind(null,t));return}t.destroyed=!0,t._bufs=[],t._lens=[],ft.fsync(t.fd,e);function e(){t.fd!==1&&t.fd!==2?ft.close(t.fd,r):r();}function r(n){if(n){t.emit("error",n);return}t._ending&&!t._writing&&t.emit("finish"),t.emit("close");}}Ir.SonicBoom=Ir;Ir.default=Ir;XR.exports=Ir;});var mh=A((zV,iC)=>{var kr={exit:[],beforeExit:[]},eC={exit:WH,beforeExit:BH},zi;function jH(){zi===void 0&&(zi=new FinalizationRegistry(UH));}function HH(t){kr[t].length>0||process.on(t,eC[t]);}function tC(t){kr[t].length>0||(process.removeListener(t,eC[t]),kr.exit.length===0&&kr.beforeExit.length===0&&(zi=void 0));}function WH(){rC("exit");}function BH(){rC("beforeExit");}function rC(t){for(let e of kr[t]){let r=e.deref(),n=e.fn;r!==void 0&&n(r,t);}kr[t]=[];}function UH(t){for(let e of ["exit","beforeExit"]){let r=kr[e].indexOf(t);kr[e].splice(r,r+1),tC(e);}}function nC(t,e,r){if(e===void 0)throw new Error("the object can't be undefined");HH(t);let n=new WeakRef(e);n.fn=r,jH(),zi.register(e,n),kr[t].push(n);}function zH(t,e){nC("exit",t,e);}function VH(t,e){nC("beforeExit",t,e);}function GH(t){if(zi!==void 0){zi.unregister(t);for(let e of ["exit","beforeExit"])kr[e]=kr[e].filter(r=>{let n=r.deref();return n&&n!==t}),tC(e);}}iC.exports={register:zH,registerBeforeExit:VH,unregister:GH};});var sC=A((VV,KH)=>{KH.exports={name:"thread-stream",version:"2.4.1",description:"A streaming way to send data to a Node.js Worker Thread",main:"index.js",types:"index.d.ts",dependencies:{"real-require":"^0.2.0"},devDependencies:{"@types/node":"^20.1.0","@types/tap":"^15.0.0",desm:"^1.3.0",fastbench:"^1.0.1",husky:"^8.0.1","pino-elasticsearch":"^6.0.0","sonic-boom":"^3.0.0",standard:"^17.0.0",tap:"^16.2.0","ts-node":"^10.8.0",typescript:"^4.7.2","why-is-node-running":"^2.2.2"},scripts:{test:"standard && npm run transpile && tap test/*.test.*js && tap --ts test/*.test.*ts","test:ci":"standard && npm run transpile && npm run test:ci:js && npm run test:ci:ts","test:ci:js":'tap --no-check-coverage --coverage-report=lcovonly "test/**/*.test.*js"',"test:ci:ts":'tap --ts --no-check-coverage --coverage-report=lcovonly "test/**/*.test.*ts"',"test:yarn":'npm run transpile && tap "test/**/*.test.js" --no-check-coverage',transpile:"sh ./test/ts/transpile.sh",prepare:"husky install"},standard:{ignore:["test/ts/**/*"]},repository:{type:"git",url:"git+https://github.com/mcollina/thread-stream.git"},keywords:["worker","thread","threads","stream"],author:"Matteo Collina ",license:"MIT",bugs:{url:"https://github.com/mcollina/thread-stream/issues"},homepage:"https://github.com/mcollina/thread-stream#readme"};});var aC=A((GV,oC)=>{function ZH(t,e,r,n,i){let s=Date.now()+n,o=Atomics.load(t,e);if(o===r){i(null,"ok");return}let a=o,l=d=>{Date.now()>s?i(null,"timed-out"):setTimeout(()=>{a=o,o=Atomics.load(t,e),o===a?l(d>=1e3?1e3:d*2):o===r?i(null,"ok"):i(null,"not-equal");},d);};l(1);}function JH(t,e,r,n,i){let s=Date.now()+n,o=Atomics.load(t,e);if(o!==r){i(null,"ok");return}let a=l=>{Date.now()>s?i(null,"timed-out"):setTimeout(()=>{o=Atomics.load(t,e),o!==r?i(null,"ok"):a(l>=1e3?1e3:l*2);},l);};a(1);}oC.exports={wait:ZH,waitDiff:JH};});var cC=A((KV,uC)=>{uC.exports={WRITE_INDEX:4,READ_INDEX:8};});var pC=A((ZV,hC)=>{var{version:YH}=sC(),{EventEmitter:XH}=oe("events"),{Worker:QH}=oe("worker_threads"),{join:eW}=oe("path"),{pathToFileURL:tW}=oe("url"),{wait:rW}=aC(),{WRITE_INDEX:Kt,READ_INDEX:Ur}=cC(),nW=oe("buffer"),iW=oe("assert"),Y=Symbol("kImpl"),sW=nW.constants.MAX_STRING_LENGTH,uo=class{constructor(e){this._value=e;}deref(){return this._value}},bu=class{register(){}unregister(){}},oW=process.env.NODE_V8_COVERAGE?bu:global.FinalizationRegistry||bu,aW=process.env.NODE_V8_COVERAGE?uo:global.WeakRef||uo,lC=new oW(t=>{t.exited||t.terminate();});function uW(t,e){let{filename:r,workerData:n}=e,s=("__bundlerPathsOverrides"in globalThis?globalThis.__bundlerPathsOverrides:{})["thread-stream-worker"]||eW(__dirname,"lib","worker.js"),o=new QH(s,{...e.workerOpts,trackUnmanagedFds:!1,workerData:{filename:r.indexOf("file://")===0?r:tW(r).href,dataBuf:t[Y].dataBuf,stateBuf:t[Y].stateBuf,workerData:{$context:{threadStreamVersion:YH},...n}}});return o.stream=new uo(t),o.on("message",cW),o.on("exit",dC),lC.register(t,o),o}function fC(t){iW(!t[Y].sync),t[Y].needDrain&&(t[Y].needDrain=!1,t.emit("drain"));}function yu(t){let e=Atomics.load(t[Y].state,Kt),r=t[Y].data.length-e;if(r>0){if(t[Y].buf.length===0){t[Y].flushing=!1,t[Y].ending?vh(t):t[Y].needDrain&&process.nextTick(fC,t);return}let n=t[Y].buf.slice(0,r),i=Buffer.byteLength(n);i<=r?(t[Y].buf=t[Y].buf.slice(r),vu(t,n,yu.bind(null,t))):t.flush(()=>{if(!t.destroyed){for(Atomics.store(t[Y].state,Ur,0),Atomics.store(t[Y].state,Kt,0);i>t[Y].data.length;)r=r/2,n=t[Y].buf.slice(0,r),i=Buffer.byteLength(n);t[Y].buf=t[Y].buf.slice(r),vu(t,n,yu.bind(null,t));}});}else if(r===0){if(e===0&&t[Y].buf.length===0)return;t.flush(()=>{Atomics.store(t[Y].state,Ur,0),Atomics.store(t[Y].state,Kt,0),yu(t);});}else zr(t,new Error("overwritten"));}function cW(t){let e=this.stream.deref();if(e===void 0){this.exited=!0,this.terminate();return}switch(t.code){case"READY":this.stream=new aW(e),e.flush(()=>{e[Y].ready=!0,e.emit("ready");});break;case"ERROR":zr(e,t.err);break;case"EVENT":Array.isArray(t.args)?e.emit(t.name,...t.args):e.emit(t.name,t.args);break;case"WARNING":process.emitWarning(t.err);break;default:zr(e,new Error("this should not happen: "+t.code));}}function dC(t){let e=this.stream.deref();e!==void 0&&(lC.unregister(e),e.worker.exited=!0,e.worker.off("exit",dC),zr(e,t!==0?new Error("the worker thread exited"):null));}var _h=class extends XH{constructor(e={}){if(super(),e.bufferSize<4)throw new Error("bufferSize must at least fit a 4-byte utf-8 char");this[Y]={},this[Y].stateBuf=new SharedArrayBuffer(128),this[Y].state=new Int32Array(this[Y].stateBuf),this[Y].dataBuf=new SharedArrayBuffer(e.bufferSize||4*1024*1024),this[Y].data=Buffer.from(this[Y].dataBuf),this[Y].sync=e.sync||!1,this[Y].ending=!1,this[Y].ended=!1,this[Y].needDrain=!1,this[Y].destroyed=!1,this[Y].flushing=!1,this[Y].ready=!1,this[Y].finished=!1,this[Y].errored=null,this[Y].closed=!1,this[Y].buf="",this.worker=uW(this,e);}write(e){if(this[Y].destroyed)return yh(this,new Error("the worker has exited")),!1;if(this[Y].ending)return yh(this,new Error("the worker is ending")),!1;if(this[Y].flushing&&this[Y].buf.length+e.length>=sW)try{gh(this),this[Y].flushing=!0;}catch(r){return zr(this,r),!1}if(this[Y].buf+=e,this[Y].sync)try{return gh(this),!0}catch(r){return zr(this,r),!1}return this[Y].flushing||(this[Y].flushing=!0,setImmediate(yu,this)),this[Y].needDrain=this[Y].data.length-this[Y].buf.length-Atomics.load(this[Y].state,Kt)<=0,!this[Y].needDrain}end(){this[Y].destroyed||(this[Y].ending=!0,vh(this));}flush(e){if(this[Y].destroyed){typeof e=="function"&&process.nextTick(e,new Error("the worker has exited"));return}let r=Atomics.load(this[Y].state,Kt);rW(this[Y].state,Ur,r,1/0,(n,i)=>{if(n){zr(this,n),process.nextTick(e,n);return}if(i==="not-equal"){this.flush(e);return}process.nextTick(e);});}flushSync(){this[Y].destroyed||(gh(this),bh(this));}unref(){this.worker.unref();}ref(){this.worker.ref();}get ready(){return this[Y].ready}get destroyed(){return this[Y].destroyed}get closed(){return this[Y].closed}get writable(){return !this[Y].destroyed&&!this[Y].ending}get writableEnded(){return this[Y].ending}get writableFinished(){return this[Y].finished}get writableNeedDrain(){return this[Y].needDrain}get writableObjectMode(){return !1}get writableErrored(){return this[Y].errored}};function yh(t,e){setImmediate(()=>{t.emit("error",e);});}function zr(t,e){t[Y].destroyed||(t[Y].destroyed=!0,e&&(t[Y].errored=e,yh(t,e)),t.worker.exited?setImmediate(()=>{t[Y].closed=!0,t.emit("close");}):t.worker.terminate().catch(()=>{}).then(()=>{t[Y].closed=!0,t.emit("close");}));}function vu(t,e,r){let n=Atomics.load(t[Y].state,Kt),i=Buffer.byteLength(e);return t[Y].data.write(e,n),Atomics.store(t[Y].state,Kt,n+i),Atomics.notify(t[Y].state,Kt),r(),!0}function vh(t){if(!(t[Y].ended||!t[Y].ending||t[Y].flushing)){t[Y].ended=!0;try{t.flushSync();let e=Atomics.load(t[Y].state,Ur);Atomics.store(t[Y].state,Kt,-1),Atomics.notify(t[Y].state,Kt);let r=0;for(;e!==-1;){if(Atomics.wait(t[Y].state,Ur,e,1e3),e=Atomics.load(t[Y].state,Ur),e===-2){zr(t,new Error("end() failed"));return}if(++r===10){zr(t,new Error("end() took too long (10s)"));return}}process.nextTick(()=>{t[Y].finished=!0,t.emit("finish");});}catch(e){zr(t,e);}}}function gh(t){let e=()=>{t[Y].ending?vh(t):t[Y].needDrain&&process.nextTick(fC,t);};for(t[Y].flushing=!1;t[Y].buf.length!==0;){let r=Atomics.load(t[Y].state,Kt),n=t[Y].data.length-r;if(n===0){bh(t),Atomics.store(t[Y].state,Ur,0),Atomics.store(t[Y].state,Kt,0);continue}else if(n<0)throw new Error("overwritten");let i=t[Y].buf.slice(0,n),s=Buffer.byteLength(i);if(s<=n)t[Y].buf=t[Y].buf.slice(n),vu(t,i,e);else {for(bh(t),Atomics.store(t[Y].state,Ur,0),Atomics.store(t[Y].state,Kt,0);s>t[Y].buf.length;)n=n/2,i=t[Y].buf.slice(0,n),s=Buffer.byteLength(i);t[Y].buf=t[Y].buf.slice(n),vu(t,i,e);}}}function bh(t){if(t[Y].flushing)throw new Error("unable to flush while flushing");let e=Atomics.load(t[Y].state,Kt),r=0;for(;;){let n=Atomics.load(t[Y].state,Ur);if(n===-2)throw Error("_flushSync failed");if(n!==e)Atomics.wait(t[Y].state,Ur,n,1e3);else break;if(++r===10)throw new Error("_flushSync took too long (10s)")}}hC.exports=_h;});var Eh=A((JV,mC)=>{var{createRequire:lW}=oe("module"),fW=ih(),{join:wh,isAbsolute:dW,sep:hW}=oe("path"),pW=hh(),Sh=mh(),mW=pC();function gW(t){Sh.register(t,yW),Sh.registerBeforeExit(t,bW),t.on("close",function(){Sh.unregister(t);});}function _W(t,e,r){let n=new mW({filename:t,workerData:e,workerOpts:r});n.on("ready",i),n.on("close",function(){process.removeListener("exit",s);}),process.on("exit",s);function i(){process.removeListener("exit",s),n.unref(),r.autoEnd!==!1&&gW(n);}function s(){n.closed||(n.flushSync(),pW(100),n.end());}return n}function yW(t){t.ref(),t.flushSync(),t.end(),t.once("close",function(){t.unref();});}function bW(t){t.flushSync();}function vW(t){let{pipeline:e,targets:r,levels:n,dedupe:i,options:s={},worker:o={},caller:a=fW()}=t,l=typeof a=="string"?[a]:a,d="__bundlerPathsOverrides"in globalThis?globalThis.__bundlerPathsOverrides:{},c=t.target;if(c&&r)throw new Error("only one of target or targets can be specified");return r?(c=d["pino-worker"]||wh(__dirname,"worker.js"),s.targets=r.map(m=>({...m,target:p(m.target)}))):e&&(c=d["pino-pipeline-worker"]||wh(__dirname,"worker-pipeline.js"),s.targets=e.map(m=>({...m,target:p(m.target)}))),n&&(s.levels=n),i&&(s.dedupe=i),_W(p(c),s,o);function p(m){if(m=d[m]||m,dW(m)||m.indexOf("file://")===0)return m;if(m==="pino/file")return wh(__dirname,"..","file.js");let _;for(let w of l)try{let E=w==="node:repl"?process.cwd()+hW:w;_=lW(E).resolve(m);break}catch{continue}if(!_)throw new Error(`unable to determine transport target for "${m}"`);return _}}mC.exports=vW;});var Eu=A((YV,xC)=>{var gC=zR(),{mapHttpRequest:wW,mapHttpResponse:SW}=nh(),Ch=QR(),_C=mh(),{lsCacheSym:EW,chindingsSym:vC,writeSym:yC,serializersSym:wC,formatOptsSym:bC,endSym:RW,stringifiersSym:SC,stringifySym:EC,stringifySafeSym:xh,wildcardFirstSym:RC,nestedKeySym:CW,formattersSym:CC,messageKeySym:xW,errorKeySym:TW,nestedKeyStrSym:AW,msgPrefixSym:wu}=Ui(),{isMainThread:PW}=oe("worker_threads"),DW=Eh();function Vi(){}function OW(t,e){if(!e)return r;return function(...i){e.call(this,i,r,t);};function r(n,...i){if(typeof n=="object"){let s=n;n!==null&&(n.method&&n.headers&&n.socket?n=wW(n):typeof n.setHeader=="function"&&(n=SW(n)));let o;s===null&&i.length===0?o=[null]:(s=i.shift(),o=i),typeof this[wu]=="string"&&s!==void 0&&s!==null&&(s=this[wu]+s),this[yC](n,gC(s,o,this[bC]),t);}else {let s=n===void 0?i.shift():n;typeof this[wu]=="string"&&s!==void 0&&s!==null&&(s=this[wu]+s),this[yC](null,gC(s,i,this[bC]),t);}}}function Rh(t){let e="",r=0,n=!1,i=255,s=t.length;if(s>100)return JSON.stringify(t);for(var o=0;o=32;o++)i=t.charCodeAt(o),(i===34||i===92)&&(e+=t.slice(r,o)+"\\",r=o,n=!0);return n?e+=t.slice(r):e=t,i<32?JSON.stringify(t):'"'+e+'"'}function IW(t,e,r,n){let i=this[EC],s=this[xh],o=this[SC],a=this[RW],l=this[vC],d=this[wC],c=this[CC],p=this[xW],m=this[TW],_=this[EW][r]+n;_=_+l;let w;c.log&&(t=c.log(t));let E=o[RC],P="";for(let g in t)if(w=t[g],Object.prototype.hasOwnProperty.call(t,g)&&w!==void 0){d[g]?w=d[g](w):g===m&&d.err&&(w=d.err(w));let S=o[g]||E;switch(typeof w){case"undefined":case"function":continue;case"number":Number.isFinite(w)===!1&&(w=null);case"boolean":S&&(w=S(w));break;case"string":w=(S||Rh)(w);break;default:w=(S||i)(w,s);}if(w===void 0)continue;let I=Rh(g);P+=","+I+":"+w;}let D="";if(e!==void 0){w=d[p]?d[p](e):e;let g=o[p]||E;switch(typeof w){case"function":break;case"number":Number.isFinite(w)===!1&&(w=null);case"boolean":g&&(w=g(w)),D=',"'+p+'":'+w;break;case"string":w=(g||Rh)(w),D=',"'+p+'":'+w;break;default:w=(g||i)(w,s),D=',"'+p+'":'+w;}}return this[CW]&&P?_+this[AW]+P.slice(1)+"}"+D+a:_+P+D+a}function kW(t,e){let r,n=t[vC],i=t[EC],s=t[xh],o=t[SC],a=o[RC],l=t[wC],d=t[CC].bindings;e=d(e);for(let c in e)if(r=e[c],(c!=="level"&&c!=="serializers"&&c!=="formatters"&&c!=="customLevels"&&e.hasOwnProperty(c)&&r!==void 0)===!0){if(r=l[c]?l[c](r):r,r=(o[c]||a||i)(r,s),r===void 0)continue;n+=',"'+c+'":'+r;}return n}function MW(t){return t.write!==t.constructor.prototype.write}var FW=process.env.NODE_V8_COVERAGE||process.env.V8_COVERAGE;function Su(t){let e=new Ch(t);return e.on("error",r),!FW&&!t.sync&&PW&&(_C.register(e,NW),e.on("close",function(){_C.unregister(e);})),e;function r(n){if(n.code==="EPIPE"){e.write=Vi,e.end=Vi,e.flushSync=Vi,e.destroy=Vi;return}e.removeListener("error",r),e.emit("error",n);}}function NW(t,e){t.destroyed||(e==="beforeExit"?(t.flush(),t.on("drain",function(){t.end();})):t.flushSync());}function qW(t){return function(r,n,i={},s){if(typeof i=="string")s=Su({dest:i}),i={};else if(typeof s=="string"){if(i&&i.transport)throw Error("only one of option.transport or stream can be specified");s=Su({dest:s});}else if(i instanceof Ch||i.writable||i._writableState)s=i,i={};else if(i.transport){if(i.transport instanceof Ch||i.transport.writable||i.transport._writableState)throw Error("option.transport do not allow stream, please pass to option directly. e.g. pino(transport)");if(i.transport.targets&&i.transport.targets.length&&i.formatters&&typeof i.formatters.level=="function")throw Error("option.transport.targets do not allow custom level formatters");let l;i.customLevels&&(l=i.useOnlyCustomLevels?i.customLevels:Object.assign({},i.levels,i.customLevels)),s=DW({caller:n,...i.transport,levels:l});}if(i=Object.assign({},t,i),i.serializers=Object.assign({},t.serializers,i.serializers),i.formatters=Object.assign({},t.formatters,i.formatters),i.prettyPrint)throw new Error("prettyPrint option is no longer supported, see the pino-pretty package (https://github.com/pinojs/pino-pretty)");let{enabled:o,onChild:a}=i;return o===!1&&(i.level="silent"),a||(i.onChild=Vi),s||(MW(process.stdout)?s=process.stdout:s=Su({fd:process.stdout.fd||1})),{opts:i,stream:s}}}function LW(t,e){try{return JSON.stringify(t)}catch{try{return (e||this[xh])(t)}catch{return '"[unable to serialize, circular reference is too complex to analyze]"'}}}function $W(t,e,r){return {level:t,bindings:e,log:r}}function jW(t){let e=Number(t);return typeof t=="string"&&Number.isFinite(e)?e:t===void 0?1:t}xC.exports={noop:Vi,buildSafeSonicBoom:Su,asChindings:kW,asJson:IW,genLog:OW,createArgsNormalizer:qW,stringify:LW,buildFormatters:$W,normalizeDestFileDescriptor:jW};});var Ru=A((XV,AC)=>{var{lsCacheSym:HW,levelValSym:Th,useOnlyCustomLevelsSym:WW,streamSym:BW,formattersSym:UW,hooksSym:zW}=Ui(),{noop:VW,genLog:ni}=Eu(),Mr={trace:10,debug:20,info:30,warn:40,error:50,fatal:60},TC={fatal:t=>{let e=ni(Mr.fatal,t);return function(...r){let n=this[BW];if(e.call(this,...r),typeof n.flushSync=="function")try{n.flushSync();}catch{}}},error:t=>ni(Mr.error,t),warn:t=>ni(Mr.warn,t),info:t=>ni(Mr.info,t),debug:t=>ni(Mr.debug,t),trace:t=>ni(Mr.trace,t)},Ah=Object.keys(Mr).reduce((t,e)=>(t[Mr[e]]=e,t),{}),GW=Object.keys(Ah).reduce((t,e)=>(t[e]='{"level":'+Number(e),t),{});function KW(t){let e=t[UW].level,{labels:r}=t.levels,n={};for(let i in r){let s=e(r[i],Number(i));n[i]=JSON.stringify(s).slice(0,-1);}return t[HW]=n,t}function ZW(t,e){if(e)return !1;switch(t){case"fatal":case"error":case"warn":case"info":case"debug":case"trace":return !0;default:return !1}}function JW(t){let{labels:e,values:r}=this.levels;if(typeof t=="number"){if(e[t]===void 0)throw Error("unknown level value"+t);t=e[t];}if(r[t]===void 0)throw Error("unknown level "+t);let n=this[Th],i=this[Th]=r[t],s=this[WW],o=this[zW].logMethod;for(let a in r){if(i>r[a]){this[a]=VW;continue}this[a]=ZW(a,s)?TC[a](o):ni(r[a],o);}this.emit("level-change",t,i,e[n],n,this);}function YW(t){let{levels:e,levelVal:r}=this;return e&&e.labels?e.labels[r]:""}function XW(t){let{values:e}=this.levels,r=e[t];return r!==void 0&&r>=this[Th]}function QW(t=null,e=!1){let r=t?Object.keys(t).reduce((s,o)=>(s[t[o]]=o,s),{}):null,n=Object.assign(Object.create(Object.prototype,{Infinity:{value:"silent"}}),e?null:Ah,r),i=Object.assign(Object.create(Object.prototype,{silent:{value:1/0}}),e?null:Mr,t);return {labels:n,values:i}}function eB(t,e,r){if(typeof t=="number"){if(![].concat(Object.keys(e||{}).map(s=>e[s]),r?[]:Object.keys(Ah).map(s=>+s),1/0).includes(t))throw Error(`default level:${t} must be included in custom levels`);return}let n=Object.assign(Object.create(Object.prototype,{silent:{value:1/0}}),r?null:Mr,e);if(!(t in n))throw Error(`default level:${t} must be included in custom levels`)}function tB(t,e){let{labels:r,values:n}=t;for(let i in e){if(i in n)throw Error("levels cannot be overridden");if(e[i]in r)throw Error("pre-existing level values cannot be used for new levels")}}AC.exports={initialLsCache:GW,genLsCache:KW,levelMethods:TC,getLevel:YW,setLevel:JW,isLevelEnabled:XW,mappings:QW,levels:Mr,assertNoLevelCollisions:tB,assertDefaultLevelFound:eB};});var Ph=A((QV,PC)=>{PC.exports={version:"8.17.1"};});var jC=A((t8,$C)=>{var{EventEmitter:rB}=oe("events"),{lsCacheSym:nB,levelValSym:iB,setLevelSym:Oh,getLevelSym:DC,chindingsSym:Ih,parsedChindingsSym:sB,mixinSym:oB,asJsonSym:FC,writeSym:aB,mixinMergeStrategySym:uB,timeSym:cB,timeSliceIndexSym:lB,streamSym:NC,serializersSym:ii,formattersSym:Dh,errorKeySym:fB,messageKeySym:dB,useOnlyCustomLevelsSym:hB,needsMetadataGsym:pB,redactFmtSym:mB,stringifySym:gB,formatOptsSym:_B,stringifiersSym:yB,msgPrefixSym:OC}=Ui(),{getLevel:bB,setLevel:vB,isLevelEnabled:wB,mappings:SB,initialLsCache:EB,genLsCache:RB,assertNoLevelCollisions:CB}=Ru(),{asChindings:qC,asJson:xB,buildFormatters:IC,stringify:kC}=Eu(),{version:TB}=Ph(),AB=fh(),PB=class{},LC={constructor:PB,child:DB,bindings:OB,setBindings:IB,flush:NB,isLevelEnabled:wB,version:TB,get level(){return this[DC]()},set level(t){this[Oh](t);},get levelVal(){return this[iB]},set levelVal(t){throw Error("levelVal is read-only")},[nB]:EB,[aB]:MB,[FC]:xB,[DC]:bB,[Oh]:vB};Object.setPrototypeOf(LC,rB.prototype);$C.exports=function(){return Object.create(LC)};var MC=t=>t;function DB(t,e){if(!t)throw Error("missing bindings for child Pino");e=e||{};let r=this[ii],n=this[Dh],i=Object.create(this);if(e.hasOwnProperty("serializers")===!0){i[ii]=Object.create(null);for(let c in r)i[ii][c]=r[c];let l=Object.getOwnPropertySymbols(r);for(var s=0;s{var{hasOwnProperty:Cu}=Object.prototype,oi=Fh();oi.configure=Fh;oi.stringify=oi;oi.default=oi;Nh.stringify=oi;Nh.configure=Fh;UC.exports=oi;var qB=/[\u0000-\u001f\u0022\u005c\ud800-\udfff]|[\ud800-\udbff](?![\udc00-\udfff])|(?:[^\ud800-\udbff]|^)[\udc00-\udfff]/;function xn(t){return t.length<5e3&&!qB.test(t)?`"${t}"`:JSON.stringify(t)}function kh(t){if(t.length>200)return t.sort();for(let e=1;er;)t[n]=t[n-1],n--;t[n]=r;}return t}var LB=Object.getOwnPropertyDescriptor(Object.getPrototypeOf(Object.getPrototypeOf(new Int8Array)),Symbol.toStringTag).get;function Mh(t){return LB.call(t)!==void 0&&t.length!==0}function HC(t,e,r){t.length= 1`)}return r===void 0?1/0:r}function si(t){return t===1?"1 item":`${t} items`}function jB(t){let e=new Set;for(let r of t)(typeof r=="string"||typeof r=="number")&&e.add(String(r));return e}function HB(t){if(Cu.call(t,"strict")){let e=t.strict;if(typeof e!="boolean")throw new TypeError('The "strict" argument must be of type boolean');if(e)return r=>{let n=`Object can not safely be stringified. Received type ${typeof r}`;throw typeof r!="function"&&(n+=` (${r.toString()})`),new Error(n)}}}function Fh(t){t={...t};let e=HB(t);e&&(t.bigint===void 0&&(t.bigint=!1),"circularValue"in t||(t.circularValue=Error));let r=$B(t),n=WC(t,"bigint"),i=WC(t,"deterministic"),s=BC(t,"maximumDepth"),o=BC(t,"maximumBreadth");function a(m,_,w,E,P,D){let g=_[m];switch(typeof g=="object"&&g!==null&&typeof g.toJSON=="function"&&(g=g.toJSON(m)),g=E.call(_,m,g),typeof g){case"string":return xn(g);case"object":{if(g===null)return "null";if(w.indexOf(g)!==-1)return r;let S="",I=",",L=D;if(Array.isArray(g)){if(g.length===0)return "[]";if(so){let ye=g.length-o-1;S+=`${I}"... ${si(ye)} not stringified"`;}return P!==""&&(S+=` -${L}`),w.pop(),`[${S}]`}let Z=Object.keys(g),K=Z.length;if(K===0)return "{}";if(so){let N=K-o;S+=`${B}"...":${U}"${si(N)} not stringified"`,B=I;}return P!==""&&B.length>1&&(S=` -${D}${S} -${L}`),w.pop(),`{${S}}`}case"number":return isFinite(g)?String(g):e?e(g):"null";case"boolean":return g===!0?"true":"false";case"undefined":return;case"bigint":if(n)return String(g);default:return e?e(g):void 0}}function l(m,_,w,E,P,D){switch(typeof _=="object"&&_!==null&&typeof _.toJSON=="function"&&(_=_.toJSON(m)),typeof _){case"string":return xn(_);case"object":{if(_===null)return "null";if(w.indexOf(_)!==-1)return r;let g=D,S="",I=",";if(Array.isArray(_)){if(_.length===0)return "[]";if(so){let Q=_.length-o-1;S+=`${I}"... ${si(Q)} not stringified"`;}return P!==""&&(S+=` -${g}`),w.pop(),`[${S}]`}w.push(_);let L="";P!==""&&(D+=P,I=`, -${D}`,L=" ");let Z="";for(let K of E){let U=l(K,_[K],w,E,P,D);U!==void 0&&(S+=`${Z}${xn(K)}:${L}${U}`,Z=I);}return P!==""&&Z.length>1&&(S=` -${D}${S} -${g}`),w.pop(),`{${S}}`}case"number":return isFinite(_)?String(_):e?e(_):"null";case"boolean":return _===!0?"true":"false";case"undefined":return;case"bigint":if(n)return String(_);default:return e?e(_):void 0}}function d(m,_,w,E,P){switch(typeof _){case"string":return xn(_);case"object":{if(_===null)return "null";if(typeof _.toJSON=="function"){if(_=_.toJSON(m),typeof _!="object")return d(m,_,w,E,P);if(_===null)return "null"}if(w.indexOf(_)!==-1)return r;let D=P;if(Array.isArray(_)){if(_.length===0)return "[]";if(so){let ae=_.length-o-1;U+=`${B}"... ${si(ae)} not stringified"`;}return U+=` -${D}`,w.pop(),`[${U}]`}let g=Object.keys(_),S=g.length;if(S===0)return "{}";if(so){let U=S-o;L+=`${Z}"...": "${si(U)} not stringified"`,Z=I;}return Z!==""&&(L=` -${P}${L} -${D}`),w.pop(),`{${L}}`}case"number":return isFinite(_)?String(_):e?e(_):"null";case"boolean":return _===!0?"true":"false";case"undefined":return;case"bigint":if(n)return String(_);default:return e?e(_):void 0}}function c(m,_,w){switch(typeof _){case"string":return xn(_);case"object":{if(_===null)return "null";if(typeof _.toJSON=="function"){if(_=_.toJSON(m),typeof _!="object")return c(m,_,w);if(_===null)return "null"}if(w.indexOf(_)!==-1)return r;let E="";if(Array.isArray(_)){if(_.length===0)return "[]";if(so){let K=_.length-o-1;E+=`,"... ${si(K)} not stringified"`;}return w.pop(),`[${E}]`}let P=Object.keys(_),D=P.length;if(D===0)return "{}";if(so){let I=D-o;E+=`${g}"...":"${si(I)} not stringified"`;}return w.pop(),`{${E}}`}case"number":return isFinite(_)?String(_):e?e(_):"null";case"boolean":return _===!0?"true":"false";case"undefined":return;case"bigint":if(n)return String(_);default:return e?e(_):void 0}}function p(m,_,w){if(arguments.length>1){let E="";if(typeof w=="number"?E=" ".repeat(Math.min(w,10)):typeof w=="string"&&(E=w.slice(0,10)),_!=null){if(typeof _=="function")return a("",{"":m},[],_,E,"");if(Array.isArray(_))return l("",m,[],jB(_),E,"")}if(E.length!==0)return d("",m,[],E,"")}return c("",m,[])}return p}});var KC=A((r8,GC)=>{var qh=Symbol.for("pino.metadata"),{levels:VC}=Ru(),WB=VC.info;function BB(t,e){let r=0;t=t||[],e=e||{dedupe:!1};let n=Object.create(VC);n.silent=1/0,e.levels&&typeof e.levels=="object"&&Object.keys(e.levels).forEach(c=>{n[c]=e.levels[c];});let i={write:s,add:a,flushSync:o,end:l,minLevel:0,streams:[],clone:d,[qh]:!0,streamLevels:n};return Array.isArray(t)?t.forEach(a,i):a.call(i,t),t=null,i;function s(c){let p,m=this.lastLevel,{streams:_}=this,w=0,E;for(let P=zB(_.length,e.dedupe);GB(P,_.length,e.dedupe);P=VB(P,e.dedupe))if(p=_[P],p.level<=m){if(w!==0&&w!==p.level)break;if(E=p.stream,E[qh]){let{lastTime:D,lastMsg:g,lastObj:S,lastLogger:I}=this;E.lastLevel=m,E.lastTime=D,E.lastMsg=g,E.lastObj=S,E.lastLogger=I;}E.write(c),e.dedupe&&(w=p.level);}else if(!e.dedupe)break}function o(){for(let{stream:c}of this.streams)typeof c.flushSync=="function"&&c.flushSync();}function a(c){if(!c)return i;let p=typeof c.write=="function"||c.stream,m=c.write?c:c.stream;if(!p)throw Error("stream object needs to implement either StreamEntry or DestinationStream interface");let{streams:_,streamLevels:w}=this,E;typeof c.levelVal=="number"?E=c.levelVal:typeof c.level=="string"?E=w[c.level]:typeof c.level=="number"?E=c.level:E=WB;let P={stream:m,level:E,levelVal:void 0,id:r++};return _.unshift(P),_.sort(UB),this.minLevel=_[0].level,i}function l(){for(let{stream:c}of this.streams)typeof c.flushSync=="function"&&c.flushSync(),c.end();}function d(c){let p=new Array(this.streams.length);for(let m=0;m=0:t{var KB=oe("os"),rx=nh(),ZB=ih(),JB=fh(),nx=BR(),YB=jC(),ix=Ui(),{configure:XB}=zC(),{assertDefaultLevelFound:QB,mappings:sx,genLsCache:eU,levels:tU}=Ru(),{createArgsNormalizer:rU,asChindings:nU,buildSafeSonicBoom:ZC,buildFormatters:iU,stringify:Lh,normalizeDestFileDescriptor:JC,noop:sU}=Eu(),{version:oU}=Ph(),{chindingsSym:YC,redactFmtSym:aU,serializersSym:XC,timeSym:uU,timeSliceIndexSym:cU,streamSym:lU,stringifySym:QC,stringifySafeSym:$h,stringifiersSym:ex,setLevelSym:fU,endSym:dU,formatOptsSym:hU,messageKeySym:pU,errorKeySym:mU,nestedKeySym:gU,mixinSym:_U,useOnlyCustomLevelsSym:yU,formattersSym:tx,hooksSym:bU,nestedKeyStrSym:vU,mixinMergeStrategySym:wU,msgPrefixSym:SU}=ix,{epochTime:ox,nullTime:EU}=nx,{pid:RU}=process,CU=KB.hostname(),xU=rx.err,TU={level:"info",levels:tU,messageKey:"msg",errorKey:"err",nestedKey:null,enabled:!0,base:{pid:RU,hostname:CU},serializers:Object.assign(Object.create(null),{err:xU}),formatters:Object.assign(Object.create(null),{bindings(t){return t},level(t,e){return {level:e}}}),hooks:{logMethod:void 0},timestamp:ox,name:void 0,redact:null,customLevels:null,useOnlyCustomLevels:!1,depthLimit:5,edgeLimit:100},AU=rU(TU),PU=Object.assign(Object.create(null),rx);function jh(...t){let e={},{opts:r,stream:n}=AU(e,ZB(),...t),{redact:i,crlf:s,serializers:o,timestamp:a,messageKey:l,errorKey:d,nestedKey:c,base:p,name:m,level:_,customLevels:w,mixin:E,mixinMergeStrategy:P,useOnlyCustomLevels:D,formatters:g,hooks:S,depthLimit:I,edgeLimit:L,onChild:Z,msgPrefix:K}=r,U=XB({maximumDepth:I,maximumBreadth:L}),B=iU(g.level,g.bindings,g.log),Q=Lh.bind({[$h]:U}),N=i?JB(i,Q):{},te=i?{stringify:N[aU]}:{stringify:Q},ae="}"+(s?`\r -`:` -`),ye=nU.bind(null,{[YC]:"",[XC]:o,[ex]:N,[QC]:Lh,[$h]:U,[tx]:B}),q="";p!==null&&(m===void 0?q=ye(p):q=ye(Object.assign({},p,{name:m})));let W=a instanceof Function?a:a?ox:EU,me=W().indexOf(":")+1;if(D&&!w)throw Error("customLevels is required if useOnlyCustomLevels is set true");if(E&&typeof E!="function")throw Error(`Unknown mixin type "${typeof E}" - expected "function"`);if(K&&typeof K!="string")throw Error(`Unknown msgPrefix type "${typeof K}" - expected "string"`);QB(_,w,D);let pe=sx(w,D);return Object.assign(e,{levels:pe,[yU]:D,[lU]:n,[uU]:W,[cU]:me,[QC]:Lh,[$h]:U,[ex]:N,[dU]:ae,[hU]:te,[pU]:l,[mU]:d,[gU]:c,[vU]:c?`,${JSON.stringify(c)}:{`:"",[XC]:o,[_U]:E,[wU]:P,[YC]:q,[tx]:B,[bU]:S,silent:sU,onChild:Z,[SU]:K}),Object.setPrototypeOf(e,YB()),eU(e),e[fU](_),e}yr.exports=jh;yr.exports.destination=(t=process.stdout.fd)=>typeof t=="object"?(t.dest=JC(t.dest||process.stdout.fd),ZC(t)):ZC({dest:JC(t),minLength:0});yr.exports.transport=Eh();yr.exports.multistream=KC();yr.exports.levels=sx();yr.exports.stdSerializers=PU;yr.exports.stdTimeFunctions=Object.assign({},nx);yr.exports.symbols=ix;yr.exports.version=oU;yr.exports.default=jh;yr.exports.pino=jh;});var co=A(Tn=>{Object.defineProperty(Tn,"__esModule",{value:!0});Tn.Frequency=Tn.KeepLogFiles=void 0;(function(t){t[t.days=1]="days",t[t.fileCount=2]="fileCount";})(Tn.KeepLogFiles||(Tn.KeepLogFiles={}));(function(t){t[t.daily=1]="daily",t[t.minutes=2]="minutes",t[t.hours=3]="hours",t[t.date=4]="date",t[t.none=5]="none";})(Tn.Frequency||(Tn.Frequency={}));});var cx=A(Wh=>{Object.defineProperty(Wh,"__esModule",{value:!0});var ux=co(),Hh=class{static fileStreamRotatorOptions(e){return Object.assign(Object.assign({},{filename:"",verbose:!1,end_stream:!1,utc:!1,create_symlink:!1,symlink_name:"current.log"}),e)}static fileOptions(e){return Object.assign(Object.assign({},{flags:"a"}),e)}static auditSettings(e){let r={keepSettings:{type:ux.KeepLogFiles.fileCount,amount:10},auditFilename:"audit.json",hashType:"md5",extension:"",files:[]};return Object.assign(Object.assign({},r),e)}static rotationSettings(e){let r={filename:"",frequency:ux.Frequency.none,utc:!1};return Object.assign(Object.assign({},r),e)}static extractParam(e,r=!0){let n=e.toString().match(/(\w)$/),i={number:0};n?.length==2&&(i.letter=n[1].toLowerCase());let s=e.toString().match(/^(\d+)/);return s?.length==2&&(i.number=Number(s[1])),i}};Wh.default=Hh;});var xu=A(Gi=>{Object.defineProperty(Gi,"__esModule",{value:!0});Gi.Logger=Gi.makeDirectory=void 0;var IU=oe("fs"),kU=oe("path");function MU(t){if(t.trim()!==""){var e=kU.dirname(t);try{IU.mkdirSync(e,{recursive:!0});}catch(r){if(r.code!=="EEXIST")throw r}}}Gi.makeDirectory=MU;var Bh=class t{constructor(){this.isVerbose=!1,this.allowDebug=!1;}static getInstance(e,r){return t.instance||(t.instance=new t,t.instance.isVerbose=e??!1,t.instance.allowDebug=r??!1),t.instance}static verbose(...e){t.getInstance().isVerbose&&console.log.apply(null,[new Date,"[FileStreamRotator:VERBOSE]",...e]);}static log(...e){console.log.apply(null,[new Date,"[FileStreamRotator]",...e]);}static debug(...e){t.getInstance().allowDebug&&console.debug.apply(null,[new Date,"[FileStreamRotator:DEBUG]",...e]);}};Gi.Logger=Bh;});var fx=A(zh=>{Object.defineProperty(zh,"__esModule",{value:!0});var lx=oe("fs"),ur=co(),ai=xu(),Uh=class t{constructor(e,r){var n;switch(this.currentSize=0,this.lastDate="",this.fileIndx=0,this.settings=e,this.settings.frequency){case ur.Frequency.hours:this.settings.amount&&this.settings.amount<13?this.settings.amount&&this.settings.amount>0||(this.settings.amount=1):this.settings.amount=12,this.isFormatValidForHour()||(ai.Logger.log("Date format not suitable for X hours rotation. Changing date format to 'YMDHm'"),this.settings.format="YMDHm");break;case ur.Frequency.minutes:this.settings.amount&&this.settings.amount<31?this.settings.amount&&this.settings.amount>0||(this.settings.amount=1):this.settings.amount=30,this.isFormatValidForMinutes()||(this.settings.format="YMDHm",ai.Logger.log("Date format not suitable for X minutes rotation. Changing date format to 'YMDHm'"));break;case ur.Frequency.daily:this.isFormatValidForDaily()||(this.settings.format="YMD",ai.Logger.log("Date format not suitable for daily rotation. Changing date format to 'YMD'"));break}if(this.settings.frequency!==ur.Frequency.none&&!this.settings.filename.match("%DATE%")&&(this.settings.filename+=".%DATE%",ai.Logger.log("Appending date to the end of the filename")),this.lastDate=this.getDateString(),this.settings.maxSize&&r){let s=new Date(r.date),o=(n=this.settings.extension)!==null&&n!==void 0?n:"";if(ai.Logger.debug(this.getDateString(s)==this.getDateString(new Date),this.getDateString(s),this.getDateString(new Date)),this.getDateString(s)==this.getDateString(new Date)){let a=r.name.match(RegExp("(\\d+)"+o.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")));a&&(this.fileIndx=Number(a[1]));var i=this.getSizeForFile(this.getNewFilename());i&&(this.currentSize=i),this.lastDate=this.getDateString(s),ai.Logger.debug("LOADED LAST ENTRY",this.currentSize,this.lastDate,this.fileIndx);}else {var i=this.getSizeForFile(this.getNewFilename());i&&(this.currentSize=i),ai.Logger.debug("CURRENT FILE:",this.getNewFilename(),this.currentSize);}}}getSizeForFile(e){try{if(lx.existsSync(e)){var r=lx.statSync(this.getNewFilename());if(r)return r.size}}catch{return}}hasMaxSizeReached(){return this.settings.maxSize?this.currentSize>this.settings.maxSize:!1}shouldRotate(){let e=this.hasMaxSizeReached();switch(this.settings.frequency){case ur.Frequency.none:return e;case ur.Frequency.hours:case ur.Frequency.minutes:case ur.Frequency.date:case ur.Frequency.daily:default:let r=this.getDateString();return this.lastDate!=r?!0:e}}isFormatValidForDaily(){let e=new Date(2022,2,20,1,2,3),r=new Date(2022,2,20,23,55,45),n=new Date(2022,2,21,2,55,45);return this.getDateString(e)===this.getDateString(r)&&this.getDateString(e)!==this.getDateString(n)}isFormatValidForHour(){if(!this.settings.amount||this.settings.frequency!=ur.Frequency.hours)return !1;let e=new Date(2022,2,20,1,2,3),r=new Date(2022,2,20,2+this.settings.amount,55,45);return this.getDateString(e)!==this.getDateString(r)}isFormatValidForMinutes(){if(!this.settings.amount||this.settings.frequency!=ur.Frequency.minutes)return !1;let e=new Date(2022,2,20,1,2,3),r=new Date(2022,2,20,1,2+this.settings.amount,45);return this.getDateString(e)!==this.getDateString(r)}getDateString(e){let r=e||new Date,n=t.getDateComponents(r,this.settings.utc),i=this.settings.format;if(i){switch(this.settings.frequency){case ur.Frequency.hours:if(this.settings.amount){var s=Math.floor(n.hour/this.settings.amount)*this.settings.amount;n.hour=s,n.minute=0,n.second=0;}case ur.Frequency.minutes:if(this.settings.amount){var o=Math.floor(n.minute/this.settings.amount)*this.settings.amount;n.minute=o,n.second=0;}}return i?.replace(/D+/,n.day.toString().padStart(2,"0")).replace(/M+/,n.month.toString().padStart(2,"0")).replace(/Y+/,n.year.toString()).replace(/H+/,n.hour.toString().padStart(2,"0")).replace(/m+/,n.minute.toString().padStart(2,"0")).replace(/s+/,n.second.toString().padStart(2,"0")).replace(/A+/,n.hour>11?"PM":"AM")}return ""}getFilename(e,r){return e.replace("%DATE%",this.lastDate)+(this.settings.maxSize||this.fileIndx>0?"."+this.fileIndx:"")+(r||"")}getNewFilename(){return this.getFilename(this.settings.filename,this.settings.extension)}addBytes(e){this.currentSize+=e;}rotate(e=!1){return e?(this.fileIndx+=1,this.currentSize=0,this.lastDate=this.getDateString()):this.shouldRotate()&&(this.hasMaxSizeReached()?this.fileIndx+=1:this.fileIndx=0,this.currentSize=0,this.lastDate=this.getDateString()),this.getNewFilename()}static getDateComponents(e,r){return r?{day:e.getUTCDate(),month:e.getUTCMonth()+1,year:e.getUTCFullYear(),hour:e.getUTCHours(),minute:e.getUTCMinutes(),second:e.getUTCSeconds(),utc:r,source:e}:{day:e.getDate(),month:e.getMonth()+1,year:e.getFullYear(),hour:e.getHours(),minute:e.getMinutes(),second:e.getSeconds(),utc:r,source:e}}static createDate(e,r){return new Date(e.year,e.month,e.day,e.hour,e.minute,e.second)}};zh.default=Uh;});var dx=A(Kh=>{Object.defineProperty(Kh,"__esModule",{value:!0});var Tu=oe("fs"),FU=oe("path"),NU=co(),Vr=xu(),Vh=oe("crypto"),Gh=class{constructor(e,r){this.config=e,this.emitter=r,this.loadLog();}loadLog(){var e;if(!this.config.keepSettings||((e=this.config.keepSettings)===null||e===void 0?void 0:e.amount)==0||!this.config.auditFilename||this.config.auditFilename==""){Vr.Logger.verbose("no existing audit settings found",this.config);return}try{let r=FU.resolve(this.config.auditFilename),n=JSON.parse(Tu.readFileSync(r,{encoding:"utf-8"}));this.config.files=n.files;}catch(r){if(r.code!=="ENOENT"){Vr.Logger.log("Failed to load config file",r);return}}}writeLog(){if(this.config.auditFilename)try{(0, Vr.makeDirectory)(this.config.auditFilename),Tu.writeFileSync(this.config.auditFilename,JSON.stringify(this.config,null,4));}catch(e){Vr.Logger.verbose("ERROR: Failed to store log audit",e);}}addLog(e){var r;if(!this.config.keepSettings||((r=this.config.keepSettings)===null||r===void 0?void 0:r.amount)==0||!this.config.auditFilename||this.config.auditFilename==""){Vr.Logger.verbose("audit log missing");return}if(this.config.files.findIndex(s=>s.name===e)!==-1){Vr.Logger.debug("file already in the log",e);return}var n=Date.now();if(this.config.files.push({date:n,name:e,hash:Vh.createHash(this.config.hashType).update(e+"LOG_FILE"+n).digest("hex")}),Vr.Logger.debug(`added file ${e} to log`),this.config.keepSettings&&this.config.keepSettings.amount){if(this.config.keepSettings.type==NU.KeepLogFiles.days){let s=Date.now()-86400*this.config.keepSettings.amount*1e3,o=this.config.files.filter(a=>{if(a.date>=s)return !0;this.removeLog(a);});this.config.files=o;}else if(this.config.files.length>this.config.keepSettings.amount){var i=this.config.files.splice(-this.config.keepSettings.amount);this.config.files.length>0&&this.config.files.filter(s=>(this.removeLog(s),!1)),this.config.files=i;}}this.writeLog();}removeLog(e){if(e.hash===Vh.createHash(this.config.hashType).update(e.name+"LOG_FILE"+e.date).digest("hex"))try{Tu.existsSync(e.name)&&(Vr.Logger.debug("removing log file",e.name),Tu.unlinkSync(e.name),this.emitter.emit("logRemoved",e.name));}catch{Vr.Logger.verbose("Could not remove old log file: ",e.name);}else Vr.Logger.debug("incorrect hash",e.name,e.hash,Vh.createHash(this.config.hashType).update(e.name+"LOG_FILE"+e.date).digest("hex"));}};Kh.default=Gh;});var px=A(Yh=>{Object.defineProperty(Yh,"__esModule",{value:!0});var qU=oe("stream"),Ki=oe("fs"),Zh=oe("path"),an=co(),un=cx(),hx=fx(),LU=dx(),lo=xu(),Jh=class t extends qU.EventEmitter{constructor(e,r=!1){var n,i;super(),this.config={},this.config=this.parseOptions(e),lo.Logger.getInstance(e.verbose,r),this.auditManager=new LU.default((n=this.config.auditSettings)!==null&&n!==void 0?n:un.default.auditSettings({}),this);let s=this.auditManager.config.files.slice(-1).shift();this.rotator=new hx.default((i=this.config.rotationSettings)!==null&&i!==void 0?i:un.default.rotationSettings({}),s),this.rotate();}static getStream(e){return new t(e)}parseOptions(e){var r,n,i,s;let o={};o.options=un.default.fileStreamRotatorOptions(e),o.fileOptions=un.default.fileOptions((r=e.file_options)!==null&&r!==void 0?r:{});let a=un.default.auditSettings({});if(e.audit_file&&(a.auditFilename=e.audit_file),e.audit_hash_type&&(a.hashType=e.audit_hash_type),e.extension&&(a.extension=e.extension),e.max_logs){let d=un.default.extractParam(e.max_logs);a.keepSettings={type:((n=d.letter)===null||n===void 0?void 0:n.toLowerCase())=="d"?an.KeepLogFiles.days:an.KeepLogFiles.fileCount,amount:d.number};}switch(o.auditSettings=a,o.rotationSettings=un.default.rotationSettings({filename:e.filename,extension:e.extension}),e.date_format&&!e.frequency?o.rotationSettings.frequency=an.Frequency.date:o.rotationSettings.frequency=an.Frequency.none,e.date_format&&(o.rotationSettings.format=e.date_format),o.rotationSettings.utc=(i=e.utc)!==null&&i!==void 0?i:!1,e.frequency){case"daily":o.rotationSettings.frequency=an.Frequency.daily;break;case"custom":case"date":o.rotationSettings.frequency=an.Frequency.date;break;case"test":o.rotationSettings.frequency=an.Frequency.minutes,o.rotationSettings.amount=1;break;default:if(e.frequency){let d=un.default.extractParam(e.frequency);!((s=d.letter)===null||s===void 0)&&s.match(/^([mh])$/)&&(o.rotationSettings.frequency=d.letter=="h"?an.Frequency.hours:an.Frequency.minutes,o.rotationSettings.amount=d.number);}}if(e.size){let d=un.default.extractParam(e.size);switch(d.letter){case"k":o.rotationSettings.maxSize=d.number*1024;break;case"m":o.rotationSettings.maxSize=d.number*1024*1024;break;case"g":o.rotationSettings.maxSize=d.number*1024*1024*1024;break}}this.rotator=new hx.default(o.rotationSettings);this.rotator.getNewFilename();return o}rotate(e=!1){var r;let n=this.currentFile;this.rotator.rotate(e),this.currentFile=this.rotator.getNewFilename(),this.currentFile!=n&&(this.fs&&(((r=this.config.options)===null||r===void 0?void 0:r.end_stream)===!0?this.fs.end():this.fs.destroy()),n&&this.auditManager.addLog(n),this.createNewLog(this.currentFile),this.emit("new",this.currentFile),this.emit("rotate",n,this.currentFile,e));}createNewLog(e){var r;(0, lo.makeDirectory)(e),this.auditManager.addLog(e);let n={};this.config.fileOptions&&(n=this.config.fileOptions),this.fs=Ki.createWriteStream(e,n),this.bubbleEvents(this.fs,e),!((r=this.config.options)===null||r===void 0)&&r.create_symlink&&this.createCurrentSymLink(e);}write(e,r){this.fs&&(this.rotator.shouldRotate()&&this.rotate(),this.fs.write(e,r??"utf8"),this.rotator.addBytes(Buffer.byteLength(e,r)),this.rotator.hasMaxSizeReached()&&this.rotate());}end(e){this.fs&&(this.fs.end(e),this.fs=void 0);}bubbleEvents(e,r){e.on("close",()=>{this.emit("close");}),e.on("finish",()=>{this.emit("finish");}),e.on("error",n=>{this.emit("error",n);}),e.on("open",n=>{this.emit("open",r);});}createCurrentSymLink(e){var r,n;if(!e)return;let i=(n=(r=this.config.options)===null||r===void 0?void 0:r.symlink_name)!==null&&n!==void 0?n:"current.log",s=Zh.dirname(e),o=Zh.basename(e),a=s+Zh.sep+i;try{if(Ki.existsSync(a)){if(Ki.lstatSync(a).isSymbolicLink()){Ki.unlinkSync(a),Ki.symlinkSync(o,a);return}lo.Logger.verbose("Could not create symlink file as file with the same name exists: ",a);}else Ki.symlinkSync(o,a);}catch(l){lo.Logger.verbose("[Could not create symlink file: ",a," -> ",o),lo.Logger.debug("error creating sym link",a,l);}}test(){return {config:this.config,rotator:this.rotator}}};Yh.default=Jh;});var mx=A(Au=>{Object.defineProperty(Au,"__esModule",{value:!0});Au.getStream=void 0;var $U=px();function jU(t){return new $U.default(t)}Au.getStream=jU;});var vx=A((R8,bx)=>{bx.exports=function(){function t(n,i){function s(){this.constructor=n;}s.prototype=i.prototype,n.prototype=new s;}function e(n,i,s,o,a,l){this.message=n,this.expected=i,this.found=s,this.offset=o,this.line=a,this.column=l,this.name="SyntaxError";}t(e,Error);function r(n){var i=arguments.length>1?arguments[1]:{},s={},o={start:xg},a=xg,d=function(){return zg},c=s,p="#",m={type:"literal",value:"#",description:'"#"'},_=void 0,w={type:"any",description:"any character"},E="[",P={type:"literal",value:"[",description:'"["'},D="]",g={type:"literal",value:"]",description:'"]"'},S=function(u){gl(Ct("ObjectPath",u,Et,Rt));},I=function(u){gl(Ct("ArrayPath",u,Et,Rt));},L=function(u,h){return u.concat(h)},Z=function(u){return [u]},K=function(u){return u},U=".",B={type:"literal",value:".",description:'"."'},Q="=",N={type:"literal",value:"=",description:'"="'},te=function(u,h){gl(Ct("Assign",h,Et,Rt,u));},ae=function(u){return u.join("")},ye=function(u){return u.value},q='"""',W={type:"literal",value:'"""',description:'"\\"\\"\\""'},me=null,pe=function(u){return Ct("String",u.join(""),Et,Rt)},we='"',Xe={type:"literal",value:'"',description:'"\\""'},Ke="'''",Pt={type:"literal",value:"'''",description:`"'''"`},Lr="'",zt={type:"literal",value:"'",description:`"'"`},St=function(u){return u},Ce=function(u){return u},Cr="\\",fn={type:"literal",value:"\\",description:'"\\\\"'},ce=function(){return ""},Me="e",ie={type:"literal",value:"e",description:'"e"'},be="E",$e={type:"literal",value:"E",description:'"E"'},ot=function(u,h){return Ct("Float",parseFloat(u+"e"+h),Et,Rt)},He=function(u){return Ct("Float",parseFloat(u),Et,Rt)},fr="+",Nt={type:"literal",value:"+",description:'"+"'},Hn=function(u){return u.join("")},qt="-",Vt={type:"literal",value:"-",description:'"-"'},Wn=function(u){return "-"+u.join("")},yi=function(u){return Ct("Integer",parseInt(u,10),Et,Rt)},tr="true",Bn={type:"literal",value:"true",description:'"true"'},Un=function(){return Ct("Boolean",!0,Et,Rt)},Yr="false",zn={type:"literal",value:"false",description:'"false"'},xr=function(){return Ct("Boolean",!1,Et,Rt)},k=function(){return Ct("Array",[],Et,Rt)},G=function(u){return Ct("Array",u?[u]:[],Et,Rt)},se=function(u){return Ct("Array",u,Et,Rt)},fe=function(u,h){return Ct("Array",u.concat(h),Et,Rt)},qe=function(u){return u},ke=",",ze={type:"literal",value:",",description:'","'},Ze="{",Te={type:"literal",value:"{",description:'"{"'},We="}",Ie={type:"literal",value:"}",description:'"}"'},et=function(u){return Ct("InlineTable",u,Et,Rt)},ut=function(u,h){return Ct("InlineTableValue",h,Et,Rt,u)},Lt=function(u){return "."+u},bi=function(u){return u.join("")},dn=":",hn={type:"literal",value:":",description:'":"'},Ss=function(u){return u.join("")},Es="T",No={type:"literal",value:"T",description:'"T"'},j="Z",v={type:"literal",value:"Z",description:'"Z"'},M=function(u,h){return Ct("Date",new Date(u+"T"+h+"Z"),Et,Rt)},F=function(u,h){return Ct("Date",new Date(u+"T"+h),Et,Rt)},R=/^[ \t]/,y={type:"class",value:"[ \\t]",description:"[ \\t]"},$=` -`,J={type:"literal",value:` -`,description:'"\\n"'},Ee="\r",tt={type:"literal",value:"\r",description:'"\\r"'},Ve=/^[0-9a-f]/i,rr={type:"class",value:"[0-9a-f]i",description:"[0-9a-f]i"},nr=/^[0-9]/,Tr={type:"class",value:"[0-9]",description:"[0-9]"},Pe="_",pn={type:"literal",value:"_",description:'"_"'},vi=function(){return ""},qo=/^[A-Za-z0-9_\-]/,L1={type:"class",value:"[A-Za-z0-9_\\-]",description:"[A-Za-z0-9_\\-]"},$1=function(u){return u.join("")},gg='\\"',j1={type:"literal",value:'\\"',description:'"\\\\\\""'},H1=function(){return '"'},_g="\\\\",W1={type:"literal",value:"\\\\",description:'"\\\\\\\\"'},B1=function(){return "\\"},yg="\\b",U1={type:"literal",value:"\\b",description:'"\\\\b"'},z1=function(){return "\b"},bg="\\t",V1={type:"literal",value:"\\t",description:'"\\\\t"'},G1=function(){return " "},vg="\\n",K1={type:"literal",value:"\\n",description:'"\\\\n"'},Z1=function(){return ` -`},wg="\\f",J1={type:"literal",value:"\\f",description:'"\\\\f"'},Y1=function(){return "\f"},Sg="\\r",X1={type:"literal",value:"\\r",description:'"\\\\r"'},Q1=function(){return "\r"},Eg="\\U",eO={type:"literal",value:"\\U",description:'"\\\\U"'},Rg=function(u){return vO(u.join(""))},Cg="\\u",tO={type:"literal",value:"\\u",description:'"\\\\u"'},f=0,de=0,Rs=0,ll={line:1,column:1,seenCR:!1},Lo=0,fl=[],X=0,ee={},$o;if("startRule"in i){if(!(i.startRule in o))throw new Error(`Can't start parsing from rule "`+i.startRule+'".');a=o[i.startRule];}function Et(){return dl(de).line}function Rt(){return dl(de).column}function dl(u){function h(b,x,O){var H,V;for(H=x;Hu&&(Rs=0,ll={line:1,column:1,seenCR:!1}),h(ll,Rs,u),Rs=u),ll}function re(u){fLo&&(Lo=f,fl=[]),fl.push(u));}function hl(u,h,b){function x(le){var Se=1;for(le.sort(function(Be,Fe){return Be.descriptionFe.description?1:0});Se1?Fe.slice(0,-1).join(", ")+" or "+Fe[le.length-1]:Fe[0],ct=Se?'"'+Be(Se)+'"':"end of input","Expected "+nt+" but "+ct+" found."}var H=dl(b),V=bf?(H=n.charAt(f),f++):(H=s,X===0&&re(w)),H!==s?(O=[O,H],x=O):(f=x,x=c)):(f=x,x=c);x!==s;)b.push(x),x=f,O=f,X++,H=hr(),H===s&&(H=Wo()),X--,H===s?O=_:(f=O,O=c),O!==s?(n.length>f?(H=n.charAt(f),f++):(H=s,X===0&&re(w)),H!==s?(O=[O,H],x=O):(f=x,x=c)):(f=x,x=c);b!==s?(h=[h,b],u=h):(f=u,u=c);}else f=u,u=c;return ee[V]={nextPos:f,result:u},u}function nO(){var u,h,b,x,O,H,V=f*49+4,le=ee[V];if(le)return f=le.nextPos,le.result;if(u=f,n.charCodeAt(f)===91?(h=E,f++):(h=s,X===0&&re(P)),h!==s){for(b=[],x=he();x!==s;)b.push(x),x=he();if(b!==s)if(x=Ag(),x!==s){for(O=[],H=he();H!==s;)O.push(H),H=he();O!==s?(n.charCodeAt(f)===93?(H=D,f++):(H=s,X===0&&re(g)),H!==s?(de=u,h=S(x),u=h):(f=u,u=c)):(f=u,u=c);}else f=u,u=c;else f=u,u=c;}else f=u,u=c;return ee[V]={nextPos:f,result:u},u}function iO(){var u,h,b,x,O,H,V,le,Se=f*49+5,Be=ee[Se];if(Be)return f=Be.nextPos,Be.result;if(u=f,n.charCodeAt(f)===91?(h=E,f++):(h=s,X===0&&re(P)),h!==s)if(n.charCodeAt(f)===91?(b=E,f++):(b=s,X===0&&re(P)),b!==s){for(x=[],O=he();O!==s;)x.push(O),O=he();if(x!==s)if(O=Ag(),O!==s){for(H=[],V=he();V!==s;)H.push(V),V=he();H!==s?(n.charCodeAt(f)===93?(V=D,f++):(V=s,X===0&&re(g)),V!==s?(n.charCodeAt(f)===93?(le=D,f++):(le=s,X===0&&re(g)),le!==s?(de=u,h=I(O),u=h):(f=u,u=c)):(f=u,u=c)):(f=u,u=c);}else f=u,u=c;else f=u,u=c;}else f=u,u=c;else f=u,u=c;return ee[Se]={nextPos:f,result:u},u}function Ag(){var u,h,b,x=f*49+6,O=ee[x];if(O)return f=O.nextPos,O.result;if(u=f,h=[],b=Dg(),b!==s)for(;b!==s;)h.push(b),b=Dg();else h=c;return h!==s?(b=Pg(),b!==s?(de=u,h=L(h,b),u=h):(f=u,u=c)):(f=u,u=c),u===s&&(u=f,h=Pg(),h!==s&&(de=u,h=Z(h)),u=h),ee[x]={nextPos:f,result:u},u}function Pg(){var u,h,b,x,O,H=f*49+7,V=ee[H];if(V)return f=V.nextPos,V.result;for(u=f,h=[],b=he();b!==s;)h.push(b),b=he();if(h!==s)if(b=Cs(),b!==s){for(x=[],O=he();O!==s;)x.push(O),O=he();x!==s?(de=u,h=K(b),u=h):(f=u,u=c);}else f=u,u=c;else f=u,u=c;if(u===s){for(u=f,h=[],b=he();b!==s;)h.push(b),b=he();if(h!==s)if(b=pl(),b!==s){for(x=[],O=he();O!==s;)x.push(O),O=he();x!==s?(de=u,h=K(b),u=h):(f=u,u=c);}else f=u,u=c;else f=u,u=c;}return ee[H]={nextPos:f,result:u},u}function Dg(){var u,h,b,x,O,H,V,le=f*49+8,Se=ee[le];if(Se)return f=Se.nextPos,Se.result;for(u=f,h=[],b=he();b!==s;)h.push(b),b=he();if(h!==s)if(b=Cs(),b!==s){for(x=[],O=he();O!==s;)x.push(O),O=he();if(x!==s)if(n.charCodeAt(f)===46?(O=U,f++):(O=s,X===0&&re(B)),O!==s){for(H=[],V=he();V!==s;)H.push(V),V=he();H!==s?(de=u,h=K(b),u=h):(f=u,u=c);}else f=u,u=c;else f=u,u=c;}else f=u,u=c;else f=u,u=c;if(u===s){for(u=f,h=[],b=he();b!==s;)h.push(b),b=he();if(h!==s)if(b=pl(),b!==s){for(x=[],O=he();O!==s;)x.push(O),O=he();if(x!==s)if(n.charCodeAt(f)===46?(O=U,f++):(O=s,X===0&&re(B)),O!==s){for(H=[],V=he();V!==s;)H.push(V),V=he();H!==s?(de=u,h=K(b),u=h):(f=u,u=c);}else f=u,u=c;else f=u,u=c;}else f=u,u=c;else f=u,u=c;}return ee[le]={nextPos:f,result:u},u}function sO(){var u,h,b,x,O,H,V=f*49+9,le=ee[V];if(le)return f=le.nextPos,le.result;if(u=f,h=Cs(),h!==s){for(b=[],x=he();x!==s;)b.push(x),x=he();if(b!==s)if(n.charCodeAt(f)===61?(x=Q,f++):(x=s,X===0&&re(N)),x!==s){for(O=[],H=he();H!==s;)O.push(H),H=he();O!==s?(H=wi(),H!==s?(de=u,h=te(h,H),u=h):(f=u,u=c)):(f=u,u=c);}else f=u,u=c;else f=u,u=c;}else f=u,u=c;if(u===s)if(u=f,h=pl(),h!==s){for(b=[],x=he();x!==s;)b.push(x),x=he();if(b!==s)if(n.charCodeAt(f)===61?(x=Q,f++):(x=s,X===0&&re(N)),x!==s){for(O=[],H=he();H!==s;)O.push(H),H=he();O!==s?(H=wi(),H!==s?(de=u,h=te(h,H),u=h):(f=u,u=c)):(f=u,u=c);}else f=u,u=c;else f=u,u=c;}else f=u,u=c;return ee[V]={nextPos:f,result:u},u}function Cs(){var u,h,b,x=f*49+10,O=ee[x];if(O)return f=O.nextPos,O.result;if(u=f,h=[],b=Bg(),b!==s)for(;b!==s;)h.push(b),b=Bg();else h=c;return h!==s&&(de=u,h=ae(h)),u=h,ee[x]={nextPos:f,result:u},u}function pl(){var u,h,b=f*49+11,x=ee[b];return x?(f=x.nextPos,x.result):(u=f,h=Og(),h!==s&&(de=u,h=ye(h)),u=h,u===s&&(u=f,h=Ig(),h!==s&&(de=u,h=ye(h)),u=h),ee[b]={nextPos:f,result:u},u)}function wi(){var u,h=f*49+12,b=ee[h];return b?(f=b.nextPos,b.result):(u=oO(),u===s&&(u=_O(),u===s&&(u=lO(),u===s&&(u=fO(),u===s&&(u=dO(),u===s&&(u=hO(),u===s&&(u=pO())))))),ee[h]={nextPos:f,result:u},u)}function oO(){var u,h=f*49+13,b=ee[h];return b?(f=b.nextPos,b.result):(u=aO(),u===s&&(u=Og(),u===s&&(u=uO(),u===s&&(u=Ig()))),ee[h]={nextPos:f,result:u},u)}function aO(){var u,h,b,x,O,H=f*49+14,V=ee[H];if(V)return f=V.nextPos,V.result;if(u=f,n.substr(f,3)===q?(h=q,f+=3):(h=s,X===0&&re(W)),h!==s)if(b=hr(),b===s&&(b=me),b!==s){for(x=[],O=Fg();O!==s;)x.push(O),O=Fg();x!==s?(n.substr(f,3)===q?(O=q,f+=3):(O=s,X===0&&re(W)),O!==s?(de=u,h=pe(x),u=h):(f=u,u=c)):(f=u,u=c);}else f=u,u=c;else f=u,u=c;return ee[H]={nextPos:f,result:u},u}function Og(){var u,h,b,x,O=f*49+15,H=ee[O];if(H)return f=H.nextPos,H.result;if(u=f,n.charCodeAt(f)===34?(h=we,f++):(h=s,X===0&&re(Xe)),h!==s){for(b=[],x=kg();x!==s;)b.push(x),x=kg();b!==s?(n.charCodeAt(f)===34?(x=we,f++):(x=s,X===0&&re(Xe)),x!==s?(de=u,h=pe(b),u=h):(f=u,u=c)):(f=u,u=c);}else f=u,u=c;return ee[O]={nextPos:f,result:u},u}function uO(){var u,h,b,x,O,H=f*49+16,V=ee[H];if(V)return f=V.nextPos,V.result;if(u=f,n.substr(f,3)===Ke?(h=Ke,f+=3):(h=s,X===0&&re(Pt)),h!==s)if(b=hr(),b===s&&(b=me),b!==s){for(x=[],O=Ng();O!==s;)x.push(O),O=Ng();x!==s?(n.substr(f,3)===Ke?(O=Ke,f+=3):(O=s,X===0&&re(Pt)),O!==s?(de=u,h=pe(x),u=h):(f=u,u=c)):(f=u,u=c);}else f=u,u=c;else f=u,u=c;return ee[H]={nextPos:f,result:u},u}function Ig(){var u,h,b,x,O=f*49+17,H=ee[O];if(H)return f=H.nextPos,H.result;if(u=f,n.charCodeAt(f)===39?(h=Lr,f++):(h=s,X===0&&re(zt)),h!==s){for(b=[],x=Mg();x!==s;)b.push(x),x=Mg();b!==s?(n.charCodeAt(f)===39?(x=Lr,f++):(x=s,X===0&&re(zt)),x!==s?(de=u,h=pe(b),u=h):(f=u,u=c)):(f=u,u=c);}else f=u,u=c;return ee[O]={nextPos:f,result:u},u}function kg(){var u,h,b,x=f*49+18,O=ee[x];return O?(f=O.nextPos,O.result):(u=Ug(),u===s&&(u=f,h=f,X++,n.charCodeAt(f)===34?(b=we,f++):(b=s,X===0&&re(Xe)),X--,b===s?h=_:(f=h,h=c),h!==s?(n.length>f?(b=n.charAt(f),f++):(b=s,X===0&&re(w)),b!==s?(de=u,h=St(b),u=h):(f=u,u=c)):(f=u,u=c)),ee[x]={nextPos:f,result:u},u)}function Mg(){var u,h,b,x=f*49+19,O=ee[x];return O?(f=O.nextPos,O.result):(u=f,h=f,X++,n.charCodeAt(f)===39?(b=Lr,f++):(b=s,X===0&&re(zt)),X--,b===s?h=_:(f=h,h=c),h!==s?(n.length>f?(b=n.charAt(f),f++):(b=s,X===0&&re(w)),b!==s?(de=u,h=St(b),u=h):(f=u,u=c)):(f=u,u=c),ee[x]={nextPos:f,result:u},u)}function Fg(){var u,h,b,x=f*49+20,O=ee[x];return O?(f=O.nextPos,O.result):(u=Ug(),u===s&&(u=cO(),u===s&&(u=f,h=f,X++,n.substr(f,3)===q?(b=q,f+=3):(b=s,X===0&&re(W)),X--,b===s?h=_:(f=h,h=c),h!==s?(n.length>f?(b=n.charAt(f),f++):(b=s,X===0&&re(w)),b!==s?(de=u,h=Ce(b),u=h):(f=u,u=c)):(f=u,u=c))),ee[x]={nextPos:f,result:u},u)}function cO(){var u,h,b,x,O,H=f*49+21,V=ee[H];if(V)return f=V.nextPos,V.result;if(u=f,n.charCodeAt(f)===92?(h=Cr,f++):(h=s,X===0&&re(fn)),h!==s)if(b=hr(),b!==s){for(x=[],O=Wg();O!==s;)x.push(O),O=Wg();x!==s?(de=u,h=ce(),u=h):(f=u,u=c);}else f=u,u=c;else f=u,u=c;return ee[H]={nextPos:f,result:u},u}function Ng(){var u,h,b,x=f*49+22,O=ee[x];return O?(f=O.nextPos,O.result):(u=f,h=f,X++,n.substr(f,3)===Ke?(b=Ke,f+=3):(b=s,X===0&&re(Pt)),X--,b===s?h=_:(f=h,h=c),h!==s?(n.length>f?(b=n.charAt(f),f++):(b=s,X===0&&re(w)),b!==s?(de=u,h=St(b),u=h):(f=u,u=c)):(f=u,u=c),ee[x]={nextPos:f,result:u},u)}function lO(){var u,h,b,x,O=f*49+23,H=ee[O];return H?(f=H.nextPos,H.result):(u=f,h=qg(),h===s&&(h=ml()),h!==s?(n.charCodeAt(f)===101?(b=Me,f++):(b=s,X===0&&re(ie)),b===s&&(n.charCodeAt(f)===69?(b=be,f++):(b=s,X===0&&re($e))),b!==s?(x=ml(),x!==s?(de=u,h=ot(h,x),u=h):(f=u,u=c)):(f=u,u=c)):(f=u,u=c),u===s&&(u=f,h=qg(),h!==s&&(de=u,h=He(h)),u=h),ee[O]={nextPos:f,result:u},u)}function qg(){var u,h,b,x,O,H,V=f*49+24,le=ee[V];return le?(f=le.nextPos,le.result):(u=f,n.charCodeAt(f)===43?(h=fr,f++):(h=s,X===0&&re(Nt)),h===s&&(h=me),h!==s?(b=f,x=xs(),x!==s?(n.charCodeAt(f)===46?(O=U,f++):(O=s,X===0&&re(B)),O!==s?(H=xs(),H!==s?(x=[x,O,H],b=x):(f=b,b=c)):(f=b,b=c)):(f=b,b=c),b!==s?(de=u,h=Hn(b),u=h):(f=u,u=c)):(f=u,u=c),u===s&&(u=f,n.charCodeAt(f)===45?(h=qt,f++):(h=s,X===0&&re(Vt)),h!==s?(b=f,x=xs(),x!==s?(n.charCodeAt(f)===46?(O=U,f++):(O=s,X===0&&re(B)),O!==s?(H=xs(),H!==s?(x=[x,O,H],b=x):(f=b,b=c)):(f=b,b=c)):(f=b,b=c),b!==s?(de=u,h=Wn(b),u=h):(f=u,u=c)):(f=u,u=c)),ee[V]={nextPos:f,result:u},u)}function fO(){var u,h,b=f*49+25,x=ee[b];return x?(f=x.nextPos,x.result):(u=f,h=ml(),h!==s&&(de=u,h=yi(h)),u=h,ee[b]={nextPos:f,result:u},u)}function ml(){var u,h,b,x,O,H=f*49+26,V=ee[H];if(V)return f=V.nextPos,V.result;if(u=f,n.charCodeAt(f)===43?(h=fr,f++):(h=s,X===0&&re(Nt)),h===s&&(h=me),h!==s){if(b=[],x=je(),x!==s)for(;x!==s;)b.push(x),x=je();else b=c;b!==s?(x=f,X++,n.charCodeAt(f)===46?(O=U,f++):(O=s,X===0&&re(B)),X--,O===s?x=_:(f=x,x=c),x!==s?(de=u,h=Hn(b),u=h):(f=u,u=c)):(f=u,u=c);}else f=u,u=c;if(u===s)if(u=f,n.charCodeAt(f)===45?(h=qt,f++):(h=s,X===0&&re(Vt)),h!==s){if(b=[],x=je(),x!==s)for(;x!==s;)b.push(x),x=je();else b=c;b!==s?(x=f,X++,n.charCodeAt(f)===46?(O=U,f++):(O=s,X===0&&re(B)),X--,O===s?x=_:(f=x,x=c),x!==s?(de=u,h=Wn(b),u=h):(f=u,u=c)):(f=u,u=c);}else f=u,u=c;return ee[H]={nextPos:f,result:u},u}function dO(){var u,h,b=f*49+27,x=ee[b];return x?(f=x.nextPos,x.result):(u=f,n.substr(f,4)===tr?(h=tr,f+=4):(h=s,X===0&&re(Bn)),h!==s&&(de=u,h=Un()),u=h,u===s&&(u=f,n.substr(f,5)===Yr?(h=Yr,f+=5):(h=s,X===0&&re(zn)),h!==s&&(de=u,h=xr()),u=h),ee[b]={nextPos:f,result:u},u)}function hO(){var u,h,b,x,O,H=f*49+28,V=ee[H];if(V)return f=V.nextPos,V.result;if(u=f,n.charCodeAt(f)===91?(h=E,f++):(h=s,X===0&&re(P)),h!==s){for(b=[],x=dr();x!==s;)b.push(x),x=dr();b!==s?(n.charCodeAt(f)===93?(x=D,f++):(x=s,X===0&&re(g)),x!==s?(de=u,h=k(),u=h):(f=u,u=c)):(f=u,u=c);}else f=u,u=c;if(u===s&&(u=f,n.charCodeAt(f)===91?(h=E,f++):(h=s,X===0&&re(P)),h!==s?(b=Lg(),b===s&&(b=me),b!==s?(n.charCodeAt(f)===93?(x=D,f++):(x=s,X===0&&re(g)),x!==s?(de=u,h=G(b),u=h):(f=u,u=c)):(f=u,u=c)):(f=u,u=c),u===s)){if(u=f,n.charCodeAt(f)===91?(h=E,f++):(h=s,X===0&&re(P)),h!==s){if(b=[],x=Ho(),x!==s)for(;x!==s;)b.push(x),x=Ho();else b=c;b!==s?(n.charCodeAt(f)===93?(x=D,f++):(x=s,X===0&&re(g)),x!==s?(de=u,h=se(b),u=h):(f=u,u=c)):(f=u,u=c);}else f=u,u=c;if(u===s)if(u=f,n.charCodeAt(f)===91?(h=E,f++):(h=s,X===0&&re(P)),h!==s){if(b=[],x=Ho(),x!==s)for(;x!==s;)b.push(x),x=Ho();else b=c;b!==s?(x=Lg(),x!==s?(n.charCodeAt(f)===93?(O=D,f++):(O=s,X===0&&re(g)),O!==s?(de=u,h=fe(b,x),u=h):(f=u,u=c)):(f=u,u=c)):(f=u,u=c);}else f=u,u=c;}return ee[H]={nextPos:f,result:u},u}function Lg(){var u,h,b,x,O,H=f*49+29,V=ee[H];if(V)return f=V.nextPos,V.result;for(u=f,h=[],b=dr();b!==s;)h.push(b),b=dr();if(h!==s)if(b=wi(),b!==s){for(x=[],O=dr();O!==s;)x.push(O),O=dr();x!==s?(de=u,h=qe(b),u=h):(f=u,u=c);}else f=u,u=c;else f=u,u=c;return ee[H]={nextPos:f,result:u},u}function Ho(){var u,h,b,x,O,H,V,le=f*49+30,Se=ee[le];if(Se)return f=Se.nextPos,Se.result;for(u=f,h=[],b=dr();b!==s;)h.push(b),b=dr();if(h!==s)if(b=wi(),b!==s){for(x=[],O=dr();O!==s;)x.push(O),O=dr();if(x!==s)if(n.charCodeAt(f)===44?(O=ke,f++):(O=s,X===0&&re(ze)),O!==s){for(H=[],V=dr();V!==s;)H.push(V),V=dr();H!==s?(de=u,h=qe(b),u=h):(f=u,u=c);}else f=u,u=c;else f=u,u=c;}else f=u,u=c;else f=u,u=c;return ee[le]={nextPos:f,result:u},u}function dr(){var u,h=f*49+31,b=ee[h];return b?(f=b.nextPos,b.result):(u=he(),u===s&&(u=hr(),u===s&&(u=jo())),ee[h]={nextPos:f,result:u},u)}function pO(){var u,h,b,x,O,H,V=f*49+32,le=ee[V];if(le)return f=le.nextPos,le.result;if(u=f,n.charCodeAt(f)===123?(h=Ze,f++):(h=s,X===0&&re(Te)),h!==s){for(b=[],x=he();x!==s;)b.push(x),x=he();if(b!==s){for(x=[],O=$g();O!==s;)x.push(O),O=$g();if(x!==s){for(O=[],H=he();H!==s;)O.push(H),H=he();O!==s?(n.charCodeAt(f)===125?(H=We,f++):(H=s,X===0&&re(Ie)),H!==s?(de=u,h=et(x),u=h):(f=u,u=c)):(f=u,u=c);}else f=u,u=c;}else f=u,u=c;}else f=u,u=c;return ee[V]={nextPos:f,result:u},u}function $g(){var u,h,b,x,O,H,V,le,Se,Be,Fe,nt=f*49+33,ct=ee[nt];if(ct)return f=ct.nextPos,ct.result;for(u=f,h=[],b=he();b!==s;)h.push(b),b=he();if(h!==s)if(b=Cs(),b!==s){for(x=[],O=he();O!==s;)x.push(O),O=he();if(x!==s)if(n.charCodeAt(f)===61?(O=Q,f++):(O=s,X===0&&re(N)),O!==s){for(H=[],V=he();V!==s;)H.push(V),V=he();if(H!==s)if(V=wi(),V!==s){for(le=[],Se=he();Se!==s;)le.push(Se),Se=he();if(le!==s)if(n.charCodeAt(f)===44?(Se=ke,f++):(Se=s,X===0&&re(ze)),Se!==s){for(Be=[],Fe=he();Fe!==s;)Be.push(Fe),Fe=he();Be!==s?(de=u,h=ut(b,V),u=h):(f=u,u=c);}else f=u,u=c;else f=u,u=c;}else f=u,u=c;else f=u,u=c;}else f=u,u=c;else f=u,u=c;}else f=u,u=c;else f=u,u=c;if(u===s){for(u=f,h=[],b=he();b!==s;)h.push(b),b=he();if(h!==s)if(b=Cs(),b!==s){for(x=[],O=he();O!==s;)x.push(O),O=he();if(x!==s)if(n.charCodeAt(f)===61?(O=Q,f++):(O=s,X===0&&re(N)),O!==s){for(H=[],V=he();V!==s;)H.push(V),V=he();H!==s?(V=wi(),V!==s?(de=u,h=ut(b,V),u=h):(f=u,u=c)):(f=u,u=c);}else f=u,u=c;else f=u,u=c;}else f=u,u=c;else f=u,u=c;}return ee[nt]={nextPos:f,result:u},u}function jg(){var u,h,b,x=f*49+34,O=ee[x];return O?(f=O.nextPos,O.result):(u=f,n.charCodeAt(f)===46?(h=U,f++):(h=s,X===0&&re(B)),h!==s?(b=xs(),b!==s?(de=u,h=Lt(b),u=h):(f=u,u=c)):(f=u,u=c),ee[x]={nextPos:f,result:u},u)}function Hg(){var u,h,b,x,O,H,V,le,Se,Be,Fe,nt,ct=f*49+35,ir=ee[ct];return ir?(f=ir.nextPos,ir.result):(u=f,h=f,b=je(),b!==s?(x=je(),x!==s?(O=je(),O!==s?(H=je(),H!==s?(n.charCodeAt(f)===45?(V=qt,f++):(V=s,X===0&&re(Vt)),V!==s?(le=je(),le!==s?(Se=je(),Se!==s?(n.charCodeAt(f)===45?(Be=qt,f++):(Be=s,X===0&&re(Vt)),Be!==s?(Fe=je(),Fe!==s?(nt=je(),nt!==s?(b=[b,x,O,H,V,le,Se,Be,Fe,nt],h=b):(f=h,h=c)):(f=h,h=c)):(f=h,h=c)):(f=h,h=c)):(f=h,h=c)):(f=h,h=c)):(f=h,h=c)):(f=h,h=c)):(f=h,h=c)):(f=h,h=c),h!==s&&(de=u,h=bi(h)),u=h,ee[ct]={nextPos:f,result:u},u)}function mO(){var u,h,b,x,O,H,V,le,Se,Be,Fe,nt=f*49+36,ct=ee[nt];return ct?(f=ct.nextPos,ct.result):(u=f,h=f,b=je(),b!==s?(x=je(),x!==s?(n.charCodeAt(f)===58?(O=dn,f++):(O=s,X===0&&re(hn)),O!==s?(H=je(),H!==s?(V=je(),V!==s?(n.charCodeAt(f)===58?(le=dn,f++):(le=s,X===0&&re(hn)),le!==s?(Se=je(),Se!==s?(Be=je(),Be!==s?(Fe=jg(),Fe===s&&(Fe=me),Fe!==s?(b=[b,x,O,H,V,le,Se,Be,Fe],h=b):(f=h,h=c)):(f=h,h=c)):(f=h,h=c)):(f=h,h=c)):(f=h,h=c)):(f=h,h=c)):(f=h,h=c)):(f=h,h=c)):(f=h,h=c),h!==s&&(de=u,h=Ss(h)),u=h,ee[nt]={nextPos:f,result:u},u)}function gO(){var u,h,b,x,O,H,V,le,Se,Be,Fe,nt,ct,ir,Si,mn,sr,Vg=f*49+37,_l=ee[Vg];return _l?(f=_l.nextPos,_l.result):(u=f,h=f,b=je(),b!==s?(x=je(),x!==s?(n.charCodeAt(f)===58?(O=dn,f++):(O=s,X===0&&re(hn)),O!==s?(H=je(),H!==s?(V=je(),V!==s?(n.charCodeAt(f)===58?(le=dn,f++):(le=s,X===0&&re(hn)),le!==s?(Se=je(),Se!==s?(Be=je(),Be!==s?(Fe=jg(),Fe===s&&(Fe=me),Fe!==s?(n.charCodeAt(f)===45?(nt=qt,f++):(nt=s,X===0&&re(Vt)),nt===s&&(n.charCodeAt(f)===43?(nt=fr,f++):(nt=s,X===0&&re(Nt))),nt!==s?(ct=je(),ct!==s?(ir=je(),ir!==s?(n.charCodeAt(f)===58?(Si=dn,f++):(Si=s,X===0&&re(hn)),Si!==s?(mn=je(),mn!==s?(sr=je(),sr!==s?(b=[b,x,O,H,V,le,Se,Be,Fe,nt,ct,ir,Si,mn,sr],h=b):(f=h,h=c)):(f=h,h=c)):(f=h,h=c)):(f=h,h=c)):(f=h,h=c)):(f=h,h=c)):(f=h,h=c)):(f=h,h=c)):(f=h,h=c)):(f=h,h=c)):(f=h,h=c)):(f=h,h=c)):(f=h,h=c)):(f=h,h=c)):(f=h,h=c),h!==s&&(de=u,h=Ss(h)),u=h,ee[Vg]={nextPos:f,result:u},u)}function _O(){var u,h,b,x,O,H=f*49+38,V=ee[H];return V?(f=V.nextPos,V.result):(u=f,h=Hg(),h!==s?(n.charCodeAt(f)===84?(b=Es,f++):(b=s,X===0&&re(No)),b!==s?(x=mO(),x!==s?(n.charCodeAt(f)===90?(O=j,f++):(O=s,X===0&&re(v)),O!==s?(de=u,h=M(h,x),u=h):(f=u,u=c)):(f=u,u=c)):(f=u,u=c)):(f=u,u=c),u===s&&(u=f,h=Hg(),h!==s?(n.charCodeAt(f)===84?(b=Es,f++):(b=s,X===0&&re(No)),b!==s?(x=gO(),x!==s?(de=u,h=F(h,x),u=h):(f=u,u=c)):(f=u,u=c)):(f=u,u=c)),ee[H]={nextPos:f,result:u},u)}function he(){var u,h=f*49+39,b=ee[h];return b?(f=b.nextPos,b.result):(R.test(n.charAt(f))?(u=n.charAt(f),f++):(u=s,X===0&&re(y)),ee[h]={nextPos:f,result:u},u)}function hr(){var u,h,b,x=f*49+40,O=ee[x];return O?(f=O.nextPos,O.result):(n.charCodeAt(f)===10?(u=$,f++):(u=s,X===0&&re(J)),u===s&&(u=f,n.charCodeAt(f)===13?(h=Ee,f++):(h=s,X===0&&re(tt)),h!==s?(n.charCodeAt(f)===10?(b=$,f++):(b=s,X===0&&re(J)),b!==s?(h=[h,b],u=h):(f=u,u=c)):(f=u,u=c)),ee[x]={nextPos:f,result:u},u)}function Wg(){var u,h=f*49+41,b=ee[h];return b?(f=b.nextPos,b.result):(u=hr(),u===s&&(u=he()),ee[h]={nextPos:f,result:u},u)}function Wo(){var u,h,b=f*49+42,x=ee[b];return x?(f=x.nextPos,x.result):(u=f,X++,n.length>f?(h=n.charAt(f),f++):(h=s,X===0&&re(w)),X--,h===s?u=_:(f=u,u=c),ee[b]={nextPos:f,result:u},u)}function pr(){var u,h=f*49+43,b=ee[h];return b?(f=b.nextPos,b.result):(Ve.test(n.charAt(f))?(u=n.charAt(f),f++):(u=s,X===0&&re(rr)),ee[h]={nextPos:f,result:u},u)}function je(){var u,h,b=f*49+44,x=ee[b];return x?(f=x.nextPos,x.result):(nr.test(n.charAt(f))?(u=n.charAt(f),f++):(u=s,X===0&&re(Tr)),u===s&&(u=f,n.charCodeAt(f)===95?(h=Pe,f++):(h=s,X===0&&re(pn)),h!==s&&(de=u,h=vi()),u=h),ee[b]={nextPos:f,result:u},u)}function Bg(){var u,h=f*49+45,b=ee[h];return b?(f=b.nextPos,b.result):(qo.test(n.charAt(f))?(u=n.charAt(f),f++):(u=s,X===0&&re(L1)),ee[h]={nextPos:f,result:u},u)}function xs(){var u,h,b,x=f*49+46,O=ee[x];if(O)return f=O.nextPos,O.result;if(u=f,h=[],b=je(),b!==s)for(;b!==s;)h.push(b),b=je();else h=c;return h!==s&&(de=u,h=$1(h)),u=h,ee[x]={nextPos:f,result:u},u}function Ug(){var u,h,b=f*49+47,x=ee[b];return x?(f=x.nextPos,x.result):(u=f,n.substr(f,2)===gg?(h=gg,f+=2):(h=s,X===0&&re(j1)),h!==s&&(de=u,h=H1()),u=h,u===s&&(u=f,n.substr(f,2)===_g?(h=_g,f+=2):(h=s,X===0&&re(W1)),h!==s&&(de=u,h=B1()),u=h,u===s&&(u=f,n.substr(f,2)===yg?(h=yg,f+=2):(h=s,X===0&&re(U1)),h!==s&&(de=u,h=z1()),u=h,u===s&&(u=f,n.substr(f,2)===bg?(h=bg,f+=2):(h=s,X===0&&re(V1)),h!==s&&(de=u,h=G1()),u=h,u===s&&(u=f,n.substr(f,2)===vg?(h=vg,f+=2):(h=s,X===0&&re(K1)),h!==s&&(de=u,h=Z1()),u=h,u===s&&(u=f,n.substr(f,2)===wg?(h=wg,f+=2):(h=s,X===0&&re(J1)),h!==s&&(de=u,h=Y1()),u=h,u===s&&(u=f,n.substr(f,2)===Sg?(h=Sg,f+=2):(h=s,X===0&&re(X1)),h!==s&&(de=u,h=Q1()),u=h,u===s&&(u=yO()))))))),ee[b]={nextPos:f,result:u},u)}function yO(){var u,h,b,x,O,H,V,le,Se,Be,Fe,nt=f*49+48,ct=ee[nt];return ct?(f=ct.nextPos,ct.result):(u=f,n.substr(f,2)===Eg?(h=Eg,f+=2):(h=s,X===0&&re(eO)),h!==s?(b=f,x=pr(),x!==s?(O=pr(),O!==s?(H=pr(),H!==s?(V=pr(),V!==s?(le=pr(),le!==s?(Se=pr(),Se!==s?(Be=pr(),Be!==s?(Fe=pr(),Fe!==s?(x=[x,O,H,V,le,Se,Be,Fe],b=x):(f=b,b=c)):(f=b,b=c)):(f=b,b=c)):(f=b,b=c)):(f=b,b=c)):(f=b,b=c)):(f=b,b=c)):(f=b,b=c),b!==s?(de=u,h=Rg(b),u=h):(f=u,u=c)):(f=u,u=c),u===s&&(u=f,n.substr(f,2)===Cg?(h=Cg,f+=2):(h=s,X===0&&re(tO)),h!==s?(b=f,x=pr(),x!==s?(O=pr(),O!==s?(H=pr(),H!==s?(V=pr(),V!==s?(x=[x,O,H,V],b=x):(f=b,b=c)):(f=b,b=c)):(f=b,b=c)):(f=b,b=c),b!==s?(de=u,h=Rg(b),u=h):(f=u,u=c)):(f=u,u=c)),ee[nt]={nextPos:f,result:u},u)}var zg=[];function bO(u,h,b){var x=new Error(u);throw x.line=h,x.column=b,x}function gl(u){zg.push(u);}function Ct(u,h,b,x,O){var H={type:u,value:h,line:b(),column:x()};return O&&(H.key=O),H}function vO(u,h,b){var x=parseInt("0x"+u);if(!isFinite(x)||Math.floor(x)!=x||x<0||x>1114111||x>55295&&x<57344)bO("Invalid Unicode escape code: "+u,h,b);else return wO(x)}function wO(){var u=16384,h=[],b,x,O=-1,H=arguments.length;if(!H)return "";for(var V="";++O>10)+55296,x=le%1024+56320,h.push(b,x)),(O+1==H||h.length>u)&&(V+=String.fromCharCode.apply(null,h),h.length=0);}return V}if($o=a(),$o!==s&&f===n.length)return $o;throw $o!==s&&f{function UU(t){var e=[],r=[],n="",i=Object.create(null),s=i;return a(t);function a(g){for(var S,I=0;I"u"?N===S.length-1?Q[te]=I:Q[te]=Object.create(null):N!==S.length-1&&r.indexOf(U)>-1&&l("Cannot redefine existing key '"+U+"'.",L,Z),Q=Q[te],Q instanceof Array&&Q.length&&N-1?'"'+g+'"':g}}wx.exports={compile:UU};});var Rx=A((x8,Ex)=>{var zU=vx(),VU=Sx();Ex.exports={parse:function(t){var e=zU.parse(t.toString());return VU.compile(e)}};});var Lx=A((ui,qx)=>{var sp=oe("crypto");ui=qx.exports=mo;function mo(t,e){return e=Fx(t,e),e3(t,e)}ui.sha1=function(t){return mo(t)};ui.keys=function(t){return mo(t,{excludeValues:!0,algorithm:"sha1",encoding:"hex"})};ui.MD5=function(t){return mo(t,{algorithm:"md5",encoding:"hex"})};ui.keysMD5=function(t){return mo(t,{algorithm:"md5",encoding:"hex",excludeValues:!0})};var Yi=sp.getHashes?sp.getHashes().slice():["sha1","md5"];Yi.push("passthrough");var kx=["buffer","hex","binary","base64"];function Fx(t,e){e=e||{};var r={};if(r.algorithm=e.algorithm||"sha1",r.encoding=e.encoding||"hex",r.excludeValues=!!e.excludeValues,r.algorithm=r.algorithm.toLowerCase(),r.encoding=r.encoding.toLowerCase(),r.ignoreUnknown=e.ignoreUnknown===!0,r.respectType=e.respectType!==!1,r.respectFunctionNames=e.respectFunctionNames!==!1,r.respectFunctionProperties=e.respectFunctionProperties!==!1,r.unorderedArrays=e.unorderedArrays===!0,r.unorderedSets=e.unorderedSets!==!1,r.unorderedObjects=e.unorderedObjects!==!1,r.replacer=e.replacer||void 0,r.excludeKeys=e.excludeKeys||void 0,typeof t>"u")throw new Error("Object argument required.");for(var n=0;n"u"&&(r.write=r.update,r.end=r.update);var n=op(e,r);if(n.dispatch(t),r.update||r.end(""),r.digest)return r.digest(e.encoding==="buffer"?void 0:e.encoding);var i=r.read();return e.encoding==="buffer"?i:i.toString(e.encoding)}ui.writeToStream=function(t,e,r){return typeof r>"u"&&(r=e,e={}),e=Fx(t,e),op(e,r).dispatch(t)};function op(t,e,r){r=r||[];var n=function(i){return e.update?e.update(i,"utf8"):e.write(i,"utf8")};return {dispatch:function(i){t.replacer&&(i=t.replacer(i));var s=typeof i;return i===null&&(s="null"),this["_"+s](i)},_object:function(i){var s=/\[object (.*)\]/i,o=Object.prototype.toString.call(i),a=s.exec(o);a?a=a[1]:a="unknown:["+o+"]",a=a.toLowerCase();var l=null;if((l=r.indexOf(i))>=0)return this.dispatch("[CIRCULAR:"+l+"]");if(r.push(i),typeof Buffer<"u"&&Buffer.isBuffer&&Buffer.isBuffer(i))return n("buffer:"),n(i);if(a!=="object"&&a!=="function"&&a!=="asyncfunction")if(this["_"+a])this["_"+a](i);else {if(t.ignoreUnknown)return n("["+a+"]");throw new Error('Unknown object type "'+a+'"')}else {var d=Object.keys(i);t.unorderedObjects&&(d=d.sort()),t.respectType!==!1&&!Mx(i)&&d.splice(0,0,"prototype","__proto__","constructor"),t.excludeKeys&&(d=d.filter(function(p){return !t.excludeKeys(p)})),n("object:"+d.length+":");var c=this;return d.forEach(function(p){c.dispatch(p),n(":"),t.excludeValues||c.dispatch(i[p]),n(",");})}},_array:function(i,s){s=typeof s<"u"?s:t.unorderedArrays!==!1;var o=this;if(n("array:"+i.length+":"),!s||i.length<=1)return i.forEach(function(d){return o.dispatch(d)});var a=[],l=i.map(function(d){var c=new Nx,p=r.slice(),m=op(t,c,p);return m.dispatch(d),a=a.concat(p.slice(r.length)),c.read().toString()});return r=r.concat(a),l.sort(),this._array(l,!1)},_date:function(i){return n("date:"+i.toJSON())},_symbol:function(i){return n("symbol:"+i.toString())},_error:function(i){return n("error:"+i.toString())},_boolean:function(i){return n("bool:"+i.toString())},_string:function(i){n("string:"+i.length+":"),n(i.toString());},_function:function(i){n("fn:"),Mx(i)?this.dispatch("[native]"):this.dispatch(i.toString()),t.respectFunctionNames!==!1&&this.dispatch("function-name:"+String(i.name)),t.respectFunctionProperties&&this._object(i);},_number:function(i){return n("number:"+i.toString())},_xml:function(i){return n("xml:"+i.toString())},_null:function(){return n("Null")},_undefined:function(){return n("Undefined")},_regexp:function(i){return n("regex:"+i.toString())},_uint8array:function(i){return n("uint8array:"),this.dispatch(Array.prototype.slice.call(i))},_uint8clampedarray:function(i){return n("uint8clampedarray:"),this.dispatch(Array.prototype.slice.call(i))},_int8array:function(i){return n("int8array:"),this.dispatch(Array.prototype.slice.call(i))},_uint16array:function(i){return n("uint16array:"),this.dispatch(Array.prototype.slice.call(i))},_int16array:function(i){return n("int16array:"),this.dispatch(Array.prototype.slice.call(i))},_uint32array:function(i){return n("uint32array:"),this.dispatch(Array.prototype.slice.call(i))},_int32array:function(i){return n("int32array:"),this.dispatch(Array.prototype.slice.call(i))},_float32array:function(i){return n("float32array:"),this.dispatch(Array.prototype.slice.call(i))},_float64array:function(i){return n("float64array:"),this.dispatch(Array.prototype.slice.call(i))},_arraybuffer:function(i){return n("arraybuffer:"),this.dispatch(new Uint8Array(i))},_url:function(i){return n("url:"+i.toString())},_map:function(i){n("map:");var s=Array.from(i);return this._array(s,t.unorderedSets!==!1)},_set:function(i){n("set:");var s=Array.from(i);return this._array(s,t.unorderedSets!==!1)},_file:function(i){return n("file:"),this.dispatch([i.name,i.size,i.type,i.lastModfied])},_blob:function(){if(t.ignoreUnknown)return n("[blob]");throw Error(`Hashing Blob objects is currently not supported -(see https://github.com/puleos/object-hash/issues/26) -Use "options.replacer" or "options.ignoreUnknown" -`)},_domwindow:function(){return n("domwindow")},_bigint:function(i){return n("bigint:"+i.toString())},_process:function(){return n("process")},_timer:function(){return n("timer")},_pipe:function(){return n("pipe")},_tcp:function(){return n("tcp")},_udp:function(){return n("udp")},_tty:function(){return n("tty")},_statwatcher:function(){return n("statwatcher")},_securecontext:function(){return n("securecontext")},_connection:function(){return n("connection")},_zlib:function(){return n("zlib")},_context:function(){return n("context")},_nodescript:function(){return n("nodescript")},_httpparser:function(){return n("httpparser")},_dataview:function(){return n("dataview")},_signal:function(){return n("signal")},_fsevent:function(){return n("fsevent")},_tlswrap:function(){return n("tlswrap")}}}function Nx(){return {buf:"",write:function(t){this.buf+=t;},end:function(t){this.buf+=t;},read:function(){return this.buf}}}});var zx=A((exports,module)=>{var Module=Module!==void 0?Module:{},TreeSitter=function(){var initPromise,document=typeof window=="object"?{currentScript:window.document.currentScript}:null;class Parser{constructor(){this.initialize();}initialize(){throw new Error("cannot construct a Parser before calling `init()`")}static init(moduleOptions){return initPromise||(Module=Object.assign({},Module,moduleOptions),initPromise=new Promise(resolveInitPromise=>{var moduleOverrides=Object.assign({},Module),arguments_=[],thisProgram="./this.program",quit_=(t,e)=>{throw e},ENVIRONMENT_IS_WEB=typeof window=="object",ENVIRONMENT_IS_WORKER=typeof importScripts=="function",ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string",scriptDirectory="",read_,readAsync,readBinary;function locateFile(t){return Module.locateFile?Module.locateFile(t,scriptDirectory):scriptDirectory+t}function logExceptionOnExit(t){t instanceof ExitStatus||err("exiting due to exception: "+t);}if(ENVIRONMENT_IS_NODE){var fs=oe("fs"),nodePath=oe("path");scriptDirectory=ENVIRONMENT_IS_WORKER?nodePath.dirname(scriptDirectory)+"/":__dirname+"/",read_=(t,e)=>(t=isFileURI(t)?new URL(t):nodePath.normalize(t),fs.readFileSync(t,e?void 0:"utf8")),readBinary=t=>{var e=read_(t,!0);return e.buffer||(e=new Uint8Array(e)),e},readAsync=(t,e,r)=>{t=isFileURI(t)?new URL(t):nodePath.normalize(t),fs.readFile(t,function(n,i){n?r(n):e(i.buffer);});},process.argv.length>1&&(thisProgram=process.argv[1].replace(/\\/g,"/")),arguments_=process.argv.slice(2),typeof module<"u"&&(module.exports=Module),quit_=(t,e)=>{if(keepRuntimeAlive())throw process.exitCode=t,e;logExceptionOnExit(e),process.exit(t);},Module.inspect=function(){return "[Emscripten Module object]"};}else (ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER)&&(ENVIRONMENT_IS_WORKER?scriptDirectory=self.location.href:document!==void 0&&document.currentScript&&(scriptDirectory=document.currentScript.src),scriptDirectory=scriptDirectory.indexOf("blob:")!==0?scriptDirectory.substr(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1):"",read_=t=>{var e=new XMLHttpRequest;return e.open("GET",t,!1),e.send(null),e.responseText},ENVIRONMENT_IS_WORKER&&(readBinary=t=>{var e=new XMLHttpRequest;return e.open("GET",t,!1),e.responseType="arraybuffer",e.send(null),new Uint8Array(e.response)}),readAsync=(t,e,r)=>{var n=new XMLHttpRequest;n.open("GET",t,!0),n.responseType="arraybuffer",n.onload=()=>{n.status==200||n.status==0&&n.response?e(n.response):r();},n.onerror=r,n.send(null);},t=>document.title=t);Module.print||console.log.bind(console);var err=Module.printErr||console.warn.bind(console);Object.assign(Module,moduleOverrides),moduleOverrides=null,Module.arguments&&(arguments_=Module.arguments),Module.thisProgram&&(thisProgram=Module.thisProgram),Module.quit&&(quit_=Module.quit);var STACK_ALIGN=16,dynamicLibraries=Module.dynamicLibraries||[],wasmBinary;Module.wasmBinary&&(wasmBinary=Module.wasmBinary);var noExitRuntime=Module.noExitRuntime||!0,wasmMemory;typeof WebAssembly!="object"&&abort("no native wasm support detected");var ABORT=!1,EXITSTATUS,UTF8Decoder=typeof TextDecoder<"u"?new TextDecoder("utf8"):void 0,buffer,HEAP8,HEAPU8,HEAP16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function UTF8ArrayToString(t,e,r){for(var n=e+r,i=e;t[i]&&!(i>=n);)++i;if(i-e>16&&t.buffer&&UTF8Decoder)return UTF8Decoder.decode(t.subarray(e,i));for(var s="";e>10,56320|1023&d);}}else s+=String.fromCharCode((31&o)<<6|a);}else s+=String.fromCharCode(o);}return s}function UTF8ToString(t,e){return t?UTF8ArrayToString(HEAPU8,t,e):""}function stringToUTF8Array(t,e,r,n){if(!(n>0))return 0;for(var i=r,s=r+n-1,o=0;o=55296&&a<=57343&&(a=65536+((1023&a)<<10)|1023&t.charCodeAt(++o)),a<=127){if(r>=s)break;e[r++]=a;}else if(a<=2047){if(r+1>=s)break;e[r++]=192|a>>6,e[r++]=128|63&a;}else if(a<=65535){if(r+2>=s)break;e[r++]=224|a>>12,e[r++]=128|a>>6&63,e[r++]=128|63&a;}else {if(r+3>=s)break;e[r++]=240|a>>18,e[r++]=128|a>>12&63,e[r++]=128|a>>6&63,e[r++]=128|63&a;}}return e[r]=0,r-i}function stringToUTF8(t,e,r){return stringToUTF8Array(t,HEAPU8,e,r)}function lengthBytesUTF8(t){for(var e=0,r=0;r=55296&&n<=57343?(e+=4,++r):e+=3;}return e}function updateGlobalBufferAndViews(t){buffer=t,Module.HEAP8=HEAP8=new Int8Array(t),Module.HEAP16=HEAP16=new Int16Array(t),Module.HEAP32=HEAP32=new Int32Array(t),Module.HEAPU8=HEAPU8=new Uint8Array(t),Module.HEAPU16=new Uint16Array(t),Module.HEAPU32=HEAPU32=new Uint32Array(t),Module.HEAPF32=HEAPF32=new Float32Array(t),Module.HEAPF64=HEAPF64=new Float64Array(t);}var INITIAL_MEMORY=Module.INITIAL_MEMORY||33554432;wasmMemory=Module.wasmMemory?Module.wasmMemory:new WebAssembly.Memory({initial:INITIAL_MEMORY/65536,maximum:32768}),wasmMemory&&(buffer=wasmMemory.buffer),INITIAL_MEMORY=buffer.byteLength,updateGlobalBufferAndViews(buffer);var wasmTable=new WebAssembly.Table({initial:20,element:"anyfunc"}),__ATPRERUN__=[],__ATINIT__=[],__ATMAIN__=[],__ATPOSTRUN__=[],__RELOC_FUNCS__=[],runtimeInitialized=!1;function keepRuntimeAlive(){return noExitRuntime}function preRun(){if(Module.preRun)for(typeof Module.preRun=="function"&&(Module.preRun=[Module.preRun]);Module.preRun.length;)addOnPreRun(Module.preRun.shift());callRuntimeCallbacks(__ATPRERUN__);}function initRuntime(){runtimeInitialized=!0,callRuntimeCallbacks(__RELOC_FUNCS__),callRuntimeCallbacks(__ATINIT__);}function preMain(){callRuntimeCallbacks(__ATMAIN__);}function postRun(){if(Module.postRun)for(typeof Module.postRun=="function"&&(Module.postRun=[Module.postRun]);Module.postRun.length;)addOnPostRun(Module.postRun.shift());callRuntimeCallbacks(__ATPOSTRUN__);}function addOnPreRun(t){__ATPRERUN__.unshift(t);}function addOnInit(t){__ATINIT__.unshift(t);}function addOnPostRun(t){__ATPOSTRUN__.unshift(t);}var runDependencies=0,dependenciesFulfilled=null;function addRunDependency(t){runDependencies++,Module.monitorRunDependencies&&Module.monitorRunDependencies(runDependencies);}function removeRunDependency(t){if(runDependencies--,Module.monitorRunDependencies&&Module.monitorRunDependencies(runDependencies),runDependencies==0&&(dependenciesFulfilled)){var e=dependenciesFulfilled;dependenciesFulfilled=null,e();}}function abort(t){throw Module.onAbort&&Module.onAbort(t),err(t="Aborted("+t+")"),ABORT=!0,EXITSTATUS=1,t+=". Build with -sASSERTIONS for more info.",new WebAssembly.RuntimeError(t)}var dataURIPrefix="data:application/octet-stream;base64,",wasmBinaryFile,tempDouble,tempI64;function isDataURI(t){return t.startsWith(dataURIPrefix)}function isFileURI(t){return t.startsWith("file://")}function getBinary(t){try{if(t==wasmBinaryFile&&wasmBinary)return new Uint8Array(wasmBinary);if(readBinary)return readBinary(t);throw "both async and sync fetching of the wasm failed"}catch(e){abort(e);}}function getBinaryPromise(){if(!wasmBinary&&(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER)){if(typeof fetch=="function"&&!isFileURI(wasmBinaryFile))return fetch(wasmBinaryFile,{credentials:"same-origin"}).then(function(t){if(!t.ok)throw "failed to load wasm binary file at '"+wasmBinaryFile+"'";return t.arrayBuffer()}).catch(function(){return getBinary(wasmBinaryFile)});if(readAsync)return new Promise(function(t,e){readAsync(wasmBinaryFile,function(r){t(new Uint8Array(r));},e);})}return Promise.resolve().then(function(){return getBinary(wasmBinaryFile)})}function createWasm(){var t={env:asmLibraryArg,wasi_snapshot_preview1:asmLibraryArg,"GOT.mem":new Proxy(asmLibraryArg,GOTHandler),"GOT.func":new Proxy(asmLibraryArg,GOTHandler)};function e(i,s){var o=i.exports;o=relocateExports(o,1024);var a=getDylinkMetadata(s);a.neededDynlibs&&(dynamicLibraries=a.neededDynlibs.concat(dynamicLibraries)),mergeLibSymbols(o),Module.asm=o,addOnInit(Module.asm.__wasm_call_ctors),__RELOC_FUNCS__.push(Module.asm.__wasm_apply_data_relocs),removeRunDependency();}function r(i){e(i.instance,i.module);}function n(i){return getBinaryPromise().then(function(s){return WebAssembly.instantiate(s,t)}).then(function(s){return s}).then(i,function(s){err("failed to asynchronously prepare wasm: "+s),abort(s);})}if(addRunDependency(),Module.instantiateWasm)try{return Module.instantiateWasm(t,e)}catch(i){return err("Module.instantiateWasm callback failed with error: "+i),!1}return wasmBinary||typeof WebAssembly.instantiateStreaming!="function"||isDataURI(wasmBinaryFile)||isFileURI(wasmBinaryFile)||ENVIRONMENT_IS_NODE||typeof fetch!="function"?n(r):fetch(wasmBinaryFile,{credentials:"same-origin"}).then(function(i){return WebAssembly.instantiateStreaming(i,t).then(r,function(s){return err("wasm streaming compile failed: "+s),err("falling back to ArrayBuffer instantiation"),n(r)})}),{}}wasmBinaryFile="tree-sitter.wasm",isDataURI(wasmBinaryFile)||(wasmBinaryFile=locateFile(wasmBinaryFile));function ExitStatus(t){this.name="ExitStatus",this.message="Program terminated with exit("+t+")",this.status=t;}var GOT={},CurrentModuleWeakSymbols=new Set([]),GOTHandler={get:function(t,e){var r=GOT[e];return r||(r=GOT[e]=new WebAssembly.Global({value:"i32",mutable:!0})),CurrentModuleWeakSymbols.has(e)||(r.required=!0),r}};function callRuntimeCallbacks(t){for(;t.length>0;)t.shift()(Module);}function getDylinkMetadata(t){var e=0,r=0;function n(){for(var D=0,g=1;;){var S=t[e++];if(D+=(127&S)*g,g*=128,!(128&S))break}return D}function i(){var D=n();return UTF8ArrayToString(t,(e+=D)-D,D)}function s(D,g){if(D)throw new Error(g)}var o="dylink.0";if(t instanceof WebAssembly.Module){var a=WebAssembly.Module.customSections(t,o);a.length===0&&(o="dylink",a=WebAssembly.Module.customSections(t,o)),s(a.length===0,"need dylink section"),r=(t=new Uint8Array(a[0])).length;}else {s(new Uint32Array(new Uint8Array(t.subarray(0,24)).buffer)[0]!=1836278016,"need to see wasm magic number"),s(t[8]!==0,"need the dylink section to be first"),e=9;var l=n();r=e+l,o=i();}var d={neededDynlibs:[],tlsExports:new Set,weakImports:new Set};if(o=="dylink"){d.memorySize=n(),d.memoryAlign=n(),d.tableSize=n(),d.tableAlign=n();for(var c=n(),p=0;p>0];case"i16":return HEAP16[t>>1];case"i32":case"i64":return HEAP32[t>>2];case"float":return HEAPF32[t>>2];case"double":return HEAPF64[t>>3];case"*":return HEAPU32[t>>2];default:abort("invalid type for getValue: "+e);}return null}function asmjsMangle(t){return t.indexOf("dynCall_")==0||["stackAlloc","stackSave","stackRestore","getTempRet0","setTempRet0"].includes(t)?t:"_"+t}function mergeLibSymbols(t,e){for(var r in t)if(t.hasOwnProperty(r)){asmLibraryArg.hasOwnProperty(r)||(asmLibraryArg[r]=t[r]);var n=asmjsMangle(r);Module.hasOwnProperty(n)||(Module[n]=t[r]),r=="__main_argc_argv"&&(Module._main=t[r]);}}var LDSO={loadedLibsByName:{},loadedLibsByHandle:{}};function dynCallLegacy(t,e,r){var n=Module["dynCall_"+t];return r&&r.length?n.apply(null,[e].concat(r)):n.call(null,e)}var wasmTableMirror=[];function getWasmTableEntry(t){var e=wasmTableMirror[t];return e||(t>=wasmTableMirror.length&&(wasmTableMirror.length=t+1),wasmTableMirror[t]=e=wasmTable.get(t)),e}function dynCall(t,e,r){return t.includes("j")?dynCallLegacy(t,e,r):getWasmTableEntry(e).apply(null,r)}function createInvokeFunction(t){return function(){var e=stackSave();try{return dynCall(t,arguments[0],Array.prototype.slice.call(arguments,1))}catch(r){if(stackRestore(e),r!==r+0)throw r;_setThrew(1,0);}}}var ___heap_base=78144;function zeroMemory(t,e){return HEAPU8.fill(0,t,t+e),t}function getMemory(t){if(runtimeInitialized)return zeroMemory(_malloc(t),t);var e=___heap_base,r=e+t+15&-16;return ___heap_base=r,GOT.__heap_base.value=r,e}function isInternalSym(t){return ["__cpp_exception","__c_longjmp","__wasm_apply_data_relocs","__dso_handle","__tls_size","__tls_align","__set_stack_limits","_emscripten_tls_init","__wasm_init_tls","__wasm_call_ctors","__start_em_asm","__stop_em_asm"].includes(t)}function uleb128Encode(t,e){t<128?e.push(t):e.push(t%128|128,t>>7);}function sigToWasmTypes(t){for(var e={i:"i32",j:"i32",f:"f32",d:"f64",p:"i32"},r={parameters:[],results:t[0]=="v"?[]:[e[t[0]]]},n=1;n>0];if(firstLoad){var memAlign=Math.pow(2,metadata.memoryAlign);memAlign=Math.max(memAlign,STACK_ALIGN);var memoryBase=metadata.memorySize?alignMemory(getMemory(metadata.memorySize+memAlign),memAlign):0,tableBase=metadata.tableSize?wasmTable.length:0;handle&&(HEAP8[handle+12>>0]=1,HEAPU32[handle+16>>2]=memoryBase,HEAP32[handle+20>>2]=metadata.memorySize,HEAPU32[handle+24>>2]=tableBase,HEAP32[handle+28>>2]=metadata.tableSize);}else memoryBase=HEAPU32[handle+16>>2],tableBase=HEAPU32[handle+24>>2];var tableGrowthNeeded=tableBase+metadata.tableSize-wasmTable.length,moduleExports;function resolveSymbol(t){var e=resolveGlobalSymbol(t,!1);return e||(e=moduleExports[t]),e}tableGrowthNeeded>0&&wasmTable.grow(tableGrowthNeeded);var proxyHandler={get:function(t,e){switch(e){case"__memory_base":return memoryBase;case"__table_base":return tableBase}if(e in asmLibraryArg)return asmLibraryArg[e];var r;return e in t||(t[e]=function(){return r||(r=resolveSymbol(e)),r.apply(null,arguments)}),t[e]}},proxy=new Proxy({},proxyHandler),info={"GOT.mem":new Proxy({},GOTHandler),"GOT.func":new Proxy({},GOTHandler),env:proxy,wasi_snapshot_preview1:proxy};function postInstantiation(instance){function addEmAsm(addr,body){for(var args=[],arity=0;arity<16&&body.indexOf("$"+arity)!=-1;arity++)args.push("$"+arity);args=args.join(",");var func="("+args+" ) => { "+body+"};";eval(func);}if(updateTableMap(tableBase,metadata.tableSize),moduleExports=relocateExports(instance.exports,memoryBase),flags.allowUndefined||reportUndefinedSymbols(),"__start_em_asm"in moduleExports)for(var start=moduleExports.__start_em_asm,stop=moduleExports.__stop_em_asm;startd(new Uint8Array(p)),c);});if(!readBinary)throw new Error(a+": file not found, and synchronous loading of external files is not available");return readBinary(a)}function s(){if(typeof preloadedWasm<"u"&&preloadedWasm[t]){var a=preloadedWasm[t];return e.loadAsync?Promise.resolve(a):a}return e.loadAsync?i(t).then(function(l){return loadWebAssemblyModule(l,e,r)}):loadWebAssemblyModule(i(t),e,r)}function o(a){n.global&&mergeLibSymbols(a),n.module=a;}return n={refcount:e.nodelete?1/0:1,name:t,module:"loading",global:e.global},LDSO.loadedLibsByName[t]=n,r&&(LDSO.loadedLibsByHandle[r]=n),e.loadAsync?s().then(function(a){return o(a),!0}):(o(s()),!0)}function reportUndefinedSymbols(){for(var t in GOT)if(GOT[t].value==0){var e=resolveGlobalSymbol(t,!0);if(!e&&!GOT[t].required)continue;if(typeof e=="function")GOT[t].value=addFunction(e,e.sig);else {if(typeof e!="number")throw new Error("bad export type for `"+t+"`: "+typeof e);GOT[t].value=e;}}}function preloadDylibs(){dynamicLibraries.length?(addRunDependency(),dynamicLibraries.reduce(function(t,e){return t.then(function(){return loadDynamicLibrary(e,{loadAsync:!0,global:!0,nodelete:!0,allowUndefined:!0})})},Promise.resolve()).then(function(){reportUndefinedSymbols(),removeRunDependency();})):reportUndefinedSymbols();}function setValue(t,e,r="i8"){switch(r.endsWith("*")&&(r="*"),r){case"i1":case"i8":HEAP8[t>>0]=e;break;case"i16":HEAP16[t>>1]=e;break;case"i32":HEAP32[t>>2]=e;break;case"i64":tempI64=[e>>>0,(tempDouble=e,+Math.abs(tempDouble)>=1?tempDouble>0?(0|Math.min(+Math.floor(tempDouble/4294967296),4294967295))>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[t>>2]=tempI64[0],HEAP32[t+4>>2]=tempI64[1];break;case"float":HEAPF32[t>>2]=e;break;case"double":HEAPF64[t>>3]=e;break;case"*":HEAPU32[t>>2]=e;break;default:abort("invalid type for setValue: "+r);}}var ___memory_base=new WebAssembly.Global({value:"i32",mutable:!1},1024),___stack_pointer=new WebAssembly.Global({value:"i32",mutable:!0},78144),___table_base=new WebAssembly.Global({value:"i32",mutable:!1},1),nowIsMonotonic=!0,_emscripten_get_now;function __emscripten_get_now_is_monotonic(){return nowIsMonotonic}function _abort(){abort("");}function _emscripten_memcpy_big(t,e,r){HEAPU8.copyWithin(t,e,e+r);}function getHeapMax(){return 2147483648}function emscripten_realloc_buffer(t){try{return wasmMemory.grow(t-buffer.byteLength+65535>>>16),updateGlobalBufferAndViews(wasmMemory.buffer),1}catch{}}function _emscripten_resize_heap(t){var e=HEAPU8.length;t>>>=0;var r=getHeapMax();if(t>r)return !1;for(var n=1;n<=4;n*=2){var i=e*(1+.2/n);if(i=Math.min(i,t+100663296),emscripten_realloc_buffer(Math.min(r,(s=Math.max(t,i))+((o=65536)-s%o)%o)))return !0}var s,o;return !1}__emscripten_get_now_is_monotonic.sig="i",Module._abort=_abort,_abort.sig="v",_emscripten_get_now=ENVIRONMENT_IS_NODE?()=>{var t=process.hrtime();return 1e3*t[0]+t[1]/1e6}:()=>performance.now(),_emscripten_get_now.sig="d",_emscripten_memcpy_big.sig="vppp",_emscripten_resize_heap.sig="ip";var SYSCALLS={DEFAULT_POLLMASK:5,calculateAt:function(t,e,r){if(PATH.isAbs(e))return e;var n;if(t===-100?n=FS.cwd():n=SYSCALLS.getStreamFromFD(t).path,e.length==0){if(!r)throw new FS.ErrnoError(44);return n}return PATH.join2(n,e)},doStat:function(t,e,r){try{var n=t(e);}catch(a){if(a&&a.node&&PATH.normalize(e)!==PATH.normalize(FS.getPath(a.node)))return -54;throw a}HEAP32[r>>2]=n.dev,HEAP32[r+8>>2]=n.ino,HEAP32[r+12>>2]=n.mode,HEAPU32[r+16>>2]=n.nlink,HEAP32[r+20>>2]=n.uid,HEAP32[r+24>>2]=n.gid,HEAP32[r+28>>2]=n.rdev,tempI64=[n.size>>>0,(tempDouble=n.size,+Math.abs(tempDouble)>=1?tempDouble>0?(0|Math.min(+Math.floor(tempDouble/4294967296),4294967295))>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[r+40>>2]=tempI64[0],HEAP32[r+44>>2]=tempI64[1],HEAP32[r+48>>2]=4096,HEAP32[r+52>>2]=n.blocks;var i=n.atime.getTime(),s=n.mtime.getTime(),o=n.ctime.getTime();return tempI64=[Math.floor(i/1e3)>>>0,(tempDouble=Math.floor(i/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?(0|Math.min(+Math.floor(tempDouble/4294967296),4294967295))>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[r+56>>2]=tempI64[0],HEAP32[r+60>>2]=tempI64[1],HEAPU32[r+64>>2]=i%1e3*1e3,tempI64=[Math.floor(s/1e3)>>>0,(tempDouble=Math.floor(s/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?(0|Math.min(+Math.floor(tempDouble/4294967296),4294967295))>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[r+72>>2]=tempI64[0],HEAP32[r+76>>2]=tempI64[1],HEAPU32[r+80>>2]=s%1e3*1e3,tempI64=[Math.floor(o/1e3)>>>0,(tempDouble=Math.floor(o/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?(0|Math.min(+Math.floor(tempDouble/4294967296),4294967295))>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[r+88>>2]=tempI64[0],HEAP32[r+92>>2]=tempI64[1],HEAPU32[r+96>>2]=o%1e3*1e3,tempI64=[n.ino>>>0,(tempDouble=n.ino,+Math.abs(tempDouble)>=1?tempDouble>0?(0|Math.min(+Math.floor(tempDouble/4294967296),4294967295))>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[r+104>>2]=tempI64[0],HEAP32[r+108>>2]=tempI64[1],0},doMsync:function(t,e,r,n,i){if(!FS.isFile(e.node.mode))throw new FS.ErrnoError(43);if(2&n)return 0;var s=HEAPU8.slice(t,t+r);FS.msync(e,s,i,r,n);},varargs:void 0,get:function(){return SYSCALLS.varargs+=4,HEAP32[SYSCALLS.varargs-4>>2]},getStr:function(t){return UTF8ToString(t)},getStreamFromFD:function(t){var e=FS.getStream(t);if(!e)throw new FS.ErrnoError(8);return e}};function _proc_exit(t){EXITSTATUS=t,keepRuntimeAlive()||(Module.onExit&&Module.onExit(t),ABORT=!0),quit_(t,new ExitStatus(t));}function exitJS(t,e){EXITSTATUS=t,_proc_exit(t);}_proc_exit.sig="vi";var _exit=exitJS;function _fd_close(t){try{var e=SYSCALLS.getStreamFromFD(t);return FS.close(e),0}catch(r){if(typeof FS>"u"||!(r instanceof FS.ErrnoError))throw r;return r.errno}}function convertI32PairToI53Checked(t,e){return e+2097152>>>0<4194305-!!t?(t>>>0)+4294967296*e:NaN}function _fd_seek(t,e,r,n,i){try{var s=convertI32PairToI53Checked(e,r);if(isNaN(s))return 61;var o=SYSCALLS.getStreamFromFD(t);return FS.llseek(o,s,n),tempI64=[o.position>>>0,(tempDouble=o.position,+Math.abs(tempDouble)>=1?tempDouble>0?(0|Math.min(+Math.floor(tempDouble/4294967296),4294967295))>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[i>>2]=tempI64[0],HEAP32[i+4>>2]=tempI64[1],o.getdents&&s===0&&n===0&&(o.getdents=null),0}catch(a){if(typeof FS>"u"||!(a instanceof FS.ErrnoError))throw a;return a.errno}}function doWritev(t,e,r,n){for(var i=0,s=0;s>2],a=HEAPU32[e+4>>2];e+=8;var l=FS.write(t,HEAP8,o,a,n);if(l<0)return -1;i+=l,n!==void 0&&(n+=l);}return i}function _fd_write(t,e,r,n){try{var i=doWritev(SYSCALLS.getStreamFromFD(t),e,r);return HEAPU32[n>>2]=i,0}catch(s){if(typeof FS>"u"||!(s instanceof FS.ErrnoError))throw s;return s.errno}}function _tree_sitter_log_callback(t,e){if(currentLogCallback){let r=UTF8ToString(e);currentLogCallback(r,t!==0);}}function _tree_sitter_parse_callback(t,e,r,n,i){var s=currentParseCallback(e,{row:r,column:n});typeof s=="string"?(setValue(i,s.length,"i32"),stringToUTF16(s,t,10240)):setValue(i,0,"i32");}function handleException(t){if(t instanceof ExitStatus||t=="unwind")return EXITSTATUS;quit_(1,t);}function allocateUTF8OnStack(t){var e=lengthBytesUTF8(t)+1,r=stackAlloc(e);return stringToUTF8Array(t,HEAP8,r,e),r}function stringToUTF16(t,e,r){if(r===void 0&&(r=2147483647),r<2)return 0;for(var n=e,i=(r-=2)<2*t.length?r/2:t.length,s=0;s>1]=o,e+=2;}return HEAP16[e>>1]=0,e-n}function AsciiToString(t){for(var e="";;){var r=HEAPU8[t++>>0];if(!r)return e;e+=String.fromCharCode(r);}}_exit.sig="vi",_fd_close.sig="ii",_fd_seek.sig="iijip",_fd_write.sig="iippp";var asmLibraryArg={__heap_base:___heap_base,__indirect_function_table:wasmTable,__memory_base:___memory_base,__stack_pointer:___stack_pointer,__table_base:___table_base,_emscripten_get_now_is_monotonic:__emscripten_get_now_is_monotonic,abort:_abort,emscripten_get_now:_emscripten_get_now,emscripten_memcpy_big:_emscripten_memcpy_big,emscripten_resize_heap:_emscripten_resize_heap,exit:_exit,fd_close:_fd_close,fd_seek:_fd_seek,fd_write:_fd_write,memory:wasmMemory,tree_sitter_log_callback:_tree_sitter_log_callback,tree_sitter_parse_callback:_tree_sitter_parse_callback};createWasm();Module.___wasm_call_ctors=function(){return (Module.___wasm_call_ctors=Module.asm.__wasm_call_ctors).apply(null,arguments)};Module.___wasm_apply_data_relocs=function(){return (Module.___wasm_apply_data_relocs=Module.asm.__wasm_apply_data_relocs).apply(null,arguments)};var _malloc=Module._malloc=function(){return (_malloc=Module._malloc=Module.asm.malloc).apply(null,arguments)};Module._calloc=function(){return (Module._calloc=Module.asm.calloc).apply(null,arguments)};Module._realloc=function(){return (Module._realloc=Module.asm.realloc).apply(null,arguments)};Module._free=function(){return (Module._free=Module.asm.free).apply(null,arguments)};Module._ts_language_symbol_count=function(){return (Module._ts_language_symbol_count=Module.asm.ts_language_symbol_count).apply(null,arguments)};Module._ts_language_version=function(){return (Module._ts_language_version=Module.asm.ts_language_version).apply(null,arguments)};Module._ts_language_field_count=function(){return (Module._ts_language_field_count=Module.asm.ts_language_field_count).apply(null,arguments)};Module._ts_language_symbol_name=function(){return (Module._ts_language_symbol_name=Module.asm.ts_language_symbol_name).apply(null,arguments)};Module._ts_language_symbol_for_name=function(){return (Module._ts_language_symbol_for_name=Module.asm.ts_language_symbol_for_name).apply(null,arguments)};Module._ts_language_symbol_type=function(){return (Module._ts_language_symbol_type=Module.asm.ts_language_symbol_type).apply(null,arguments)};Module._ts_language_field_name_for_id=function(){return (Module._ts_language_field_name_for_id=Module.asm.ts_language_field_name_for_id).apply(null,arguments)};Module._memset=function(){return (Module._memset=Module.asm.memset).apply(null,arguments)};Module._memcpy=function(){return (Module._memcpy=Module.asm.memcpy).apply(null,arguments)};Module._ts_parser_delete=function(){return (Module._ts_parser_delete=Module.asm.ts_parser_delete).apply(null,arguments)};Module._ts_parser_reset=function(){return (Module._ts_parser_reset=Module.asm.ts_parser_reset).apply(null,arguments)};Module._ts_parser_set_language=function(){return (Module._ts_parser_set_language=Module.asm.ts_parser_set_language).apply(null,arguments)};Module._ts_parser_timeout_micros=function(){return (Module._ts_parser_timeout_micros=Module.asm.ts_parser_timeout_micros).apply(null,arguments)};Module._ts_parser_set_timeout_micros=function(){return (Module._ts_parser_set_timeout_micros=Module.asm.ts_parser_set_timeout_micros).apply(null,arguments)};Module._memmove=function(){return (Module._memmove=Module.asm.memmove).apply(null,arguments)};Module._memcmp=function(){return (Module._memcmp=Module.asm.memcmp).apply(null,arguments)};Module._ts_query_new=function(){return (Module._ts_query_new=Module.asm.ts_query_new).apply(null,arguments)};Module._ts_query_delete=function(){return (Module._ts_query_delete=Module.asm.ts_query_delete).apply(null,arguments)};Module._iswspace=function(){return (Module._iswspace=Module.asm.iswspace).apply(null,arguments)};Module._iswalnum=function(){return (Module._iswalnum=Module.asm.iswalnum).apply(null,arguments)};Module._ts_query_pattern_count=function(){return (Module._ts_query_pattern_count=Module.asm.ts_query_pattern_count).apply(null,arguments)};Module._ts_query_capture_count=function(){return (Module._ts_query_capture_count=Module.asm.ts_query_capture_count).apply(null,arguments)};Module._ts_query_string_count=function(){return (Module._ts_query_string_count=Module.asm.ts_query_string_count).apply(null,arguments)};Module._ts_query_capture_name_for_id=function(){return (Module._ts_query_capture_name_for_id=Module.asm.ts_query_capture_name_for_id).apply(null,arguments)};Module._ts_query_string_value_for_id=function(){return (Module._ts_query_string_value_for_id=Module.asm.ts_query_string_value_for_id).apply(null,arguments)};Module._ts_query_predicates_for_pattern=function(){return (Module._ts_query_predicates_for_pattern=Module.asm.ts_query_predicates_for_pattern).apply(null,arguments)};Module._ts_tree_copy=function(){return (Module._ts_tree_copy=Module.asm.ts_tree_copy).apply(null,arguments)};Module._ts_tree_delete=function(){return (Module._ts_tree_delete=Module.asm.ts_tree_delete).apply(null,arguments)};Module._ts_init=function(){return (Module._ts_init=Module.asm.ts_init).apply(null,arguments)};Module._ts_parser_new_wasm=function(){return (Module._ts_parser_new_wasm=Module.asm.ts_parser_new_wasm).apply(null,arguments)};Module._ts_parser_enable_logger_wasm=function(){return (Module._ts_parser_enable_logger_wasm=Module.asm.ts_parser_enable_logger_wasm).apply(null,arguments)};Module._ts_parser_parse_wasm=function(){return (Module._ts_parser_parse_wasm=Module.asm.ts_parser_parse_wasm).apply(null,arguments)};Module._ts_language_type_is_named_wasm=function(){return (Module._ts_language_type_is_named_wasm=Module.asm.ts_language_type_is_named_wasm).apply(null,arguments)};Module._ts_language_type_is_visible_wasm=function(){return (Module._ts_language_type_is_visible_wasm=Module.asm.ts_language_type_is_visible_wasm).apply(null,arguments)};Module._ts_tree_root_node_wasm=function(){return (Module._ts_tree_root_node_wasm=Module.asm.ts_tree_root_node_wasm).apply(null,arguments)};Module._ts_tree_edit_wasm=function(){return (Module._ts_tree_edit_wasm=Module.asm.ts_tree_edit_wasm).apply(null,arguments)};Module._ts_tree_get_changed_ranges_wasm=function(){return (Module._ts_tree_get_changed_ranges_wasm=Module.asm.ts_tree_get_changed_ranges_wasm).apply(null,arguments)};Module._ts_tree_cursor_new_wasm=function(){return (Module._ts_tree_cursor_new_wasm=Module.asm.ts_tree_cursor_new_wasm).apply(null,arguments)};Module._ts_tree_cursor_delete_wasm=function(){return (Module._ts_tree_cursor_delete_wasm=Module.asm.ts_tree_cursor_delete_wasm).apply(null,arguments)};Module._ts_tree_cursor_reset_wasm=function(){return (Module._ts_tree_cursor_reset_wasm=Module.asm.ts_tree_cursor_reset_wasm).apply(null,arguments)};Module._ts_tree_cursor_goto_first_child_wasm=function(){return (Module._ts_tree_cursor_goto_first_child_wasm=Module.asm.ts_tree_cursor_goto_first_child_wasm).apply(null,arguments)};Module._ts_tree_cursor_goto_next_sibling_wasm=function(){return (Module._ts_tree_cursor_goto_next_sibling_wasm=Module.asm.ts_tree_cursor_goto_next_sibling_wasm).apply(null,arguments)};Module._ts_tree_cursor_goto_parent_wasm=function(){return (Module._ts_tree_cursor_goto_parent_wasm=Module.asm.ts_tree_cursor_goto_parent_wasm).apply(null,arguments)};Module._ts_tree_cursor_current_node_type_id_wasm=function(){return (Module._ts_tree_cursor_current_node_type_id_wasm=Module.asm.ts_tree_cursor_current_node_type_id_wasm).apply(null,arguments)};Module._ts_tree_cursor_current_node_is_named_wasm=function(){return (Module._ts_tree_cursor_current_node_is_named_wasm=Module.asm.ts_tree_cursor_current_node_is_named_wasm).apply(null,arguments)};Module._ts_tree_cursor_current_node_is_missing_wasm=function(){return (Module._ts_tree_cursor_current_node_is_missing_wasm=Module.asm.ts_tree_cursor_current_node_is_missing_wasm).apply(null,arguments)};Module._ts_tree_cursor_current_node_id_wasm=function(){return (Module._ts_tree_cursor_current_node_id_wasm=Module.asm.ts_tree_cursor_current_node_id_wasm).apply(null,arguments)};Module._ts_tree_cursor_start_position_wasm=function(){return (Module._ts_tree_cursor_start_position_wasm=Module.asm.ts_tree_cursor_start_position_wasm).apply(null,arguments)};Module._ts_tree_cursor_end_position_wasm=function(){return (Module._ts_tree_cursor_end_position_wasm=Module.asm.ts_tree_cursor_end_position_wasm).apply(null,arguments)};Module._ts_tree_cursor_start_index_wasm=function(){return (Module._ts_tree_cursor_start_index_wasm=Module.asm.ts_tree_cursor_start_index_wasm).apply(null,arguments)};Module._ts_tree_cursor_end_index_wasm=function(){return (Module._ts_tree_cursor_end_index_wasm=Module.asm.ts_tree_cursor_end_index_wasm).apply(null,arguments)};Module._ts_tree_cursor_current_field_id_wasm=function(){return (Module._ts_tree_cursor_current_field_id_wasm=Module.asm.ts_tree_cursor_current_field_id_wasm).apply(null,arguments)};Module._ts_tree_cursor_current_node_wasm=function(){return (Module._ts_tree_cursor_current_node_wasm=Module.asm.ts_tree_cursor_current_node_wasm).apply(null,arguments)};Module._ts_node_symbol_wasm=function(){return (Module._ts_node_symbol_wasm=Module.asm.ts_node_symbol_wasm).apply(null,arguments)};Module._ts_node_child_count_wasm=function(){return (Module._ts_node_child_count_wasm=Module.asm.ts_node_child_count_wasm).apply(null,arguments)};Module._ts_node_named_child_count_wasm=function(){return (Module._ts_node_named_child_count_wasm=Module.asm.ts_node_named_child_count_wasm).apply(null,arguments)};Module._ts_node_child_wasm=function(){return (Module._ts_node_child_wasm=Module.asm.ts_node_child_wasm).apply(null,arguments)};Module._ts_node_named_child_wasm=function(){return (Module._ts_node_named_child_wasm=Module.asm.ts_node_named_child_wasm).apply(null,arguments)};Module._ts_node_child_by_field_id_wasm=function(){return (Module._ts_node_child_by_field_id_wasm=Module.asm.ts_node_child_by_field_id_wasm).apply(null,arguments)};Module._ts_node_next_sibling_wasm=function(){return (Module._ts_node_next_sibling_wasm=Module.asm.ts_node_next_sibling_wasm).apply(null,arguments)};Module._ts_node_prev_sibling_wasm=function(){return (Module._ts_node_prev_sibling_wasm=Module.asm.ts_node_prev_sibling_wasm).apply(null,arguments)};Module._ts_node_next_named_sibling_wasm=function(){return (Module._ts_node_next_named_sibling_wasm=Module.asm.ts_node_next_named_sibling_wasm).apply(null,arguments)};Module._ts_node_prev_named_sibling_wasm=function(){return (Module._ts_node_prev_named_sibling_wasm=Module.asm.ts_node_prev_named_sibling_wasm).apply(null,arguments)};Module._ts_node_parent_wasm=function(){return (Module._ts_node_parent_wasm=Module.asm.ts_node_parent_wasm).apply(null,arguments)};Module._ts_node_descendant_for_index_wasm=function(){return (Module._ts_node_descendant_for_index_wasm=Module.asm.ts_node_descendant_for_index_wasm).apply(null,arguments)};Module._ts_node_named_descendant_for_index_wasm=function(){return (Module._ts_node_named_descendant_for_index_wasm=Module.asm.ts_node_named_descendant_for_index_wasm).apply(null,arguments)};Module._ts_node_descendant_for_position_wasm=function(){return (Module._ts_node_descendant_for_position_wasm=Module.asm.ts_node_descendant_for_position_wasm).apply(null,arguments)};Module._ts_node_named_descendant_for_position_wasm=function(){return (Module._ts_node_named_descendant_for_position_wasm=Module.asm.ts_node_named_descendant_for_position_wasm).apply(null,arguments)};Module._ts_node_start_point_wasm=function(){return (Module._ts_node_start_point_wasm=Module.asm.ts_node_start_point_wasm).apply(null,arguments)};Module._ts_node_end_point_wasm=function(){return (Module._ts_node_end_point_wasm=Module.asm.ts_node_end_point_wasm).apply(null,arguments)};Module._ts_node_start_index_wasm=function(){return (Module._ts_node_start_index_wasm=Module.asm.ts_node_start_index_wasm).apply(null,arguments)};Module._ts_node_end_index_wasm=function(){return (Module._ts_node_end_index_wasm=Module.asm.ts_node_end_index_wasm).apply(null,arguments)};Module._ts_node_to_string_wasm=function(){return (Module._ts_node_to_string_wasm=Module.asm.ts_node_to_string_wasm).apply(null,arguments)};Module._ts_node_children_wasm=function(){return (Module._ts_node_children_wasm=Module.asm.ts_node_children_wasm).apply(null,arguments)};Module._ts_node_named_children_wasm=function(){return (Module._ts_node_named_children_wasm=Module.asm.ts_node_named_children_wasm).apply(null,arguments)};Module._ts_node_descendants_of_type_wasm=function(){return (Module._ts_node_descendants_of_type_wasm=Module.asm.ts_node_descendants_of_type_wasm).apply(null,arguments)};Module._ts_node_is_named_wasm=function(){return (Module._ts_node_is_named_wasm=Module.asm.ts_node_is_named_wasm).apply(null,arguments)};Module._ts_node_has_changes_wasm=function(){return (Module._ts_node_has_changes_wasm=Module.asm.ts_node_has_changes_wasm).apply(null,arguments)};Module._ts_node_has_error_wasm=function(){return (Module._ts_node_has_error_wasm=Module.asm.ts_node_has_error_wasm).apply(null,arguments)};Module._ts_node_is_missing_wasm=function(){return (Module._ts_node_is_missing_wasm=Module.asm.ts_node_is_missing_wasm).apply(null,arguments)};Module._ts_query_matches_wasm=function(){return (Module._ts_query_matches_wasm=Module.asm.ts_query_matches_wasm).apply(null,arguments)};Module._ts_query_captures_wasm=function(){return (Module._ts_query_captures_wasm=Module.asm.ts_query_captures_wasm).apply(null,arguments)};Module.___cxa_atexit=function(){return (Module.___cxa_atexit=Module.asm.__cxa_atexit).apply(null,arguments)};Module._iswdigit=function(){return (Module._iswdigit=Module.asm.iswdigit).apply(null,arguments)};Module._iswalpha=function(){return (Module._iswalpha=Module.asm.iswalpha).apply(null,arguments)};Module._iswlower=function(){return (Module._iswlower=Module.asm.iswlower).apply(null,arguments)};Module._memchr=function(){return (Module._memchr=Module.asm.memchr).apply(null,arguments)};Module._strlen=function(){return (Module._strlen=Module.asm.strlen).apply(null,arguments)};Module._towupper=function(){return (Module._towupper=Module.asm.towupper).apply(null,arguments)};var _setThrew=Module._setThrew=function(){return (_setThrew=Module._setThrew=Module.asm.setThrew).apply(null,arguments)},stackSave=Module.stackSave=function(){return (stackSave=Module.stackSave=Module.asm.stackSave).apply(null,arguments)},stackRestore=Module.stackRestore=function(){return (stackRestore=Module.stackRestore=Module.asm.stackRestore).apply(null,arguments)},stackAlloc=Module.stackAlloc=function(){return (stackAlloc=Module.stackAlloc=Module.asm.stackAlloc).apply(null,arguments)};Module.__Znwm=function(){return (Module.__Znwm=Module.asm._Znwm).apply(null,arguments)};Module.__ZdlPv=function(){return (Module.__ZdlPv=Module.asm._ZdlPv).apply(null,arguments)};Module.__ZNSt3__212basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEED2Ev=function(){return (Module.__ZNSt3__212basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEED2Ev=Module.asm._ZNSt3__212basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEED2Ev).apply(null,arguments)};Module.__ZNSt3__212basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE9__grow_byEmmmmmm=function(){return (Module.__ZNSt3__212basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE9__grow_byEmmmmmm=Module.asm._ZNSt3__212basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE9__grow_byEmmmmmm).apply(null,arguments)};Module.__ZNSt3__212basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE6__initEPKcm=function(){return (Module.__ZNSt3__212basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE6__initEPKcm=Module.asm._ZNSt3__212basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE6__initEPKcm).apply(null,arguments)};Module.__ZNSt3__212basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE7reserveEm=function(){return (Module.__ZNSt3__212basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE7reserveEm=Module.asm._ZNSt3__212basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE7reserveEm).apply(null,arguments)};Module.__ZNKSt3__212basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE4copyEPcmm=function(){return (Module.__ZNKSt3__212basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE4copyEPcmm=Module.asm._ZNKSt3__212basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE4copyEPcmm).apply(null,arguments)};Module.__ZNSt3__212basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE9push_backEc=function(){return (Module.__ZNSt3__212basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE9push_backEc=Module.asm._ZNSt3__212basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE9push_backEc).apply(null,arguments)};Module.__ZNSt3__212basic_stringIwNS_11char_traitsIwEENS_9allocatorIwEEED2Ev=function(){return (Module.__ZNSt3__212basic_stringIwNS_11char_traitsIwEENS_9allocatorIwEEED2Ev=Module.asm._ZNSt3__212basic_stringIwNS_11char_traitsIwEENS_9allocatorIwEEED2Ev).apply(null,arguments)};Module.__ZNSt3__212basic_stringIwNS_11char_traitsIwEENS_9allocatorIwEEE9push_backEw=function(){return (Module.__ZNSt3__212basic_stringIwNS_11char_traitsIwEENS_9allocatorIwEEE9push_backEw=Module.asm._ZNSt3__212basic_stringIwNS_11char_traitsIwEENS_9allocatorIwEEE9push_backEw).apply(null,arguments)};Module.__ZNSt3__212basic_stringIwNS_11char_traitsIwEENS_9allocatorIwEEE6resizeEmw=function(){return (Module.__ZNSt3__212basic_stringIwNS_11char_traitsIwEENS_9allocatorIwEEE6resizeEmw=Module.asm._ZNSt3__212basic_stringIwNS_11char_traitsIwEENS_9allocatorIwEEE6resizeEmw).apply(null,arguments)};Module.dynCall_jiji=function(){return (Module.dynCall_jiji=Module.asm.dynCall_jiji).apply(null,arguments)};Module._orig$ts_parser_timeout_micros=function(){return (Module._orig$ts_parser_timeout_micros=Module.asm.orig$ts_parser_timeout_micros).apply(null,arguments)};Module._orig$ts_parser_set_timeout_micros=function(){return (Module._orig$ts_parser_set_timeout_micros=Module.asm.orig$ts_parser_set_timeout_micros).apply(null,arguments)};var calledRun;function callMain(t){var e=Module._main;if(e){(t=t||[]).unshift(thisProgram);var r=t.length,n=stackAlloc(4*(r+1)),i=n>>2;t.forEach(o=>{HEAP32[i++]=allocateUTF8OnStack(o);}),HEAP32[i]=0;try{var s=e(r,n);return exitJS(s),s}catch(o){return handleException(o)}}}Module.AsciiToString=AsciiToString,Module.stringToUTF16=stringToUTF16,dependenciesFulfilled=function t(){calledRun||run(),calledRun||(dependenciesFulfilled=t);};var dylibsLoaded=!1;function run(t){function e(){calledRun||(calledRun=!0,Module.calledRun=!0,ABORT||(initRuntime(),preMain(),Module.onRuntimeInitialized&&Module.onRuntimeInitialized(),shouldRunNow&&callMain(t),postRun()));}t=t||arguments_,runDependencies>0||!dylibsLoaded&&(preloadDylibs(),dylibsLoaded=!0,runDependencies>0)||(preRun(),runDependencies>0||(Module.setStatus?(Module.setStatus("Running..."),setTimeout(function(){setTimeout(function(){Module.setStatus("");},1),e();},1)):e()));}if(Module.preInit)for(typeof Module.preInit=="function"&&(Module.preInit=[Module.preInit]);Module.preInit.length>0;)Module.preInit.pop()();var shouldRunNow=!0;Module.noInitialRun&&(shouldRunNow=!1),run();let C=Module,INTERNAL={},SIZE_OF_INT=4,SIZE_OF_NODE=5*SIZE_OF_INT,SIZE_OF_POINT=2*SIZE_OF_INT,SIZE_OF_RANGE=2*SIZE_OF_INT+2*SIZE_OF_POINT,ZERO_POINT={row:0,column:0},QUERY_WORD_REGEX=/[\w-.]*/g,PREDICATE_STEP_TYPE_CAPTURE=1,PREDICATE_STEP_TYPE_STRING=2,LANGUAGE_FUNCTION_REGEX=/^_?tree_sitter_\w+/;var VERSION,MIN_COMPATIBLE_VERSION,TRANSFER_BUFFER,currentParseCallback,currentLogCallback;class ParserImpl{static init(){TRANSFER_BUFFER=C._ts_init(),VERSION=getValue(TRANSFER_BUFFER,"i32"),MIN_COMPATIBLE_VERSION=getValue(TRANSFER_BUFFER+SIZE_OF_INT,"i32");}initialize(){C._ts_parser_new_wasm(),this[0]=getValue(TRANSFER_BUFFER,"i32"),this[1]=getValue(TRANSFER_BUFFER+SIZE_OF_INT,"i32");}delete(){C._ts_parser_delete(this[0]),C._free(this[1]),this[0]=0,this[1]=0;}setLanguage(e){let r;if(e){if(e.constructor!==Language)throw new Error("Argument must be a Language");{r=e[0];let n=C._ts_language_version(r);if(ne.slice(l,c);else {if(typeof e!="function")throw new Error("Argument must be a string or a function");currentParseCallback=e;}this.logCallback?(currentLogCallback=this.logCallback,C._ts_parser_enable_logger_wasm(this[0],1)):(currentLogCallback=null,C._ts_parser_enable_logger_wasm(this[0],0));let i=0,s=0;if(n&&n.includedRanges){i=n.includedRanges.length,s=C._calloc(i,SIZE_OF_RANGE);let l=s;for(let d=0;d0){let s=n;for(let o=0;o0){let n=r;for(let i=0;i0){let n=r;for(let i=0;i0){let c=l;for(let p=0;p0){if(g[0].type!=="string")throw new Error("Predicates must begin with a literal value");let K=g[0].value,U=!0;switch(K){case"not-eq?":U=!1;case"eq?":if(g.length!==3)throw new Error("Wrong number of arguments to `#eq?` predicate. Expected 2, got "+(g.length-1));if(g[1].type!=="capture")throw new Error(`First argument of \`#eq?\` predicate must be a capture. Got "${g[1].value}"`);if(g[2].type==="capture"){let te=g[1].name,ae=g[2].name;w[E].push(function(ye){let q,W;for(let me of ye)me.name===te&&(q=me.node),me.name===ae&&(W=me.node);return q===void 0||W===void 0||q.text===W.text===U});}else {let te=g[1].name,ae=g[2].value;w[E].push(function(ye){for(let q of ye)if(q.name===te)return q.node.text===ae===U;return !0});}break;case"not-match?":U=!1;case"match?":if(g.length!==3)throw new Error(`Wrong number of arguments to \`#match?\` predicate. Expected 2, got ${g.length-1}.`);if(g[1].type!=="capture")throw new Error(`First argument of \`#match?\` predicate must be a capture. Got "${g[1].value}".`);if(g[2].type!=="string")throw new Error(`Second argument of \`#match?\` predicate must be a string. Got @${g[2].value}.`);let B=g[1].name,Q=new RegExp(g[2].value);w[E].push(function(te){for(let ae of te)if(ae.name===B)return Q.test(ae.node.text)===U;return !0});break;case"set!":if(g.length<2||g.length>3)throw new Error(`Wrong number of arguments to \`#set!\` predicate. Expected 1 or 2. Got ${g.length-1}.`);if(g.some(te=>te.type!=="string"))throw new Error('Arguments to `#set!` predicate must be a strings.".');c[E]||(c[E]={}),c[E][g[1].value]=g[2]?g[2].value:null;break;case"is?":case"is-not?":if(g.length<2||g.length>3)throw new Error(`Wrong number of arguments to \`#${K}\` predicate. Expected 1 or 2. Got ${g.length-1}.`);if(g.some(te=>te.type!=="string"))throw new Error(`Arguments to \`#${K}\` predicate must be a strings.".`);let N=K==="is?"?p:m;N[E]||(N[E]={}),N[E][g[1].value]=g[2]?g[2].value:null;break;default:_[E].push({operator:K,operands:g.slice(1)});}g.length=0;}}Object.freeze(c[E]),Object.freeze(p[E]),Object.freeze(m[E]);}return C._free(n),new Query(INTERNAL,i,l,w,_,Object.freeze(c),Object.freeze(p),Object.freeze(m))}static load(e){let r;if(e instanceof Uint8Array)r=Promise.resolve(e);else {let i=e;if(typeof process<"u"&&process.versions&&process.versions.node){let s=oe("fs");r=Promise.resolve(s.readFileSync(i));}else r=fetch(i).then(s=>s.arrayBuffer().then(o=>{if(s.ok)return new Uint8Array(o);{let a=new TextDecoder("utf-8").decode(o);throw new Error(`Language.load failed with status ${s.status}. - -${a}`)}}));}let n=typeof loadSideModule=="function"?loadSideModule:loadWebAssemblyModule;return r.then(i=>n(i,{loadAsync:!0})).then(i=>{let s=Object.keys(i),o=s.find(l=>LANGUAGE_FUNCTION_REGEX.test(l)&&!l.includes("external_scanner_"));o||console.log(`Couldn't find language function in WASM file. Symbols: -${JSON.stringify(s,null,2)}`);let a=i[o]();return new Language(INTERNAL,a)})}}class Query{constructor(e,r,n,i,s,o,a,l){assertInternal(e),this[0]=r,this.captureNames=n,this.textPredicates=i,this.predicates=s,this.setProperties=o,this.assertedProperties=a,this.refutedProperties=l,this.exceededMatchLimit=!1;}delete(){C._ts_query_delete(this[0]),this[0]=0;}matches(e,r,n,i){r||(r=ZERO_POINT),n||(n=ZERO_POINT),i||(i={});let s=i.matchLimit;if(s===void 0)s=0;else if(typeof s!="number")throw new Error("Arguments must be numbers");marshalNode(e),C._ts_query_matches_wasm(this[0],e.tree[0],r.row,r.column,n.row,n.column,s);let o=getValue(TRANSFER_BUFFER,"i32"),a=getValue(TRANSFER_BUFFER+SIZE_OF_INT,"i32"),l=getValue(TRANSFER_BUFFER+2*SIZE_OF_INT,"i32"),d=new Array(o);this.exceededMatchLimit=!!l;let c=0,p=a;for(let m=0;mP(E))){d[c++]={pattern:_,captures:E};let P=this.setProperties[_];P&&(d[m].setProperties=P);let D=this.assertedProperties[_];D&&(d[m].assertedProperties=D);let g=this.refutedProperties[_];g&&(d[m].refutedProperties=g);}}return d.length=c,C._free(a),d}captures(e,r,n,i){r||(r=ZERO_POINT),n||(n=ZERO_POINT),i||(i={});let s=i.matchLimit;if(s===void 0)s=0;else if(typeof s!="number")throw new Error("Arguments must be numbers");marshalNode(e),C._ts_query_captures_wasm(this[0],e.tree[0],r.row,r.column,n.row,n.column,s);let o=getValue(TRANSFER_BUFFER,"i32"),a=getValue(TRANSFER_BUFFER+SIZE_OF_INT,"i32"),l=getValue(TRANSFER_BUFFER+2*SIZE_OF_INT,"i32"),d=[];this.exceededMatchLimit=!!l;let c=[],p=a;for(let m=0;mP(c))){let P=c[E],D=this.setProperties[_];D&&(P.setProperties=D);let g=this.assertedProperties[_];g&&(P.assertedProperties=g);let S=this.refutedProperties[_];S&&(P.refutedProperties=S),d.push(P);}}return C._free(a),d}predicatesForPattern(e){return this.predicates[e]}didExceedMatchLimit(){return this.exceededMatchLimit}}function getText(t,e,r){let n=r-e,i=t.textCallback(e,null,r);for(e+=i.length;e0))break;e+=s.length,i+=s;}return e>r&&(i=i.slice(0,n)),i}function unmarshalCaptures(t,e,r,n){for(let i=0,s=n.length;i{ParserImpl.init(),resolveInitPromise();};}))}}return Parser}();typeof exports=="object"&&(module.exports=TreeSitter);});var pp=A((D5,aT)=>{var hp=class{constructor(e={}){let r=e.base||1.001,n=e.precision||1e-9;if(!(r>1)||!(r<1.5))throw new Error("base must be a number between 1 and 1.5");if(r+=1e-9,r=2**(1/Math.ceil(Math.log(2)/Math.log(r))),r===1)throw new Error("base too close to 1");n=Number.parseFloat(""+n);let i=Math.ceil(1/(r-1)),s=n*i;this._thresh=s,this._precision=n,this._base=r;}getBase(){return this._base}getPrecision(){return this._precision}round(e){if(typeof e!="number"&&(e=Number.parseFloat(e)),Number.isNaN(e))throw new Error("Attempt to round a non-numeric value: "+e);return e<0?-this.round(-e):ee)return yo(e,t,r);let n=-Math.floor(Math.log(e-t)/Math.log(r));for(;Math.ceil(t*r**n)<=Math.floor(e*r**n);)n--;return n++,n>=0?Math.ceil(t*r**n)/r**n:Math.ceil(t/r**-n)*r**-n}aT.exports={Binning:hp,shorten:yo};});var cT=A((O5,uT)=>{var{Binning:d3}=pp(),h3="stats-logscale/univariate@1.0",bo=class t extends d3{constructor(e={}){super(e),this.storage=new Map,this._count=0,this._cache={},this.neat=new vo(this),e.bins&&this.addWeighted(e.bins);}add(...e){return this._cache={},e.forEach(r=>{let n=this.round(r),i=this.storage.get(n)??0;this.storage.set(n,i+1),this._count++;}),this}addWeighted(e){return this._cache={},e.forEach(r=>{let n=r[0],i=Number.parseFloat(r[1]);if(Number.isNaN(i))throw new Error("Attempt to provide a non-numeric weight");let s=this.round(n),o=(this.storage.get(s)??0)+i;o<=0?(this.storage.delete(s),this._count+=i-o):(this.storage.set(s,o),this._count+=i);}),this}toJSON(){return {version:h3,precision:this.getPrecision(),base:this.getBase(),bins:this.getBins()}}clone(e={}){let r=this.getBins(e);return e.transform&&(r=r.map(n=>[e.transform(n[0]),n[1]])),new t({precision:e.precision??this.getPrecision(),base:e.base??this.getBase(),bins:r})}getBins(e){if(this._cache.data||(this._cache.data=[...this.storage].sort((a,l)=>a[0]-l[0])),!e)return this._cache.data;let r=Math.max(e.min??-1/0,this.percentile(e.ltrim??0)),n=Math.min(e.max??1/0,this.percentile(100-(e.rtrim??0)));if(!e.winsorize)return this._cache.data.filter(a=>a[0]>=r&&a[0]<=n);let i=[this.round(r),0],s=[this.round(n),0],o=[i];for(let[a,l]of this._cache.data)a<=i[0]?i[1]+=l:a>=s[0]?s[1]+=l:o.push([a,l]);return s[1]>0&&o.push(s),o}count(){return this._count}min(){let e=this.getBins();return this.lower(e[0][0])}max(){let e=this.getBins();return this.upper(e[e.length-1][0])}sumOf(e){let r=0;return [...this.storage].forEach(n=>{r+=n[1]*e(n[0]);}),r}E(e){return this._count?this.sumOf(e)/this._count:void 0}mean(){return this._count?this.sumOf(e=>e)/this._count:void 0}stdev(){if(this._count<2)return;let e=this.mean();return Math.sqrt(this.sumOf(r=>(r-e)*(r-e))/(this._count-1))}skewness(){let e=this.count();return e<3?void 0:e*e/((e-1)*(e-2))*this.momentStd(3)}kurtosis(){let e=this.count();if(e<4)return;let r=e*e*(e+1)/((e-1)*(e-2)*(e-3)),n=(e-1)*(e-1)/((e-2)*(e-3));return this.momentStd(4)*r-3*n}moment(e,r){if(!Number.isInteger(e))throw new Error("Cannot calculate non-integer moment (did you mean momentAbs?)");return r===void 0&&(r=this.mean()),this.E(n=>(n-r)**e)}momentAbs(e=1,r){return r===void 0&&(r=this.mean()),this.E(n=>Math.abs(n-r)**e)}momentStd(e){return this.moment(e)/this.stdev()**e}quantile(e){let r=e*this._count,n=this._cumulative(),i=0,s=n.length;for(;i+1=r?s=l:i=l;}let o=this.lower(n[i][0]),a=this.upper(n[i][0])-o;return o+a*(r-n[i][1])/(n[i][2]-n[i][1])}percentile(e){return this.quantile(e/100)}median(){return this.quantile(.5)}cdf(e){return this._rawCdf(e)/this._count}_rawCdf(e){let r=this._cumulative(),n=this.round(e),i=0,s=r.length;for(;i=r.length)return this._count;let o=i>0?r[i-1][2]:0,a=n!==r[i][0]?0:(r[i][2]-r[i][1])*(e-this.lower(e))/(this.upper(e)-this.lower(e));return o+a}histogram(e={}){if(!this._count)return [];let r=this.min(),n=this.max(),i=e.count||10,s=[],o=r,a=(n-r)/i;for(let l=0;l1;)s[l][0]-=s[l-1][0];if(s[0][0]-=this._rawCdf(r),e.scale){let l=0;for(let d=0;d{let e=!!t.match(/\+/);e&&(t=t.replace("+",""));let r=bo.prototype[t];if(typeof r!="function")throw new Error('method "'+t+'" is cached but never defined');bo.prototype[t]=e?function(...n){if(this._count===0)return;this._cache[t]===void 0&&(this._cache[t]={});let i=n.join(":");return this._cache[t][i]===void 0&&(this._cache[t][i]=r.apply(this,n)),this._cache[t][i]}:function(){if(this._count!==0)return this._cache[t]===void 0&&(this._cache[t]=r.apply(this)),this._cache[t]};});var vo=class{constructor(e){this._main=e;}min(){if(!this._main._count)return;let e=this._main.getBins();return this._main.shorten(e[0][0])}max(){if(!this._main._count)return;let e=this._main.getBins();return this._main.shorten(e[e.length-1][0])}};["E","kurtosis","mean","median","moment","momentAbs","momentStd","percentile","quantile","skewness","stdev","sumOf"].forEach(t=>{vo.prototype[t]=function(e){return this._main.shorten(this._main[t](e))};});["cdf","count"].forEach(t=>{vo.prototype[t]=function(e){return this._main[t](e)};});uT.exports={Univariate:bo};});var lT=A((I5,mp)=>{(()=>{let{Binning:t}=pp(),{Univariate:e}=cT(),r={Binning:t,Univariate:e};typeof window<"u"&&(window.logstat=r),typeof mp=="object"&&(mp.exports=r);})();});var Uu=A(pt=>{Object.defineProperty(pt,"__esModule",{value:!0});pt.thenable=pt.typedArray=pt.stringArray=pt.array=pt.func=pt.error=pt.number=pt.string=pt.boolean=void 0;function g3(t){return t===!0||t===!1}pt.boolean=g3;function dT(t){return typeof t=="string"||t instanceof String}pt.string=dT;function _3(t){return typeof t=="number"||t instanceof Number}pt.number=_3;function y3(t){return t instanceof Error}pt.error=y3;function hT(t){return typeof t=="function"}pt.func=hT;function pT(t){return Array.isArray(t)}pt.array=pT;function b3(t){return pT(t)&&t.every(e=>dT(e))}pt.stringArray=b3;function v3(t,e){return Array.isArray(t)&&t.every(e)}pt.typedArray=v3;function w3(t){return t&&hT(t.then)}pt.thenable=w3;});var Qi=A(It=>{Object.defineProperty(It,"__esModule",{value:!0});It.stringArray=It.array=It.func=It.error=It.number=It.string=It.boolean=void 0;function S3(t){return t===!0||t===!1}It.boolean=S3;function mT(t){return typeof t=="string"||t instanceof String}It.string=mT;function E3(t){return typeof t=="number"||t instanceof Number}It.number=E3;function R3(t){return t instanceof Error}It.error=R3;function C3(t){return typeof t=="function"}It.func=C3;function gT(t){return Array.isArray(t)}It.array=gT;function x3(t){return gT(t)&&t.every(e=>mT(e))}It.stringArray=x3;});var Hp=A(_e=>{Object.defineProperty(_e,"__esModule",{value:!0});_e.Message=_e.NotificationType9=_e.NotificationType8=_e.NotificationType7=_e.NotificationType6=_e.NotificationType5=_e.NotificationType4=_e.NotificationType3=_e.NotificationType2=_e.NotificationType1=_e.NotificationType0=_e.NotificationType=_e.RequestType9=_e.RequestType8=_e.RequestType7=_e.RequestType6=_e.RequestType5=_e.RequestType4=_e.RequestType3=_e.RequestType2=_e.RequestType1=_e.RequestType=_e.RequestType0=_e.AbstractMessageSignature=_e.ParameterStructures=_e.ResponseError=_e.ErrorCodes=void 0;var li=Qi(),_p;(function(t){t.ParseError=-32700,t.InvalidRequest=-32600,t.MethodNotFound=-32601,t.InvalidParams=-32602,t.InternalError=-32603,t.jsonrpcReservedErrorRangeStart=-32099,t.serverErrorStart=-32099,t.MessageWriteError=-32099,t.MessageReadError=-32098,t.PendingResponseRejected=-32097,t.ConnectionInactive=-32096,t.ServerNotInitialized=-32002,t.UnknownErrorCode=-32001,t.jsonrpcReservedErrorRangeEnd=-32e3,t.serverErrorEnd=-32e3;})(_p||(_e.ErrorCodes=_p={}));var yp=class t extends Error{constructor(e,r,n){super(r),this.code=li.number(e)?e:_p.UnknownErrorCode,this.data=n,Object.setPrototypeOf(this,t.prototype);}toJson(){let e={code:this.code,message:this.message};return this.data!==void 0&&(e.data=this.data),e}};_e.ResponseError=yp;var Jt=class t{constructor(e){this.kind=e;}static is(e){return e===t.auto||e===t.byName||e===t.byPosition}toString(){return this.kind}};_e.ParameterStructures=Jt;Jt.auto=new Jt("auto");Jt.byPosition=new Jt("byPosition");Jt.byName=new Jt("byName");var Qe=class{constructor(e,r){this.method=e,this.numberOfParams=r;}get parameterStructures(){return Jt.auto}};_e.AbstractMessageSignature=Qe;var bp=class extends Qe{constructor(e){super(e,0);}};_e.RequestType0=bp;var vp=class extends Qe{constructor(e,r=Jt.auto){super(e,1),this._parameterStructures=r;}get parameterStructures(){return this._parameterStructures}};_e.RequestType=vp;var wp=class extends Qe{constructor(e,r=Jt.auto){super(e,1),this._parameterStructures=r;}get parameterStructures(){return this._parameterStructures}};_e.RequestType1=wp;var Sp=class extends Qe{constructor(e){super(e,2);}};_e.RequestType2=Sp;var Ep=class extends Qe{constructor(e){super(e,3);}};_e.RequestType3=Ep;var Rp=class extends Qe{constructor(e){super(e,4);}};_e.RequestType4=Rp;var Cp=class extends Qe{constructor(e){super(e,5);}};_e.RequestType5=Cp;var xp=class extends Qe{constructor(e){super(e,6);}};_e.RequestType6=xp;var Tp=class extends Qe{constructor(e){super(e,7);}};_e.RequestType7=Tp;var Ap=class extends Qe{constructor(e){super(e,8);}};_e.RequestType8=Ap;var Pp=class extends Qe{constructor(e){super(e,9);}};_e.RequestType9=Pp;var Dp=class extends Qe{constructor(e,r=Jt.auto){super(e,1),this._parameterStructures=r;}get parameterStructures(){return this._parameterStructures}};_e.NotificationType=Dp;var Op=class extends Qe{constructor(e){super(e,0);}};_e.NotificationType0=Op;var Ip=class extends Qe{constructor(e,r=Jt.auto){super(e,1),this._parameterStructures=r;}get parameterStructures(){return this._parameterStructures}};_e.NotificationType1=Ip;var kp=class extends Qe{constructor(e){super(e,2);}};_e.NotificationType2=kp;var Mp=class extends Qe{constructor(e){super(e,3);}};_e.NotificationType3=Mp;var Fp=class extends Qe{constructor(e){super(e,4);}};_e.NotificationType4=Fp;var Np=class extends Qe{constructor(e){super(e,5);}};_e.NotificationType5=Np;var qp=class extends Qe{constructor(e){super(e,6);}};_e.NotificationType6=qp;var Lp=class extends Qe{constructor(e){super(e,7);}};_e.NotificationType7=Lp;var $p=class extends Qe{constructor(e){super(e,8);}};_e.NotificationType8=$p;var jp=class extends Qe{constructor(e){super(e,9);}};_e.NotificationType9=jp;var _T;(function(t){function e(i){let s=i;return s&&li.string(s.method)&&(li.string(s.id)||li.number(s.id))}t.isRequest=e;function r(i){let s=i;return s&&li.string(s.method)&&i.id===void 0}t.isNotification=r;function n(i){let s=i;return s&&(s.result!==void 0||!!s.error)&&(li.string(s.id)||li.number(s.id)||s.id===null)}t.isResponse=n;})(_T||(_e.Message=_T={}));});var Bp=A(Pn=>{var yT;Object.defineProperty(Pn,"__esModule",{value:!0});Pn.LRUCache=Pn.LinkedMap=Pn.Touch=void 0;var kt;(function(t){t.None=0,t.First=1,t.AsOld=t.First,t.Last=2,t.AsNew=t.Last;})(kt||(Pn.Touch=kt={}));var zu=class{constructor(){this[yT]="LinkedMap",this._map=new Map,this._head=void 0,this._tail=void 0,this._size=0,this._state=0;}clear(){this._map.clear(),this._head=void 0,this._tail=void 0,this._size=0,this._state++;}isEmpty(){return !this._head&&!this._tail}get size(){return this._size}get first(){return this._head?.value}get last(){return this._tail?.value}has(e){return this._map.has(e)}get(e,r=kt.None){let n=this._map.get(e);if(n)return r!==kt.None&&this.touch(n,r),n.value}set(e,r,n=kt.None){let i=this._map.get(e);if(i)i.value=r,n!==kt.None&&this.touch(i,n);else {switch(i={key:e,value:r,next:void 0,previous:void 0},n){case kt.None:this.addItemLast(i);break;case kt.First:this.addItemFirst(i);break;case kt.Last:this.addItemLast(i);break;default:this.addItemLast(i);break}this._map.set(e,i),this._size++;}return this}delete(e){return !!this.remove(e)}remove(e){let r=this._map.get(e);if(r)return this._map.delete(e),this.removeItem(r),this._size--,r.value}shift(){if(!this._head&&!this._tail)return;if(!this._head||!this._tail)throw new Error("Invalid list");let e=this._head;return this._map.delete(e.key),this.removeItem(e),this._size--,e.value}forEach(e,r){let n=this._state,i=this._head;for(;i;){if(r?e.bind(r)(i.value,i.key,this):e(i.value,i.key,this),this._state!==n)throw new Error("LinkedMap got modified during iteration.");i=i.next;}}keys(){let e=this._state,r=this._head,n={[Symbol.iterator]:()=>n,next:()=>{if(this._state!==e)throw new Error("LinkedMap got modified during iteration.");if(r){let i={value:r.key,done:!1};return r=r.next,i}else return {value:void 0,done:!0}}};return n}values(){let e=this._state,r=this._head,n={[Symbol.iterator]:()=>n,next:()=>{if(this._state!==e)throw new Error("LinkedMap got modified during iteration.");if(r){let i={value:r.value,done:!1};return r=r.next,i}else return {value:void 0,done:!0}}};return n}entries(){let e=this._state,r=this._head,n={[Symbol.iterator]:()=>n,next:()=>{if(this._state!==e)throw new Error("LinkedMap got modified during iteration.");if(r){let i={value:[r.key,r.value],done:!1};return r=r.next,i}else return {value:void 0,done:!0}}};return n}[(yT=Symbol.toStringTag,Symbol.iterator)](){return this.entries()}trimOld(e){if(e>=this.size)return;if(e===0){this.clear();return}let r=this._head,n=this.size;for(;r&&n>e;)this._map.delete(r.key),r=r.next,n--;this._head=r,this._size=n,r&&(r.previous=void 0),this._state++;}addItemFirst(e){if(!this._head&&!this._tail)this._tail=e;else if(this._head)e.next=this._head,this._head.previous=e;else throw new Error("Invalid list");this._head=e,this._state++;}addItemLast(e){if(!this._head&&!this._tail)this._head=e;else if(this._tail)e.previous=this._tail,this._tail.next=e;else throw new Error("Invalid list");this._tail=e,this._state++;}removeItem(e){if(e===this._head&&e===this._tail)this._head=void 0,this._tail=void 0;else if(e===this._head){if(!e.next)throw new Error("Invalid list");e.next.previous=void 0,this._head=e.next;}else if(e===this._tail){if(!e.previous)throw new Error("Invalid list");e.previous.next=void 0,this._tail=e.previous;}else {let r=e.next,n=e.previous;if(!r||!n)throw new Error("Invalid list");r.previous=n,n.next=r;}e.next=void 0,e.previous=void 0,this._state++;}touch(e,r){if(!this._head||!this._tail)throw new Error("Invalid list");if(!(r!==kt.First&&r!==kt.Last)){if(r===kt.First){if(e===this._head)return;let n=e.next,i=e.previous;e===this._tail?(i.next=void 0,this._tail=i):(n.previous=i,i.next=n),e.previous=void 0,e.next=this._head,this._head.previous=e,this._head=e,this._state++;}else if(r===kt.Last){if(e===this._tail)return;let n=e.next,i=e.previous;e===this._head?(n.previous=void 0,this._head=n):(n.previous=i,i.next=n),e.next=void 0,e.previous=this._tail,this._tail.next=e,this._tail=e,this._state++;}}}toJSON(){let e=[];return this.forEach((r,n)=>{e.push([n,r]);}),e}fromJSON(e){this.clear();for(let[r,n]of e)this.set(r,n);}};Pn.LinkedMap=zu;var Wp=class extends zu{constructor(e,r=1){super(),this._limit=e,this._ratio=Math.min(Math.max(0,r),1);}get limit(){return this._limit}set limit(e){this._limit=e,this.checkTrim();}get ratio(){return this._ratio}set ratio(e){this._ratio=Math.min(Math.max(0,e),1),this.checkTrim();}get(e,r=kt.AsNew){return super.get(e,r)}peek(e){return super.get(e,kt.None)}set(e,r){return super.set(e,r,kt.Last),this.checkTrim(),this}checkTrim(){this.size>this._limit&&this.trimOld(Math.round(this._limit*this._ratio));}};Pn.LRUCache=Wp;});var vT=A(Vu=>{Object.defineProperty(Vu,"__esModule",{value:!0});Vu.Disposable=void 0;var bT;(function(t){function e(r){return {dispose:r}}t.create=e;})(bT||(Vu.Disposable=bT={}));});var Dn=A(Vp=>{Object.defineProperty(Vp,"__esModule",{value:!0});var Up;function zp(){if(Up===void 0)throw new Error("No runtime abstraction layer installed");return Up}(function(t){function e(r){if(r===void 0)throw new Error("No runtime abstraction layer provided");Up=r;}t.install=e;})(zp||(zp={}));Vp.default=zp;});var ts=A(es=>{Object.defineProperty(es,"__esModule",{value:!0});es.Emitter=es.Event=void 0;var T3=Dn(),wT;(function(t){let e={dispose(){}};t.None=function(){return e};})(wT||(es.Event=wT={}));var Gp=class{add(e,r=null,n){this._callbacks||(this._callbacks=[],this._contexts=[]),this._callbacks.push(e),this._contexts.push(r),Array.isArray(n)&&n.push({dispose:()=>this.remove(e,r)});}remove(e,r=null){if(!this._callbacks)return;let n=!1;for(let i=0,s=this._callbacks.length;i{this._callbacks||(this._callbacks=new Gp),this._options&&this._options.onFirstListenerAdd&&this._callbacks.isEmpty()&&this._options.onFirstListenerAdd(this),this._callbacks.add(e,r);let i={dispose:()=>{this._callbacks&&(this._callbacks.remove(e,r),i.dispose=t._noop,this._options&&this._options.onLastListenerRemove&&this._callbacks.isEmpty()&&this._options.onLastListenerRemove(this));}};return Array.isArray(n)&&n.push(i),i}),this._event}fire(e){this._callbacks&&this._callbacks.invoke.call(this._callbacks,e);}dispose(){this._callbacks&&(this._callbacks.dispose(),this._callbacks=void 0);}};es.Emitter=Gu;Gu._noop=function(){};});var Ju=A(rs=>{Object.defineProperty(rs,"__esModule",{value:!0});rs.CancellationTokenSource=rs.CancellationToken=void 0;var A3=Dn(),P3=Qi(),Kp=ts(),Ku;(function(t){t.None=Object.freeze({isCancellationRequested:!1,onCancellationRequested:Kp.Event.None}),t.Cancelled=Object.freeze({isCancellationRequested:!0,onCancellationRequested:Kp.Event.None});function e(r){let n=r;return n&&(n===t.None||n===t.Cancelled||P3.boolean(n.isCancellationRequested)&&!!n.onCancellationRequested)}t.is=e;})(Ku||(rs.CancellationToken=Ku={}));var D3=Object.freeze(function(t,e){let r=(0, A3.default)().timer.setTimeout(t.bind(e),0);return {dispose(){r.dispose();}}}),Zu=class{constructor(){this._isCancelled=!1;}cancel(){this._isCancelled||(this._isCancelled=!0,this._emitter&&(this._emitter.fire(void 0),this.dispose()));}get isCancellationRequested(){return this._isCancelled}get onCancellationRequested(){return this._isCancelled?D3:(this._emitter||(this._emitter=new Kp.Emitter),this._emitter.event)}dispose(){this._emitter&&(this._emitter.dispose(),this._emitter=void 0);}},Zp=class{get token(){return this._token||(this._token=new Zu),this._token}cancel(){this._token?this._token.cancel():this._token=Ku.Cancelled;}dispose(){this._token?this._token instanceof Zu&&this._token.dispose():this._token=Ku.None;}};rs.CancellationTokenSource=Zp;});var ST=A(ns=>{Object.defineProperty(ns,"__esModule",{value:!0});ns.SharedArrayReceiverStrategy=ns.SharedArraySenderStrategy=void 0;var O3=Ju(),wo;(function(t){t.Continue=0,t.Cancelled=1;})(wo||(wo={}));var Jp=class{constructor(){this.buffers=new Map;}enableCancellation(e){if(e.id===null)return;let r=new SharedArrayBuffer(4),n=new Int32Array(r,0,1);n[0]=wo.Continue,this.buffers.set(e.id,r),e.$cancellationData=r;}async sendCancellation(e,r){let n=this.buffers.get(r);if(n===void 0)return;let i=new Int32Array(n,0,1);Atomics.store(i,0,wo.Cancelled);}cleanup(e){this.buffers.delete(e);}dispose(){this.buffers.clear();}};ns.SharedArraySenderStrategy=Jp;var Yp=class{constructor(e){this.data=new Int32Array(e,0,1);}get isCancellationRequested(){return Atomics.load(this.data,0)===wo.Cancelled}get onCancellationRequested(){throw new Error("Cancellation over SharedArrayBuffer doesn't support cancellation events")}},Xp=class{constructor(e){this.token=new Yp(e);}cancel(){}dispose(){}},Qp=class{constructor(){this.kind="request";}createCancellationTokenSource(e){let r=e.$cancellationData;return r===void 0?new O3.CancellationTokenSource:new Xp(r)}};ns.SharedArrayReceiverStrategy=Qp;});var tm=A(Yu=>{Object.defineProperty(Yu,"__esModule",{value:!0});Yu.Semaphore=void 0;var I3=Dn(),em=class{constructor(e=1){if(e<=0)throw new Error("Capacity must be greater than 0");this._capacity=e,this._active=0,this._waiting=[];}lock(e){return new Promise((r,n)=>{this._waiting.push({thunk:e,resolve:r,reject:n}),this.runNext();})}get active(){return this._active}runNext(){this._waiting.length===0||this._active===this._capacity||(0, I3.default)().timer.setImmediate(()=>this.doRunNext());}doRunNext(){if(this._waiting.length===0||this._active===this._capacity)return;let e=this._waiting.shift();if(this._active++,this._active>this._capacity)throw new Error("To many thunks active");try{let r=e.thunk();r instanceof Promise?r.then(n=>{this._active--,e.resolve(n),this.runNext();},n=>{this._active--,e.reject(n),this.runNext();}):(this._active--,e.resolve(r),this.runNext());}catch(r){this._active--,e.reject(r),this.runNext();}}};Yu.Semaphore=em;});var RT=A(On=>{Object.defineProperty(On,"__esModule",{value:!0});On.ReadableStreamMessageReader=On.AbstractMessageReader=On.MessageReader=void 0;var nm=Dn(),is=Qi(),rm=ts(),k3=tm(),ET;(function(t){function e(r){let n=r;return n&&is.func(n.listen)&&is.func(n.dispose)&&is.func(n.onError)&&is.func(n.onClose)&&is.func(n.onPartialMessage)}t.is=e;})(ET||(On.MessageReader=ET={}));var Xu=class{constructor(){this.errorEmitter=new rm.Emitter,this.closeEmitter=new rm.Emitter,this.partialMessageEmitter=new rm.Emitter;}dispose(){this.errorEmitter.dispose(),this.closeEmitter.dispose();}get onError(){return this.errorEmitter.event}fireError(e){this.errorEmitter.fire(this.asError(e));}get onClose(){return this.closeEmitter.event}fireClose(){this.closeEmitter.fire(void 0);}get onPartialMessage(){return this.partialMessageEmitter.event}firePartialMessage(e){this.partialMessageEmitter.fire(e);}asError(e){return e instanceof Error?e:new Error(`Reader received error. Reason: ${is.string(e.message)?e.message:"unknown"}`)}};On.AbstractMessageReader=Xu;var im;(function(t){function e(r){let n,s,o=new Map,a,l=new Map;if(r===void 0||typeof r=="string")n=r??"utf-8";else {if(n=r.charset??"utf-8",r.contentDecoder!==void 0&&(s=r.contentDecoder,o.set(s.name,s)),r.contentDecoders!==void 0)for(let d of r.contentDecoders)o.set(d.name,d);if(r.contentTypeDecoder!==void 0&&(a=r.contentTypeDecoder,l.set(a.name,a)),r.contentTypeDecoders!==void 0)for(let d of r.contentTypeDecoders)l.set(d.name,d);}return a===void 0&&(a=(0, nm.default)().applicationJson.decoder,l.set(a.name,a)),{charset:n,contentDecoder:s,contentDecoders:o,contentTypeDecoder:a,contentTypeDecoders:l}}t.fromOptions=e;})(im||(im={}));var sm=class extends Xu{constructor(e,r){super(),this.readable=e,this.options=im.fromOptions(r),this.buffer=(0, nm.default)().messageBuffer.create(this.options.charset),this._partialMessageTimeout=1e4,this.nextMessageLength=-1,this.messageToken=0,this.readSemaphore=new k3.Semaphore(1);}set partialMessageTimeout(e){this._partialMessageTimeout=e;}get partialMessageTimeout(){return this._partialMessageTimeout}listen(e){this.nextMessageLength=-1,this.messageToken=0,this.partialMessageTimer=void 0,this.callback=e;let r=this.readable.onData(n=>{this.onData(n);});return this.readable.onError(n=>this.fireError(n)),this.readable.onClose(()=>this.fireClose()),r}onData(e){try{for(this.buffer.append(e);;){if(this.nextMessageLength===-1){let n=this.buffer.tryReadHeaders(!0);if(!n)return;let i=n.get("content-length");if(!i){this.fireError(new Error(`Header must provide a Content-Length property. -${JSON.stringify(Object.fromEntries(n))}`));return}let s=parseInt(i);if(isNaN(s)){this.fireError(new Error(`Content-Length value must be a number. Got ${i}`));return}this.nextMessageLength=s;}let r=this.buffer.tryReadBody(this.nextMessageLength);if(r===void 0){this.setPartialMessageTimer();return}this.clearPartialMessageTimer(),this.nextMessageLength=-1,this.readSemaphore.lock(async()=>{let n=this.options.contentDecoder!==void 0?await this.options.contentDecoder.decode(r):r,i=await this.options.contentTypeDecoder.decode(n,this.options);this.callback(i);}).catch(n=>{this.fireError(n);});}}catch(r){this.fireError(r);}}clearPartialMessageTimer(){this.partialMessageTimer&&(this.partialMessageTimer.dispose(),this.partialMessageTimer=void 0);}setPartialMessageTimer(){this.clearPartialMessageTimer(),!(this._partialMessageTimeout<=0)&&(this.partialMessageTimer=(0, nm.default)().timer.setTimeout((e,r)=>{this.partialMessageTimer=void 0,e===this.messageToken&&(this.firePartialMessage({messageToken:e,waitingTime:r}),this.setPartialMessageTimer());},this._partialMessageTimeout,this.messageToken,this._partialMessageTimeout));}};On.ReadableStreamMessageReader=sm;});var PT=A(In=>{Object.defineProperty(In,"__esModule",{value:!0});In.WriteableStreamMessageWriter=In.AbstractMessageWriter=In.MessageWriter=void 0;var CT=Dn(),So=Qi(),M3=tm(),xT=ts(),F3="Content-Length: ",TT=`\r -`,AT;(function(t){function e(r){let n=r;return n&&So.func(n.dispose)&&So.func(n.onClose)&&So.func(n.onError)&&So.func(n.write)}t.is=e;})(AT||(In.MessageWriter=AT={}));var Qu=class{constructor(){this.errorEmitter=new xT.Emitter,this.closeEmitter=new xT.Emitter;}dispose(){this.errorEmitter.dispose(),this.closeEmitter.dispose();}get onError(){return this.errorEmitter.event}fireError(e,r,n){this.errorEmitter.fire([this.asError(e),r,n]);}get onClose(){return this.closeEmitter.event}fireClose(){this.closeEmitter.fire(void 0);}asError(e){return e instanceof Error?e:new Error(`Writer received error. Reason: ${So.string(e.message)?e.message:"unknown"}`)}};In.AbstractMessageWriter=Qu;var om;(function(t){function e(r){return r===void 0||typeof r=="string"?{charset:r??"utf-8",contentTypeEncoder:(0, CT.default)().applicationJson.encoder}:{charset:r.charset??"utf-8",contentEncoder:r.contentEncoder,contentTypeEncoder:r.contentTypeEncoder??(0, CT.default)().applicationJson.encoder}}t.fromOptions=e;})(om||(om={}));var am=class extends Qu{constructor(e,r){super(),this.writable=e,this.options=om.fromOptions(r),this.errorCount=0,this.writeSemaphore=new M3.Semaphore(1),this.writable.onError(n=>this.fireError(n)),this.writable.onClose(()=>this.fireClose());}async write(e){return this.writeSemaphore.lock(async()=>this.options.contentTypeEncoder.encode(e,this.options).then(n=>this.options.contentEncoder!==void 0?this.options.contentEncoder.encode(n):n).then(n=>{let i=[];return i.push(F3,n.byteLength.toString(),TT),i.push(TT),this.doWrite(e,i,n)},n=>{throw this.fireError(n),n}))}async doWrite(e,r,n){try{return await this.writable.write(r.join(""),"ascii"),this.writable.write(n)}catch(i){return this.handleError(i,e),Promise.reject(i)}}handleError(e,r){this.errorCount++,this.fireError(e,r,this.errorCount);}end(){this.writable.end();}};In.WriteableStreamMessageWriter=am;});var DT=A(ec=>{Object.defineProperty(ec,"__esModule",{value:!0});ec.AbstractMessageBuffer=void 0;var N3=13,q3=10,L3=`\r -`,um=class{constructor(e="utf-8"){this._encoding=e,this._chunks=[],this._totalLength=0;}get encoding(){return this._encoding}append(e){let r=typeof e=="string"?this.fromString(e,this._encoding):e;this._chunks.push(r),this._totalLength+=r.byteLength;}tryReadHeaders(e=!1){if(this._chunks.length===0)return;let r=0,n=0,i=0,s=0;e:for(;nthis._totalLength)throw new Error("Cannot read so many bytes!");if(this._chunks[0].byteLength===e){let s=this._chunks[0];return this._chunks.shift(),this._totalLength-=e,this.asNative(s)}if(this._chunks[0].byteLength>e){let s=this._chunks[0],o=this.asNative(s,e);return this._chunks[0]=s.slice(e),this._totalLength-=e,o}let r=this.allocNative(e),n=0,i=0;for(;e>0;){let s=this._chunks[i];if(s.byteLength>e){let o=s.slice(0,e);r.set(o,n),n+=e,this._chunks[i]=s.slice(e),this._totalLength-=e,e-=e;}else r.set(s,n),n+=s.byteLength,this._chunks.shift(),this._totalLength-=s.byteLength,e-=s.byteLength;}return r}};ec.AbstractMessageBuffer=um;});var FT=A(xe=>{Object.defineProperty(xe,"__esModule",{value:!0});xe.createMessageConnection=xe.ConnectionOptions=xe.MessageStrategy=xe.CancellationStrategy=xe.CancellationSenderStrategy=xe.CancellationReceiverStrategy=xe.RequestCancellationReceiverStrategy=xe.IdCancellationReceiverStrategy=xe.ConnectionStrategy=xe.ConnectionError=xe.ConnectionErrors=xe.LogTraceNotification=xe.SetTraceNotification=xe.TraceFormat=xe.TraceValues=xe.Trace=xe.NullLogger=xe.ProgressType=xe.ProgressToken=void 0;var OT=Dn(),st=Qi(),ve=Hp(),IT=Bp(),Eo=ts(),cm=Ju(),xo;(function(t){t.type=new ve.NotificationType("$/cancelRequest");})(xo||(xo={}));var lm;(function(t){function e(r){return typeof r=="string"||typeof r=="number"}t.is=e;})(lm||(xe.ProgressToken=lm={}));var Ro;(function(t){t.type=new ve.NotificationType("$/progress");})(Ro||(Ro={}));var fm=class{constructor(){}};xe.ProgressType=fm;var dm;(function(t){function e(r){return st.func(r)}t.is=e;})(dm||(dm={}));xe.NullLogger=Object.freeze({error:()=>{},warn:()=>{},info:()=>{},log:()=>{}});var Le;(function(t){t[t.Off=0]="Off",t[t.Messages=1]="Messages",t[t.Compact=2]="Compact",t[t.Verbose=3]="Verbose";})(Le||(xe.Trace=Le={}));var kT;(function(t){t.Off="off",t.Messages="messages",t.Compact="compact",t.Verbose="verbose";})(kT||(xe.TraceValues=kT={}));(function(t){function e(n){if(!st.string(n))return t.Off;switch(n=n.toLowerCase(),n){case"off":return t.Off;case"messages":return t.Messages;case"compact":return t.Compact;case"verbose":return t.Verbose;default:return t.Off}}t.fromString=e;function r(n){switch(n){case t.Off:return "off";case t.Messages:return "messages";case t.Compact:return "compact";case t.Verbose:return "verbose";default:return "off"}}t.toString=r;})(Le||(xe.Trace=Le={}));var cr;(function(t){t.Text="text",t.JSON="json";})(cr||(xe.TraceFormat=cr={}));(function(t){function e(r){return st.string(r)?(r=r.toLowerCase(),r==="json"?t.JSON:t.Text):t.Text}t.fromString=e;})(cr||(xe.TraceFormat=cr={}));var hm;(function(t){t.type=new ve.NotificationType("$/setTrace");})(hm||(xe.SetTraceNotification=hm={}));var tc;(function(t){t.type=new ve.NotificationType("$/logTrace");})(tc||(xe.LogTraceNotification=tc={}));var Co;(function(t){t[t.Closed=1]="Closed",t[t.Disposed=2]="Disposed",t[t.AlreadyListening=3]="AlreadyListening";})(Co||(xe.ConnectionErrors=Co={}));var ss=class t extends Error{constructor(e,r){super(r),this.code=e,Object.setPrototypeOf(this,t.prototype);}};xe.ConnectionError=ss;var pm;(function(t){function e(r){let n=r;return n&&st.func(n.cancelUndispatched)}t.is=e;})(pm||(xe.ConnectionStrategy=pm={}));var rc;(function(t){function e(r){let n=r;return n&&(n.kind===void 0||n.kind==="id")&&st.func(n.createCancellationTokenSource)&&(n.dispose===void 0||st.func(n.dispose))}t.is=e;})(rc||(xe.IdCancellationReceiverStrategy=rc={}));var mm;(function(t){function e(r){let n=r;return n&&n.kind==="request"&&st.func(n.createCancellationTokenSource)&&(n.dispose===void 0||st.func(n.dispose))}t.is=e;})(mm||(xe.RequestCancellationReceiverStrategy=mm={}));var nc;(function(t){t.Message=Object.freeze({createCancellationTokenSource(r){return new cm.CancellationTokenSource}});function e(r){return rc.is(r)||mm.is(r)}t.is=e;})(nc||(xe.CancellationReceiverStrategy=nc={}));var ic;(function(t){t.Message=Object.freeze({sendCancellation(r,n){return r.sendNotification(xo.type,{id:n})},cleanup(r){}});function e(r){let n=r;return n&&st.func(n.sendCancellation)&&st.func(n.cleanup)}t.is=e;})(ic||(xe.CancellationSenderStrategy=ic={}));var sc;(function(t){t.Message=Object.freeze({receiver:nc.Message,sender:ic.Message});function e(r){let n=r;return n&&nc.is(n.receiver)&&ic.is(n.sender)}t.is=e;})(sc||(xe.CancellationStrategy=sc={}));var oc;(function(t){function e(r){let n=r;return n&&st.func(n.handleMessage)}t.is=e;})(oc||(xe.MessageStrategy=oc={}));var MT;(function(t){function e(r){let n=r;return n&&(sc.is(n.cancellationStrategy)||pm.is(n.connectionStrategy)||oc.is(n.messageStrategy))}t.is=e;})(MT||(xe.ConnectionOptions=MT={}));var Fr;(function(t){t[t.New=1]="New",t[t.Listening=2]="Listening",t[t.Closed=3]="Closed",t[t.Disposed=4]="Disposed";})(Fr||(Fr={}));function $3(t,e,r,n){let i=r!==void 0?r:xe.NullLogger,s=0,o=0,a=0,l="2.0",d,c=new Map,p,m=new Map,_=new Map,w,E=new IT.LinkedMap,P=new Map,D=new Set,g=new Map,S=Le.Off,I=cr.Text,L,Z=Fr.New,K=new Eo.Emitter,U=new Eo.Emitter,B=new Eo.Emitter,Q=new Eo.Emitter,N=new Eo.Emitter,te=n&&n.cancellationStrategy?n.cancellationStrategy:sc.Message;function ae(k){if(k===null)throw new Error("Can't send requests with id null since the response can't be correlated.");return "req-"+k.toString()}function ye(k){return k===null?"res-unknown-"+(++a).toString():"res-"+k.toString()}function q(){return "not-"+(++o).toString()}function W(k,G){ve.Message.isRequest(G)?k.set(ae(G.id),G):ve.Message.isResponse(G)?k.set(ye(G.id),G):k.set(q(),G);}function me(k){}function pe(){return Z===Fr.Listening}function we(){return Z===Fr.Closed}function Xe(){return Z===Fr.Disposed}function Ke(){(Z===Fr.New||Z===Fr.Listening)&&(Z=Fr.Closed,U.fire(void 0));}function Pt(k){K.fire([k,void 0,void 0]);}function Lr(k){K.fire(k);}t.onClose(Ke),t.onError(Pt),e.onClose(Ke),e.onError(Lr);function zt(){w||E.size===0||(w=(0, OT.default)().timer.setImmediate(()=>{w=void 0,Ce();}));}function St(k){ve.Message.isRequest(k)?fn(k):ve.Message.isNotification(k)?Me(k):ve.Message.isResponse(k)?ce(k):ie(k);}function Ce(){if(E.size===0)return;let k=E.shift();try{let G=n?.messageStrategy;oc.is(G)?G.handleMessage(k,St):St(k);}finally{zt();}}let Cr=k=>{try{if(ve.Message.isNotification(k)&&k.method===xo.type.method){let G=k.params.id,se=ae(G),fe=E.get(se);if(ve.Message.isRequest(fe)){let ke=n?.connectionStrategy,ze=ke&&ke.cancelUndispatched?ke.cancelUndispatched(fe,me):void 0;if(ze&&(ze.error!==void 0||ze.result!==void 0)){E.delete(se),g.delete(G),ze.id=fe.id,He(ze,k.method,Date.now()),e.write(ze).catch(()=>i.error("Sending response for canceled message failed."));return}}let qe=g.get(G);if(qe!==void 0){qe.cancel(),Nt(k);return}else D.add(G);}W(E,k);}finally{zt();}};function fn(k){if(Xe())return;function G(Te,We,Ie){let et={jsonrpc:l,id:k.id};Te instanceof ve.ResponseError?et.error=Te.toJson():et.result=Te===void 0?null:Te,He(et,We,Ie),e.write(et).catch(()=>i.error("Sending response failed."));}function se(Te,We,Ie){let et={jsonrpc:l,id:k.id,error:Te.toJson()};He(et,We,Ie),e.write(et).catch(()=>i.error("Sending response failed."));}function fe(Te,We,Ie){Te===void 0&&(Te=null);let et={jsonrpc:l,id:k.id,result:Te};He(et,We,Ie),e.write(et).catch(()=>i.error("Sending response failed."));}fr(k);let qe=c.get(k.method),ke,ze;qe&&(ke=qe.type,ze=qe.handler);let Ze=Date.now();if(ze||d){let Te=k.id??String(Date.now()),We=rc.is(te.receiver)?te.receiver.createCancellationTokenSource(Te):te.receiver.createCancellationTokenSource(k);k.id!==null&&D.has(k.id)&&We.cancel(),k.id!==null&&g.set(Te,We);try{let Ie;if(ze)if(k.params===void 0){if(ke!==void 0&&ke.numberOfParams!==0){se(new ve.ResponseError(ve.ErrorCodes.InvalidParams,`Request ${k.method} defines ${ke.numberOfParams} params but received none.`),k.method,Ze);return}Ie=ze(We.token);}else if(Array.isArray(k.params)){if(ke!==void 0&&ke.parameterStructures===ve.ParameterStructures.byName){se(new ve.ResponseError(ve.ErrorCodes.InvalidParams,`Request ${k.method} defines parameters by name but received parameters by position`),k.method,Ze);return}Ie=ze(...k.params,We.token);}else {if(ke!==void 0&&ke.parameterStructures===ve.ParameterStructures.byPosition){se(new ve.ResponseError(ve.ErrorCodes.InvalidParams,`Request ${k.method} defines parameters by position but received parameters by name`),k.method,Ze);return}Ie=ze(k.params,We.token);}else d&&(Ie=d(k.method,k.params,We.token));let et=Ie;Ie?et.then?et.then(ut=>{g.delete(Te),G(ut,k.method,Ze);},ut=>{g.delete(Te),ut instanceof ve.ResponseError?se(ut,k.method,Ze):ut&&st.string(ut.message)?se(new ve.ResponseError(ve.ErrorCodes.InternalError,`Request ${k.method} failed with message: ${ut.message}`),k.method,Ze):se(new ve.ResponseError(ve.ErrorCodes.InternalError,`Request ${k.method} failed unexpectedly without providing any details.`),k.method,Ze);}):(g.delete(Te),G(Ie,k.method,Ze)):(g.delete(Te),fe(Ie,k.method,Ze));}catch(Ie){g.delete(Te),Ie instanceof ve.ResponseError?G(Ie,k.method,Ze):Ie&&st.string(Ie.message)?se(new ve.ResponseError(ve.ErrorCodes.InternalError,`Request ${k.method} failed with message: ${Ie.message}`),k.method,Ze):se(new ve.ResponseError(ve.ErrorCodes.InternalError,`Request ${k.method} failed unexpectedly without providing any details.`),k.method,Ze);}}else se(new ve.ResponseError(ve.ErrorCodes.MethodNotFound,`Unhandled method ${k.method}`),k.method,Ze);}function ce(k){if(!Xe())if(k.id===null)k.error?i.error(`Received response message without id: Error is: -${JSON.stringify(k.error,void 0,4)}`):i.error("Received response message without id. No further error information provided.");else {let G=k.id,se=P.get(G);if(Hn(k,se),se!==void 0){P.delete(G);try{if(k.error){let fe=k.error;se.reject(new ve.ResponseError(fe.code,fe.message,fe.data));}else if(k.result!==void 0)se.resolve(k.result);else throw new Error("Should never happen.")}catch(fe){fe.message?i.error(`Response handler '${se.method}' failed with message: ${fe.message}`):i.error(`Response handler '${se.method}' failed unexpectedly.`);}}}}function Me(k){if(Xe())return;let G,se;if(k.method===xo.type.method){let fe=k.params.id;D.delete(fe),Nt(k);return}else {let fe=m.get(k.method);fe&&(se=fe.handler,G=fe.type);}if(se||p)try{if(Nt(k),se)if(k.params===void 0)G!==void 0&&G.numberOfParams!==0&&G.parameterStructures!==ve.ParameterStructures.byName&&i.error(`Notification ${k.method} defines ${G.numberOfParams} params but received none.`),se();else if(Array.isArray(k.params)){let fe=k.params;k.method===Ro.type.method&&fe.length===2&&lm.is(fe[0])?se({token:fe[0],value:fe[1]}):(G!==void 0&&(G.parameterStructures===ve.ParameterStructures.byName&&i.error(`Notification ${k.method} defines parameters by name but received parameters by position`),G.numberOfParams!==k.params.length&&i.error(`Notification ${k.method} defines ${G.numberOfParams} params but received ${fe.length} arguments`)),se(...fe));}else G!==void 0&&G.parameterStructures===ve.ParameterStructures.byPosition&&i.error(`Notification ${k.method} defines parameters by position but received parameters by name`),se(k.params);else p&&p(k.method,k.params);}catch(fe){fe.message?i.error(`Notification handler '${k.method}' failed with message: ${fe.message}`):i.error(`Notification handler '${k.method}' failed unexpectedly.`);}else B.fire(k);}function ie(k){if(!k){i.error("Received empty message.");return}i.error(`Received message which is neither a response nor a notification message: -${JSON.stringify(k,null,4)}`);let G=k;if(st.string(G.id)||st.number(G.id)){let se=G.id,fe=P.get(se);fe&&fe.reject(new Error("The received response has neither a result nor an error property."));}}function be(k){if(k!=null)switch(S){case Le.Verbose:return JSON.stringify(k,null,4);case Le.Compact:return JSON.stringify(k);default:return}}function $e(k){if(!(S===Le.Off||!L))if(I===cr.Text){let G;(S===Le.Verbose||S===Le.Compact)&&k.params&&(G=`Params: ${be(k.params)} - -`),L.log(`Sending request '${k.method} - (${k.id})'.`,G);}else qt("send-request",k);}function ot(k){if(!(S===Le.Off||!L))if(I===cr.Text){let G;(S===Le.Verbose||S===Le.Compact)&&(k.params?G=`Params: ${be(k.params)} - -`:G=`No parameters provided. - -`),L.log(`Sending notification '${k.method}'.`,G);}else qt("send-notification",k);}function He(k,G,se){if(!(S===Le.Off||!L))if(I===cr.Text){let fe;(S===Le.Verbose||S===Le.Compact)&&(k.error&&k.error.data?fe=`Error data: ${be(k.error.data)} - -`:k.result?fe=`Result: ${be(k.result)} - -`:k.error===void 0&&(fe=`No result returned. - -`)),L.log(`Sending response '${G} - (${k.id})'. Processing request took ${Date.now()-se}ms`,fe);}else qt("send-response",k);}function fr(k){if(!(S===Le.Off||!L))if(I===cr.Text){let G;(S===Le.Verbose||S===Le.Compact)&&k.params&&(G=`Params: ${be(k.params)} - -`),L.log(`Received request '${k.method} - (${k.id})'.`,G);}else qt("receive-request",k);}function Nt(k){if(!(S===Le.Off||!L||k.method===tc.type.method))if(I===cr.Text){let G;(S===Le.Verbose||S===Le.Compact)&&(k.params?G=`Params: ${be(k.params)} - -`:G=`No parameters provided. - -`),L.log(`Received notification '${k.method}'.`,G);}else qt("receive-notification",k);}function Hn(k,G){if(!(S===Le.Off||!L))if(I===cr.Text){let se;if((S===Le.Verbose||S===Le.Compact)&&(k.error&&k.error.data?se=`Error data: ${be(k.error.data)} - -`:k.result?se=`Result: ${be(k.result)} - -`:k.error===void 0&&(se=`No result returned. - -`)),G){let fe=k.error?` Request failed: ${k.error.message} (${k.error.code}).`:"";L.log(`Received response '${G.method} - (${k.id})' in ${Date.now()-G.timerStart}ms.${fe}`,se);}else L.log(`Received response ${k.id} without active response promise.`,se);}else qt("receive-response",k);}function qt(k,G){if(!L||S===Le.Off)return;let se={isLSPMessage:!0,type:k,message:G,timestamp:Date.now()};L.log(se);}function Vt(){if(we())throw new ss(Co.Closed,"Connection is closed.");if(Xe())throw new ss(Co.Disposed,"Connection is disposed.")}function Wn(){if(pe())throw new ss(Co.AlreadyListening,"Connection is already listening")}function yi(){if(!pe())throw new Error("Call listen() first.")}function tr(k){return k===void 0?null:k}function Bn(k){if(k!==null)return k}function Un(k){return k!=null&&!Array.isArray(k)&&typeof k=="object"}function Yr(k,G){switch(k){case ve.ParameterStructures.auto:return Un(G)?Bn(G):[tr(G)];case ve.ParameterStructures.byName:if(!Un(G))throw new Error("Received parameters by name but param is not an object literal.");return Bn(G);case ve.ParameterStructures.byPosition:return [tr(G)];default:throw new Error(`Unknown parameter structure ${k.toString()}`)}}function zn(k,G){let se,fe=k.numberOfParams;switch(fe){case 0:se=void 0;break;case 1:se=Yr(k.parameterStructures,G[0]);break;default:se=[];for(let qe=0;qe{Vt();let se,fe;if(st.string(k)){se=k;let ke=G[0],ze=0,Ze=ve.ParameterStructures.auto;ve.ParameterStructures.is(ke)&&(ze=1,Ze=ke);let Te=G.length,We=Te-ze;switch(We){case 0:fe=void 0;break;case 1:fe=Yr(Ze,G[ze]);break;default:if(Ze===ve.ParameterStructures.byName)throw new Error(`Received ${We} parameters for 'by Name' notification parameter structure.`);fe=G.slice(ze,Te).map(Ie=>tr(Ie));break}}else {let ke=G;se=k.method,fe=zn(k,ke);}let qe={jsonrpc:l,method:se,params:fe};return ot(qe),e.write(qe).catch(ke=>{throw i.error("Sending notification failed."),ke})},onNotification:(k,G)=>{Vt();let se;return st.func(k)?p=k:G&&(st.string(k)?(se=k,m.set(k,{type:void 0,handler:G})):(se=k.method,m.set(k.method,{type:k,handler:G}))),{dispose:()=>{se!==void 0?m.delete(se):p=void 0;}}},onProgress:(k,G,se)=>{if(_.has(G))throw new Error(`Progress handler for token ${G} already registered`);return _.set(G,se),{dispose:()=>{_.delete(G);}}},sendProgress:(k,G,se)=>xr.sendNotification(Ro.type,{token:G,value:se}),onUnhandledProgress:Q.event,sendRequest:(k,...G)=>{Vt(),yi();let se,fe,qe;if(st.string(k)){se=k;let Te=G[0],We=G[G.length-1],Ie=0,et=ve.ParameterStructures.auto;ve.ParameterStructures.is(Te)&&(Ie=1,et=Te);let ut=G.length;cm.CancellationToken.is(We)&&(ut=ut-1,qe=We);let Lt=ut-Ie;switch(Lt){case 0:fe=void 0;break;case 1:fe=Yr(et,G[Ie]);break;default:if(et===ve.ParameterStructures.byName)throw new Error(`Received ${Lt} parameters for 'by Name' request parameter structure.`);fe=G.slice(Ie,ut).map(bi=>tr(bi));break}}else {let Te=G;se=k.method,fe=zn(k,Te);let We=k.numberOfParams;qe=cm.CancellationToken.is(Te[We])?Te[We]:void 0;}let ke=s++,ze;qe&&(ze=qe.onCancellationRequested(()=>{let Te=te.sender.sendCancellation(xr,ke);return Te===void 0?(i.log(`Received no promise from cancellation strategy when cancelling id ${ke}`),Promise.resolve()):Te.catch(()=>{i.log(`Sending cancellation messages for id ${ke} failed`);})}));let Ze={jsonrpc:l,id:ke,method:se,params:fe};return $e(Ze),typeof te.sender.enableCancellation=="function"&&te.sender.enableCancellation(Ze),new Promise(async(Te,We)=>{let Ie=Lt=>{Te(Lt),te.sender.cleanup(ke),ze?.dispose();},et=Lt=>{We(Lt),te.sender.cleanup(ke),ze?.dispose();},ut={method:se,timerStart:Date.now(),resolve:Ie,reject:et};try{await e.write(Ze),P.set(ke,ut);}catch(Lt){throw i.error("Sending request failed."),ut.reject(new ve.ResponseError(ve.ErrorCodes.MessageWriteError,Lt.message?Lt.message:"Unknown reason")),Lt}})},onRequest:(k,G)=>{Vt();let se=null;return dm.is(k)?(se=void 0,d=k):st.string(k)?(se=null,G!==void 0&&(se=k,c.set(k,{handler:G,type:void 0}))):G!==void 0&&(se=k.method,c.set(k.method,{type:k,handler:G})),{dispose:()=>{se!==null&&(se!==void 0?c.delete(se):d=void 0);}}},hasPendingResponse:()=>P.size>0,trace:async(k,G,se)=>{let fe=!1,qe=cr.Text;se!==void 0&&(st.boolean(se)?fe=se:(fe=se.sendNotification||!1,qe=se.traceFormat||cr.Text)),S=k,I=qe,S===Le.Off?L=void 0:L=G,fe&&!we()&&!Xe()&&await xr.sendNotification(hm.type,{value:Le.toString(k)});},onError:K.event,onClose:U.event,onUnhandledNotification:B.event,onDispose:N.event,end:()=>{e.end();},dispose:()=>{if(Xe())return;Z=Fr.Disposed,N.fire(void 0);let k=new ve.ResponseError(ve.ErrorCodes.PendingResponseRejected,"Pending response rejected since connection got disposed");for(let G of P.values())G.reject(k);P=new Map,g=new Map,D=new Set,E=new IT.LinkedMap,st.func(e.dispose)&&e.dispose(),st.func(t.dispose)&&t.dispose();},listen:()=>{Vt(),Wn(),Z=Fr.Listening,t.listen(Cr);},inspect:()=>{(0, OT.default)().console.log("inspect");}};return xr.onNotification(tc.type,k=>{if(S===Le.Off||!L)return;let G=S===Le.Verbose||S===Le.Compact;L.log(k.message,G?k.verbose:void 0);}),xr.onNotification(Ro.type,k=>{let G=_.get(k.token);G?G(k.value):Q.fire(k);}),xr}xe.createMessageConnection=$3;});var ac=A(z=>{Object.defineProperty(z,"__esModule",{value:!0});z.ProgressType=z.ProgressToken=z.createMessageConnection=z.NullLogger=z.ConnectionOptions=z.ConnectionStrategy=z.AbstractMessageBuffer=z.WriteableStreamMessageWriter=z.AbstractMessageWriter=z.MessageWriter=z.ReadableStreamMessageReader=z.AbstractMessageReader=z.MessageReader=z.SharedArrayReceiverStrategy=z.SharedArraySenderStrategy=z.CancellationToken=z.CancellationTokenSource=z.Emitter=z.Event=z.Disposable=z.LRUCache=z.Touch=z.LinkedMap=z.ParameterStructures=z.NotificationType9=z.NotificationType8=z.NotificationType7=z.NotificationType6=z.NotificationType5=z.NotificationType4=z.NotificationType3=z.NotificationType2=z.NotificationType1=z.NotificationType0=z.NotificationType=z.ErrorCodes=z.ResponseError=z.RequestType9=z.RequestType8=z.RequestType7=z.RequestType6=z.RequestType5=z.RequestType4=z.RequestType3=z.RequestType2=z.RequestType1=z.RequestType0=z.RequestType=z.Message=z.RAL=void 0;z.MessageStrategy=z.CancellationStrategy=z.CancellationSenderStrategy=z.CancellationReceiverStrategy=z.ConnectionError=z.ConnectionErrors=z.LogTraceNotification=z.SetTraceNotification=z.TraceFormat=z.TraceValues=z.Trace=void 0;var Ye=Hp();Object.defineProperty(z,"Message",{enumerable:!0,get:function(){return Ye.Message}});Object.defineProperty(z,"RequestType",{enumerable:!0,get:function(){return Ye.RequestType}});Object.defineProperty(z,"RequestType0",{enumerable:!0,get:function(){return Ye.RequestType0}});Object.defineProperty(z,"RequestType1",{enumerable:!0,get:function(){return Ye.RequestType1}});Object.defineProperty(z,"RequestType2",{enumerable:!0,get:function(){return Ye.RequestType2}});Object.defineProperty(z,"RequestType3",{enumerable:!0,get:function(){return Ye.RequestType3}});Object.defineProperty(z,"RequestType4",{enumerable:!0,get:function(){return Ye.RequestType4}});Object.defineProperty(z,"RequestType5",{enumerable:!0,get:function(){return Ye.RequestType5}});Object.defineProperty(z,"RequestType6",{enumerable:!0,get:function(){return Ye.RequestType6}});Object.defineProperty(z,"RequestType7",{enumerable:!0,get:function(){return Ye.RequestType7}});Object.defineProperty(z,"RequestType8",{enumerable:!0,get:function(){return Ye.RequestType8}});Object.defineProperty(z,"RequestType9",{enumerable:!0,get:function(){return Ye.RequestType9}});Object.defineProperty(z,"ResponseError",{enumerable:!0,get:function(){return Ye.ResponseError}});Object.defineProperty(z,"ErrorCodes",{enumerable:!0,get:function(){return Ye.ErrorCodes}});Object.defineProperty(z,"NotificationType",{enumerable:!0,get:function(){return Ye.NotificationType}});Object.defineProperty(z,"NotificationType0",{enumerable:!0,get:function(){return Ye.NotificationType0}});Object.defineProperty(z,"NotificationType1",{enumerable:!0,get:function(){return Ye.NotificationType1}});Object.defineProperty(z,"NotificationType2",{enumerable:!0,get:function(){return Ye.NotificationType2}});Object.defineProperty(z,"NotificationType3",{enumerable:!0,get:function(){return Ye.NotificationType3}});Object.defineProperty(z,"NotificationType4",{enumerable:!0,get:function(){return Ye.NotificationType4}});Object.defineProperty(z,"NotificationType5",{enumerable:!0,get:function(){return Ye.NotificationType5}});Object.defineProperty(z,"NotificationType6",{enumerable:!0,get:function(){return Ye.NotificationType6}});Object.defineProperty(z,"NotificationType7",{enumerable:!0,get:function(){return Ye.NotificationType7}});Object.defineProperty(z,"NotificationType8",{enumerable:!0,get:function(){return Ye.NotificationType8}});Object.defineProperty(z,"NotificationType9",{enumerable:!0,get:function(){return Ye.NotificationType9}});Object.defineProperty(z,"ParameterStructures",{enumerable:!0,get:function(){return Ye.ParameterStructures}});var gm=Bp();Object.defineProperty(z,"LinkedMap",{enumerable:!0,get:function(){return gm.LinkedMap}});Object.defineProperty(z,"LRUCache",{enumerable:!0,get:function(){return gm.LRUCache}});Object.defineProperty(z,"Touch",{enumerable:!0,get:function(){return gm.Touch}});var j3=vT();Object.defineProperty(z,"Disposable",{enumerable:!0,get:function(){return j3.Disposable}});var NT=ts();Object.defineProperty(z,"Event",{enumerable:!0,get:function(){return NT.Event}});Object.defineProperty(z,"Emitter",{enumerable:!0,get:function(){return NT.Emitter}});var qT=Ju();Object.defineProperty(z,"CancellationTokenSource",{enumerable:!0,get:function(){return qT.CancellationTokenSource}});Object.defineProperty(z,"CancellationToken",{enumerable:!0,get:function(){return qT.CancellationToken}});var LT=ST();Object.defineProperty(z,"SharedArraySenderStrategy",{enumerable:!0,get:function(){return LT.SharedArraySenderStrategy}});Object.defineProperty(z,"SharedArrayReceiverStrategy",{enumerable:!0,get:function(){return LT.SharedArrayReceiverStrategy}});var _m=RT();Object.defineProperty(z,"MessageReader",{enumerable:!0,get:function(){return _m.MessageReader}});Object.defineProperty(z,"AbstractMessageReader",{enumerable:!0,get:function(){return _m.AbstractMessageReader}});Object.defineProperty(z,"ReadableStreamMessageReader",{enumerable:!0,get:function(){return _m.ReadableStreamMessageReader}});var ym=PT();Object.defineProperty(z,"MessageWriter",{enumerable:!0,get:function(){return ym.MessageWriter}});Object.defineProperty(z,"AbstractMessageWriter",{enumerable:!0,get:function(){return ym.AbstractMessageWriter}});Object.defineProperty(z,"WriteableStreamMessageWriter",{enumerable:!0,get:function(){return ym.WriteableStreamMessageWriter}});var H3=DT();Object.defineProperty(z,"AbstractMessageBuffer",{enumerable:!0,get:function(){return H3.AbstractMessageBuffer}});var At=FT();Object.defineProperty(z,"ConnectionStrategy",{enumerable:!0,get:function(){return At.ConnectionStrategy}});Object.defineProperty(z,"ConnectionOptions",{enumerable:!0,get:function(){return At.ConnectionOptions}});Object.defineProperty(z,"NullLogger",{enumerable:!0,get:function(){return At.NullLogger}});Object.defineProperty(z,"createMessageConnection",{enumerable:!0,get:function(){return At.createMessageConnection}});Object.defineProperty(z,"ProgressToken",{enumerable:!0,get:function(){return At.ProgressToken}});Object.defineProperty(z,"ProgressType",{enumerable:!0,get:function(){return At.ProgressType}});Object.defineProperty(z,"Trace",{enumerable:!0,get:function(){return At.Trace}});Object.defineProperty(z,"TraceValues",{enumerable:!0,get:function(){return At.TraceValues}});Object.defineProperty(z,"TraceFormat",{enumerable:!0,get:function(){return At.TraceFormat}});Object.defineProperty(z,"SetTraceNotification",{enumerable:!0,get:function(){return At.SetTraceNotification}});Object.defineProperty(z,"LogTraceNotification",{enumerable:!0,get:function(){return At.LogTraceNotification}});Object.defineProperty(z,"ConnectionErrors",{enumerable:!0,get:function(){return At.ConnectionErrors}});Object.defineProperty(z,"ConnectionError",{enumerable:!0,get:function(){return At.ConnectionError}});Object.defineProperty(z,"CancellationReceiverStrategy",{enumerable:!0,get:function(){return At.CancellationReceiverStrategy}});Object.defineProperty(z,"CancellationSenderStrategy",{enumerable:!0,get:function(){return At.CancellationSenderStrategy}});Object.defineProperty(z,"CancellationStrategy",{enumerable:!0,get:function(){return At.CancellationStrategy}});Object.defineProperty(z,"MessageStrategy",{enumerable:!0,get:function(){return At.MessageStrategy}});var W3=Dn();z.RAL=W3.default;});var HT=A(Sm=>{Object.defineProperty(Sm,"__esModule",{value:!0});var $T=oe("util"),cn=ac(),uc=class t extends cn.AbstractMessageBuffer{constructor(e="utf-8"){super(e);}emptyBuffer(){return t.emptyBuffer}fromString(e,r){return Buffer.from(e,r)}toString(e,r){return e instanceof Buffer?e.toString(r):new $T.TextDecoder(r).decode(e)}asNative(e,r){return r===void 0?e instanceof Buffer?e:Buffer.from(e):e instanceof Buffer?e.slice(0,r):Buffer.from(e,0,r)}allocNative(e){return Buffer.allocUnsafe(e)}};uc.emptyBuffer=Buffer.allocUnsafe(0);var bm=class{constructor(e){this.stream=e;}onClose(e){return this.stream.on("close",e),cn.Disposable.create(()=>this.stream.off("close",e))}onError(e){return this.stream.on("error",e),cn.Disposable.create(()=>this.stream.off("error",e))}onEnd(e){return this.stream.on("end",e),cn.Disposable.create(()=>this.stream.off("end",e))}onData(e){return this.stream.on("data",e),cn.Disposable.create(()=>this.stream.off("data",e))}},vm=class{constructor(e){this.stream=e;}onClose(e){return this.stream.on("close",e),cn.Disposable.create(()=>this.stream.off("close",e))}onError(e){return this.stream.on("error",e),cn.Disposable.create(()=>this.stream.off("error",e))}onEnd(e){return this.stream.on("end",e),cn.Disposable.create(()=>this.stream.off("end",e))}write(e,r){return new Promise((n,i)=>{let s=o=>{o==null?n():i(o);};typeof e=="string"?this.stream.write(e,r,s):this.stream.write(e,s);})}end(){this.stream.end();}},jT=Object.freeze({messageBuffer:Object.freeze({create:t=>new uc(t)}),applicationJson:Object.freeze({encoder:Object.freeze({name:"application/json",encode:(t,e)=>{try{return Promise.resolve(Buffer.from(JSON.stringify(t,void 0,0),e.charset))}catch(r){return Promise.reject(r)}}}),decoder:Object.freeze({name:"application/json",decode:(t,e)=>{try{return t instanceof Buffer?Promise.resolve(JSON.parse(t.toString(e.charset))):Promise.resolve(JSON.parse(new $T.TextDecoder(e.charset).decode(t)))}catch(r){return Promise.reject(r)}}})}),stream:Object.freeze({asReadableStream:t=>new bm(t),asWritableStream:t=>new vm(t)}),console,timer:Object.freeze({setTimeout(t,e,...r){let n=setTimeout(t,e,...r);return {dispose:()=>clearTimeout(n)}},setImmediate(t,...e){let r=setImmediate(t,...e);return {dispose:()=>clearImmediate(r)}},setInterval(t,e,...r){let n=setInterval(t,e,...r);return {dispose:()=>clearInterval(n)}}})});function wm(){return jT}(function(t){function e(){cn.RAL.install(jT);}t.install=e;})(wm||(wm={}));Sm.default=wm;});var hi=A(Oe=>{var B3=Oe&&Oe.__createBinding||(Object.create?function(t,e,r,n){n===void 0&&(n=r);var i=Object.getOwnPropertyDescriptor(e,r);(!i||("get"in i?!e.__esModule:i.writable||i.configurable))&&(i={enumerable:!0,get:function(){return e[r]}}),Object.defineProperty(t,n,i);}:function(t,e,r,n){n===void 0&&(n=r),t[n]=e[r];}),U3=Oe&&Oe.__exportStar||function(t,e){for(var r in t)r!=="default"&&!Object.prototype.hasOwnProperty.call(e,r)&&B3(e,t,r);};Object.defineProperty(Oe,"__esModule",{value:!0});Oe.createMessageConnection=Oe.createServerSocketTransport=Oe.createClientSocketTransport=Oe.createServerPipeTransport=Oe.createClientPipeTransport=Oe.generateRandomPipeName=Oe.StreamMessageWriter=Oe.StreamMessageReader=Oe.SocketMessageWriter=Oe.SocketMessageReader=Oe.PortMessageWriter=Oe.PortMessageReader=Oe.IPCMessageWriter=Oe.IPCMessageReader=void 0;var os=HT();os.default.install();var WT=oe("path"),z3=oe("os"),V3=oe("crypto"),fc=oe("net"),lr=ac();U3(ac(),Oe);var Em=class extends lr.AbstractMessageReader{constructor(e){super(),this.process=e;let r=this.process;r.on("error",n=>this.fireError(n)),r.on("close",()=>this.fireClose());}listen(e){return this.process.on("message",e),lr.Disposable.create(()=>this.process.off("message",e))}};Oe.IPCMessageReader=Em;var Rm=class extends lr.AbstractMessageWriter{constructor(e){super(),this.process=e,this.errorCount=0;let r=this.process;r.on("error",n=>this.fireError(n)),r.on("close",()=>this.fireClose);}write(e){try{return typeof this.process.send=="function"&&this.process.send(e,void 0,void 0,r=>{r?(this.errorCount++,this.handleError(r,e)):this.errorCount=0;}),Promise.resolve()}catch(r){return this.handleError(r,e),Promise.reject(r)}}handleError(e,r){this.errorCount++,this.fireError(e,r,this.errorCount);}end(){}};Oe.IPCMessageWriter=Rm;var Cm=class extends lr.AbstractMessageReader{constructor(e){super(),this.onData=new lr.Emitter,e.on("close",()=>this.fireClose),e.on("error",r=>this.fireError(r)),e.on("message",r=>{this.onData.fire(r);});}listen(e){return this.onData.event(e)}};Oe.PortMessageReader=Cm;var xm=class extends lr.AbstractMessageWriter{constructor(e){super(),this.port=e,this.errorCount=0,e.on("close",()=>this.fireClose()),e.on("error",r=>this.fireError(r));}write(e){try{return this.port.postMessage(e),Promise.resolve()}catch(r){return this.handleError(r,e),Promise.reject(r)}}handleError(e,r){this.errorCount++,this.fireError(e,r,this.errorCount);}end(){}};Oe.PortMessageWriter=xm;var fi=class extends lr.ReadableStreamMessageReader{constructor(e,r="utf-8"){super((0, os.default)().stream.asReadableStream(e),r);}};Oe.SocketMessageReader=fi;var di=class extends lr.WriteableStreamMessageWriter{constructor(e,r){super((0, os.default)().stream.asWritableStream(e),r),this.socket=e;}dispose(){super.dispose(),this.socket.destroy();}};Oe.SocketMessageWriter=di;var cc=class extends lr.ReadableStreamMessageReader{constructor(e,r){super((0, os.default)().stream.asReadableStream(e),r);}};Oe.StreamMessageReader=cc;var lc=class extends lr.WriteableStreamMessageWriter{constructor(e,r){super((0, os.default)().stream.asWritableStream(e),r);}};Oe.StreamMessageWriter=lc;var BT=process.env.XDG_RUNTIME_DIR,G3=new Map([["linux",107],["darwin",103]]);function K3(){let t=(0, V3.randomBytes)(21).toString("hex");if(process.platform==="win32")return `\\\\.\\pipe\\vscode-jsonrpc-${t}-sock`;let e;BT?e=WT.join(BT,`vscode-ipc-${t}.sock`):e=WT.join(z3.tmpdir(),`vscode-${t}.sock`);let r=G3.get(process.platform);return r!==void 0&&e.length>r&&(0, os.default)().console.warn(`WARNING: IPC handle "${e}" is longer than ${r} characters.`),e}Oe.generateRandomPipeName=K3;function Z3(t,e="utf-8"){let r,n=new Promise((i,s)=>{r=i;});return new Promise((i,s)=>{let o=(0, fc.createServer)(a=>{o.close(),r([new fi(a,e),new di(a,e)]);});o.on("error",s),o.listen(t,()=>{o.removeListener("error",s),i({onConnected:()=>n});});})}Oe.createClientPipeTransport=Z3;function J3(t,e="utf-8"){let r=(0, fc.createConnection)(t);return [new fi(r,e),new di(r,e)]}Oe.createServerPipeTransport=J3;function Y3(t,e="utf-8"){let r,n=new Promise((i,s)=>{r=i;});return new Promise((i,s)=>{let o=(0, fc.createServer)(a=>{o.close(),r([new fi(a,e),new di(a,e)]);});o.on("error",s),o.listen(t,"127.0.0.1",()=>{o.removeListener("error",s),i({onConnected:()=>n});});})}Oe.createClientSocketTransport=Y3;function X3(t,e="utf-8"){let r=(0, fc.createConnection)(t,"127.0.0.1");return [new fi(r,e),new di(r,e)]}Oe.createServerSocketTransport=X3;function Q3(t){let e=t;return e.read!==void 0&&e.addListener!==void 0}function e4(t){let e=t;return e.write!==void 0&&e.addListener!==void 0}function t4(t,e,r,n){r||(r=lr.NullLogger);let i=Q3(t)?new cc(t):t,s=e4(e)?new lc(e):e;return lr.ConnectionStrategy.is(n)&&(n={connectionStrategy:n}),(0, lr.createMessageConnection)(i,s,r,n)}Oe.createMessageConnection=t4;});var Tm=A((xK,UT)=>{UT.exports=hi();});var hc=A((zT,dc)=>{(function(t){if(typeof dc=="object"&&typeof dc.exports=="object"){var e=t(oe,zT);e!==void 0&&(dc.exports=e);}else typeof define=="function"&&define.amd&&define(["require","exports"],t);})(function(t,e){Object.defineProperty(e,"__esModule",{value:!0}),e.TextDocument=e.EOL=e.WorkspaceFolder=e.InlineCompletionContext=e.SelectedCompletionInfo=e.InlineCompletionTriggerKind=e.InlineCompletionList=e.InlineCompletionItem=e.StringValue=e.InlayHint=e.InlayHintLabelPart=e.InlayHintKind=e.InlineValueContext=e.InlineValueEvaluatableExpression=e.InlineValueVariableLookup=e.InlineValueText=e.SemanticTokens=e.SemanticTokenModifiers=e.SemanticTokenTypes=e.SelectionRange=e.DocumentLink=e.FormattingOptions=e.CodeLens=e.CodeAction=e.CodeActionContext=e.CodeActionTriggerKind=e.CodeActionKind=e.DocumentSymbol=e.WorkspaceSymbol=e.SymbolInformation=e.SymbolTag=e.SymbolKind=e.DocumentHighlight=e.DocumentHighlightKind=e.SignatureInformation=e.ParameterInformation=e.Hover=e.MarkedString=e.CompletionList=e.CompletionItem=e.CompletionItemLabelDetails=e.InsertTextMode=e.InsertReplaceEdit=e.CompletionItemTag=e.InsertTextFormat=e.CompletionItemKind=e.MarkupContent=e.MarkupKind=e.TextDocumentItem=e.OptionalVersionedTextDocumentIdentifier=e.VersionedTextDocumentIdentifier=e.TextDocumentIdentifier=e.WorkspaceChange=e.WorkspaceEdit=e.DeleteFile=e.RenameFile=e.CreateFile=e.TextDocumentEdit=e.AnnotatedTextEdit=e.ChangeAnnotationIdentifier=e.ChangeAnnotation=e.TextEdit=e.Command=e.Diagnostic=e.CodeDescription=e.DiagnosticTag=e.DiagnosticSeverity=e.DiagnosticRelatedInformation=e.FoldingRange=e.FoldingRangeKind=e.ColorPresentation=e.ColorInformation=e.Color=e.LocationLink=e.Location=e.Range=e.Position=e.uinteger=e.integer=e.URI=e.DocumentUri=void 0;var r;(function(v){function M(F){return typeof F=="string"}v.is=M;})(r||(e.DocumentUri=r={}));var n;(function(v){function M(F){return typeof F=="string"}v.is=M;})(n||(e.URI=n={}));var i;(function(v){v.MIN_VALUE=-2147483648,v.MAX_VALUE=2147483647;function M(F){return typeof F=="number"&&v.MIN_VALUE<=F&&F<=v.MAX_VALUE}v.is=M;})(i||(e.integer=i={}));var s;(function(v){v.MIN_VALUE=0,v.MAX_VALUE=2147483647;function M(F){return typeof F=="number"&&v.MIN_VALUE<=F&&F<=v.MAX_VALUE}v.is=M;})(s||(e.uinteger=s={}));var o;(function(v){function M(R,y){return R===Number.MAX_VALUE&&(R=s.MAX_VALUE),y===Number.MAX_VALUE&&(y=s.MAX_VALUE),{line:R,character:y}}v.create=M;function F(R){var y=R;return j.objectLiteral(y)&&j.uinteger(y.line)&&j.uinteger(y.character)}v.is=F;})(o||(e.Position=o={}));var a;(function(v){function M(R,y,$,J){if(j.uinteger(R)&&j.uinteger(y)&&j.uinteger($)&&j.uinteger(J))return {start:o.create(R,y),end:o.create($,J)};if(o.is(R)&&o.is(y))return {start:R,end:y};throw new Error("Range#create called with invalid arguments[".concat(R,", ").concat(y,", ").concat($,", ").concat(J,"]"))}v.create=M;function F(R){var y=R;return j.objectLiteral(y)&&o.is(y.start)&&o.is(y.end)}v.is=F;})(a||(e.Range=a={}));var l;(function(v){function M(R,y){return {uri:R,range:y}}v.create=M;function F(R){var y=R;return j.objectLiteral(y)&&a.is(y.range)&&(j.string(y.uri)||j.undefined(y.uri))}v.is=F;})(l||(e.Location=l={}));var d;(function(v){function M(R,y,$,J){return {targetUri:R,targetRange:y,targetSelectionRange:$,originSelectionRange:J}}v.create=M;function F(R){var y=R;return j.objectLiteral(y)&&a.is(y.targetRange)&&j.string(y.targetUri)&&a.is(y.targetSelectionRange)&&(a.is(y.originSelectionRange)||j.undefined(y.originSelectionRange))}v.is=F;})(d||(e.LocationLink=d={}));var c;(function(v){function M(R,y,$,J){return {red:R,green:y,blue:$,alpha:J}}v.create=M;function F(R){var y=R;return j.objectLiteral(y)&&j.numberRange(y.red,0,1)&&j.numberRange(y.green,0,1)&&j.numberRange(y.blue,0,1)&&j.numberRange(y.alpha,0,1)}v.is=F;})(c||(e.Color=c={}));var p;(function(v){function M(R,y){return {range:R,color:y}}v.create=M;function F(R){var y=R;return j.objectLiteral(y)&&a.is(y.range)&&c.is(y.color)}v.is=F;})(p||(e.ColorInformation=p={}));var m;(function(v){function M(R,y,$){return {label:R,textEdit:y,additionalTextEdits:$}}v.create=M;function F(R){var y=R;return j.objectLiteral(y)&&j.string(y.label)&&(j.undefined(y.textEdit)||L.is(y))&&(j.undefined(y.additionalTextEdits)||j.typedArray(y.additionalTextEdits,L.is))}v.is=F;})(m||(e.ColorPresentation=m={}));var _;(function(v){v.Comment="comment",v.Imports="imports",v.Region="region";})(_||(e.FoldingRangeKind=_={}));var w;(function(v){function M(R,y,$,J,Ee,tt){var Ve={startLine:R,endLine:y};return j.defined($)&&(Ve.startCharacter=$),j.defined(J)&&(Ve.endCharacter=J),j.defined(Ee)&&(Ve.kind=Ee),j.defined(tt)&&(Ve.collapsedText=tt),Ve}v.create=M;function F(R){var y=R;return j.objectLiteral(y)&&j.uinteger(y.startLine)&&j.uinteger(y.startLine)&&(j.undefined(y.startCharacter)||j.uinteger(y.startCharacter))&&(j.undefined(y.endCharacter)||j.uinteger(y.endCharacter))&&(j.undefined(y.kind)||j.string(y.kind))}v.is=F;})(w||(e.FoldingRange=w={}));var E;(function(v){function M(R,y){return {location:R,message:y}}v.create=M;function F(R){var y=R;return j.defined(y)&&l.is(y.location)&&j.string(y.message)}v.is=F;})(E||(e.DiagnosticRelatedInformation=E={}));var P;(function(v){v.Error=1,v.Warning=2,v.Information=3,v.Hint=4;})(P||(e.DiagnosticSeverity=P={}));var D;(function(v){v.Unnecessary=1,v.Deprecated=2;})(D||(e.DiagnosticTag=D={}));var g;(function(v){function M(F){var R=F;return j.objectLiteral(R)&&j.string(R.href)}v.is=M;})(g||(e.CodeDescription=g={}));var S;(function(v){function M(R,y,$,J,Ee,tt){var Ve={range:R,message:y};return j.defined($)&&(Ve.severity=$),j.defined(J)&&(Ve.code=J),j.defined(Ee)&&(Ve.source=Ee),j.defined(tt)&&(Ve.relatedInformation=tt),Ve}v.create=M;function F(R){var y,$=R;return j.defined($)&&a.is($.range)&&j.string($.message)&&(j.number($.severity)||j.undefined($.severity))&&(j.integer($.code)||j.string($.code)||j.undefined($.code))&&(j.undefined($.codeDescription)||j.string((y=$.codeDescription)===null||y===void 0?void 0:y.href))&&(j.string($.source)||j.undefined($.source))&&(j.undefined($.relatedInformation)||j.typedArray($.relatedInformation,E.is))}v.is=F;})(S||(e.Diagnostic=S={}));var I;(function(v){function M(R,y){for(var $=[],J=2;J0&&(Ee.arguments=$),Ee}v.create=M;function F(R){var y=R;return j.defined(y)&&j.string(y.title)&&j.string(y.command)}v.is=F;})(I||(e.Command=I={}));var L;(function(v){function M($,J){return {range:$,newText:J}}v.replace=M;function F($,J){return {range:{start:$,end:$},newText:J}}v.insert=F;function R($){return {range:$,newText:""}}v.del=R;function y($){var J=$;return j.objectLiteral(J)&&j.string(J.newText)&&a.is(J.range)}v.is=y;})(L||(e.TextEdit=L={}));var Z;(function(v){function M(R,y,$){var J={label:R};return y!==void 0&&(J.needsConfirmation=y),$!==void 0&&(J.description=$),J}v.create=M;function F(R){var y=R;return j.objectLiteral(y)&&j.string(y.label)&&(j.boolean(y.needsConfirmation)||y.needsConfirmation===void 0)&&(j.string(y.description)||y.description===void 0)}v.is=F;})(Z||(e.ChangeAnnotation=Z={}));var K;(function(v){function M(F){var R=F;return j.string(R)}v.is=M;})(K||(e.ChangeAnnotationIdentifier=K={}));var U;(function(v){function M($,J,Ee){return {range:$,newText:J,annotationId:Ee}}v.replace=M;function F($,J,Ee){return {range:{start:$,end:$},newText:J,annotationId:Ee}}v.insert=F;function R($,J){return {range:$,newText:"",annotationId:J}}v.del=R;function y($){var J=$;return L.is(J)&&(Z.is(J.annotationId)||K.is(J.annotationId))}v.is=y;})(U||(e.AnnotatedTextEdit=U={}));var B;(function(v){function M(R,y){return {textDocument:R,edits:y}}v.create=M;function F(R){var y=R;return j.defined(y)&&we.is(y.textDocument)&&Array.isArray(y.edits)}v.is=F;})(B||(e.TextDocumentEdit=B={}));var Q;(function(v){function M(R,y,$){var J={kind:"create",uri:R};return y!==void 0&&(y.overwrite!==void 0||y.ignoreIfExists!==void 0)&&(J.options=y),$!==void 0&&(J.annotationId=$),J}v.create=M;function F(R){var y=R;return y&&y.kind==="create"&&j.string(y.uri)&&(y.options===void 0||(y.options.overwrite===void 0||j.boolean(y.options.overwrite))&&(y.options.ignoreIfExists===void 0||j.boolean(y.options.ignoreIfExists)))&&(y.annotationId===void 0||K.is(y.annotationId))}v.is=F;})(Q||(e.CreateFile=Q={}));var N;(function(v){function M(R,y,$,J){var Ee={kind:"rename",oldUri:R,newUri:y};return $!==void 0&&($.overwrite!==void 0||$.ignoreIfExists!==void 0)&&(Ee.options=$),J!==void 0&&(Ee.annotationId=J),Ee}v.create=M;function F(R){var y=R;return y&&y.kind==="rename"&&j.string(y.oldUri)&&j.string(y.newUri)&&(y.options===void 0||(y.options.overwrite===void 0||j.boolean(y.options.overwrite))&&(y.options.ignoreIfExists===void 0||j.boolean(y.options.ignoreIfExists)))&&(y.annotationId===void 0||K.is(y.annotationId))}v.is=F;})(N||(e.RenameFile=N={}));var te;(function(v){function M(R,y,$){var J={kind:"delete",uri:R};return y!==void 0&&(y.recursive!==void 0||y.ignoreIfNotExists!==void 0)&&(J.options=y),$!==void 0&&(J.annotationId=$),J}v.create=M;function F(R){var y=R;return y&&y.kind==="delete"&&j.string(y.uri)&&(y.options===void 0||(y.options.recursive===void 0||j.boolean(y.options.recursive))&&(y.options.ignoreIfNotExists===void 0||j.boolean(y.options.ignoreIfNotExists)))&&(y.annotationId===void 0||K.is(y.annotationId))}v.is=F;})(te||(e.DeleteFile=te={}));var ae;(function(v){function M(F){var R=F;return R&&(R.changes!==void 0||R.documentChanges!==void 0)&&(R.documentChanges===void 0||R.documentChanges.every(function(y){return j.string(y.kind)?Q.is(y)||N.is(y)||te.is(y):B.is(y)}))}v.is=M;})(ae||(e.WorkspaceEdit=ae={}));var ye=function(){function v(M,F){this.edits=M,this.changeAnnotations=F;}return v.prototype.insert=function(M,F,R){var y,$;if(R===void 0?y=L.insert(M,F):K.is(R)?($=R,y=U.insert(M,F,R)):(this.assertChangeAnnotations(this.changeAnnotations),$=this.changeAnnotations.manage(R),y=U.insert(M,F,$)),this.edits.push(y),$!==void 0)return $},v.prototype.replace=function(M,F,R){var y,$;if(R===void 0?y=L.replace(M,F):K.is(R)?($=R,y=U.replace(M,F,R)):(this.assertChangeAnnotations(this.changeAnnotations),$=this.changeAnnotations.manage(R),y=U.replace(M,F,$)),this.edits.push(y),$!==void 0)return $},v.prototype.delete=function(M,F){var R,y;if(F===void 0?R=L.del(M):K.is(F)?(y=F,R=U.del(M,F)):(this.assertChangeAnnotations(this.changeAnnotations),y=this.changeAnnotations.manage(F),R=U.del(M,y)),this.edits.push(R),y!==void 0)return y},v.prototype.add=function(M){this.edits.push(M);},v.prototype.all=function(){return this.edits},v.prototype.clear=function(){this.edits.splice(0,this.edits.length);},v.prototype.assertChangeAnnotations=function(M){if(M===void 0)throw new Error("Text edit change is not configured to manage change annotations.")},v}(),q=function(){function v(M){this._annotations=M===void 0?Object.create(null):M,this._counter=0,this._size=0;}return v.prototype.all=function(){return this._annotations},Object.defineProperty(v.prototype,"size",{get:function(){return this._size},enumerable:!1,configurable:!0}),v.prototype.manage=function(M,F){var R;if(K.is(M)?R=M:(R=this.nextId(),F=M),this._annotations[R]!==void 0)throw new Error("Id ".concat(R," is already in use."));if(F===void 0)throw new Error("No annotation provided for id ".concat(R));return this._annotations[R]=F,this._size++,R},v.prototype.nextId=function(){return this._counter++,this._counter.toString()},v}(),W=function(){function v(M){var F=this;this._textEditChanges=Object.create(null),M!==void 0?(this._workspaceEdit=M,M.documentChanges?(this._changeAnnotations=new q(M.changeAnnotations),M.changeAnnotations=this._changeAnnotations.all(),M.documentChanges.forEach(function(R){if(B.is(R)){var y=new ye(R.edits,F._changeAnnotations);F._textEditChanges[R.textDocument.uri]=y;}})):M.changes&&Object.keys(M.changes).forEach(function(R){var y=new ye(M.changes[R]);F._textEditChanges[R]=y;})):this._workspaceEdit={};}return Object.defineProperty(v.prototype,"edit",{get:function(){return this.initDocumentChanges(),this._changeAnnotations!==void 0&&(this._changeAnnotations.size===0?this._workspaceEdit.changeAnnotations=void 0:this._workspaceEdit.changeAnnotations=this._changeAnnotations.all()),this._workspaceEdit},enumerable:!1,configurable:!0}),v.prototype.getTextEditChange=function(M){if(we.is(M)){if(this.initDocumentChanges(),this._workspaceEdit.documentChanges===void 0)throw new Error("Workspace edit is not configured for document changes.");var F={uri:M.uri,version:M.version},R=this._textEditChanges[F.uri];if(!R){var y=[],$={textDocument:F,edits:y};this._workspaceEdit.documentChanges.push($),R=new ye(y,this._changeAnnotations),this._textEditChanges[F.uri]=R;}return R}else {if(this.initChanges(),this._workspaceEdit.changes===void 0)throw new Error("Workspace edit is not configured for normal text edit changes.");var R=this._textEditChanges[M];if(!R){var y=[];this._workspaceEdit.changes[M]=y,R=new ye(y),this._textEditChanges[M]=R;}return R}},v.prototype.initDocumentChanges=function(){this._workspaceEdit.documentChanges===void 0&&this._workspaceEdit.changes===void 0&&(this._changeAnnotations=new q,this._workspaceEdit.documentChanges=[],this._workspaceEdit.changeAnnotations=this._changeAnnotations.all());},v.prototype.initChanges=function(){this._workspaceEdit.documentChanges===void 0&&this._workspaceEdit.changes===void 0&&(this._workspaceEdit.changes=Object.create(null));},v.prototype.createFile=function(M,F,R){if(this.initDocumentChanges(),this._workspaceEdit.documentChanges===void 0)throw new Error("Workspace edit is not configured for document changes.");var y;Z.is(F)||K.is(F)?y=F:R=F;var $,J;if(y===void 0?$=Q.create(M,R):(J=K.is(y)?y:this._changeAnnotations.manage(y),$=Q.create(M,R,J)),this._workspaceEdit.documentChanges.push($),J!==void 0)return J},v.prototype.renameFile=function(M,F,R,y){if(this.initDocumentChanges(),this._workspaceEdit.documentChanges===void 0)throw new Error("Workspace edit is not configured for document changes.");var $;Z.is(R)||K.is(R)?$=R:y=R;var J,Ee;if($===void 0?J=N.create(M,F,y):(Ee=K.is($)?$:this._changeAnnotations.manage($),J=N.create(M,F,y,Ee)),this._workspaceEdit.documentChanges.push(J),Ee!==void 0)return Ee},v.prototype.deleteFile=function(M,F,R){if(this.initDocumentChanges(),this._workspaceEdit.documentChanges===void 0)throw new Error("Workspace edit is not configured for document changes.");var y;Z.is(F)||K.is(F)?y=F:R=F;var $,J;if(y===void 0?$=te.create(M,R):(J=K.is(y)?y:this._changeAnnotations.manage(y),$=te.create(M,R,J)),this._workspaceEdit.documentChanges.push($),J!==void 0)return J},v}();e.WorkspaceChange=W;var me;(function(v){function M(R){return {uri:R}}v.create=M;function F(R){var y=R;return j.defined(y)&&j.string(y.uri)}v.is=F;})(me||(e.TextDocumentIdentifier=me={}));var pe;(function(v){function M(R,y){return {uri:R,version:y}}v.create=M;function F(R){var y=R;return j.defined(y)&&j.string(y.uri)&&j.integer(y.version)}v.is=F;})(pe||(e.VersionedTextDocumentIdentifier=pe={}));var we;(function(v){function M(R,y){return {uri:R,version:y}}v.create=M;function F(R){var y=R;return j.defined(y)&&j.string(y.uri)&&(y.version===null||j.integer(y.version))}v.is=F;})(we||(e.OptionalVersionedTextDocumentIdentifier=we={}));var Xe;(function(v){function M(R,y,$,J){return {uri:R,languageId:y,version:$,text:J}}v.create=M;function F(R){var y=R;return j.defined(y)&&j.string(y.uri)&&j.string(y.languageId)&&j.integer(y.version)&&j.string(y.text)}v.is=F;})(Xe||(e.TextDocumentItem=Xe={}));var Ke;(function(v){v.PlainText="plaintext",v.Markdown="markdown";function M(F){var R=F;return R===v.PlainText||R===v.Markdown}v.is=M;})(Ke||(e.MarkupKind=Ke={}));var Pt;(function(v){function M(F){var R=F;return j.objectLiteral(F)&&Ke.is(R.kind)&&j.string(R.value)}v.is=M;})(Pt||(e.MarkupContent=Pt={}));var Lr;(function(v){v.Text=1,v.Method=2,v.Function=3,v.Constructor=4,v.Field=5,v.Variable=6,v.Class=7,v.Interface=8,v.Module=9,v.Property=10,v.Unit=11,v.Value=12,v.Enum=13,v.Keyword=14,v.Snippet=15,v.Color=16,v.File=17,v.Reference=18,v.Folder=19,v.EnumMember=20,v.Constant=21,v.Struct=22,v.Event=23,v.Operator=24,v.TypeParameter=25;})(Lr||(e.CompletionItemKind=Lr={}));var zt;(function(v){v.PlainText=1,v.Snippet=2;})(zt||(e.InsertTextFormat=zt={}));var St;(function(v){v.Deprecated=1;})(St||(e.CompletionItemTag=St={}));var Ce;(function(v){function M(R,y,$){return {newText:R,insert:y,replace:$}}v.create=M;function F(R){var y=R;return y&&j.string(y.newText)&&a.is(y.insert)&&a.is(y.replace)}v.is=F;})(Ce||(e.InsertReplaceEdit=Ce={}));var Cr;(function(v){v.asIs=1,v.adjustIndentation=2;})(Cr||(e.InsertTextMode=Cr={}));var fn;(function(v){function M(F){var R=F;return R&&(j.string(R.detail)||R.detail===void 0)&&(j.string(R.description)||R.description===void 0)}v.is=M;})(fn||(e.CompletionItemLabelDetails=fn={}));var ce;(function(v){function M(F){return {label:F}}v.create=M;})(ce||(e.CompletionItem=ce={}));var Me;(function(v){function M(F,R){return {items:F||[],isIncomplete:!!R}}v.create=M;})(Me||(e.CompletionList=Me={}));var ie;(function(v){function M(R){return R.replace(/[\\`*_{}[\]()#+\-.!]/g,"\\$&")}v.fromPlainText=M;function F(R){var y=R;return j.string(y)||j.objectLiteral(y)&&j.string(y.language)&&j.string(y.value)}v.is=F;})(ie||(e.MarkedString=ie={}));var be;(function(v){function M(F){var R=F;return !!R&&j.objectLiteral(R)&&(Pt.is(R.contents)||ie.is(R.contents)||j.typedArray(R.contents,ie.is))&&(F.range===void 0||a.is(F.range))}v.is=M;})(be||(e.Hover=be={}));var $e;(function(v){function M(F,R){return R?{label:F,documentation:R}:{label:F}}v.create=M;})($e||(e.ParameterInformation=$e={}));var ot;(function(v){function M(F,R){for(var y=[],$=2;$=0;rr--){var nr=tt[rr],Tr=$.offsetAt(nr.range.start),Pe=$.offsetAt(nr.range.end);if(Pe<=Ve)Ee=Ee.substring(0,Tr)+nr.newText+Ee.substring(Pe,Ee.length);else throw new Error("Overlapping edit");Ve=Tr;}return Ee}v.applyEdits=R;function y($,J){if($.length<=1)return $;var Ee=$.length/2|0,tt=$.slice(0,Ee),Ve=$.slice(Ee);y(tt,J),y(Ve,J);for(var rr=0,nr=0,Tr=0;rr0&&M.push(F.length),this._lineOffsets=M;}return this._lineOffsets},v.prototype.positionAt=function(M){M=Math.max(Math.min(M,this._content.length),0);var F=this.getLineOffsets(),R=0,y=F.length;if(y===0)return o.create(0,M);for(;RM?y=$:R=$+1;}var J=R-1;return o.create(J,M-F[J])},v.prototype.offsetAt=function(M){var F=this.getLineOffsets();if(M.line>=F.length)return this._content.length;if(M.line<0)return 0;var R=F[M.line],y=M.line+1"u"}v.undefined=R;function y(Pe){return Pe===!0||Pe===!1}v.boolean=y;function $(Pe){return M.call(Pe)==="[object String]"}v.string=$;function J(Pe){return M.call(Pe)==="[object Number]"}v.number=J;function Ee(Pe,pn,vi){return M.call(Pe)==="[object Number]"&&pn<=Pe&&Pe<=vi}v.numberRange=Ee;function tt(Pe){return M.call(Pe)==="[object Number]"&&-2147483648<=Pe&&Pe<=2147483647}v.integer=tt;function Ve(Pe){return M.call(Pe)==="[object Number]"&&0<=Pe&&Pe<=2147483647}v.uinteger=Ve;function rr(Pe){return M.call(Pe)==="[object Function]"}v.func=rr;function nr(Pe){return Pe!==null&&typeof Pe=="object"}v.objectLiteral=nr;function Tr(Pe,pn){return Array.isArray(Pe)&&Pe.every(pn)}v.typedArray=Tr;})(j||(j={}));});});var rt=A(Yt=>{Object.defineProperty(Yt,"__esModule",{value:!0});Yt.ProtocolNotificationType=Yt.ProtocolNotificationType0=Yt.ProtocolRequestType=Yt.ProtocolRequestType0=Yt.RegistrationType=Yt.MessageDirection=void 0;var as=hi(),VT;(function(t){t.clientToServer="clientToServer",t.serverToClient="serverToClient",t.both="both";})(VT||(Yt.MessageDirection=VT={}));var Am=class{constructor(e){this.method=e;}};Yt.RegistrationType=Am;var Pm=class extends as.RequestType0{constructor(e){super(e);}};Yt.ProtocolRequestType0=Pm;var Dm=class extends as.RequestType{constructor(e){super(e,as.ParameterStructures.byName);}};Yt.ProtocolRequestType=Dm;var Om=class extends as.NotificationType0{constructor(e){super(e);}};Yt.ProtocolNotificationType0=Om;var Im=class extends as.NotificationType{constructor(e){super(e,as.ParameterStructures.byName);}};Yt.ProtocolNotificationType=Im;});var pc=A(mt=>{Object.defineProperty(mt,"__esModule",{value:!0});mt.objectLiteral=mt.typedArray=mt.stringArray=mt.array=mt.func=mt.error=mt.number=mt.string=mt.boolean=void 0;function r4(t){return t===!0||t===!1}mt.boolean=r4;function GT(t){return typeof t=="string"||t instanceof String}mt.string=GT;function n4(t){return typeof t=="number"||t instanceof Number}mt.number=n4;function i4(t){return t instanceof Error}mt.error=i4;function s4(t){return typeof t=="function"}mt.func=s4;function KT(t){return Array.isArray(t)}mt.array=KT;function o4(t){return KT(t)&&t.every(e=>GT(e))}mt.stringArray=o4;function a4(t,e){return Array.isArray(t)&&t.every(e)}mt.typedArray=a4;function u4(t){return t!==null&&typeof t=="object"}mt.objectLiteral=u4;});var YT=A(mc=>{Object.defineProperty(mc,"__esModule",{value:!0});mc.ImplementationRequest=void 0;var ZT=rt(),JT;(function(t){t.method="textDocument/implementation",t.messageDirection=ZT.MessageDirection.clientToServer,t.type=new ZT.ProtocolRequestType(t.method);})(JT||(mc.ImplementationRequest=JT={}));});var eA=A(gc=>{Object.defineProperty(gc,"__esModule",{value:!0});gc.TypeDefinitionRequest=void 0;var XT=rt(),QT;(function(t){t.method="textDocument/typeDefinition",t.messageDirection=XT.MessageDirection.clientToServer,t.type=new XT.ProtocolRequestType(t.method);})(QT||(gc.TypeDefinitionRequest=QT={}));});var nA=A(us=>{Object.defineProperty(us,"__esModule",{value:!0});us.DidChangeWorkspaceFoldersNotification=us.WorkspaceFoldersRequest=void 0;var _c=rt(),tA;(function(t){t.method="workspace/workspaceFolders",t.messageDirection=_c.MessageDirection.serverToClient,t.type=new _c.ProtocolRequestType0(t.method);})(tA||(us.WorkspaceFoldersRequest=tA={}));var rA;(function(t){t.method="workspace/didChangeWorkspaceFolders",t.messageDirection=_c.MessageDirection.clientToServer,t.type=new _c.ProtocolNotificationType(t.method);})(rA||(us.DidChangeWorkspaceFoldersNotification=rA={}));});var oA=A(yc=>{Object.defineProperty(yc,"__esModule",{value:!0});yc.ConfigurationRequest=void 0;var iA=rt(),sA;(function(t){t.method="workspace/configuration",t.messageDirection=iA.MessageDirection.serverToClient,t.type=new iA.ProtocolRequestType(t.method);})(sA||(yc.ConfigurationRequest=sA={}));});var cA=A(cs=>{Object.defineProperty(cs,"__esModule",{value:!0});cs.ColorPresentationRequest=cs.DocumentColorRequest=void 0;var bc=rt(),aA;(function(t){t.method="textDocument/documentColor",t.messageDirection=bc.MessageDirection.clientToServer,t.type=new bc.ProtocolRequestType(t.method);})(aA||(cs.DocumentColorRequest=aA={}));var uA;(function(t){t.method="textDocument/colorPresentation",t.messageDirection=bc.MessageDirection.clientToServer,t.type=new bc.ProtocolRequestType(t.method);})(uA||(cs.ColorPresentationRequest=uA={}));});var dA=A(ls=>{Object.defineProperty(ls,"__esModule",{value:!0});ls.FoldingRangeRefreshRequest=ls.FoldingRangeRequest=void 0;var vc=rt(),lA;(function(t){t.method="textDocument/foldingRange",t.messageDirection=vc.MessageDirection.clientToServer,t.type=new vc.ProtocolRequestType(t.method);})(lA||(ls.FoldingRangeRequest=lA={}));var fA;(function(t){t.method="workspace/foldingRange/refresh",t.messageDirection=vc.MessageDirection.serverToClient,t.type=new vc.ProtocolRequestType0(t.method);})(fA||(ls.FoldingRangeRefreshRequest=fA={}));});var mA=A(wc=>{Object.defineProperty(wc,"__esModule",{value:!0});wc.DeclarationRequest=void 0;var hA=rt(),pA;(function(t){t.method="textDocument/declaration",t.messageDirection=hA.MessageDirection.clientToServer,t.type=new hA.ProtocolRequestType(t.method);})(pA||(wc.DeclarationRequest=pA={}));});var yA=A(Sc=>{Object.defineProperty(Sc,"__esModule",{value:!0});Sc.SelectionRangeRequest=void 0;var gA=rt(),_A;(function(t){t.method="textDocument/selectionRange",t.messageDirection=gA.MessageDirection.clientToServer,t.type=new gA.ProtocolRequestType(t.method);})(_A||(Sc.SelectionRangeRequest=_A={}));});var SA=A(kn=>{Object.defineProperty(kn,"__esModule",{value:!0});kn.WorkDoneProgressCancelNotification=kn.WorkDoneProgressCreateRequest=kn.WorkDoneProgress=void 0;var c4=hi(),Ec=rt(),bA;(function(t){t.type=new c4.ProgressType;function e(r){return r===t.type}t.is=e;})(bA||(kn.WorkDoneProgress=bA={}));var vA;(function(t){t.method="window/workDoneProgress/create",t.messageDirection=Ec.MessageDirection.serverToClient,t.type=new Ec.ProtocolRequestType(t.method);})(vA||(kn.WorkDoneProgressCreateRequest=vA={}));var wA;(function(t){t.method="window/workDoneProgress/cancel",t.messageDirection=Ec.MessageDirection.clientToServer,t.type=new Ec.ProtocolNotificationType(t.method);})(wA||(kn.WorkDoneProgressCancelNotification=wA={}));});var xA=A(Mn=>{Object.defineProperty(Mn,"__esModule",{value:!0});Mn.CallHierarchyOutgoingCallsRequest=Mn.CallHierarchyIncomingCallsRequest=Mn.CallHierarchyPrepareRequest=void 0;var ds=rt(),EA;(function(t){t.method="textDocument/prepareCallHierarchy",t.messageDirection=ds.MessageDirection.clientToServer,t.type=new ds.ProtocolRequestType(t.method);})(EA||(Mn.CallHierarchyPrepareRequest=EA={}));var RA;(function(t){t.method="callHierarchy/incomingCalls",t.messageDirection=ds.MessageDirection.clientToServer,t.type=new ds.ProtocolRequestType(t.method);})(RA||(Mn.CallHierarchyIncomingCallsRequest=RA={}));var CA;(function(t){t.method="callHierarchy/outgoingCalls",t.messageDirection=ds.MessageDirection.clientToServer,t.type=new ds.ProtocolRequestType(t.method);})(CA||(Mn.CallHierarchyOutgoingCallsRequest=CA={}));});var IA=A(Xt=>{Object.defineProperty(Xt,"__esModule",{value:!0});Xt.SemanticTokensRefreshRequest=Xt.SemanticTokensRangeRequest=Xt.SemanticTokensDeltaRequest=Xt.SemanticTokensRequest=Xt.SemanticTokensRegistrationType=Xt.TokenFormat=void 0;var ln=rt(),TA;(function(t){t.Relative="relative";})(TA||(Xt.TokenFormat=TA={}));var To;(function(t){t.method="textDocument/semanticTokens",t.type=new ln.RegistrationType(t.method);})(To||(Xt.SemanticTokensRegistrationType=To={}));var AA;(function(t){t.method="textDocument/semanticTokens/full",t.messageDirection=ln.MessageDirection.clientToServer,t.type=new ln.ProtocolRequestType(t.method),t.registrationMethod=To.method;})(AA||(Xt.SemanticTokensRequest=AA={}));var PA;(function(t){t.method="textDocument/semanticTokens/full/delta",t.messageDirection=ln.MessageDirection.clientToServer,t.type=new ln.ProtocolRequestType(t.method),t.registrationMethod=To.method;})(PA||(Xt.SemanticTokensDeltaRequest=PA={}));var DA;(function(t){t.method="textDocument/semanticTokens/range",t.messageDirection=ln.MessageDirection.clientToServer,t.type=new ln.ProtocolRequestType(t.method),t.registrationMethod=To.method;})(DA||(Xt.SemanticTokensRangeRequest=DA={}));var OA;(function(t){t.method="workspace/semanticTokens/refresh",t.messageDirection=ln.MessageDirection.serverToClient,t.type=new ln.ProtocolRequestType0(t.method);})(OA||(Xt.SemanticTokensRefreshRequest=OA={}));});var FA=A(Rc=>{Object.defineProperty(Rc,"__esModule",{value:!0});Rc.ShowDocumentRequest=void 0;var kA=rt(),MA;(function(t){t.method="window/showDocument",t.messageDirection=kA.MessageDirection.serverToClient,t.type=new kA.ProtocolRequestType(t.method);})(MA||(Rc.ShowDocumentRequest=MA={}));});var LA=A(Cc=>{Object.defineProperty(Cc,"__esModule",{value:!0});Cc.LinkedEditingRangeRequest=void 0;var NA=rt(),qA;(function(t){t.method="textDocument/linkedEditingRange",t.messageDirection=NA.MessageDirection.clientToServer,t.type=new NA.ProtocolRequestType(t.method);})(qA||(Cc.LinkedEditingRangeRequest=qA={}));});var VA=A(Mt=>{Object.defineProperty(Mt,"__esModule",{value:!0});Mt.WillDeleteFilesRequest=Mt.DidDeleteFilesNotification=Mt.DidRenameFilesNotification=Mt.WillRenameFilesRequest=Mt.DidCreateFilesNotification=Mt.WillCreateFilesRequest=Mt.FileOperationPatternKind=void 0;var br=rt(),$A;(function(t){t.file="file",t.folder="folder";})($A||(Mt.FileOperationPatternKind=$A={}));var jA;(function(t){t.method="workspace/willCreateFiles",t.messageDirection=br.MessageDirection.clientToServer,t.type=new br.ProtocolRequestType(t.method);})(jA||(Mt.WillCreateFilesRequest=jA={}));var HA;(function(t){t.method="workspace/didCreateFiles",t.messageDirection=br.MessageDirection.clientToServer,t.type=new br.ProtocolNotificationType(t.method);})(HA||(Mt.DidCreateFilesNotification=HA={}));var WA;(function(t){t.method="workspace/willRenameFiles",t.messageDirection=br.MessageDirection.clientToServer,t.type=new br.ProtocolRequestType(t.method);})(WA||(Mt.WillRenameFilesRequest=WA={}));var BA;(function(t){t.method="workspace/didRenameFiles",t.messageDirection=br.MessageDirection.clientToServer,t.type=new br.ProtocolNotificationType(t.method);})(BA||(Mt.DidRenameFilesNotification=BA={}));var UA;(function(t){t.method="workspace/didDeleteFiles",t.messageDirection=br.MessageDirection.clientToServer,t.type=new br.ProtocolNotificationType(t.method);})(UA||(Mt.DidDeleteFilesNotification=UA={}));var zA;(function(t){t.method="workspace/willDeleteFiles",t.messageDirection=br.MessageDirection.clientToServer,t.type=new br.ProtocolRequestType(t.method);})(zA||(Mt.WillDeleteFilesRequest=zA={}));});var YA=A(Fn=>{Object.defineProperty(Fn,"__esModule",{value:!0});Fn.MonikerRequest=Fn.MonikerKind=Fn.UniquenessLevel=void 0;var GA=rt(),KA;(function(t){t.document="document",t.project="project",t.group="group",t.scheme="scheme",t.global="global";})(KA||(Fn.UniquenessLevel=KA={}));var ZA;(function(t){t.$import="import",t.$export="export",t.local="local";})(ZA||(Fn.MonikerKind=ZA={}));var JA;(function(t){t.method="textDocument/moniker",t.messageDirection=GA.MessageDirection.clientToServer,t.type=new GA.ProtocolRequestType(t.method);})(JA||(Fn.MonikerRequest=JA={}));});var tP=A(Nn=>{Object.defineProperty(Nn,"__esModule",{value:!0});Nn.TypeHierarchySubtypesRequest=Nn.TypeHierarchySupertypesRequest=Nn.TypeHierarchyPrepareRequest=void 0;var hs=rt(),XA;(function(t){t.method="textDocument/prepareTypeHierarchy",t.messageDirection=hs.MessageDirection.clientToServer,t.type=new hs.ProtocolRequestType(t.method);})(XA||(Nn.TypeHierarchyPrepareRequest=XA={}));var QA;(function(t){t.method="typeHierarchy/supertypes",t.messageDirection=hs.MessageDirection.clientToServer,t.type=new hs.ProtocolRequestType(t.method);})(QA||(Nn.TypeHierarchySupertypesRequest=QA={}));var eP;(function(t){t.method="typeHierarchy/subtypes",t.messageDirection=hs.MessageDirection.clientToServer,t.type=new hs.ProtocolRequestType(t.method);})(eP||(Nn.TypeHierarchySubtypesRequest=eP={}));});var iP=A(ps=>{Object.defineProperty(ps,"__esModule",{value:!0});ps.InlineValueRefreshRequest=ps.InlineValueRequest=void 0;var xc=rt(),rP;(function(t){t.method="textDocument/inlineValue",t.messageDirection=xc.MessageDirection.clientToServer,t.type=new xc.ProtocolRequestType(t.method);})(rP||(ps.InlineValueRequest=rP={}));var nP;(function(t){t.method="workspace/inlineValue/refresh",t.messageDirection=xc.MessageDirection.serverToClient,t.type=new xc.ProtocolRequestType0(t.method);})(nP||(ps.InlineValueRefreshRequest=nP={}));});var uP=A(qn=>{Object.defineProperty(qn,"__esModule",{value:!0});qn.InlayHintRefreshRequest=qn.InlayHintResolveRequest=qn.InlayHintRequest=void 0;var ms=rt(),sP;(function(t){t.method="textDocument/inlayHint",t.messageDirection=ms.MessageDirection.clientToServer,t.type=new ms.ProtocolRequestType(t.method);})(sP||(qn.InlayHintRequest=sP={}));var oP;(function(t){t.method="inlayHint/resolve",t.messageDirection=ms.MessageDirection.clientToServer,t.type=new ms.ProtocolRequestType(t.method);})(oP||(qn.InlayHintResolveRequest=oP={}));var aP;(function(t){t.method="workspace/inlayHint/refresh",t.messageDirection=ms.MessageDirection.serverToClient,t.type=new ms.ProtocolRequestType0(t.method);})(aP||(qn.InlayHintRefreshRequest=aP={}));});var mP=A(vr=>{Object.defineProperty(vr,"__esModule",{value:!0});vr.DiagnosticRefreshRequest=vr.WorkspaceDiagnosticRequest=vr.DocumentDiagnosticRequest=vr.DocumentDiagnosticReportKind=vr.DiagnosticServerCancellationData=void 0;var pP=hi(),l4=pc(),gs=rt(),cP;(function(t){function e(r){let n=r;return n&&l4.boolean(n.retriggerRequest)}t.is=e;})(cP||(vr.DiagnosticServerCancellationData=cP={}));var lP;(function(t){t.Full="full",t.Unchanged="unchanged";})(lP||(vr.DocumentDiagnosticReportKind=lP={}));var fP;(function(t){t.method="textDocument/diagnostic",t.messageDirection=gs.MessageDirection.clientToServer,t.type=new gs.ProtocolRequestType(t.method),t.partialResult=new pP.ProgressType;})(fP||(vr.DocumentDiagnosticRequest=fP={}));var dP;(function(t){t.method="workspace/diagnostic",t.messageDirection=gs.MessageDirection.clientToServer,t.type=new gs.ProtocolRequestType(t.method),t.partialResult=new pP.ProgressType;})(dP||(vr.WorkspaceDiagnosticRequest=dP={}));var hP;(function(t){t.method="workspace/diagnostic/refresh",t.messageDirection=gs.MessageDirection.serverToClient,t.type=new gs.ProtocolRequestType0(t.method);})(hP||(vr.DiagnosticRefreshRequest=hP={}));});var SP=A(at=>{Object.defineProperty(at,"__esModule",{value:!0});at.DidCloseNotebookDocumentNotification=at.DidSaveNotebookDocumentNotification=at.DidChangeNotebookDocumentNotification=at.NotebookCellArrayChange=at.DidOpenNotebookDocumentNotification=at.NotebookDocumentSyncRegistrationType=at.NotebookDocument=at.NotebookCell=at.ExecutionSummary=at.NotebookCellKind=void 0;var Ao=hc(),Nr=pc(),Gr=rt(),km;(function(t){t.Markup=1,t.Code=2;function e(r){return r===1||r===2}t.is=e;})(km||(at.NotebookCellKind=km={}));var Mm;(function(t){function e(i,s){let o={executionOrder:i};return (s===!0||s===!1)&&(o.success=s),o}t.create=e;function r(i){let s=i;return Nr.objectLiteral(s)&&Ao.uinteger.is(s.executionOrder)&&(s.success===void 0||Nr.boolean(s.success))}t.is=r;function n(i,s){return i===s?!0:i==null||s===null||s===void 0?!1:i.executionOrder===s.executionOrder&&i.success===s.success}t.equals=n;})(Mm||(at.ExecutionSummary=Mm={}));var Tc;(function(t){function e(s,o){return {kind:s,document:o}}t.create=e;function r(s){let o=s;return Nr.objectLiteral(o)&&km.is(o.kind)&&Ao.DocumentUri.is(o.document)&&(o.metadata===void 0||Nr.objectLiteral(o.metadata))}t.is=r;function n(s,o){let a=new Set;return s.document!==o.document&&a.add("document"),s.kind!==o.kind&&a.add("kind"),s.executionSummary!==o.executionSummary&&a.add("executionSummary"),(s.metadata!==void 0||o.metadata!==void 0)&&!i(s.metadata,o.metadata)&&a.add("metadata"),(s.executionSummary!==void 0||o.executionSummary!==void 0)&&!Mm.equals(s.executionSummary,o.executionSummary)&&a.add("executionSummary"),a}t.diff=n;function i(s,o){if(s===o)return !0;if(s==null||o===null||o===void 0||typeof s!=typeof o||typeof s!="object")return !1;let a=Array.isArray(s),l=Array.isArray(o);if(a!==l)return !1;if(a&&l){if(s.length!==o.length)return !1;for(let d=0;d{Object.defineProperty(Ac,"__esModule",{value:!0});Ac.InlineCompletionRequest=void 0;var EP=rt(),RP;(function(t){t.method="textDocument/inlineCompletion",t.messageDirection=EP.MessageDirection.clientToServer,t.type=new EP.ProtocolRequestType(t.method);})(RP||(Ac.InlineCompletionRequest=RP={}));});var LD=A(T=>{Object.defineProperty(T,"__esModule",{value:!0});T.WorkspaceSymbolRequest=T.CodeActionResolveRequest=T.CodeActionRequest=T.DocumentSymbolRequest=T.DocumentHighlightRequest=T.ReferencesRequest=T.DefinitionRequest=T.SignatureHelpRequest=T.SignatureHelpTriggerKind=T.HoverRequest=T.CompletionResolveRequest=T.CompletionRequest=T.CompletionTriggerKind=T.PublishDiagnosticsNotification=T.WatchKind=T.RelativePattern=T.FileChangeType=T.DidChangeWatchedFilesNotification=T.WillSaveTextDocumentWaitUntilRequest=T.WillSaveTextDocumentNotification=T.TextDocumentSaveReason=T.DidSaveTextDocumentNotification=T.DidCloseTextDocumentNotification=T.DidChangeTextDocumentNotification=T.TextDocumentContentChangeEvent=T.DidOpenTextDocumentNotification=T.TextDocumentSyncKind=T.TelemetryEventNotification=T.LogMessageNotification=T.ShowMessageRequest=T.ShowMessageNotification=T.MessageType=T.DidChangeConfigurationNotification=T.ExitNotification=T.ShutdownRequest=T.InitializedNotification=T.InitializeErrorCodes=T.InitializeRequest=T.WorkDoneProgressOptions=T.TextDocumentRegistrationOptions=T.StaticRegistrationOptions=T.PositionEncodingKind=T.FailureHandlingKind=T.ResourceOperationKind=T.UnregistrationRequest=T.RegistrationRequest=T.DocumentSelector=T.NotebookCellTextDocumentFilter=T.NotebookDocumentFilter=T.TextDocumentFilter=void 0;T.MonikerRequest=T.MonikerKind=T.UniquenessLevel=T.WillDeleteFilesRequest=T.DidDeleteFilesNotification=T.WillRenameFilesRequest=T.DidRenameFilesNotification=T.WillCreateFilesRequest=T.DidCreateFilesNotification=T.FileOperationPatternKind=T.LinkedEditingRangeRequest=T.ShowDocumentRequest=T.SemanticTokensRegistrationType=T.SemanticTokensRefreshRequest=T.SemanticTokensRangeRequest=T.SemanticTokensDeltaRequest=T.SemanticTokensRequest=T.TokenFormat=T.CallHierarchyPrepareRequest=T.CallHierarchyOutgoingCallsRequest=T.CallHierarchyIncomingCallsRequest=T.WorkDoneProgressCancelNotification=T.WorkDoneProgressCreateRequest=T.WorkDoneProgress=T.SelectionRangeRequest=T.DeclarationRequest=T.FoldingRangeRefreshRequest=T.FoldingRangeRequest=T.ColorPresentationRequest=T.DocumentColorRequest=T.ConfigurationRequest=T.DidChangeWorkspaceFoldersNotification=T.WorkspaceFoldersRequest=T.TypeDefinitionRequest=T.ImplementationRequest=T.ApplyWorkspaceEditRequest=T.ExecuteCommandRequest=T.PrepareRenameRequest=T.RenameRequest=T.PrepareSupportDefaultBehavior=T.DocumentOnTypeFormattingRequest=T.DocumentRangesFormattingRequest=T.DocumentRangeFormattingRequest=T.DocumentFormattingRequest=T.DocumentLinkResolveRequest=T.DocumentLinkRequest=T.CodeLensRefreshRequest=T.CodeLensResolveRequest=T.CodeLensRequest=T.WorkspaceSymbolResolveRequest=void 0;T.InlineCompletionRequest=T.DidCloseNotebookDocumentNotification=T.DidSaveNotebookDocumentNotification=T.DidChangeNotebookDocumentNotification=T.NotebookCellArrayChange=T.DidOpenNotebookDocumentNotification=T.NotebookDocumentSyncRegistrationType=T.NotebookDocument=T.NotebookCell=T.ExecutionSummary=T.NotebookCellKind=T.DiagnosticRefreshRequest=T.WorkspaceDiagnosticRequest=T.DocumentDiagnosticRequest=T.DocumentDiagnosticReportKind=T.DiagnosticServerCancellationData=T.InlayHintRefreshRequest=T.InlayHintResolveRequest=T.InlayHintRequest=T.InlineValueRefreshRequest=T.InlineValueRequest=T.TypeHierarchySupertypesRequest=T.TypeHierarchySubtypesRequest=T.TypeHierarchyPrepareRequest=void 0;var ne=rt(),xP=hc(),wt=pc(),f4=YT();Object.defineProperty(T,"ImplementationRequest",{enumerable:!0,get:function(){return f4.ImplementationRequest}});var d4=eA();Object.defineProperty(T,"TypeDefinitionRequest",{enumerable:!0,get:function(){return d4.TypeDefinitionRequest}});var MD=nA();Object.defineProperty(T,"WorkspaceFoldersRequest",{enumerable:!0,get:function(){return MD.WorkspaceFoldersRequest}});Object.defineProperty(T,"DidChangeWorkspaceFoldersNotification",{enumerable:!0,get:function(){return MD.DidChangeWorkspaceFoldersNotification}});var h4=oA();Object.defineProperty(T,"ConfigurationRequest",{enumerable:!0,get:function(){return h4.ConfigurationRequest}});var FD=cA();Object.defineProperty(T,"DocumentColorRequest",{enumerable:!0,get:function(){return FD.DocumentColorRequest}});Object.defineProperty(T,"ColorPresentationRequest",{enumerable:!0,get:function(){return FD.ColorPresentationRequest}});var ND=dA();Object.defineProperty(T,"FoldingRangeRequest",{enumerable:!0,get:function(){return ND.FoldingRangeRequest}});Object.defineProperty(T,"FoldingRangeRefreshRequest",{enumerable:!0,get:function(){return ND.FoldingRangeRefreshRequest}});var p4=mA();Object.defineProperty(T,"DeclarationRequest",{enumerable:!0,get:function(){return p4.DeclarationRequest}});var m4=yA();Object.defineProperty(T,"SelectionRangeRequest",{enumerable:!0,get:function(){return m4.SelectionRangeRequest}});var $m=SA();Object.defineProperty(T,"WorkDoneProgress",{enumerable:!0,get:function(){return $m.WorkDoneProgress}});Object.defineProperty(T,"WorkDoneProgressCreateRequest",{enumerable:!0,get:function(){return $m.WorkDoneProgressCreateRequest}});Object.defineProperty(T,"WorkDoneProgressCancelNotification",{enumerable:!0,get:function(){return $m.WorkDoneProgressCancelNotification}});var jm=xA();Object.defineProperty(T,"CallHierarchyIncomingCallsRequest",{enumerable:!0,get:function(){return jm.CallHierarchyIncomingCallsRequest}});Object.defineProperty(T,"CallHierarchyOutgoingCallsRequest",{enumerable:!0,get:function(){return jm.CallHierarchyOutgoingCallsRequest}});Object.defineProperty(T,"CallHierarchyPrepareRequest",{enumerable:!0,get:function(){return jm.CallHierarchyPrepareRequest}});var ys=IA();Object.defineProperty(T,"TokenFormat",{enumerable:!0,get:function(){return ys.TokenFormat}});Object.defineProperty(T,"SemanticTokensRequest",{enumerable:!0,get:function(){return ys.SemanticTokensRequest}});Object.defineProperty(T,"SemanticTokensDeltaRequest",{enumerable:!0,get:function(){return ys.SemanticTokensDeltaRequest}});Object.defineProperty(T,"SemanticTokensRangeRequest",{enumerable:!0,get:function(){return ys.SemanticTokensRangeRequest}});Object.defineProperty(T,"SemanticTokensRefreshRequest",{enumerable:!0,get:function(){return ys.SemanticTokensRefreshRequest}});Object.defineProperty(T,"SemanticTokensRegistrationType",{enumerable:!0,get:function(){return ys.SemanticTokensRegistrationType}});var g4=FA();Object.defineProperty(T,"ShowDocumentRequest",{enumerable:!0,get:function(){return g4.ShowDocumentRequest}});var _4=LA();Object.defineProperty(T,"LinkedEditingRangeRequest",{enumerable:!0,get:function(){return _4.LinkedEditingRangeRequest}});var pi=VA();Object.defineProperty(T,"FileOperationPatternKind",{enumerable:!0,get:function(){return pi.FileOperationPatternKind}});Object.defineProperty(T,"DidCreateFilesNotification",{enumerable:!0,get:function(){return pi.DidCreateFilesNotification}});Object.defineProperty(T,"WillCreateFilesRequest",{enumerable:!0,get:function(){return pi.WillCreateFilesRequest}});Object.defineProperty(T,"DidRenameFilesNotification",{enumerable:!0,get:function(){return pi.DidRenameFilesNotification}});Object.defineProperty(T,"WillRenameFilesRequest",{enumerable:!0,get:function(){return pi.WillRenameFilesRequest}});Object.defineProperty(T,"DidDeleteFilesNotification",{enumerable:!0,get:function(){return pi.DidDeleteFilesNotification}});Object.defineProperty(T,"WillDeleteFilesRequest",{enumerable:!0,get:function(){return pi.WillDeleteFilesRequest}});var Hm=YA();Object.defineProperty(T,"UniquenessLevel",{enumerable:!0,get:function(){return Hm.UniquenessLevel}});Object.defineProperty(T,"MonikerKind",{enumerable:!0,get:function(){return Hm.MonikerKind}});Object.defineProperty(T,"MonikerRequest",{enumerable:!0,get:function(){return Hm.MonikerRequest}});var Wm=tP();Object.defineProperty(T,"TypeHierarchyPrepareRequest",{enumerable:!0,get:function(){return Wm.TypeHierarchyPrepareRequest}});Object.defineProperty(T,"TypeHierarchySubtypesRequest",{enumerable:!0,get:function(){return Wm.TypeHierarchySubtypesRequest}});Object.defineProperty(T,"TypeHierarchySupertypesRequest",{enumerable:!0,get:function(){return Wm.TypeHierarchySupertypesRequest}});var qD=iP();Object.defineProperty(T,"InlineValueRequest",{enumerable:!0,get:function(){return qD.InlineValueRequest}});Object.defineProperty(T,"InlineValueRefreshRequest",{enumerable:!0,get:function(){return qD.InlineValueRefreshRequest}});var Bm=uP();Object.defineProperty(T,"InlayHintRequest",{enumerable:!0,get:function(){return Bm.InlayHintRequest}});Object.defineProperty(T,"InlayHintResolveRequest",{enumerable:!0,get:function(){return Bm.InlayHintResolveRequest}});Object.defineProperty(T,"InlayHintRefreshRequest",{enumerable:!0,get:function(){return Bm.InlayHintRefreshRequest}});var Po=mP();Object.defineProperty(T,"DiagnosticServerCancellationData",{enumerable:!0,get:function(){return Po.DiagnosticServerCancellationData}});Object.defineProperty(T,"DocumentDiagnosticReportKind",{enumerable:!0,get:function(){return Po.DocumentDiagnosticReportKind}});Object.defineProperty(T,"DocumentDiagnosticRequest",{enumerable:!0,get:function(){return Po.DocumentDiagnosticRequest}});Object.defineProperty(T,"WorkspaceDiagnosticRequest",{enumerable:!0,get:function(){return Po.WorkspaceDiagnosticRequest}});Object.defineProperty(T,"DiagnosticRefreshRequest",{enumerable:!0,get:function(){return Po.DiagnosticRefreshRequest}});var Kr=SP();Object.defineProperty(T,"NotebookCellKind",{enumerable:!0,get:function(){return Kr.NotebookCellKind}});Object.defineProperty(T,"ExecutionSummary",{enumerable:!0,get:function(){return Kr.ExecutionSummary}});Object.defineProperty(T,"NotebookCell",{enumerable:!0,get:function(){return Kr.NotebookCell}});Object.defineProperty(T,"NotebookDocument",{enumerable:!0,get:function(){return Kr.NotebookDocument}});Object.defineProperty(T,"NotebookDocumentSyncRegistrationType",{enumerable:!0,get:function(){return Kr.NotebookDocumentSyncRegistrationType}});Object.defineProperty(T,"DidOpenNotebookDocumentNotification",{enumerable:!0,get:function(){return Kr.DidOpenNotebookDocumentNotification}});Object.defineProperty(T,"NotebookCellArrayChange",{enumerable:!0,get:function(){return Kr.NotebookCellArrayChange}});Object.defineProperty(T,"DidChangeNotebookDocumentNotification",{enumerable:!0,get:function(){return Kr.DidChangeNotebookDocumentNotification}});Object.defineProperty(T,"DidSaveNotebookDocumentNotification",{enumerable:!0,get:function(){return Kr.DidSaveNotebookDocumentNotification}});Object.defineProperty(T,"DidCloseNotebookDocumentNotification",{enumerable:!0,get:function(){return Kr.DidCloseNotebookDocumentNotification}});var y4=CP();Object.defineProperty(T,"InlineCompletionRequest",{enumerable:!0,get:function(){return y4.InlineCompletionRequest}});var Fm;(function(t){function e(r){let n=r;return wt.string(n)||wt.string(n.language)||wt.string(n.scheme)||wt.string(n.pattern)}t.is=e;})(Fm||(T.TextDocumentFilter=Fm={}));var Nm;(function(t){function e(r){let n=r;return wt.objectLiteral(n)&&(wt.string(n.notebookType)||wt.string(n.scheme)||wt.string(n.pattern))}t.is=e;})(Nm||(T.NotebookDocumentFilter=Nm={}));var qm;(function(t){function e(r){let n=r;return wt.objectLiteral(n)&&(wt.string(n.notebook)||Nm.is(n.notebook))&&(n.language===void 0||wt.string(n.language))}t.is=e;})(qm||(T.NotebookCellTextDocumentFilter=qm={}));var Lm;(function(t){function e(r){if(!Array.isArray(r))return !1;for(let n of r)if(!wt.string(n)&&!Fm.is(n)&&!qm.is(n))return !1;return !0}t.is=e;})(Lm||(T.DocumentSelector=Lm={}));var TP;(function(t){t.method="client/registerCapability",t.messageDirection=ne.MessageDirection.serverToClient,t.type=new ne.ProtocolRequestType(t.method);})(TP||(T.RegistrationRequest=TP={}));var AP;(function(t){t.method="client/unregisterCapability",t.messageDirection=ne.MessageDirection.serverToClient,t.type=new ne.ProtocolRequestType(t.method);})(AP||(T.UnregistrationRequest=AP={}));var PP;(function(t){t.Create="create",t.Rename="rename",t.Delete="delete";})(PP||(T.ResourceOperationKind=PP={}));var DP;(function(t){t.Abort="abort",t.Transactional="transactional",t.TextOnlyTransactional="textOnlyTransactional",t.Undo="undo";})(DP||(T.FailureHandlingKind=DP={}));var OP;(function(t){t.UTF8="utf-8",t.UTF16="utf-16",t.UTF32="utf-32";})(OP||(T.PositionEncodingKind=OP={}));var IP;(function(t){function e(r){let n=r;return n&&wt.string(n.id)&&n.id.length>0}t.hasId=e;})(IP||(T.StaticRegistrationOptions=IP={}));var kP;(function(t){function e(r){let n=r;return n&&(n.documentSelector===null||Lm.is(n.documentSelector))}t.is=e;})(kP||(T.TextDocumentRegistrationOptions=kP={}));var MP;(function(t){function e(n){let i=n;return wt.objectLiteral(i)&&(i.workDoneProgress===void 0||wt.boolean(i.workDoneProgress))}t.is=e;function r(n){let i=n;return i&&wt.boolean(i.workDoneProgress)}t.hasWorkDoneProgress=r;})(MP||(T.WorkDoneProgressOptions=MP={}));var FP;(function(t){t.method="initialize",t.messageDirection=ne.MessageDirection.clientToServer,t.type=new ne.ProtocolRequestType(t.method);})(FP||(T.InitializeRequest=FP={}));var NP;(function(t){t.unknownProtocolVersion=1;})(NP||(T.InitializeErrorCodes=NP={}));var qP;(function(t){t.method="initialized",t.messageDirection=ne.MessageDirection.clientToServer,t.type=new ne.ProtocolNotificationType(t.method);})(qP||(T.InitializedNotification=qP={}));var LP;(function(t){t.method="shutdown",t.messageDirection=ne.MessageDirection.clientToServer,t.type=new ne.ProtocolRequestType0(t.method);})(LP||(T.ShutdownRequest=LP={}));var $P;(function(t){t.method="exit",t.messageDirection=ne.MessageDirection.clientToServer,t.type=new ne.ProtocolNotificationType0(t.method);})($P||(T.ExitNotification=$P={}));var jP;(function(t){t.method="workspace/didChangeConfiguration",t.messageDirection=ne.MessageDirection.clientToServer,t.type=new ne.ProtocolNotificationType(t.method);})(jP||(T.DidChangeConfigurationNotification=jP={}));var HP;(function(t){t.Error=1,t.Warning=2,t.Info=3,t.Log=4,t.Debug=5;})(HP||(T.MessageType=HP={}));var WP;(function(t){t.method="window/showMessage",t.messageDirection=ne.MessageDirection.serverToClient,t.type=new ne.ProtocolNotificationType(t.method);})(WP||(T.ShowMessageNotification=WP={}));var BP;(function(t){t.method="window/showMessageRequest",t.messageDirection=ne.MessageDirection.serverToClient,t.type=new ne.ProtocolRequestType(t.method);})(BP||(T.ShowMessageRequest=BP={}));var UP;(function(t){t.method="window/logMessage",t.messageDirection=ne.MessageDirection.serverToClient,t.type=new ne.ProtocolNotificationType(t.method);})(UP||(T.LogMessageNotification=UP={}));var zP;(function(t){t.method="telemetry/event",t.messageDirection=ne.MessageDirection.serverToClient,t.type=new ne.ProtocolNotificationType(t.method);})(zP||(T.TelemetryEventNotification=zP={}));var VP;(function(t){t.None=0,t.Full=1,t.Incremental=2;})(VP||(T.TextDocumentSyncKind=VP={}));var GP;(function(t){t.method="textDocument/didOpen",t.messageDirection=ne.MessageDirection.clientToServer,t.type=new ne.ProtocolNotificationType(t.method);})(GP||(T.DidOpenTextDocumentNotification=GP={}));var KP;(function(t){function e(n){let i=n;return i!=null&&typeof i.text=="string"&&i.range!==void 0&&(i.rangeLength===void 0||typeof i.rangeLength=="number")}t.isIncremental=e;function r(n){let i=n;return i!=null&&typeof i.text=="string"&&i.range===void 0&&i.rangeLength===void 0}t.isFull=r;})(KP||(T.TextDocumentContentChangeEvent=KP={}));var ZP;(function(t){t.method="textDocument/didChange",t.messageDirection=ne.MessageDirection.clientToServer,t.type=new ne.ProtocolNotificationType(t.method);})(ZP||(T.DidChangeTextDocumentNotification=ZP={}));var JP;(function(t){t.method="textDocument/didClose",t.messageDirection=ne.MessageDirection.clientToServer,t.type=new ne.ProtocolNotificationType(t.method);})(JP||(T.DidCloseTextDocumentNotification=JP={}));var YP;(function(t){t.method="textDocument/didSave",t.messageDirection=ne.MessageDirection.clientToServer,t.type=new ne.ProtocolNotificationType(t.method);})(YP||(T.DidSaveTextDocumentNotification=YP={}));var XP;(function(t){t.Manual=1,t.AfterDelay=2,t.FocusOut=3;})(XP||(T.TextDocumentSaveReason=XP={}));var QP;(function(t){t.method="textDocument/willSave",t.messageDirection=ne.MessageDirection.clientToServer,t.type=new ne.ProtocolNotificationType(t.method);})(QP||(T.WillSaveTextDocumentNotification=QP={}));var eD;(function(t){t.method="textDocument/willSaveWaitUntil",t.messageDirection=ne.MessageDirection.clientToServer,t.type=new ne.ProtocolRequestType(t.method);})(eD||(T.WillSaveTextDocumentWaitUntilRequest=eD={}));var tD;(function(t){t.method="workspace/didChangeWatchedFiles",t.messageDirection=ne.MessageDirection.clientToServer,t.type=new ne.ProtocolNotificationType(t.method);})(tD||(T.DidChangeWatchedFilesNotification=tD={}));var rD;(function(t){t.Created=1,t.Changed=2,t.Deleted=3;})(rD||(T.FileChangeType=rD={}));var nD;(function(t){function e(r){let n=r;return wt.objectLiteral(n)&&(xP.URI.is(n.baseUri)||xP.WorkspaceFolder.is(n.baseUri))&&wt.string(n.pattern)}t.is=e;})(nD||(T.RelativePattern=nD={}));var iD;(function(t){t.Create=1,t.Change=2,t.Delete=4;})(iD||(T.WatchKind=iD={}));var sD;(function(t){t.method="textDocument/publishDiagnostics",t.messageDirection=ne.MessageDirection.serverToClient,t.type=new ne.ProtocolNotificationType(t.method);})(sD||(T.PublishDiagnosticsNotification=sD={}));var oD;(function(t){t.Invoked=1,t.TriggerCharacter=2,t.TriggerForIncompleteCompletions=3;})(oD||(T.CompletionTriggerKind=oD={}));var aD;(function(t){t.method="textDocument/completion",t.messageDirection=ne.MessageDirection.clientToServer,t.type=new ne.ProtocolRequestType(t.method);})(aD||(T.CompletionRequest=aD={}));var uD;(function(t){t.method="completionItem/resolve",t.messageDirection=ne.MessageDirection.clientToServer,t.type=new ne.ProtocolRequestType(t.method);})(uD||(T.CompletionResolveRequest=uD={}));var cD;(function(t){t.method="textDocument/hover",t.messageDirection=ne.MessageDirection.clientToServer,t.type=new ne.ProtocolRequestType(t.method);})(cD||(T.HoverRequest=cD={}));var lD;(function(t){t.Invoked=1,t.TriggerCharacter=2,t.ContentChange=3;})(lD||(T.SignatureHelpTriggerKind=lD={}));var fD;(function(t){t.method="textDocument/signatureHelp",t.messageDirection=ne.MessageDirection.clientToServer,t.type=new ne.ProtocolRequestType(t.method);})(fD||(T.SignatureHelpRequest=fD={}));var dD;(function(t){t.method="textDocument/definition",t.messageDirection=ne.MessageDirection.clientToServer,t.type=new ne.ProtocolRequestType(t.method);})(dD||(T.DefinitionRequest=dD={}));var hD;(function(t){t.method="textDocument/references",t.messageDirection=ne.MessageDirection.clientToServer,t.type=new ne.ProtocolRequestType(t.method);})(hD||(T.ReferencesRequest=hD={}));var pD;(function(t){t.method="textDocument/documentHighlight",t.messageDirection=ne.MessageDirection.clientToServer,t.type=new ne.ProtocolRequestType(t.method);})(pD||(T.DocumentHighlightRequest=pD={}));var mD;(function(t){t.method="textDocument/documentSymbol",t.messageDirection=ne.MessageDirection.clientToServer,t.type=new ne.ProtocolRequestType(t.method);})(mD||(T.DocumentSymbolRequest=mD={}));var gD;(function(t){t.method="textDocument/codeAction",t.messageDirection=ne.MessageDirection.clientToServer,t.type=new ne.ProtocolRequestType(t.method);})(gD||(T.CodeActionRequest=gD={}));var _D;(function(t){t.method="codeAction/resolve",t.messageDirection=ne.MessageDirection.clientToServer,t.type=new ne.ProtocolRequestType(t.method);})(_D||(T.CodeActionResolveRequest=_D={}));var yD;(function(t){t.method="workspace/symbol",t.messageDirection=ne.MessageDirection.clientToServer,t.type=new ne.ProtocolRequestType(t.method);})(yD||(T.WorkspaceSymbolRequest=yD={}));var bD;(function(t){t.method="workspaceSymbol/resolve",t.messageDirection=ne.MessageDirection.clientToServer,t.type=new ne.ProtocolRequestType(t.method);})(bD||(T.WorkspaceSymbolResolveRequest=bD={}));var vD;(function(t){t.method="textDocument/codeLens",t.messageDirection=ne.MessageDirection.clientToServer,t.type=new ne.ProtocolRequestType(t.method);})(vD||(T.CodeLensRequest=vD={}));var wD;(function(t){t.method="codeLens/resolve",t.messageDirection=ne.MessageDirection.clientToServer,t.type=new ne.ProtocolRequestType(t.method);})(wD||(T.CodeLensResolveRequest=wD={}));var SD;(function(t){t.method="workspace/codeLens/refresh",t.messageDirection=ne.MessageDirection.serverToClient,t.type=new ne.ProtocolRequestType0(t.method);})(SD||(T.CodeLensRefreshRequest=SD={}));var ED;(function(t){t.method="textDocument/documentLink",t.messageDirection=ne.MessageDirection.clientToServer,t.type=new ne.ProtocolRequestType(t.method);})(ED||(T.DocumentLinkRequest=ED={}));var RD;(function(t){t.method="documentLink/resolve",t.messageDirection=ne.MessageDirection.clientToServer,t.type=new ne.ProtocolRequestType(t.method);})(RD||(T.DocumentLinkResolveRequest=RD={}));var CD;(function(t){t.method="textDocument/formatting",t.messageDirection=ne.MessageDirection.clientToServer,t.type=new ne.ProtocolRequestType(t.method);})(CD||(T.DocumentFormattingRequest=CD={}));var xD;(function(t){t.method="textDocument/rangeFormatting",t.messageDirection=ne.MessageDirection.clientToServer,t.type=new ne.ProtocolRequestType(t.method);})(xD||(T.DocumentRangeFormattingRequest=xD={}));var TD;(function(t){t.method="textDocument/rangesFormatting",t.messageDirection=ne.MessageDirection.clientToServer,t.type=new ne.ProtocolRequestType(t.method);})(TD||(T.DocumentRangesFormattingRequest=TD={}));var AD;(function(t){t.method="textDocument/onTypeFormatting",t.messageDirection=ne.MessageDirection.clientToServer,t.type=new ne.ProtocolRequestType(t.method);})(AD||(T.DocumentOnTypeFormattingRequest=AD={}));var PD;(function(t){t.Identifier=1;})(PD||(T.PrepareSupportDefaultBehavior=PD={}));var DD;(function(t){t.method="textDocument/rename",t.messageDirection=ne.MessageDirection.clientToServer,t.type=new ne.ProtocolRequestType(t.method);})(DD||(T.RenameRequest=DD={}));var OD;(function(t){t.method="textDocument/prepareRename",t.messageDirection=ne.MessageDirection.clientToServer,t.type=new ne.ProtocolRequestType(t.method);})(OD||(T.PrepareRenameRequest=OD={}));var ID;(function(t){t.method="workspace/executeCommand",t.messageDirection=ne.MessageDirection.clientToServer,t.type=new ne.ProtocolRequestType(t.method);})(ID||(T.ExecuteCommandRequest=ID={}));var kD;(function(t){t.method="workspace/applyEdit",t.messageDirection=ne.MessageDirection.serverToClient,t.type=new ne.ProtocolRequestType("workspace/applyEdit");})(kD||(T.ApplyWorkspaceEditRequest=kD={}));});var jD=A(Pc=>{Object.defineProperty(Pc,"__esModule",{value:!0});Pc.createProtocolConnection=void 0;var $D=hi();function b4(t,e,r,n){return $D.ConnectionStrategy.is(n)&&(n={connectionStrategy:n}),(0, $D.createMessageConnection)(t,e,r,n)}Pc.createProtocolConnection=b4;});var WD=A(Qt=>{var v4=Qt&&Qt.__createBinding||(Object.create?function(t,e,r,n){n===void 0&&(n=r);var i=Object.getOwnPropertyDescriptor(e,r);(!i||("get"in i?!e.__esModule:i.writable||i.configurable))&&(i={enumerable:!0,get:function(){return e[r]}}),Object.defineProperty(t,n,i);}:function(t,e,r,n){n===void 0&&(n=r),t[n]=e[r];}),Dc=Qt&&Qt.__exportStar||function(t,e){for(var r in t)r!=="default"&&!Object.prototype.hasOwnProperty.call(e,r)&&v4(e,t,r);};Object.defineProperty(Qt,"__esModule",{value:!0});Qt.LSPErrorCodes=Qt.createProtocolConnection=void 0;Dc(hi(),Qt);Dc(hc(),Qt);Dc(rt(),Qt);Dc(LD(),Qt);var w4=jD();Object.defineProperty(Qt,"createProtocolConnection",{enumerable:!0,get:function(){return w4.createProtocolConnection}});var HD;(function(t){t.lspReservedErrorRangeStart=-32899,t.RequestFailed=-32803,t.ServerCancelled=-32802,t.ContentModified=-32801,t.RequestCancelled=-32800,t.lspReservedErrorRangeEnd=-32800;})(HD||(Qt.LSPErrorCodes=HD={}));});var dt=A(Zr=>{var S4=Zr&&Zr.__createBinding||(Object.create?function(t,e,r,n){n===void 0&&(n=r);var i=Object.getOwnPropertyDescriptor(e,r);(!i||("get"in i?!e.__esModule:i.writable||i.configurable))&&(i={enumerable:!0,get:function(){return e[r]}}),Object.defineProperty(t,n,i);}:function(t,e,r,n){n===void 0&&(n=r),t[n]=e[r];}),BD=Zr&&Zr.__exportStar||function(t,e){for(var r in t)r!=="default"&&!Object.prototype.hasOwnProperty.call(e,r)&&S4(e,t,r);};Object.defineProperty(Zr,"__esModule",{value:!0});Zr.createProtocolConnection=void 0;var E4=Tm();BD(Tm(),Zr);BD(WD(),Zr);function R4(t,e,r,n){return (0, E4.createMessageConnection)(t,e,r,n)}Zr.createProtocolConnection=R4;});var Um=A(wr=>{Object.defineProperty(wr,"__esModule",{value:!0});wr.generateUuid=wr.parse=wr.isUUID=wr.v4=wr.empty=void 0;var Do=class{constructor(e){this._value=e;}asHex(){return this._value}equals(e){return this.asHex()===e.asHex()}},Oo=class t extends Do{static _oneOf(e){return e[Math.floor(e.length*Math.random())]}static _randomHex(){return t._oneOf(t._chars)}constructor(){super([t._randomHex(),t._randomHex(),t._randomHex(),t._randomHex(),t._randomHex(),t._randomHex(),t._randomHex(),t._randomHex(),"-",t._randomHex(),t._randomHex(),t._randomHex(),t._randomHex(),"-","4",t._randomHex(),t._randomHex(),t._randomHex(),"-",t._oneOf(t._timeHighBits),t._randomHex(),t._randomHex(),t._randomHex(),"-",t._randomHex(),t._randomHex(),t._randomHex(),t._randomHex(),t._randomHex(),t._randomHex(),t._randomHex(),t._randomHex(),t._randomHex(),t._randomHex(),t._randomHex(),t._randomHex()].join(""));}};Oo._chars=["0","1","2","3","4","5","6","6","7","8","9","a","b","c","d","e","f"];Oo._timeHighBits=["8","9","a","b"];wr.empty=new Do("00000000-0000-0000-0000-000000000000");function UD(){return new Oo}wr.v4=UD;var C4=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;function zD(t){return C4.test(t)}wr.isUUID=zD;function x4(t){if(!zD(t))throw new Error("invalid uuid");return new Do(t)}wr.parse=x4;function T4(){return UD().asHex()}wr.generateUuid=T4;});var VD=A($n=>{Object.defineProperty($n,"__esModule",{value:!0});$n.attachPartialResult=$n.ProgressFeature=$n.attachWorkDone=void 0;var Ln=dt(),A4=Um(),mi=class t{constructor(e,r){this._connection=e,this._token=r,t.Instances.set(this._token,this);}begin(e,r,n,i){let s={kind:"begin",title:e,percentage:r,message:n,cancellable:i};this._connection.sendProgress(Ln.WorkDoneProgress.type,this._token,s);}report(e,r){let n={kind:"report"};typeof e=="number"?(n.percentage=e,r!==void 0&&(n.message=r)):n.message=e,this._connection.sendProgress(Ln.WorkDoneProgress.type,this._token,n);}done(){t.Instances.delete(this._token),this._connection.sendProgress(Ln.WorkDoneProgress.type,this._token,{kind:"end"});}};mi.Instances=new Map;var Oc=class extends mi{constructor(e,r){super(e,r),this._source=new Ln.CancellationTokenSource;}get token(){return this._source.token}done(){this._source.dispose(),super.done();}cancel(){this._source.cancel();}},Io=class{constructor(){}begin(){}report(){}done(){}},Ic=class extends Io{constructor(){super(),this._source=new Ln.CancellationTokenSource;}get token(){return this._source.token}done(){this._source.dispose();}cancel(){this._source.cancel();}};function P4(t,e){if(e===void 0||e.workDoneToken===void 0)return new Io;let r=e.workDoneToken;return delete e.workDoneToken,new mi(t,r)}$n.attachWorkDone=P4;var D4=t=>class extends t{constructor(){super(),this._progressSupported=!1;}initialize(e){super.initialize(e),e?.window?.workDoneProgress===!0&&(this._progressSupported=!0,this.connection.onNotification(Ln.WorkDoneProgressCancelNotification.type,r=>{let n=mi.Instances.get(r.token);(n instanceof Oc||n instanceof Ic)&&n.cancel();}));}attachWorkDoneProgress(e){return e===void 0?new Io:new mi(this.connection,e)}createWorkDoneProgress(){if(this._progressSupported){let e=(0, A4.generateUuid)();return this.connection.sendRequest(Ln.WorkDoneProgressCreateRequest.type,{token:e}).then(()=>new Oc(this.connection,e))}else return Promise.resolve(new Ic)}};$n.ProgressFeature=D4;var zm;(function(t){t.type=new Ln.ProgressType;})(zm||(zm={}));var Vm=class{constructor(e,r){this._connection=e,this._token=r;}report(e){this._connection.sendProgress(zm.type,this._token,e);}};function O4(t,e){if(e===void 0||e.partialResultToken===void 0)return;let r=e.partialResultToken;return delete e.partialResultToken,new Vm(t,r)}$n.attachPartialResult=O4;});var GD=A(kc=>{Object.defineProperty(kc,"__esModule",{value:!0});kc.ConfigurationFeature=void 0;var I4=dt(),k4=Uu(),M4=t=>class extends t{getConfiguration(e){return e?k4.string(e)?this._getConfiguration({section:e}):this._getConfiguration(e):this._getConfiguration({})}_getConfiguration(e){let r={items:Array.isArray(e)?e:[e]};return this.connection.sendRequest(I4.ConfigurationRequest.type,r).then(n=>Array.isArray(n)?Array.isArray(e)?n:n[0]:Array.isArray(e)?[]:null)}};kc.ConfigurationFeature=M4;});var KD=A(Fc=>{Object.defineProperty(Fc,"__esModule",{value:!0});Fc.WorkspaceFoldersFeature=void 0;var Mc=dt(),F4=t=>class extends t{constructor(){super(),this._notificationIsAutoRegistered=!1;}initialize(e){super.initialize(e);let r=e.workspace;r&&r.workspaceFolders&&(this._onDidChangeWorkspaceFolders=new Mc.Emitter,this.connection.onNotification(Mc.DidChangeWorkspaceFoldersNotification.type,n=>{this._onDidChangeWorkspaceFolders.fire(n.event);}));}fillServerCapabilities(e){super.fillServerCapabilities(e);let r=e.workspace?.workspaceFolders?.changeNotifications;this._notificationIsAutoRegistered=r===!0||typeof r=="string";}getWorkspaceFolders(){return this.connection.sendRequest(Mc.WorkspaceFoldersRequest.type)}get onDidChangeWorkspaceFolders(){if(!this._onDidChangeWorkspaceFolders)throw new Error("Client doesn't support sending workspace folder change events.");return !this._notificationIsAutoRegistered&&!this._unregistration&&(this._unregistration=this.connection.client.register(Mc.DidChangeWorkspaceFoldersNotification.type)),this._onDidChangeWorkspaceFolders.event}};Fc.WorkspaceFoldersFeature=F4;});var ZD=A(Nc=>{Object.defineProperty(Nc,"__esModule",{value:!0});Nc.CallHierarchyFeature=void 0;var Gm=dt(),N4=t=>class extends t{get callHierarchy(){return {onPrepare:e=>this.connection.onRequest(Gm.CallHierarchyPrepareRequest.type,(r,n)=>e(r,n,this.attachWorkDoneProgress(r),void 0)),onIncomingCalls:e=>{let r=Gm.CallHierarchyIncomingCallsRequest.type;return this.connection.onRequest(r,(n,i)=>e(n,i,this.attachWorkDoneProgress(n),this.attachPartialResultProgress(r,n)))},onOutgoingCalls:e=>{let r=Gm.CallHierarchyOutgoingCallsRequest.type;return this.connection.onRequest(r,(n,i)=>e(n,i,this.attachWorkDoneProgress(n),this.attachPartialResultProgress(r,n)))}}}};Nc.CallHierarchyFeature=N4;});var Zm=A(jn=>{Object.defineProperty(jn,"__esModule",{value:!0});jn.SemanticTokensBuilder=jn.SemanticTokensDiff=jn.SemanticTokensFeature=void 0;var qc=dt(),q4=t=>class extends t{get semanticTokens(){return {refresh:()=>this.connection.sendRequest(qc.SemanticTokensRefreshRequest.type),on:e=>{let r=qc.SemanticTokensRequest.type;return this.connection.onRequest(r,(n,i)=>e(n,i,this.attachWorkDoneProgress(n),this.attachPartialResultProgress(r,n)))},onDelta:e=>{let r=qc.SemanticTokensDeltaRequest.type;return this.connection.onRequest(r,(n,i)=>e(n,i,this.attachWorkDoneProgress(n),this.attachPartialResultProgress(r,n)))},onRange:e=>{let r=qc.SemanticTokensRangeRequest.type;return this.connection.onRequest(r,(n,i)=>e(n,i,this.attachWorkDoneProgress(n),this.attachPartialResultProgress(r,n)))}}}};jn.SemanticTokensFeature=q4;var Lc=class{constructor(e,r){this.originalSequence=e,this.modifiedSequence=r;}computeDiff(){let e=this.originalSequence.length,r=this.modifiedSequence.length,n=0;for(;n=n&&s>=n&&this.originalSequence[i]===this.modifiedSequence[s];)i--,s--;(i0&&(o-=this._prevLine,o===0&&(a-=this._prevChar)),this._data[this._dataLen++]=o,this._data[this._dataLen++]=a,this._data[this._dataLen++]=n,this._data[this._dataLen++]=i,this._data[this._dataLen++]=s,this._prevLine=e,this._prevChar=r;}get id(){return this._id.toString()}previousResult(e){this.id===e&&(this._prevData=this._data),this.initialize();}build(){return this._prevData=void 0,{resultId:this.id,data:this._data}}canBuildEdits(){return this._prevData!==void 0}buildEdits(){return this._prevData!==void 0?{resultId:this.id,edits:new Lc(this._prevData,this._data).computeDiff()}:this.build()}};jn.SemanticTokensBuilder=Km;});var JD=A($c=>{Object.defineProperty($c,"__esModule",{value:!0});$c.ShowDocumentFeature=void 0;var L4=dt(),$4=t=>class extends t{showDocument(e){return this.connection.sendRequest(L4.ShowDocumentRequest.type,e)}};$c.ShowDocumentFeature=$4;});var YD=A(jc=>{Object.defineProperty(jc,"__esModule",{value:!0});jc.FileOperationsFeature=void 0;var bs=dt(),j4=t=>class extends t{onDidCreateFiles(e){return this.connection.onNotification(bs.DidCreateFilesNotification.type,r=>{e(r);})}onDidRenameFiles(e){return this.connection.onNotification(bs.DidRenameFilesNotification.type,r=>{e(r);})}onDidDeleteFiles(e){return this.connection.onNotification(bs.DidDeleteFilesNotification.type,r=>{e(r);})}onWillCreateFiles(e){return this.connection.onRequest(bs.WillCreateFilesRequest.type,(r,n)=>e(r,n))}onWillRenameFiles(e){return this.connection.onRequest(bs.WillRenameFilesRequest.type,(r,n)=>e(r,n))}onWillDeleteFiles(e){return this.connection.onRequest(bs.WillDeleteFilesRequest.type,(r,n)=>e(r,n))}};jc.FileOperationsFeature=j4;});var XD=A(Hc=>{Object.defineProperty(Hc,"__esModule",{value:!0});Hc.LinkedEditingRangeFeature=void 0;var H4=dt(),W4=t=>class extends t{onLinkedEditingRange(e){return this.connection.onRequest(H4.LinkedEditingRangeRequest.type,(r,n)=>e(r,n,this.attachWorkDoneProgress(r),void 0))}};Hc.LinkedEditingRangeFeature=W4;});var QD=A(Wc=>{Object.defineProperty(Wc,"__esModule",{value:!0});Wc.TypeHierarchyFeature=void 0;var Jm=dt(),B4=t=>class extends t{get typeHierarchy(){return {onPrepare:e=>this.connection.onRequest(Jm.TypeHierarchyPrepareRequest.type,(r,n)=>e(r,n,this.attachWorkDoneProgress(r),void 0)),onSupertypes:e=>{let r=Jm.TypeHierarchySupertypesRequest.type;return this.connection.onRequest(r,(n,i)=>e(n,i,this.attachWorkDoneProgress(n),this.attachPartialResultProgress(r,n)))},onSubtypes:e=>{let r=Jm.TypeHierarchySubtypesRequest.type;return this.connection.onRequest(r,(n,i)=>e(n,i,this.attachWorkDoneProgress(n),this.attachPartialResultProgress(r,n)))}}}};Wc.TypeHierarchyFeature=B4;});var t1=A(Bc=>{Object.defineProperty(Bc,"__esModule",{value:!0});Bc.InlineValueFeature=void 0;var e1=dt(),U4=t=>class extends t{get inlineValue(){return {refresh:()=>this.connection.sendRequest(e1.InlineValueRefreshRequest.type),on:e=>this.connection.onRequest(e1.InlineValueRequest.type,(r,n)=>e(r,n,this.attachWorkDoneProgress(r)))}}};Bc.InlineValueFeature=U4;});var n1=A(Uc=>{Object.defineProperty(Uc,"__esModule",{value:!0});Uc.FoldingRangeFeature=void 0;var r1=dt(),z4=t=>class extends t{get foldingRange(){return {refresh:()=>this.connection.sendRequest(r1.FoldingRangeRefreshRequest.type),on:e=>{let r=r1.FoldingRangeRequest.type;return this.connection.onRequest(r,(n,i)=>e(n,i,this.attachWorkDoneProgress(n),this.attachPartialResultProgress(r,n)))}}}};Uc.FoldingRangeFeature=z4;});var i1=A(zc=>{Object.defineProperty(zc,"__esModule",{value:!0});zc.InlayHintFeature=void 0;var Ym=dt(),V4=t=>class extends t{get inlayHint(){return {refresh:()=>this.connection.sendRequest(Ym.InlayHintRefreshRequest.type),on:e=>this.connection.onRequest(Ym.InlayHintRequest.type,(r,n)=>e(r,n,this.attachWorkDoneProgress(r))),resolve:e=>this.connection.onRequest(Ym.InlayHintResolveRequest.type,(r,n)=>e(r,n))}}};zc.InlayHintFeature=V4;});var s1=A(Vc=>{Object.defineProperty(Vc,"__esModule",{value:!0});Vc.DiagnosticFeature=void 0;var ko=dt(),G4=t=>class extends t{get diagnostics(){return {refresh:()=>this.connection.sendRequest(ko.DiagnosticRefreshRequest.type),on:e=>this.connection.onRequest(ko.DocumentDiagnosticRequest.type,(r,n)=>e(r,n,this.attachWorkDoneProgress(r),this.attachPartialResultProgress(ko.DocumentDiagnosticRequest.partialResult,r))),onWorkspace:e=>this.connection.onRequest(ko.WorkspaceDiagnosticRequest.type,(r,n)=>e(r,n,this.attachWorkDoneProgress(r),this.attachPartialResultProgress(ko.WorkspaceDiagnosticRequest.partialResult,r)))}}};Vc.DiagnosticFeature=G4;});var Qm=A(Gc=>{Object.defineProperty(Gc,"__esModule",{value:!0});Gc.TextDocuments=void 0;var gi=dt(),Xm=class{constructor(e){this._configuration=e,this._syncedDocuments=new Map,this._onDidChangeContent=new gi.Emitter,this._onDidOpen=new gi.Emitter,this._onDidClose=new gi.Emitter,this._onDidSave=new gi.Emitter,this._onWillSave=new gi.Emitter;}get onDidOpen(){return this._onDidOpen.event}get onDidChangeContent(){return this._onDidChangeContent.event}get onWillSave(){return this._onWillSave.event}onWillSaveWaitUntil(e){this._willSaveWaitUntil=e;}get onDidSave(){return this._onDidSave.event}get onDidClose(){return this._onDidClose.event}get(e){return this._syncedDocuments.get(e)}all(){return Array.from(this._syncedDocuments.values())}keys(){return Array.from(this._syncedDocuments.keys())}listen(e){e.__textDocumentSync=gi.TextDocumentSyncKind.Incremental;let r=[];return r.push(e.onDidOpenTextDocument(n=>{let i=n.textDocument,s=this._configuration.create(i.uri,i.languageId,i.version,i.text);this._syncedDocuments.set(i.uri,s);let o=Object.freeze({document:s});this._onDidOpen.fire(o),this._onDidChangeContent.fire(o);})),r.push(e.onDidChangeTextDocument(n=>{let i=n.textDocument,s=n.contentChanges;if(s.length===0)return;let{version:o}=i;if(o==null)throw new Error(`Received document change event for ${i.uri} without valid version identifier`);let a=this._syncedDocuments.get(i.uri);a!==void 0&&(a=this._configuration.update(a,s,o),this._syncedDocuments.set(i.uri,a),this._onDidChangeContent.fire(Object.freeze({document:a})));})),r.push(e.onDidCloseTextDocument(n=>{let i=this._syncedDocuments.get(n.textDocument.uri);i!==void 0&&(this._syncedDocuments.delete(n.textDocument.uri),this._onDidClose.fire(Object.freeze({document:i})));})),r.push(e.onWillSaveTextDocument(n=>{let i=this._syncedDocuments.get(n.textDocument.uri);i!==void 0&&this._onWillSave.fire(Object.freeze({document:i,reason:n.reason}));})),r.push(e.onWillSaveTextDocumentWaitUntil((n,i)=>{let s=this._syncedDocuments.get(n.textDocument.uri);return s!==void 0&&this._willSaveWaitUntil?this._willSaveWaitUntil(Object.freeze({document:s,reason:n.reason}),i):[]})),r.push(e.onDidSaveTextDocument(n=>{let i=this._syncedDocuments.get(n.textDocument.uri);i!==void 0&&this._onDidSave.fire(Object.freeze({document:i}));})),gi.Disposable.create(()=>{r.forEach(n=>n.dispose());})}};Gc.TextDocuments=Xm;});var tg=A(vs=>{Object.defineProperty(vs,"__esModule",{value:!0});vs.NotebookDocuments=vs.NotebookSyncFeature=void 0;var Sr=dt(),o1=Qm(),K4=t=>class extends t{get synchronization(){return {onDidOpenNotebookDocument:e=>this.connection.onNotification(Sr.DidOpenNotebookDocumentNotification.type,r=>{e(r);}),onDidChangeNotebookDocument:e=>this.connection.onNotification(Sr.DidChangeNotebookDocumentNotification.type,r=>{e(r);}),onDidSaveNotebookDocument:e=>this.connection.onNotification(Sr.DidSaveNotebookDocumentNotification.type,r=>{e(r);}),onDidCloseNotebookDocument:e=>this.connection.onNotification(Sr.DidCloseNotebookDocumentNotification.type,r=>{e(r);})}}};vs.NotebookSyncFeature=K4;var Kc=class t{onDidOpenTextDocument(e){return this.openHandler=e,Sr.Disposable.create(()=>{this.openHandler=void 0;})}openTextDocument(e){this.openHandler&&this.openHandler(e);}onDidChangeTextDocument(e){return this.changeHandler=e,Sr.Disposable.create(()=>{this.changeHandler=e;})}changeTextDocument(e){this.changeHandler&&this.changeHandler(e);}onDidCloseTextDocument(e){return this.closeHandler=e,Sr.Disposable.create(()=>{this.closeHandler=void 0;})}closeTextDocument(e){this.closeHandler&&this.closeHandler(e);}onWillSaveTextDocument(){return t.NULL_DISPOSE}onWillSaveTextDocumentWaitUntil(){return t.NULL_DISPOSE}onDidSaveTextDocument(){return t.NULL_DISPOSE}};Kc.NULL_DISPOSE=Object.freeze({dispose:()=>{}});var eg=class{constructor(e){e instanceof o1.TextDocuments?this._cellTextDocuments=e:this._cellTextDocuments=new o1.TextDocuments(e),this.notebookDocuments=new Map,this.notebookCellMap=new Map,this._onDidOpen=new Sr.Emitter,this._onDidChange=new Sr.Emitter,this._onDidSave=new Sr.Emitter,this._onDidClose=new Sr.Emitter;}get cellTextDocuments(){return this._cellTextDocuments}getCellTextDocument(e){return this._cellTextDocuments.get(e.document)}getNotebookDocument(e){return this.notebookDocuments.get(e)}getNotebookCell(e){let r=this.notebookCellMap.get(e);return r&&r[0]}findNotebookDocumentForCell(e){let r=typeof e=="string"?e:e.document,n=this.notebookCellMap.get(r);return n&&n[1]}get onDidOpen(){return this._onDidOpen.event}get onDidSave(){return this._onDidSave.event}get onDidChange(){return this._onDidChange.event}get onDidClose(){return this._onDidClose.event}listen(e){let r=new Kc,n=[];return n.push(this.cellTextDocuments.listen(r)),n.push(e.notebooks.synchronization.onDidOpenNotebookDocument(i=>{this.notebookDocuments.set(i.notebookDocument.uri,i.notebookDocument);for(let s of i.cellTextDocuments)r.openTextDocument({textDocument:s});this.updateCellMap(i.notebookDocument),this._onDidOpen.fire(i.notebookDocument);})),n.push(e.notebooks.synchronization.onDidChangeNotebookDocument(i=>{let s=this.notebookDocuments.get(i.notebookDocument.uri);if(s===void 0)return;s.version=i.notebookDocument.version;let o=s.metadata,a=!1,l=i.change;l.metadata!==void 0&&(a=!0,s.metadata=l.metadata);let d=[],c=[],p=[],m=[];if(l.cells!==void 0){let D=l.cells;if(D.structure!==void 0){let g=D.structure.array;if(s.cells.splice(g.start,g.deleteCount,...g.cells!==void 0?g.cells:[]),D.structure.didOpen!==void 0)for(let S of D.structure.didOpen)r.openTextDocument({textDocument:S}),d.push(S.uri);if(D.structure.didClose)for(let S of D.structure.didClose)r.closeTextDocument({textDocument:S}),c.push(S.uri);}if(D.data!==void 0){let g=new Map(D.data.map(S=>[S.document,S]));for(let S=0;S<=s.cells.length;S++){let I=g.get(s.cells[S].document);if(I!==void 0){let L=s.cells.splice(S,1,I);if(p.push({old:L[0],new:I}),g.delete(I.document),g.size===0)break}}}if(D.textContent!==void 0)for(let g of D.textContent)r.changeTextDocument({textDocument:g.document,contentChanges:g.changes}),m.push(g.document.uri);}this.updateCellMap(s);let _={notebookDocument:s};a&&(_.metadata={old:o,new:s.metadata});let w=[];for(let D of d)w.push(this.getNotebookCell(D));let E=[];for(let D of c)E.push(this.getNotebookCell(D));let P=[];for(let D of m)P.push(this.getNotebookCell(D));(w.length>0||E.length>0||p.length>0||P.length>0)&&(_.cells={added:w,removed:E,changed:{data:p,textContent:P}}),(_.metadata!==void 0||_.cells!==void 0)&&this._onDidChange.fire(_);})),n.push(e.notebooks.synchronization.onDidSaveNotebookDocument(i=>{let s=this.notebookDocuments.get(i.notebookDocument.uri);s!==void 0&&this._onDidSave.fire(s);})),n.push(e.notebooks.synchronization.onDidCloseNotebookDocument(i=>{let s=this.notebookDocuments.get(i.notebookDocument.uri);if(s!==void 0){this._onDidClose.fire(s);for(let o of i.cellTextDocuments)r.closeTextDocument({textDocument:o});this.notebookDocuments.delete(i.notebookDocument.uri);for(let o of s.cells)this.notebookCellMap.delete(o.document);}})),Sr.Disposable.create(()=>{n.forEach(i=>i.dispose());})}updateCellMap(e){for(let r of e.cells)this.notebookCellMap.set(r.document,[r,e]);}};vs.NotebookDocuments=eg;});var a1=A(Zc=>{Object.defineProperty(Zc,"__esModule",{value:!0});Zc.MonikerFeature=void 0;var Z4=dt(),J4=t=>class extends t{get moniker(){return {on:e=>{let r=Z4.MonikerRequest.type;return this.connection.onRequest(r,(n,i)=>e(n,i,this.attachWorkDoneProgress(n),this.attachPartialResultProgress(r,n)))}}}};Zc.MonikerFeature=J4;});var ag=A(Ne=>{Object.defineProperty(Ne,"__esModule",{value:!0});Ne.createConnection=Ne.combineFeatures=Ne.combineNotebooksFeatures=Ne.combineLanguagesFeatures=Ne.combineWorkspaceFeatures=Ne.combineWindowFeatures=Ne.combineClientFeatures=Ne.combineTracerFeatures=Ne.combineTelemetryFeatures=Ne.combineConsoleFeatures=Ne._NotebooksImpl=Ne._LanguagesImpl=Ne.BulkUnregistration=Ne.BulkRegistration=Ne.ErrorMessageTracker=void 0;var ue=dt(),Er=Uu(),ng=Um(),Re=VD(),Y4=GD(),X4=KD(),Q4=ZD(),ez=Zm(),tz=JD(),rz=YD(),nz=XD(),iz=QD(),sz=t1(),oz=n1(),az=i1(),uz=s1(),cz=tg(),lz=a1();function rg(t){if(t!==null)return t}var ig=class{constructor(){this._messages=Object.create(null);}add(e){let r=this._messages[e];r||(r=0),r++,this._messages[e]=r;}sendErrors(e){Object.keys(this._messages).forEach(r=>{e.window.showErrorMessage(r);});}};Ne.ErrorMessageTracker=ig;var Jc=class{constructor(){}rawAttach(e){this._rawConnection=e;}attach(e){this._connection=e;}get connection(){if(!this._connection)throw new Error("Remote is not attached to a connection yet.");return this._connection}fillServerCapabilities(e){}initialize(e){}error(e){this.send(ue.MessageType.Error,e);}warn(e){this.send(ue.MessageType.Warning,e);}info(e){this.send(ue.MessageType.Info,e);}log(e){this.send(ue.MessageType.Log,e);}debug(e){this.send(ue.MessageType.Debug,e);}send(e,r){this._rawConnection&&this._rawConnection.sendNotification(ue.LogMessageNotification.type,{type:e,message:r}).catch(()=>{(0, ue.RAL)().console.error("Sending log message failed");});}},sg=class{constructor(){}attach(e){this._connection=e;}get connection(){if(!this._connection)throw new Error("Remote is not attached to a connection yet.");return this._connection}initialize(e){}fillServerCapabilities(e){}showErrorMessage(e,...r){let n={type:ue.MessageType.Error,message:e,actions:r};return this.connection.sendRequest(ue.ShowMessageRequest.type,n).then(rg)}showWarningMessage(e,...r){let n={type:ue.MessageType.Warning,message:e,actions:r};return this.connection.sendRequest(ue.ShowMessageRequest.type,n).then(rg)}showInformationMessage(e,...r){let n={type:ue.MessageType.Info,message:e,actions:r};return this.connection.sendRequest(ue.ShowMessageRequest.type,n).then(rg)}},u1=(0, tz.ShowDocumentFeature)((0, Re.ProgressFeature)(sg)),c1;(function(t){function e(){return new Yc}t.create=e;})(c1||(Ne.BulkRegistration=c1={}));var Yc=class{constructor(){this._registrations=[],this._registered=new Set;}add(e,r){let n=Er.string(e)?e:e.method;if(this._registered.has(n))throw new Error(`${n} is already added to this registration`);let i=ng.generateUuid();this._registrations.push({id:i,method:n,registerOptions:r||{}}),this._registered.add(n);}asRegistrationParams(){return {registrations:this._registrations}}},l1;(function(t){function e(){return new Mo(void 0,[])}t.create=e;})(l1||(Ne.BulkUnregistration=l1={}));var Mo=class{constructor(e,r){this._connection=e,this._unregistrations=new Map,r.forEach(n=>{this._unregistrations.set(n.method,n);});}get isAttached(){return !!this._connection}attach(e){this._connection=e;}add(e){this._unregistrations.set(e.method,e);}dispose(){let e=[];for(let n of this._unregistrations.values())e.push(n);let r={unregisterations:e};this._connection.sendRequest(ue.UnregistrationRequest.type,r).catch(()=>{this._connection.console.info("Bulk unregistration failed.");});}disposeSingle(e){let r=Er.string(e)?e:e.method,n=this._unregistrations.get(r);if(!n)return !1;let i={unregisterations:[n]};return this._connection.sendRequest(ue.UnregistrationRequest.type,i).then(()=>{this._unregistrations.delete(r);},s=>{this._connection.console.info(`Un-registering request handler for ${n.id} failed.`);}),!0}},Xc=class{attach(e){this._connection=e;}get connection(){if(!this._connection)throw new Error("Remote is not attached to a connection yet.");return this._connection}initialize(e){}fillServerCapabilities(e){}register(e,r,n){return e instanceof Yc?this.registerMany(e):e instanceof Mo?this.registerSingle1(e,r,n):this.registerSingle2(e,r)}registerSingle1(e,r,n){let i=Er.string(r)?r:r.method,s=ng.generateUuid(),o={registrations:[{id:s,method:i,registerOptions:n||{}}]};return e.isAttached||e.attach(this.connection),this.connection.sendRequest(ue.RegistrationRequest.type,o).then(a=>(e.add({id:s,method:i}),e),a=>(this.connection.console.info(`Registering request handler for ${i} failed.`),Promise.reject(a)))}registerSingle2(e,r){let n=Er.string(e)?e:e.method,i=ng.generateUuid(),s={registrations:[{id:i,method:n,registerOptions:r||{}}]};return this.connection.sendRequest(ue.RegistrationRequest.type,s).then(o=>ue.Disposable.create(()=>{this.unregisterSingle(i,n).catch(()=>{this.connection.console.info(`Un-registering capability with id ${i} failed.`);});}),o=>(this.connection.console.info(`Registering request handler for ${n} failed.`),Promise.reject(o)))}unregisterSingle(e,r){let n={unregisterations:[{id:e,method:r}]};return this.connection.sendRequest(ue.UnregistrationRequest.type,n).catch(()=>{this.connection.console.info(`Un-registering request handler for ${e} failed.`);})}registerMany(e){let r=e.asRegistrationParams();return this.connection.sendRequest(ue.RegistrationRequest.type,r).then(()=>new Mo(this._connection,r.registrations.map(n=>({id:n.id,method:n.method}))),n=>(this.connection.console.info("Bulk registration failed."),Promise.reject(n)))}},og=class{constructor(){}attach(e){this._connection=e;}get connection(){if(!this._connection)throw new Error("Remote is not attached to a connection yet.");return this._connection}initialize(e){}fillServerCapabilities(e){}applyEdit(e){function r(i){return i&&!!i.edit}let n=r(e)?e:{edit:e};return this.connection.sendRequest(ue.ApplyWorkspaceEditRequest.type,n)}},f1=(0, rz.FileOperationsFeature)((0, X4.WorkspaceFoldersFeature)((0, Y4.ConfigurationFeature)(og))),Qc=class{constructor(){this._trace=ue.Trace.Off;}attach(e){this._connection=e;}get connection(){if(!this._connection)throw new Error("Remote is not attached to a connection yet.");return this._connection}initialize(e){}fillServerCapabilities(e){}set trace(e){this._trace=e;}log(e,r){this._trace!==ue.Trace.Off&&this.connection.sendNotification(ue.LogTraceNotification.type,{message:e,verbose:this._trace===ue.Trace.Verbose?r:void 0}).catch(()=>{});}},el=class{constructor(){}attach(e){this._connection=e;}get connection(){if(!this._connection)throw new Error("Remote is not attached to a connection yet.");return this._connection}initialize(e){}fillServerCapabilities(e){}logEvent(e){this.connection.sendNotification(ue.TelemetryEventNotification.type,e).catch(()=>{this.connection.console.log("Sending TelemetryEventNotification failed");});}},tl=class{constructor(){}attach(e){this._connection=e;}get connection(){if(!this._connection)throw new Error("Remote is not attached to a connection yet.");return this._connection}initialize(e){}fillServerCapabilities(e){}attachWorkDoneProgress(e){return (0, Re.attachWorkDone)(this.connection,e)}attachPartialResultProgress(e,r){return (0, Re.attachPartialResult)(this.connection,r)}};Ne._LanguagesImpl=tl;var d1=(0, oz.FoldingRangeFeature)((0, lz.MonikerFeature)((0, uz.DiagnosticFeature)((0, az.InlayHintFeature)((0, sz.InlineValueFeature)((0, iz.TypeHierarchyFeature)((0, nz.LinkedEditingRangeFeature)((0, ez.SemanticTokensFeature)((0, Q4.CallHierarchyFeature)(tl))))))))),rl=class{constructor(){}attach(e){this._connection=e;}get connection(){if(!this._connection)throw new Error("Remote is not attached to a connection yet.");return this._connection}initialize(e){}fillServerCapabilities(e){}attachWorkDoneProgress(e){return (0, Re.attachWorkDone)(this.connection,e)}attachPartialResultProgress(e,r){return (0, Re.attachPartialResult)(this.connection,r)}};Ne._NotebooksImpl=rl;var h1=(0, cz.NotebookSyncFeature)(rl);function p1(t,e){return function(r){return e(t(r))}}Ne.combineConsoleFeatures=p1;function m1(t,e){return function(r){return e(t(r))}}Ne.combineTelemetryFeatures=m1;function g1(t,e){return function(r){return e(t(r))}}Ne.combineTracerFeatures=g1;function _1(t,e){return function(r){return e(t(r))}}Ne.combineClientFeatures=_1;function y1(t,e){return function(r){return e(t(r))}}Ne.combineWindowFeatures=y1;function b1(t,e){return function(r){return e(t(r))}}Ne.combineWorkspaceFeatures=b1;function v1(t,e){return function(r){return e(t(r))}}Ne.combineLanguagesFeatures=v1;function w1(t,e){return function(r){return e(t(r))}}Ne.combineNotebooksFeatures=w1;function fz(t,e){function r(i,s,o){return i&&s?o(i,s):i||s}return {__brand:"features",console:r(t.console,e.console,p1),tracer:r(t.tracer,e.tracer,g1),telemetry:r(t.telemetry,e.telemetry,m1),client:r(t.client,e.client,_1),window:r(t.window,e.window,y1),workspace:r(t.workspace,e.workspace,b1),languages:r(t.languages,e.languages,v1),notebooks:r(t.notebooks,e.notebooks,w1)}}Ne.combineFeatures=fz;function dz(t,e,r){let n=r&&r.console?new(r.console(Jc)):new Jc,i=t(n);n.rawAttach(i);let s=r&&r.tracer?new(r.tracer(Qc)):new Qc,o=r&&r.telemetry?new(r.telemetry(el)):new el,a=r&&r.client?new(r.client(Xc)):new Xc,l=r&&r.window?new(r.window(u1)):new u1,d=r&&r.workspace?new(r.workspace(f1)):new f1,c=r&&r.languages?new(r.languages(d1)):new d1,p=r&&r.notebooks?new(r.notebooks(h1)):new h1,m=[n,s,o,a,l,d,c,p];function _(g){return g instanceof Promise?g:Er.thenable(g)?new Promise((S,I)=>{g.then(L=>S(L),L=>I(L));}):Promise.resolve(g)}let w,E,P,D={listen:()=>i.listen(),sendRequest:(g,...S)=>i.sendRequest(Er.string(g)?g:g.method,...S),onRequest:(g,S)=>i.onRequest(g,S),sendNotification:(g,S)=>{let I=Er.string(g)?g:g.method;return i.sendNotification(I,S)},onNotification:(g,S)=>i.onNotification(g,S),onProgress:i.onProgress,sendProgress:i.sendProgress,onInitialize:g=>(E=g,{dispose:()=>{E=void 0;}}),onInitialized:g=>i.onNotification(ue.InitializedNotification.type,g),onShutdown:g=>(w=g,{dispose:()=>{w=void 0;}}),onExit:g=>(P=g,{dispose:()=>{P=void 0;}}),get console(){return n},get telemetry(){return o},get tracer(){return s},get client(){return a},get window(){return l},get workspace(){return d},get languages(){return c},get notebooks(){return p},onDidChangeConfiguration:g=>i.onNotification(ue.DidChangeConfigurationNotification.type,g),onDidChangeWatchedFiles:g=>i.onNotification(ue.DidChangeWatchedFilesNotification.type,g),__textDocumentSync:void 0,onDidOpenTextDocument:g=>i.onNotification(ue.DidOpenTextDocumentNotification.type,g),onDidChangeTextDocument:g=>i.onNotification(ue.DidChangeTextDocumentNotification.type,g),onDidCloseTextDocument:g=>i.onNotification(ue.DidCloseTextDocumentNotification.type,g),onWillSaveTextDocument:g=>i.onNotification(ue.WillSaveTextDocumentNotification.type,g),onWillSaveTextDocumentWaitUntil:g=>i.onRequest(ue.WillSaveTextDocumentWaitUntilRequest.type,g),onDidSaveTextDocument:g=>i.onNotification(ue.DidSaveTextDocumentNotification.type,g),sendDiagnostics:g=>i.sendNotification(ue.PublishDiagnosticsNotification.type,g),onHover:g=>i.onRequest(ue.HoverRequest.type,(S,I)=>g(S,I,(0, Re.attachWorkDone)(i,S),void 0)),onCompletion:g=>i.onRequest(ue.CompletionRequest.type,(S,I)=>g(S,I,(0, Re.attachWorkDone)(i,S),(0, Re.attachPartialResult)(i,S))),onCompletionResolve:g=>i.onRequest(ue.CompletionResolveRequest.type,g),onSignatureHelp:g=>i.onRequest(ue.SignatureHelpRequest.type,(S,I)=>g(S,I,(0, Re.attachWorkDone)(i,S),void 0)),onDeclaration:g=>i.onRequest(ue.DeclarationRequest.type,(S,I)=>g(S,I,(0, Re.attachWorkDone)(i,S),(0, Re.attachPartialResult)(i,S))),onDefinition:g=>i.onRequest(ue.DefinitionRequest.type,(S,I)=>g(S,I,(0, Re.attachWorkDone)(i,S),(0, Re.attachPartialResult)(i,S))),onTypeDefinition:g=>i.onRequest(ue.TypeDefinitionRequest.type,(S,I)=>g(S,I,(0, Re.attachWorkDone)(i,S),(0, Re.attachPartialResult)(i,S))),onImplementation:g=>i.onRequest(ue.ImplementationRequest.type,(S,I)=>g(S,I,(0, Re.attachWorkDone)(i,S),(0, Re.attachPartialResult)(i,S))),onReferences:g=>i.onRequest(ue.ReferencesRequest.type,(S,I)=>g(S,I,(0, Re.attachWorkDone)(i,S),(0, Re.attachPartialResult)(i,S))),onDocumentHighlight:g=>i.onRequest(ue.DocumentHighlightRequest.type,(S,I)=>g(S,I,(0, Re.attachWorkDone)(i,S),(0, Re.attachPartialResult)(i,S))),onDocumentSymbol:g=>i.onRequest(ue.DocumentSymbolRequest.type,(S,I)=>g(S,I,(0, Re.attachWorkDone)(i,S),(0, Re.attachPartialResult)(i,S))),onWorkspaceSymbol:g=>i.onRequest(ue.WorkspaceSymbolRequest.type,(S,I)=>g(S,I,(0, Re.attachWorkDone)(i,S),(0, Re.attachPartialResult)(i,S))),onWorkspaceSymbolResolve:g=>i.onRequest(ue.WorkspaceSymbolResolveRequest.type,g),onCodeAction:g=>i.onRequest(ue.CodeActionRequest.type,(S,I)=>g(S,I,(0, Re.attachWorkDone)(i,S),(0, Re.attachPartialResult)(i,S))),onCodeActionResolve:g=>i.onRequest(ue.CodeActionResolveRequest.type,(S,I)=>g(S,I)),onCodeLens:g=>i.onRequest(ue.CodeLensRequest.type,(S,I)=>g(S,I,(0, Re.attachWorkDone)(i,S),(0, Re.attachPartialResult)(i,S))),onCodeLensResolve:g=>i.onRequest(ue.CodeLensResolveRequest.type,(S,I)=>g(S,I)),onDocumentFormatting:g=>i.onRequest(ue.DocumentFormattingRequest.type,(S,I)=>g(S,I,(0, Re.attachWorkDone)(i,S),void 0)),onDocumentRangeFormatting:g=>i.onRequest(ue.DocumentRangeFormattingRequest.type,(S,I)=>g(S,I,(0, Re.attachWorkDone)(i,S),void 0)),onDocumentOnTypeFormatting:g=>i.onRequest(ue.DocumentOnTypeFormattingRequest.type,(S,I)=>g(S,I)),onRenameRequest:g=>i.onRequest(ue.RenameRequest.type,(S,I)=>g(S,I,(0, Re.attachWorkDone)(i,S),void 0)),onPrepareRename:g=>i.onRequest(ue.PrepareRenameRequest.type,(S,I)=>g(S,I)),onDocumentLinks:g=>i.onRequest(ue.DocumentLinkRequest.type,(S,I)=>g(S,I,(0, Re.attachWorkDone)(i,S),(0, Re.attachPartialResult)(i,S))),onDocumentLinkResolve:g=>i.onRequest(ue.DocumentLinkResolveRequest.type,(S,I)=>g(S,I)),onDocumentColor:g=>i.onRequest(ue.DocumentColorRequest.type,(S,I)=>g(S,I,(0, Re.attachWorkDone)(i,S),(0, Re.attachPartialResult)(i,S))),onColorPresentation:g=>i.onRequest(ue.ColorPresentationRequest.type,(S,I)=>g(S,I,(0, Re.attachWorkDone)(i,S),(0, Re.attachPartialResult)(i,S))),onFoldingRanges:g=>i.onRequest(ue.FoldingRangeRequest.type,(S,I)=>g(S,I,(0, Re.attachWorkDone)(i,S),(0, Re.attachPartialResult)(i,S))),onSelectionRanges:g=>i.onRequest(ue.SelectionRangeRequest.type,(S,I)=>g(S,I,(0, Re.attachWorkDone)(i,S),(0, Re.attachPartialResult)(i,S))),onExecuteCommand:g=>i.onRequest(ue.ExecuteCommandRequest.type,(S,I)=>g(S,I,(0, Re.attachWorkDone)(i,S),void 0)),dispose:()=>i.dispose()};for(let g of m)g.attach(D);return i.onRequest(ue.InitializeRequest.type,g=>{e.initialize(g),Er.string(g.trace)&&(s.trace=ue.Trace.fromString(g.trace));for(let S of m)S.initialize(g.capabilities);if(E){let S=E(g,new ue.CancellationTokenSource().token,(0, Re.attachWorkDone)(i,g),void 0);return _(S).then(I=>{if(I instanceof ue.ResponseError)return I;let L=I;L||(L={capabilities:{}});let Z=L.capabilities;Z||(Z={},L.capabilities=Z),Z.textDocumentSync===void 0||Z.textDocumentSync===null?Z.textDocumentSync=Er.number(D.__textDocumentSync)?D.__textDocumentSync:ue.TextDocumentSyncKind.None:!Er.number(Z.textDocumentSync)&&!Er.number(Z.textDocumentSync.change)&&(Z.textDocumentSync.change=Er.number(D.__textDocumentSync)?D.__textDocumentSync:ue.TextDocumentSyncKind.None);for(let K of m)K.fillServerCapabilities(Z);return L})}else {let S={capabilities:{textDocumentSync:ue.TextDocumentSyncKind.None}};for(let I of m)I.fillServerCapabilities(S.capabilities);return S}}),i.onRequest(ue.ShutdownRequest.type,()=>{if(e.shutdownReceived=!0,w)return w(new ue.CancellationTokenSource().token)}),i.onNotification(ue.ExitNotification.type,()=>{try{P&&P();}finally{e.shutdownReceived?e.exit(0):e.exit(1);}}),i.onNotification(ue.SetTraceNotification.type,g=>{s.trace=ue.Trace.fromString(g.value);}),D}Ne.createConnection=dz;});var S1=A(er=>{Object.defineProperty(er,"__esModule",{value:!0});er.resolveModulePath=er.FileSystem=er.resolveGlobalYarnPath=er.resolveGlobalNodePath=er.resolve=er.uriToFilePath=void 0;var hz=oe("url"),qr=oe("path"),ug=oe("fs"),dg=oe("child_process");function pz(t){let e=hz.parse(t);if(e.protocol!=="file:"||!e.path)return;let r=e.path.split("/");for(var n=0,i=r.length;n1){let s=r[0],o=r[1];s.length===0&&o.length>1&&o[1]===":"&&r.shift();}return qr.normalize(r.join("/"))}er.uriToFilePath=pz;function cg(){return process.platform==="win32"}function nl(t,e,r,n){let i="NODE_PATH",s=["var p = process;","p.on('message',function(m){","if(m.c==='e'){","p.exit(0);","}","else if(m.c==='rs'){","try{","var r=require.resolve(m.a);","p.send({c:'r',s:true,r:r});","}","catch(err){","p.send({c:'r',s:false});","}","}","});"].join("");return new Promise((o,a)=>{let l=process.env,d=Object.create(null);Object.keys(l).forEach(c=>d[c]=l[c]),e&&ug.existsSync(e)&&(d[i]?d[i]=e+qr.delimiter+d[i]:d[i]=e,n&&n(`NODE_PATH value is: ${d[i]}`)),d.ELECTRON_RUN_AS_NODE="1";try{let c=(0, dg.fork)("",[],{cwd:r,env:d,execArgv:["-e",s]});if(c.pid===void 0){a(new Error(`Starting process to resolve node module ${t} failed`));return}c.on("error",m=>{a(m);}),c.on("message",m=>{m.c==="r"&&(c.send({c:"e"}),m.s?o(m.r):a(new Error(`Failed to resolve module: ${t}`)));});let p={c:"rs",a:t};c.send(p);}catch(c){a(c);}})}er.resolve=nl;function lg(t){let e="npm",r=Object.create(null);Object.keys(process.env).forEach(s=>r[s]=process.env[s]),r.NO_UPDATE_NOTIFIER="true";let n={encoding:"utf8",env:r};cg()&&(e="npm.cmd",n.shell=!0);let i=()=>{};try{process.on("SIGPIPE",i);let s=(0, dg.spawnSync)(e,["config","get","prefix"],n).stdout;if(!s){t&&t("'npm config get prefix' didn't return a value.");return}let o=s.trim();return t&&t(`'npm config get prefix' value is: ${o}`),o.length>0?cg()?qr.join(o,"node_modules"):qr.join(o,"lib","node_modules"):void 0}catch{return}finally{process.removeListener("SIGPIPE",i);}}er.resolveGlobalNodePath=lg;function mz(t){let e="yarn",r={encoding:"utf8"};cg()&&(e="yarn.cmd",r.shell=!0);let n=()=>{};try{process.on("SIGPIPE",n);let i=(0, dg.spawnSync)(e,["global","dir","--json"],r),s=i.stdout;if(!s){t&&(t("'yarn global dir' didn't return a value."),i.stderr&&t(i.stderr));return}let o=s.trim().split(/\r?\n/);for(let a of o)try{let l=JSON.parse(a);if(l.type==="log")return qr.join(l.data,"node_modules")}catch{}return}catch{return}finally{process.removeListener("SIGPIPE",n);}}er.resolveGlobalYarnPath=mz;var fg;(function(t){let e;function r(){return e!==void 0||(process.platform==="win32"?e=!1:e=!ug.existsSync(__filename.toUpperCase())||!ug.existsSync(__filename.toLowerCase())),e}t.isCaseSensitive=r;function n(i,s){return r()?qr.normalize(s).indexOf(qr.normalize(i))===0:qr.normalize(s).toLowerCase().indexOf(qr.normalize(i).toLowerCase())===0}t.isParent=n;})(fg||(er.FileSystem=fg={}));function gz(t,e,r,n){return r?(qr.isAbsolute(r)||(r=qr.join(t,r)),nl(e,r,r,n).then(i=>fg.isParent(r,i)?i:Promise.reject(new Error(`Failed to load ${e} from node path location.`))).then(void 0,i=>nl(e,lg(n),t,n))):nl(e,lg(n),t,n)}er.resolveModulePath=gz;});var hg=A((FZ,E1)=>{E1.exports=dt();});var R1=A(il=>{Object.defineProperty(il,"__esModule",{value:!0});il.InlineCompletionFeature=void 0;var _z=dt(),yz=t=>class extends t{get inlineCompletion(){return {on:e=>this.connection.onRequest(_z.InlineCompletionRequest.type,(r,n)=>e(r,n,this.attachWorkDoneProgress(r)))}}};il.InlineCompletionFeature=yz;});var T1=A(Ft=>{var bz=Ft&&Ft.__createBinding||(Object.create?function(t,e,r,n){n===void 0&&(n=r);var i=Object.getOwnPropertyDescriptor(e,r);(!i||("get"in i?!e.__esModule:i.writable||i.configurable))&&(i={enumerable:!0,get:function(){return e[r]}}),Object.defineProperty(t,n,i);}:function(t,e,r,n){n===void 0&&(n=r),t[n]=e[r];}),x1=Ft&&Ft.__exportStar||function(t,e){for(var r in t)r!=="default"&&!Object.prototype.hasOwnProperty.call(e,r)&&bz(e,t,r);};Object.defineProperty(Ft,"__esModule",{value:!0});Ft.ProposedFeatures=Ft.NotebookDocuments=Ft.TextDocuments=Ft.SemanticTokensBuilder=void 0;var vz=Zm();Object.defineProperty(Ft,"SemanticTokensBuilder",{enumerable:!0,get:function(){return vz.SemanticTokensBuilder}});var wz=R1();x1(dt(),Ft);var Sz=Qm();Object.defineProperty(Ft,"TextDocuments",{enumerable:!0,get:function(){return Sz.TextDocuments}});var Ez=tg();Object.defineProperty(Ft,"NotebookDocuments",{enumerable:!0,get:function(){return Ez.NotebookDocuments}});x1(ag(),Ft);var C1;(function(t){t.all={__brand:"features",languages:wz.InlineCompletionFeature};})(C1||(Ft.ProposedFeatures=C1={}));});var k1=A(Rr=>{var Rz=Rr&&Rr.__createBinding||(Object.create?function(t,e,r,n){n===void 0&&(n=r);var i=Object.getOwnPropertyDescriptor(e,r);(!i||("get"in i?!e.__esModule:i.writable||i.configurable))&&(i={enumerable:!0,get:function(){return e[r]}}),Object.defineProperty(t,n,i);}:function(t,e,r,n){n===void 0&&(n=r),t[n]=e[r];}),O1=Rr&&Rr.__exportStar||function(t,e){for(var r in t)r!=="default"&&!Object.prototype.hasOwnProperty.call(e,r)&&Rz(e,t,r);};Object.defineProperty(Rr,"__esModule",{value:!0});Rr.createConnection=Rr.Files=void 0;var A1=oe("util"),pg=Uu(),Cz=ag(),Fo=S1(),_i=hg();O1(hg(),Rr);O1(T1(),Rr);var P1;(function(t){t.uriToFilePath=Fo.uriToFilePath,t.resolveGlobalNodePath=Fo.resolveGlobalNodePath,t.resolveGlobalYarnPath=Fo.resolveGlobalYarnPath,t.resolve=Fo.resolve,t.resolveModulePath=Fo.resolveModulePath;})(P1||(Rr.Files=P1={}));var ws=!1,I1;function xz(){let t="--clientProcessId";function e(r){try{let n=parseInt(r);isNaN(n)||(I1=setInterval(()=>{try{process.kill(n,0);}catch{process.exit(ws?0:1);}},3e3));}catch{}}for(let r=2;r{let e=t.processId;pg.number(e)&&I1===void 0&&setInterval(()=>{try{process.kill(e,0);}catch{process.exit(ws?0:1);}},3e3);},get shutdownReceived(){return ws},set shutdownReceived(t){ws=t;},exit:t=>{process.exit(t);}};function Az(t,e,r,n){let i,s,o,a;return t!==void 0&&t.__brand==="features"&&(i=t,t=e,e=r,r=n),_i.ConnectionStrategy.is(t)||_i.ConnectionOptions.is(t)?a=t:(s=t,o=e,a=r),Pz(s,o,a,i)}Rr.createConnection=Az;function Pz(t,e,r,n){let i=!1;if(!t&&!e&&process.argv.length>2){let l,d,c=process.argv.slice(2);for(let p=0;p{process.exit(ws?0:1);}),l.on("close",()=>{process.exit(ws?0:1);});}let a=l=>{let d=(0, _i.createProtocolConnection)(t,e,l,r);return i&&Dz(l),d};return (0, Cz.createConnection)(a,Tz,n)}function Dz(t){function e(n){return n.map(i=>typeof i=="string"?i:(0, A1.inspect)(i)).join(" ")}let r=new Map;console.assert=function(i,...s){if(!i)if(s.length===0)t.error("Assertion failed");else {let[o,...a]=s;t.error(`Assertion failed: ${o} ${e(a)}`);}},console.count=function(i="default"){let s=String(i),o=r.get(s)??0;o+=1,r.set(s,o),t.log(`${s}: ${s}`);},console.countReset=function(i){i===void 0?r.clear():r.delete(String(i));},console.debug=function(...i){t.log(e(i));},console.dir=function(i,s){t.log((0, A1.inspect)(i,s));},console.log=function(...i){t.log(e(i));},console.error=function(...i){t.error(e(i));},console.trace=function(...i){let s=new Error().stack.replace(/(.+\n){2}/,""),o="Trace";i.length!==0&&(o+=`: ${e(i)}`),t.log(`${o} -${s}`);},console.warn=function(...i){t.warn(e(i));};}});var F1=A((jZ,M1)=>{M1.exports=k1();});var zo=new Uint8Array(256),Uo=zo.length;function yl(){return Uo>zo.length-16&&(DO__default.default.randomFillSync(zo),Uo=0),zo.slice(Uo,Uo+=16)}var xt=[];for(let t=0;t<256;++t)xt.push((t+256).toString(16).slice(1));function Kg(t,e=0){return xt[t[e+0]]+xt[t[e+1]]+xt[t[e+2]]+xt[t[e+3]]+"-"+xt[t[e+4]]+xt[t[e+5]]+"-"+xt[t[e+6]]+xt[t[e+7]]+"-"+xt[t[e+8]]+xt[t[e+9]]+"-"+xt[t[e+10]]+xt[t[e+11]]+xt[t[e+12]]+xt[t[e+13]]+xt[t[e+14]]+xt[t[e+15]]}var bl={randomUUID:DO__default.default.randomUUID};function IO(t,e,r){if(bl.randomUUID&&!e&&!t)return bl.randomUUID();t=t||{};let n=t.random||(t.rng||yl)();if(n[6]=n[6]&15|64,n[8]=n[8]&63|128,e){r=r||0;for(let i=0;i<16;++i)e[r+i]=n[i];return e}return Kg(n)}var Xr=IO;var Hu=$t(xa());var wn={defaultMerge:Symbol("deepmerge-ts: default merge"),skip:Symbol("deepmerge-ts: skip")};function pF(t,e){return e}function zv(t){return typeof t!="object"||t===null?0:Array.isArray(t)?2:_F(t)?1:t instanceof Set?3:t instanceof Map?4:5}function mF(t){let e=new Set;for(let r of t)for(let n of [...Object.keys(r),...Object.getOwnPropertySymbols(r)])e.add(n);return e}function gF(t,e){return typeof t=="object"&&Object.prototype.propertyIsEnumerable.call(t,e)}function Gv(t){return {*[Symbol.iterator](){for(let e of t)for(let r of e)yield r;}}}var Vv=new Set(["[object Object]","[object Module]"]);function _F(t){if(!Vv.has(Object.prototype.toString.call(t)))return !1;let{constructor:e}=t;if(e===void 0)return !0;let r=e.prototype;return !(r===null||typeof r!="object"||!Vv.has(Object.prototype.toString.call(r))||!r.hasOwnProperty("isPrototypeOf"))}function yF(t,e,r){let n={};for(let i of mF(t)){let s=[];for(let l of t)gF(l,i)&&s.push(l[i]);if(s.length===0)continue;let o=e.metaDataUpdater(r,{key:i,parents:t}),a=Jv(s,e,o);a!==wn.skip&&(i==="__proto__"?Object.defineProperty(n,i,{value:a,configurable:!0,enumerable:!0,writable:!0}):n[i]=a);}return n}function bF(t){return t.flat()}function vF(t){return new Set(Gv(t))}function wF(t){return new Map(Gv(t))}function Kv(t){return t.at(-1)}var Ef=Object.freeze({__proto__:null,mergeArrays:bF,mergeMaps:wF,mergeOthers:Kv,mergeRecords:yF,mergeSets:vF});function Zv(...t){return SF({})(...t)}function SF(t,e){let r=EF(t,n);function n(...i){return Jv(i,r,e)}return n}function EF(t,e){return {defaultMergeFunctions:Ef,mergeFunctions:{...Ef,...Object.fromEntries(Object.entries(t).filter(([r,n])=>Object.hasOwn(Ef,r)).map(([r,n])=>n===!1?[r,Kv]:[r,n]))},metaDataUpdater:t.metaDataUpdater??pF,deepmerge:e,useImplicitDefaultMerging:t.enableImplicitDefaultMerging??!1,actions:wn}}function Jv(t,e,r){if(t.length===0)return;if(t.length===1)return Rf(t,e,r);let n=zv(t[0]);if(n!==0&&n!==5){for(let i=1;i{let e=typeof t;return t!==null&&(e==="object"||e==="function")};var Cf=new Set(["__proto__","prototype","constructor"]),AF=new Set("0123456789");function xf(t){let e=[],r="",n="start",i=!1;for(let s of t)switch(s){case"\\":{if(n==="index")throw new Error("Invalid character in an index");if(n==="indexEnd")throw new Error("Invalid character after an index");i&&(r+=s),n="property",i=!i;break}case".":{if(n==="index")throw new Error("Invalid character in an index");if(n==="indexEnd"){n="property";break}if(i){i=!1,r+=s;break}if(Cf.has(r))return [];e.push(r),r="",n="property";break}case"[":{if(n==="index")throw new Error("Invalid character in an index");if(n==="indexEnd"){n="index";break}if(i){i=!1,r+=s;break}if(n==="property"){if(Cf.has(r))return [];e.push(r),r="";}n="index";break}case"]":{if(n==="index"){e.push(Number.parseInt(r,10)),r="",n="indexEnd";break}if(n==="indexEnd")throw new Error("Invalid character after an index")}default:{if(n==="index"&&!AF.has(s))throw new Error("Invalid character in an index");if(n==="indexEnd")throw new Error("Invalid character after an index");n==="start"&&(n="property"),i&&(i=!1,r+="\\"),r+=s;}}switch(i&&(r+="\\"),n){case"property":{if(Cf.has(r))return [];e.push(r);break}case"index":throw new Error("Index was not closed");case"start":{e.push("");break}}return e}function Yv(t,e){if(typeof e!="number"&&Array.isArray(t)){let r=Number.parseInt(e,10);return Number.isInteger(r)&&t[r]===t[e]}return !1}function Xv(t,e){if(Yv(t,e))throw new Error("Cannot use string index")}function Ta(t,e,r){if(!Ls(t)||typeof e!="string")return r===void 0?t:r;let n=xf(e);if(n.length===0)return r;for(let i=0;i0&&e[e.length-1]?.endsWith(` -`)&&e.push(""),e}function Ae(t){return t.trim().length===0}function Hr(t,e){if(e===void 0)return t.match(/^[ \t]*/)?.[0]?.length??0;if(e===" ")return t.match(/^\t*/)?.[0].length??0;if(e.match(/^ *$/))return (t.match(/^ */)?.[0].length??0)/e.length;throw new Error(`Invalid indentation: ${e}`)}function Af(t,e){return e<0||e>=t.length-1?!1:Hr(t[e])t.length-1?!1:Hr(t[e-1])>Hr(t[e])}var Pf=[{open:{chars:"(",reg:/\(/},close:{chars:")",reg:/\)/}},{open:{chars:"[",reg:/\[/},close:{chars:"]",reg:/\]/}},{open:{chars:"{",reg:/\{/},close:{chars:"}",reg:/\}/}},{open:{chars:"<",reg:/<(?=\w)/},close:{chars:"/>",reg:/\/>/}},{open:{chars:"<",reg:/<(?=[/\w])/},close:{chars:">",reg:/>/}},{openOrClose:{chars:'"',reg:/"/}},{openOrClose:{chars:"'",reg:/'/}},{openOrClose:{chars:"`",reg:/`/}}],nw=/^([)\]}>"'`]|(\/>))*$/g;function Da(t){let e=[],r=0;for(;r{Object.entries(s).forEach(([o,a])=>{let l=n.match(a.reg);l&&l.index!==void 0&&l.index0&&e.includes(s)?e.splice(e.lastIndexOf(s),e.length-e.lastIndexOf(s)):e.push(s);break}case"open":{e.push(i.found.pattern.chars);break}case"close":{let s=i.found.pair;e.length>0&&"open"in s&&e[e.length-1]===s.open.chars?e.pop():e.push(i.found.pattern.chars);break}}r+=i.index+i.found.pattern.chars.length;}return e}function ki(t,e){return iw.get(t,e)}function js(t){let e=new AbortController;for(let r of t){if(r?.aborted)return e.abort(r.reason),r;r?.addEventListener("abort",()=>e.abort(r.reason),{signal:e.signal});}return e.signal}var jt=class extends Error{constructor(r){super(`${r.status} ${r.statusText}`);this.name="HttpError",this.status=r.status,this.statusText=r.statusText,this.response=r;}};function Hs(t){return t instanceof Error&&t.name==="TimeoutError"||t instanceof jt&&[408,499].includes(t.status)}function en(t){return t instanceof Error&&t.name==="AbortError"}function Df(t){let e=t.message||t.toString();return t.cause&&(e+=` -Caused by: `+Df(t.cause)),e}function Of(t){this.message=t;}Of.prototype=new Error,Of.prototype.name="InvalidCharacterError";var sw=typeof window<"u"&&window.atob&&window.atob.bind(window)||function(t){var e=String(t).replace(/=+$/,"");if(e.length%4==1)throw new Of("'atob' failed: The string to be decoded is not correctly encoded.");for(var r,n,i=0,s=0,o="";n=e.charAt(s++);~n&&(r=i%4?64*r+n:n,i++%4)?o+=String.fromCharCode(255&r>>(-2*i&6)):0)n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".indexOf(n);return o};function qF(t){var e=t.replace(/-/g,"+").replace(/_/g,"/");switch(e.length%4){case 0:break;case 2:e+="==";break;case 3:e+="=";break;default:throw "Illegal base64url string!"}try{return function(r){return decodeURIComponent(sw(r).replace(/(.)/g,function(n,i){var s=i.charCodeAt(0).toString(16).toUpperCase();return s.length<2&&(s="0"+s),"%"+s}))}(e)}catch{return sw(e)}}function Oa(t){this.message=t;}function LF(t,e){if(typeof t!="string")throw new Oa("Invalid token specified");var r=(e=e||{}).header===!0?0:1;try{return JSON.parse(qF(t.split(".")[r]))}catch(n){throw new Oa("Invalid token specified: "+n.message)}}Oa.prototype=new Error,Oa.prototype.name="InvalidTokenError";var Ia=LF;var zd=$t(Vf()),X0=$t(xa()),Q0=$t(Ud());var Vd=class extends events.EventEmitter{constructor(r){super();this.filepath=r;this.data={};}async load(){this.data=await zd.default.readJson(Gd,{throws:!1})||{};}async save(){await zd.default.outputJson(Gd,this.data);}watch(){this.watcher=Q0.default.watch(this.filepath,{interval:1e3});let r=async()=>{let n=this.data;await this.load(),(0, X0.default)(n,this.data)||super.emit("updated",this.data);};this.watcher.on("add",r),this.watcher.on("change",r);}},Gd=Vx__default.default.join(oT__default.default.homedir(),".tabby-client","agent","data.json"),ri=new Vd(Gd);var Pu=$t(ax()),yx=$t(mx());var Xh=class{constructor(){this.streamOptions={filename:Vx__default.default.join(oT__default.default.homedir(),".tabby-client","agent","logs","%DATE%"),frequency:"daily",size:"10M",max_logs:"30d",extension:".log",create_symlink:!0};}write(e){this.stream||(this.stream=yx.getStream(this.streamOptions)),this.stream.write(e);}},gx=new Xh,_x={serializers:{error:Pu.default.stdSerializers.err}},ht=gx?(0, Pu.default)(_x,gx):(0, Pu.default)(_x);ht.level="silent";var ho=[ht];ht.onChild=t=>{ho.push(t);};var Qh=class extends Error{constructor(r){super();this.cause=r;this.name="RetryLimitReachedError";}},Du=class t extends events.EventEmitter{constructor(r){super();this.endpoint=r;this.logger=ht.child({component:"Auth"});this.authApi=Jn({baseUrl:"https://app.tabbyml.com/api"});}static{this.authPageUrl="https://app.tabbyml.com/account/device-token";}static{this.tokenStrategy={polling:{interval:5e3,timeout:5*60*1e3},refresh:{interval:15*60*1e3,beforeExpire:30*60*1e3,whenLoaded:{maxTry:5,retryDelay:1e3},whenScheduled:{maxTry:60,retryDelay:30*1e3}}};}async init(r){r?.dataStore?this.dataStore=r.dataStore:(this.dataStore=ri,ri&&(ri.on("updated",async()=>{await this.load(),super.emit("updated",this.jwt);}),ri.watch())),this.scheduleRefreshToken(),await this.load();}get token(){return this.jwt?.token}get user(){return this.jwt?.payload.email}async load(){if(this.dataStore)try{await this.dataStore.load();let r=this.dataStore.data.auth?.[this.endpoint]?.jwt;if(typeof r=="string"&&this.jwt?.token!==r){this.logger.debug({storedJwt:r},"Load jwt from data store.");let n={token:r,payload:Ia(r)};n.payload.exp*1e3-Date.now()"u")return;delete this.dataStore.data.auth[this.endpoint];}await this.dataStore.save(),this.logger.debug("Save changes to data store.");}catch(r){this.logger.error({error:r},"Error when saving auth");}}async reset(){this.jwt&&(this.jwt=void 0,await this.save());}async requestAuthUrl(r){try{if(await this.reset(),r?.signal.aborted)throw r.signal.reason;this.logger.debug("Start to request device token");let n=await this.authApi.POST("/device-token",{body:{auth_url:this.endpoint},signal:r?.signal});if(n.error||!n.response.ok)throw new jt(n.response);let i=n.data;this.logger.debug({deviceToken:i},"Request device token response");let s=new URL(t.authPageUrl);return s.searchParams.append("code",i.data.code),{authUrl:s.toString(),code:i.data.code}}catch(n){throw this.logger.error({error:n},"Error when requesting token"),n}}async pollingToken(r,n){return new Promise((i,s)=>{let o=js([AbortSignal.timeout(t.tokenStrategy.polling.timeout),n?.signal]),a=setInterval(async()=>{try{let l=await this.authApi.POST("/device-token/accept",{params:{query:{code:r}},signal:o});if(l.error||!l.response.ok)throw new jt(l.response);let d=l.data;this.logger.debug({result:d},"Poll jwt response"),this.jwt={token:d.data.jwt,payload:Ia(d.data.jwt)},super.emit("updated",this.jwt),await this.save(),clearInterval(a),i(!0);}catch(l){l instanceof jt&&[400,401,403,405].includes(l.status)?this.logger.debug({error:l},"Expected error when polling jwt"):this.logger.error({error:l},"Error when polling jwt");}},t.tokenStrategy.polling.interval);o.aborted?(clearInterval(a),s(o.reason)):o.addEventListener("abort",()=>{clearInterval(a),s(o.reason);});})}async refreshToken(r,n={maxTry:1,retryDelay:1e3},i=0){try{this.logger.debug({retry:i},"Start to refresh token");let s=await this.authApi.POST("/device-token/refresh",{headers:{Authorization:`Bearer ${r.token}`}});if(s.error||!s.response.ok)throw new jt(s.response);let o=s.data;return this.logger.debug({refreshedJwt:o},"Refresh token response"),{token:o.data.jwt,payload:Ia(o.data.jwt)}}catch(s){if(s instanceof jt&&[400,401,403,405].includes(s.status))this.logger.debug({error:s},"Error when refreshing jwt");else if(this.logger.error({error:s},"Unknown error when refreshing jwt"),isetTimeout(o,n.retryDelay)),this.refreshToken(r,n,i+1);throw new Qh(s)}}scheduleRefreshToken(){setInterval(async()=>{if(this.jwt)if(this.jwt.payload.exp*1e3-Date.now(){let n=this.data;await this.load(),(0, Ax.default)(n,this.data)||super.emit("updated",this.data);};this.watcher.on("add",r),this.watcher.on("change",r);}async createTemplate(){try{await tp.default.outputFile(this.filepath,Cx);}catch(r){this.logger.error({error:r},"Failed to create config template file");}}},XU=Vx__default.default.join(oT__default.default.homedir(),".tabby-client","agent","config.toml"),Zi=new rp(XU);var po=typeof performance=="object"&&performance&&typeof performance.now=="function"?performance:Date,Dx=new Set,np=typeof process=="object"&&process?process:{},Ox=(t,e,r,n)=>{typeof np.emitWarning=="function"?np.emitWarning(t,e,r,n):console.error(`[${r}] ${e}: ${t}`);},Ou=globalThis.AbortController,Px=globalThis.AbortSignal;if(typeof Ou>"u"){Px=class{onabort;_onabort=[];reason;aborted=!1;addEventListener(n,i){this._onabort.push(i);}},Ou=class{constructor(){e();}signal=new Px;abort(n){if(!this.signal.aborted){this.signal.reason=n,this.signal.aborted=!0;for(let i of this.signal._onabort)i(n);this.signal.onabort?.(n);}}};let t=np.env?.LRU_CACHE_IGNORE_AC_WARNING!=="1",e=()=>{t&&(t=!1,Ox("AbortController is not defined. If using lru-cache in node 14, load an AbortController polyfill from the `node-abort-controller` package. A minimal polyfill is provided for use by LRUCache.fetch(), but it should not be relied upon in other contexts (eg, passing it to other APIs that use AbortController/AbortSignal might have undesirable effects). You may disable this with LRU_CACHE_IGNORE_AC_WARNING=1 in the env.","NO_ABORT_CONTROLLER","ENOTSUP",e));};}var QU=t=>!Dx.has(t),An=t=>t&&t===Math.floor(t)&&t>0&&isFinite(t),Ix=t=>An(t)?t<=Math.pow(2,8)?Uint8Array:t<=Math.pow(2,16)?Uint16Array:t<=Math.pow(2,32)?Uint32Array:t<=Number.MAX_SAFE_INTEGER?Ji:null:null,Ji=class extends Array{constructor(e){super(e),this.fill(0);}},ip=class t{heap;length;static#u=!1;static create(e){let r=Ix(e);if(!r)return [];t.#u=!0;let n=new t(e,r);return t.#u=!1,n}constructor(e,r){if(!t.#u)throw new TypeError("instantiate Stack using Stack.create(n)");this.heap=new r(e),this.length=0;}push(e){this.heap[this.length++]=e;}pop(){return this.heap[--this.length]}},Iu=class t{#u;#f;#y;#p;#P;ttl;ttlResolution;ttlAutopurge;updateAgeOnGet;updateAgeOnHas;allowStale;noDisposeOnSet;noUpdateTTL;maxEntrySize;sizeCalculation;noDeleteOnFetchRejection;noDeleteOnStaleGet;allowStaleOnFetchAbort;allowStaleOnFetchRejection;ignoreFetchAbort;#n;#m;#i;#r;#e;#c;#d;#a;#s;#g;#o;#S;#E;#_;#b;#C;#l;static unsafeExposeInternals(e){return {starts:e.#E,ttls:e.#_,sizes:e.#S,keyMap:e.#i,keyList:e.#r,valList:e.#e,next:e.#c,prev:e.#d,get head(){return e.#a},get tail(){return e.#s},free:e.#g,isBackgroundFetch:r=>e.#t(r),backgroundFetch:(r,n,i,s)=>e.#I(r,n,i,s),moveToTail:r=>e.#A(r),indexes:r=>e.#v(r),rindexes:r=>e.#w(r),isStale:r=>e.#h(r)}}get max(){return this.#u}get maxSize(){return this.#f}get calculatedSize(){return this.#m}get size(){return this.#n}get fetchMethod(){return this.#P}get dispose(){return this.#y}get disposeAfter(){return this.#p}constructor(e){let{max:r=0,ttl:n,ttlResolution:i=1,ttlAutopurge:s,updateAgeOnGet:o,updateAgeOnHas:a,allowStale:l,dispose:d,disposeAfter:c,noDisposeOnSet:p,noUpdateTTL:m,maxSize:_=0,maxEntrySize:w=0,sizeCalculation:E,fetchMethod:P,noDeleteOnFetchRejection:D,noDeleteOnStaleGet:g,allowStaleOnFetchRejection:S,allowStaleOnFetchAbort:I,ignoreFetchAbort:L}=e;if(r!==0&&!An(r))throw new TypeError("max option must be a nonnegative integer");let Z=r?Ix(r):Array;if(!Z)throw new Error("invalid max value: "+r);if(this.#u=r,this.#f=_,this.maxEntrySize=w||this.#f,this.sizeCalculation=E,this.sizeCalculation){if(!this.#f&&!this.maxEntrySize)throw new TypeError("cannot set sizeCalculation without setting maxSize or maxEntrySize");if(typeof this.sizeCalculation!="function")throw new TypeError("sizeCalculation set to non-function")}if(P!==void 0&&typeof P!="function")throw new TypeError("fetchMethod must be a function if specified");if(this.#P=P,this.#C=!!P,this.#i=new Map,this.#r=new Array(r).fill(void 0),this.#e=new Array(r).fill(void 0),this.#c=new Z(r),this.#d=new Z(r),this.#a=0,this.#s=0,this.#g=ip.create(r),this.#n=0,this.#m=0,typeof d=="function"&&(this.#y=d),typeof c=="function"?(this.#p=c,this.#o=[]):(this.#p=void 0,this.#o=void 0),this.#b=!!this.#y,this.#l=!!this.#p,this.noDisposeOnSet=!!p,this.noUpdateTTL=!!m,this.noDeleteOnFetchRejection=!!D,this.allowStaleOnFetchRejection=!!S,this.allowStaleOnFetchAbort=!!I,this.ignoreFetchAbort=!!L,this.maxEntrySize!==0){if(this.#f!==0&&!An(this.#f))throw new TypeError("maxSize must be a positive integer if specified");if(!An(this.maxEntrySize))throw new TypeError("maxEntrySize must be a positive integer if specified");this.#L();}if(this.allowStale=!!l,this.noDeleteOnStaleGet=!!g,this.updateAgeOnGet=!!o,this.updateAgeOnHas=!!a,this.ttlResolution=An(i)||i===0?i:1,this.ttlAutopurge=!!s,this.ttl=n||0,this.ttl){if(!An(this.ttl))throw new TypeError("ttl must be a positive integer if specified");this.#k();}if(this.#u===0&&this.ttl===0&&this.#f===0)throw new TypeError("At least one of max, maxSize, or ttl is required");if(!this.ttlAutopurge&&!this.#u&&!this.#f){let K="LRU_CACHE_UNBOUNDED";QU(K)&&(Dx.add(K),Ox("TTL caching without ttlAutopurge, max, or maxSize can result in unbounded memory consumption.","UnboundedCacheWarning",K,t));}}getRemainingTTL(e){return this.#i.has(e)?1/0:0}#k(){let e=new Ji(this.#u),r=new Ji(this.#u);this.#_=e,this.#E=r,this.#M=(s,o,a=po.now())=>{if(r[s]=o!==0?a:0,e[s]=o,o!==0&&this.ttlAutopurge){let l=setTimeout(()=>{this.#h(s)&&this.delete(this.#r[s]);},o+1);l.unref&&l.unref();}},this.#x=s=>{r[s]=e[s]!==0?po.now():0;},this.#R=(s,o)=>{if(e[o]){let a=e[o],l=r[o];s.ttl=a,s.start=l,s.now=n||i();let d=s.now-l;s.remainingTTL=a-d;}};let n=0,i=()=>{let s=po.now();if(this.ttlResolution>0){n=s;let o=setTimeout(()=>n=0,this.ttlResolution);o.unref&&o.unref();}return s};this.getRemainingTTL=s=>{let o=this.#i.get(s);if(o===void 0)return 0;let a=e[o],l=r[o];if(a===0||l===0)return 1/0;let d=(n||i())-l;return a-d},this.#h=s=>e[s]!==0&&r[s]!==0&&(n||i())-r[s]>e[s];}#x=()=>{};#R=()=>{};#M=()=>{};#h=()=>!1;#L(){let e=new Ji(this.#u);this.#m=0,this.#S=e,this.#T=r=>{this.#m-=e[r],e[r]=0;},this.#F=(r,n,i,s)=>{if(this.#t(n))return 0;if(!An(i))if(s){if(typeof s!="function")throw new TypeError("sizeCalculation must be a function");if(i=s(n,r),!An(i))throw new TypeError("sizeCalculation return invalid (expect positive integer)")}else throw new TypeError("invalid size value (must be positive integer). When maxSize or maxEntrySize is used, sizeCalculation or size must be set.");return i},this.#D=(r,n,i)=>{if(e[r]=n,this.#f){let s=this.#f-e[r];for(;this.#m>s;)this.#O(!0);}this.#m+=e[r],i&&(i.entrySize=n,i.totalCalculatedSize=this.#m);};}#T=e=>{};#D=(e,r,n)=>{};#F=(e,r,n,i)=>{if(n||i)throw new TypeError("cannot set size without setting maxSize or maxEntrySize on cache");return 0};*#v({allowStale:e=this.allowStale}={}){if(this.#n)for(let r=this.#s;!(!this.#N(r)||((e||!this.#h(r))&&(yield r),r===this.#a));)r=this.#d[r];}*#w({allowStale:e=this.allowStale}={}){if(this.#n)for(let r=this.#a;!(!this.#N(r)||((e||!this.#h(r))&&(yield r),r===this.#s));)r=this.#c[r];}#N(e){return e!==void 0&&this.#i.get(this.#r[e])===e}*entries(){for(let e of this.#v())this.#e[e]!==void 0&&this.#r[e]!==void 0&&!this.#t(this.#e[e])&&(yield [this.#r[e],this.#e[e]]);}*rentries(){for(let e of this.#w())this.#e[e]!==void 0&&this.#r[e]!==void 0&&!this.#t(this.#e[e])&&(yield [this.#r[e],this.#e[e]]);}*keys(){for(let e of this.#v()){let r=this.#r[e];r!==void 0&&!this.#t(this.#e[e])&&(yield r);}}*rkeys(){for(let e of this.#w()){let r=this.#r[e];r!==void 0&&!this.#t(this.#e[e])&&(yield r);}}*values(){for(let e of this.#v())this.#e[e]!==void 0&&!this.#t(this.#e[e])&&(yield this.#e[e]);}*rvalues(){for(let e of this.#w())this.#e[e]!==void 0&&!this.#t(this.#e[e])&&(yield this.#e[e]);}[Symbol.iterator](){return this.entries()}find(e,r={}){for(let n of this.#v()){let i=this.#e[n],s=this.#t(i)?i.__staleWhileFetching:i;if(s!==void 0&&e(s,this.#r[n],this))return this.get(this.#r[n],r)}}forEach(e,r=this){for(let n of this.#v()){let i=this.#e[n],s=this.#t(i)?i.__staleWhileFetching:i;s!==void 0&&e.call(r,s,this.#r[n],this);}}rforEach(e,r=this){for(let n of this.#w()){let i=this.#e[n],s=this.#t(i)?i.__staleWhileFetching:i;s!==void 0&&e.call(r,s,this.#r[n],this);}}purgeStale(){let e=!1;for(let r of this.#w({allowStale:!0}))this.#h(r)&&(this.delete(this.#r[r]),e=!0);return e}dump(){let e=[];for(let r of this.#v({allowStale:!0})){let n=this.#r[r],i=this.#e[r],s=this.#t(i)?i.__staleWhileFetching:i;if(s===void 0||n===void 0)continue;let o={value:s};if(this.#_&&this.#E){o.ttl=this.#_[r];let a=po.now()-this.#E[r];o.start=Math.floor(Date.now()-a);}this.#S&&(o.size=this.#S[r]),e.unshift([n,o]);}return e}load(e){this.clear();for(let[r,n]of e){if(n.start){let i=Date.now()-n.start;n.start=po.now()-i;}this.set(r,n.value,n);}}set(e,r,n={}){if(r===void 0)return this.delete(e),this;let{ttl:i=this.ttl,start:s,noDisposeOnSet:o=this.noDisposeOnSet,sizeCalculation:a=this.sizeCalculation,status:l}=n,{noUpdateTTL:d=this.noUpdateTTL}=n,c=this.#F(e,r,n.size||0,a);if(this.maxEntrySize&&c>this.maxEntrySize)return l&&(l.set="miss",l.maxEntrySizeExceeded=!0),this.delete(e),this;let p=this.#n===0?void 0:this.#i.get(e);if(p===void 0)p=this.#n===0?this.#s:this.#g.length!==0?this.#g.pop():this.#n===this.#u?this.#O(!1):this.#n,this.#r[p]=e,this.#e[p]=r,this.#i.set(e,p),this.#c[this.#s]=p,this.#d[p]=this.#s,this.#s=p,this.#n++,this.#D(p,c,l),l&&(l.set="add"),d=!1;else {this.#A(p);let m=this.#e[p];if(r!==m){if(this.#C&&this.#t(m)?m.__abortController.abort(new Error("replaced")):o||(this.#b&&this.#y?.(m,e,"set"),this.#l&&this.#o?.push([m,e,"set"])),this.#T(p),this.#D(p,c,l),this.#e[p]=r,l){l.set="replace";let _=m&&this.#t(m)?m.__staleWhileFetching:m;_!==void 0&&(l.oldValue=_);}}else l&&(l.set="update");}if(i!==0&&!this.#_&&this.#k(),this.#_&&(d||this.#M(p,i,s),l&&this.#R(l,p)),!o&&this.#l&&this.#o){let m=this.#o,_;for(;_=m?.shift();)this.#p?.(..._);}return this}pop(){try{for(;this.#n;){let e=this.#e[this.#a];if(this.#O(!0),this.#t(e)){if(e.__staleWhileFetching)return e.__staleWhileFetching}else if(e!==void 0)return e}}finally{if(this.#l&&this.#o){let e=this.#o,r;for(;r=e?.shift();)this.#p?.(...r);}}}#O(e){let r=this.#a,n=this.#r[r],i=this.#e[r];return this.#C&&this.#t(i)?i.__abortController.abort(new Error("evicted")):(this.#b||this.#l)&&(this.#b&&this.#y?.(i,n,"evict"),this.#l&&this.#o?.push([i,n,"evict"])),this.#T(r),e&&(this.#r[r]=void 0,this.#e[r]=void 0,this.#g.push(r)),this.#n===1?(this.#a=this.#s=0,this.#g.length=0):this.#a=this.#c[r],this.#i.delete(n),this.#n--,r}has(e,r={}){let{updateAgeOnHas:n=this.updateAgeOnHas,status:i}=r,s=this.#i.get(e);if(s!==void 0){let o=this.#e[s];if(this.#t(o)&&o.__staleWhileFetching===void 0)return !1;if(this.#h(s))i&&(i.has="stale",this.#R(i,s));else return n&&this.#x(s),i&&(i.has="hit",this.#R(i,s)),!0}else i&&(i.has="miss");return !1}peek(e,r={}){let{allowStale:n=this.allowStale}=r,i=this.#i.get(e);if(i!==void 0&&(n||!this.#h(i))){let s=this.#e[i];return this.#t(s)?s.__staleWhileFetching:s}}#I(e,r,n,i){let s=r===void 0?void 0:this.#e[r];if(this.#t(s))return s;let o=new Ou,{signal:a}=n;a?.addEventListener("abort",()=>o.abort(a.reason),{signal:o.signal});let l={signal:o.signal,options:n,context:i},d=(E,P=!1)=>{let{aborted:D}=o.signal,g=n.ignoreFetchAbort&&E!==void 0;if(n.status&&(D&&!P?(n.status.fetchAborted=!0,n.status.fetchError=o.signal.reason,g&&(n.status.fetchAbortIgnored=!0)):n.status.fetchResolved=!0),D&&!g&&!P)return p(o.signal.reason);let S=_;return this.#e[r]===_&&(E===void 0?S.__staleWhileFetching?this.#e[r]=S.__staleWhileFetching:this.delete(e):(n.status&&(n.status.fetchUpdated=!0),this.set(e,E,l.options))),E},c=E=>(n.status&&(n.status.fetchRejected=!0,n.status.fetchError=E),p(E)),p=E=>{let{aborted:P}=o.signal,D=P&&n.allowStaleOnFetchAbort,g=D||n.allowStaleOnFetchRejection,S=g||n.noDeleteOnFetchRejection,I=_;if(this.#e[r]===_&&(!S||I.__staleWhileFetching===void 0?this.delete(e):D||(this.#e[r]=I.__staleWhileFetching)),g)return n.status&&I.__staleWhileFetching!==void 0&&(n.status.returnedStale=!0),I.__staleWhileFetching;if(I.__returned===I)throw E},m=(E,P)=>{let D=this.#P?.(e,s,l);D&&D instanceof Promise&&D.then(g=>E(g),P),o.signal.addEventListener("abort",()=>{(!n.ignoreFetchAbort||n.allowStaleOnFetchAbort)&&(E(),n.allowStaleOnFetchAbort&&(E=g=>d(g,!0)));});};n.status&&(n.status.fetchDispatched=!0);let _=new Promise(m).then(d,c),w=Object.assign(_,{__abortController:o,__staleWhileFetching:s,__returned:void 0});return r===void 0?(this.set(e,w,{...l.options,status:void 0}),r=this.#i.get(e)):this.#e[r]=w,w}#t(e){if(!this.#C)return !1;let r=e;return !!r&&r instanceof Promise&&r.hasOwnProperty("__staleWhileFetching")&&r.__abortController instanceof Ou}async fetch(e,r={}){let{allowStale:n=this.allowStale,updateAgeOnGet:i=this.updateAgeOnGet,noDeleteOnStaleGet:s=this.noDeleteOnStaleGet,ttl:o=this.ttl,noDisposeOnSet:a=this.noDisposeOnSet,size:l=0,sizeCalculation:d=this.sizeCalculation,noUpdateTTL:c=this.noUpdateTTL,noDeleteOnFetchRejection:p=this.noDeleteOnFetchRejection,allowStaleOnFetchRejection:m=this.allowStaleOnFetchRejection,ignoreFetchAbort:_=this.ignoreFetchAbort,allowStaleOnFetchAbort:w=this.allowStaleOnFetchAbort,context:E,forceRefresh:P=!1,status:D,signal:g}=r;if(!this.#C)return D&&(D.fetch="get"),this.get(e,{allowStale:n,updateAgeOnGet:i,noDeleteOnStaleGet:s,status:D});let S={allowStale:n,updateAgeOnGet:i,noDeleteOnStaleGet:s,ttl:o,noDisposeOnSet:a,size:l,sizeCalculation:d,noUpdateTTL:c,noDeleteOnFetchRejection:p,allowStaleOnFetchRejection:m,allowStaleOnFetchAbort:w,ignoreFetchAbort:_,status:D,signal:g},I=this.#i.get(e);if(I===void 0){D&&(D.fetch="miss");let L=this.#I(e,I,S,E);return L.__returned=L}else {let L=this.#e[I];if(this.#t(L)){let Q=n&&L.__staleWhileFetching!==void 0;return D&&(D.fetch="inflight",Q&&(D.returnedStale=!0)),Q?L.__staleWhileFetching:L.__returned=L}let Z=this.#h(I);if(!P&&!Z)return D&&(D.fetch="hit"),this.#A(I),i&&this.#x(I),D&&this.#R(D,I),L;let K=this.#I(e,I,S,E),B=K.__staleWhileFetching!==void 0&&n;return D&&(D.fetch=Z?"stale":"refresh",B&&Z&&(D.returnedStale=!0)),B?K.__staleWhileFetching:K.__returned=K}}get(e,r={}){let{allowStale:n=this.allowStale,updateAgeOnGet:i=this.updateAgeOnGet,noDeleteOnStaleGet:s=this.noDeleteOnStaleGet,status:o}=r,a=this.#i.get(e);if(a!==void 0){let l=this.#e[a],d=this.#t(l);return o&&this.#R(o,a),this.#h(a)?(o&&(o.get="stale"),d?(o&&n&&l.__staleWhileFetching!==void 0&&(o.returnedStale=!0),n?l.__staleWhileFetching:void 0):(s||this.delete(e),o&&n&&(o.returnedStale=!0),n?l:void 0)):(o&&(o.get="hit"),d?l.__staleWhileFetching:(this.#A(a),i&&this.#x(a),l))}else o&&(o.get="miss");}#q(e,r){this.#d[r]=e,this.#c[e]=r;}#A(e){e!==this.#s&&(e===this.#a?this.#a=this.#c[e]:this.#q(this.#d[e],this.#c[e]),this.#q(this.#s,e),this.#s=e);}delete(e){let r=!1;if(this.#n!==0){let n=this.#i.get(e);if(n!==void 0)if(r=!0,this.#n===1)this.clear();else {this.#T(n);let i=this.#e[n];this.#t(i)?i.__abortController.abort(new Error("deleted")):(this.#b||this.#l)&&(this.#b&&this.#y?.(i,e,"delete"),this.#l&&this.#o?.push([i,e,"delete"])),this.#i.delete(e),this.#r[n]=void 0,this.#e[n]=void 0,n===this.#s?this.#s=this.#d[n]:n===this.#a?this.#a=this.#c[n]:(this.#c[this.#d[n]]=this.#c[n],this.#d[this.#c[n]]=this.#d[n]),this.#n--,this.#g.push(n);}}if(this.#l&&this.#o?.length){let n=this.#o,i;for(;i=n?.shift();)this.#p?.(...i);}return r}clear(){for(let e of this.#w({allowStale:!0})){let r=this.#e[e];if(this.#t(r))r.__abortController.abort(new Error("deleted"));else {let n=this.#r[e];this.#b&&this.#y?.(r,n,"delete"),this.#l&&this.#o?.push([r,n,"delete"]);}}if(this.#i.clear(),this.#e.fill(void 0),this.#r.fill(void 0),this.#_&&this.#E&&(this.#_.fill(0),this.#E.fill(0)),this.#S&&this.#S.fill(0),this.#a=0,this.#s=0,this.#g.length=0,this.#m=0,this.#n=0,this.#l&&this.#o){let e=this.#o,r;for(;r=e?.shift();)this.#p?.(...r);}}};var $x=$t(Lx());function t3(t){return t.trimEnd().match(nw)}var ci=class{constructor(e){this.filepath=e.filepath,this.language=e.language,this.text=e.text,this.position=e.position,this.indentation=e.indentation,this.prefix=e.text.slice(0,e.position),this.suffix=e.text.slice(e.position),this.prefixLines=Je(this.prefix),this.suffixLines=Je(this.suffix),this.currentLinePrefix=this.prefixLines[this.prefixLines.length-1]??"",this.currentLineSuffix=this.suffixLines[0]??"",this.clipboard=e.clipboard?.trim()??"";let r=t3(this.suffixLines[0]??"");this.mode=r?"default":"fill-in-line",this.hash=(0, $x.default)({filepath:this.filepath,language:this.language,text:this.text,position:this.position,clipboard:this.clipboard});}};var ku=class{constructor(){this.logger=ht.child({component:"CompletionCache"});this.options={maxCount:1e4,prebuildCache:{enabled:!0,perCharacter:{lines:1,max:50},perLine:{max:10},autoClosingPairCheck:{max:3}}};this.cache=new Iu({max:this.options.maxCount});}has(e){return this.cache.has(e.hash)}buildCache(e,r){this.logger.debug({key:e,value:r},"Starting to build cache");let n=this.createCacheEntries(e,r);n.forEach(i=>{this.cache.set(i.key.hash,{value:i.value,rebuildFlag:i.rebuildFlag});}),this.logger.debug({newEntries:n.length,cacheSize:this.cache.size},"Cache updated");}get(e){let r=this.cache.get(e.hash);return r?.rebuildFlag&&this.buildCache(e,r?.value),r?.value}createCacheEntries(e,r){let n=[{key:e,value:r,rebuildFlag:!1}];if(this.options.prebuildCache.enabled)for(let s of r.choices){let o=s.text.slice(e.position-s.replaceRange.start),a=this.getPerLinePositions(o);this.logger.trace({completionText:o,perLinePositions:a},"Calculate per-line cache positions");for(let d of a){let c=o.slice(0,d),p=this.generateAutoClosedPrefixes(c);for(let m of [c,...p]){let _={key:new ci({...e,text:e.text.slice(0,e.position)+m+e.text.slice(e.position),position:e.position+d}),value:{...r,choices:[{index:s.index,text:o.slice(d),replaceRange:{start:e.position+d,end:e.position+d}}]},rebuildFlag:!0};this.logger.trace({prefix:m,entry:_},"Build per-line cache entry"),n.push(_);}}let l=this.getPerCharacterPositions(o);this.logger.trace({completionText:o,perCharacterPositions:l},"Calculate per-character cache positions");for(let d of l){let c=d;for(;c>0&&o[c-1]!==` -`;)c--;let p=o.slice(0,d),m=this.generateAutoClosedPrefixes(p);for(let _ of [p,...m]){let w={key:new ci({...e,text:e.text.slice(0,e.position)+_+e.text.slice(e.position),position:e.position+d}),value:{...r,choices:[{index:s.index,text:o.slice(c),replaceRange:{start:e.position+c,end:e.position+d}}]},rebuildFlag:!1};this.logger.trace({prefix:_,entry:w},"Build per-character cache entry"),n.push(w);}}}return n.reduce((s,o)=>{let a=s.find(l=>l.key.hash===o.key.hash);return a?(a.value.choices.push(...o.value.choices),a.rebuildFlag=a.rebuildFlag||o.rebuildFlag):s.push(o),s},[])}getPerLinePositions(e){let r=[],n=this.options.prebuildCache,i=Je(e),s=0,o=0;for(;s{let a;return "open"in o?a=o.open:a=o.openOrClose,a.chars===n[n.length-1-i]}).forEach(o=>{let a;"close"in o?a=o.close:a=o.openOrClose,s+=a.chars,r.push(e+s);}),i++;return r}};function ap(t,e,r){return Math.max(t,Math.min(e,r))}var Mu=class{constructor(){this.lastCalledTimeStamp=0;this.baseInterval=200;this.calledIntervalHistory=[];this.options={baseIntervalSlideWindowAvg:{minSize:20,maxSize:100,min:100,max:400},adaptiveRate:{min:1.5,max:3},contextScoreWeights:{triggerCharacter:.5,noSuffixInCurrentLine:.4,noSuffix:.1},requestDelay:{min:100,max:1e3}};}async debounce(e,r){let{request:n,config:i,responseTime:s}=e;if(n.manually)return this.sleep(0,r);if(i.mode==="fixed")return this.sleep(i.interval,r);let o=Date.now();this.updateBaseInterval(o-this.lastCalledTimeStamp),this.lastCalledTimeStamp=o;let a=this.calcContextScore(n),d=(this.options.adaptiveRate.max-(this.options.adaptiveRate.max-this.options.adaptiveRate.min)*a)*this.baseInterval,c=ap(this.options.requestDelay.min,this.options.requestDelay.max,d-s);return this.sleep(c,r)}async sleep(e,r){return new Promise((n,i)=>{let s=setTimeout(n,Math.min(e,2147483647));r?.signal&&(r.signal.aborted?(clearTimeout(s),i(r.signal.reason)):r.signal.addEventListener("abort",()=>{clearTimeout(s),i(r.signal.reason);}));})}updateBaseInterval(e){if(!(e>this.options.baseIntervalSlideWindowAvg.max)&&(this.calledIntervalHistory.push(e),this.calledIntervalHistory.length>this.options.baseIntervalSlideWindowAvg.maxSize&&this.calledIntervalHistory.shift(),this.calledIntervalHistory.length>this.options.baseIntervalSlideWindowAvg.minSize)){let r=this.calledIntervalHistory.reduce((n,i)=>n+i,0)/this.calledIntervalHistory.length;this.baseInterval=ap(this.options.baseIntervalSlideWindowAvg.min,this.options.baseIntervalSlideWindowAvg.max,r);}}calcContextScore(e){let r=0,n=this.options.contextScoreWeights,i=e.text[e.position-1]??"";r+=i.match(/^\W*$/)?n.triggerCharacter:0;let s=e.text.slice(e.position)??"",o=Je(s)[0]??"";return r+=o.match(/^\W*$/)?n.noSuffixInCurrentLine:0,r+=s.match(/^\W*$/)?n.noSuffix:0,r=ap(0,1,r),r}};var Ge=ht.child({component:"Postprocess"});Array.prototype.distinct||(Array.prototype.distinct=function(t){return [...new Map(this.map(e=>[t?.(e)??e,e])).values()]});Array.prototype.mapAsync||(Array.prototype.mapAsync=async function(t,e){return await Promise.all(this.map((r,n)=>t.call(e,r,n,this)))});function Zt(t,e){return up(async r=>{let n=e.position-r.replaceRange.start,i=r.text.slice(n),s=await t(i,e);return r.text=r.text.slice(0,n)+(s??""),r},e)}function up(t,e){return async r=>(r.choices=(await r.choices.mapAsync(async n=>await t(n,e))).filter(n=>!!n&&!!n.text).distinct(n=>n.text),r)}function r3(t){return /\n(\s*)\n/g}function jx(){return (t,e)=>{let r=t.split(r3()),n=0,i=2,s=r.length-2;for(;s>=1;){if(Ae(r[s])){s--;continue}let o=s-1;for(;o>=0&&Ae(r[o]);)o--;if(o<0)break;let a=r[s].trim(),l=r[o].trim(),d=Math.max(.1*a.length,.1*l.length);if(ki(a,l)<=d)n++,s--;else break}return n>=i?(Ge.debug({inputBlocks:r,repetitionCount:n},"Remove repetitive blocks."),r.slice(0,s+1).join("").trimEnd()):t}}function Hx(){return t=>{let e=Je(t),r=0,n=5,i=e.length-2;for(;i>=1;){if(Ae(e[i])){i--;continue}let s=i-1;for(;s>=0&&Ae(e[s]);)s--;if(s<0)break;let o=e[i].trim(),a=e[s].trim(),l=Math.max(.1*o.length,.1*a.length);if(ki(o,a)<=l)r++,i=s;else break}return r>=n?(Ge.debug({inputLines:e,repetitionCount:r},"Remove repetitive lines."),e.slice(0,i+1).join("").trimEnd()):t}}var n3=[/(.{3,}?)\1{5,}$/g,/(.{10,}?)\1{3,}$/g];function Wx(){return t=>{let e=Je(t),r=e.length-1;for(;r>=0&&Ae(e[r]);)r--;if(r<0)return t;for(let n of n3){let i=e[r].match(n);if(i)return Ge.debug({inputLines:e,lineNumber:r,match:i},"Remove line ends with repetition."),r<1?null:e.slice(0,r).join("").trimEnd()}return t}}function Bx(){return (t,e)=>{let{suffixLines:r,currentLinePrefix:n}=e,i=Je(t);if(i.length<2)return t;let s=i.map((d,c)=>c===0?n+d:d);if(!Pa(s,i.length-1))return t;let o=i[i.length-1],a=1;for(;a=r.length)return t;let l=r[a];return o.startsWith(l.trimEnd())||l.startsWith(o.trimEnd())?(Ge.debug({inputLines:i,suffixLines:r},"Removing duplicated block closing line"),i.slice(0,i.length-1).join("").trimEnd()):t}}function i3(t,e,r,n){let i={indentLevelLimit:0,allowClosingLine:!0},{prefixLines:s,suffixLines:o,currentLinePrefix:a}=r;if(t.length==0||s.length==0)return i;let l=Ae(a),d=s.length-1;for(;d>=0&&Ae(s[d]);)d--;if(d<0)return i;let c=s[d],p=Hr(c),m=t[0],_=Ae(m),w=0;for(;w=t.length)return i;let E=t[w],P;_?P=Hr(E):P=Hr(a+E),!_&&!l?n.experimentalKeepBlockScopeWhenCompletingLine?i.indentLevelLimit=p:(i.indentLevelLimit=p+1,i.allowClosingLine&&=Af(e,0)):P>p?i.indentLevelLimit=p+1:(i.indentLevelLimit=p);let D=1;for(;D{let{prefixLines:n,suffixLines:i,currentLinePrefix:s}=r,o=Je(e),a=o.map((c,p)=>p===0?s+c:c),l=i3(o,a,r,t),d=1;for(;d=0&&t[r]?.match(/\s/);)r--;if(r<0)return 0;let n=t.lastIndexOf(` -`,r);if(n<0)return 0;let s=t.slice(n+1,e).search(/\S/);return n+1+s}function u3(t,e){let r=e;for(;r=t.length)return t.length;let n=t.indexOf(` -`,r);return n<0?t.length:n}function c3(t,e){for(let r of e){let n=t;for(;n;){if(r.includes(n.type))return n;n=n.parent;}}return t}function Jx(){return async(t,e)=>{let{position:r,text:n,language:i,prefix:s,suffix:o}=e;if(!o3.includes(i))throw new Error(`Language ${i} is not supported`);let a=Xi[i],l=await Nu(a),d=s+t+o,c=l.parse(d),p=a3(d,r),m=u3(d,r),_=c3(c.rootNode.namedDescendantForIndex(p,m),Zx[a]??[]);if(_.type=="ERROR")throw new Error("Cannot determine syntax scope.");return _.endIndex{if(t.experimentalSyntax)try{return await Jx()(e,r)}catch(i){Ge.debug({error:i},"Failed to limit scope by syntax parser");}return Ux(t.indentation)(e,r)}}function cp(t){let e={" ":0," ":0," ":0};for(let r of t)if(r.match(/^\t/))e[" "]++;else {let n=r.match(/^ */)?.[0].length??0;n>0&&(n%4===0&&e[" "]++,n%2===0&&e[" "]++);}return e[" "]>0?" ":e[" "]>e[" "]?" ":e[" "]>0?" ":null}function l3(t,e){return e===" "?t.match(/^\t*/)?.[0].length??0:(t.match(/^ */)?.[0].length??0)/e.length}function Xx(){return (t,e)=>{let{prefixLines:r,suffixLines:n,currentLinePrefix:i,indentation:s}=e,o=Je(t);if(!s)return t;let a=Ae(i)?r.slice(0,r.length-1):r;if(r.length>1&&cp(a)!==null)return t;let l=n.slice(1);if(n.length>1&&cp(l)!==null)return t;let d=o.map((m,_)=>_===0?i+m:m),c=cp(d);if(c===null||c===s)return t;let p=d.map((m,_)=>{let w=l3(m,c);if(w===0)return o[_];let E=m.slice(c.length*w);return _===0?Ae(i)?s.repeat(w).slice(i.length)+E:o[0]:s.repeat(w)+E});return Ge.debug({prefixLines:r,suffixLines:n,inputLines:o,formatted:p},"Format indentation."),p.join("")}}function lp(){return (t,e)=>{let{currentLinePrefix:r,currentLineSuffix:n}=e,i=t;return !Ae(r)&&r.match(/\s$/)&&(i=i.trimStart()),(Ae(n)||!Ae(n)&&n.match(/^\s/))&&(i=i.trimEnd()),i}}function Qx(){return (t,e)=>{let r=Je(t);if(e.mode==="fill-in-line"&&r.length>1){let n=e.currentLineSuffix.trimEnd(),i=r[0].trimEnd();if(i.endsWith(n)){let s=i.slice(0,-n.length);if(s.length>0)return Ge.debug({inputLines:r,trimmedInputLine:s},"Trim content with multiple lines"),s}return Ge.debug({inputLines:r},"Drop content with multiple lines"),null}return t}}function fp(){return (t,e)=>{let{suffixLines:r}=e,n=Je(t),i=0;for(;iAe(t)?null:t}function eT(t,e){let{currentLineSuffix:r}=e,n=r.trimEnd();if(Ae(n))return t;let i=t.text.slice(e.position-t.replaceRange.start),s=Da(i).join("");return Ae(s)||(n.startsWith(s)?(t.replaceRange.end=e.position+s.length,Ge.trace({context:e,completion:t.text,range:t.replaceRange,unpaired:s},"Adjust replace range by bracket stack")):s.startsWith(n)&&(t.replaceRange.end=e.position+n.length,Ge.trace({context:e,completion:t.text,range:t.replaceRange,unpaired:s},"Adjust replace range by bracket stack"))),t}var f3=Object.keys(Xi);async function tT(t,e){let{position:r,prefix:n,suffix:i,prefixLines:s,currentLinePrefix:o,currentLineSuffix:a,language:l}=e,d=a.trimEnd();if(Ae(d))return t;if(!f3.includes(l))throw new Error(`Language ${l} is not supported`);let c=Xi[l],p=await Nu(c),m=t.text.slice(r-t.replaceRange.start),_=Je(m),w=0,E=p.parse(n+m+i),P=E.rootNode.namedDescendantForIndex(n.length+m.length,n.length+m.length+d.length-w);for(;P.hasError()&&w{if(t.experimentalSyntax)try{return await tT(e,r)}catch(i){Ge.debug({error:i},"Failed to calculate replace range by syntax parser");}return eT(e,r)}}async function nT(t,e,r){return Promise.resolve(r).then(Zt(Qx(),t)).then(Zt(Wx(),t)).then(Zt(fp(),t)).then(Zt(lp(),t)).then(Zt(dp(),t))}async function iT(t,e,r){return Promise.resolve(r).then(Zt(jx(),t)).then(Zt(Hx(),t)).then(Zt(Yx(e.limitScope),t)).then(Zt(Bx(),t)).then(Zt(Xx(),t)).then(Zt(fp(),t)).then(Zt(lp(),t)).then(Zt(dp(),t)).then(up(rT(e.calculateReplaceRange),t))}var go="tabby-agent",_o="1.3.3";var qu=class{constructor(){this.anonymousUsageTrackingApi=Jn({baseUrl:"https://app.tabbyml.com/api"});this.logger=ht.child({component:"AnonymousUsage"});this.systemData={agent:`${go}, ${_o}`,browser:void 0,node:`${process.version} ${process.platform} ${oT__default.default.arch()} ${oT__default.default.release()}`};this.sessionProperties={};this.userProperties={};this.userPropertiesUpdated=!1;this.emittedUniqueEvent=[];this.disabled=!1;}async init(e){if(this.dataStore=e?.dataStore||ri,this.dataStore){try{await this.dataStore.load();}catch(r){this.logger.debug({error:r},"Error when loading anonymousId");}if(typeof this.dataStore.data.anonymousId=="string")this.anonymousId=this.dataStore.data.anonymousId;else {this.anonymousId=Xr(),this.dataStore.data.anonymousId=this.anonymousId;try{await this.dataStore.save();}catch(r){this.logger.debug({error:r},"Error when saving anonymousId");}}}else this.anonymousId=Xr();}setSessionProperties(e,r){Zn(this.sessionProperties,e,r);}setUserProperties(e,r){Zn(this.userProperties,e,r),this.userPropertiesUpdated=!0;}async uniqueEvent(e,r={}){await this.event(e,r,!0);}async event(e,r={},n=!1){if(this.disabled||!this.anonymousId||n&&this.emittedUniqueEvent.includes(e))return;n&&this.emittedUniqueEvent.push(e);let i={...this.systemData,...this.sessionProperties,...r};this.userPropertiesUpdated&&(Zn(i,"$set",this.userProperties),this.userPropertiesUpdated=!1);try{await this.anonymousUsageTrackingApi.POST("/usage",{body:{distinctId:this.anonymousId,event:e,properties:i}});}catch(s){this.logger.error({error:s},"Error when sending anonymous usage data");}}};var gp=$t(lT()),Lu=class{constructor(){this.sum=0;this.quantity=0;}add(e){this.sum+=e,this.quantity+=1;}mean(){if(this.quantity!==0)return this.sum/this.quantity}count(){return this.quantity}},$u=class{constructor(e){this.values=[];this.maxSize=e;}add(e){this.values.push(e),this.values.length>this.maxSize&&this.values.shift();}getValues(){return this.values}},ju=class{constructor(){this.config={windowSize:10,checks:{disable:!1,healthy:{windowSize:1,latency:3e3},slowResponseTime:{latency:5e3,count:1},highTimeoutRate:{rate:.5,count:1}}};this.autoCompletionCount=0;this.manualCompletionCount=0;this.cacheHitCount=0;this.cacheMissCount=0;this.eventMap=new Map;this.completionRequestLatencyStats=new gp.Univariate;this.completionRequestCanceledStats=new Lu;this.completionRequestTimeoutCount=0;this.recentCompletionRequestLatencies=new $u(this.config.windowSize);}add(e){let{triggerMode:r,cacheHit:n,aborted:i,requestSent:s,requestLatency:o,requestCanceled:a,requestTimeout:l}=e;i||(r==="auto"?this.autoCompletionCount+=1:this.manualCompletionCount+=1,n?this.cacheHitCount+=1:this.cacheMissCount+=1),s&&(a?this.completionRequestCanceledStats.add(o):l?this.completionRequestTimeoutCount+=1:this.completionRequestLatencyStats.add(o),a||this.recentCompletionRequestLatencies.add(o));}addEvent(e){let r=this.eventMap.get(e)||0;this.eventMap.set(e,r+1);}reset(){this.autoCompletionCount=0,this.manualCompletionCount=0,this.cacheHitCount=0,this.cacheMissCount=0,this.eventMap=new Map,this.completionRequestLatencyStats=new gp.Univariate,this.completionRequestCanceledStats=new Lu,this.completionRequestTimeoutCount=0;}resetWindowed(){this.recentCompletionRequestLatencies=new $u(this.config.windowSize);}stats(){let e=Object.fromEntries(Array.from(this.eventMap.entries()).map(([r,n])=>["count_"+r,n]));return {completion:{count_auto:this.autoCompletionCount,count_manual:this.manualCompletionCount,cache_hit:this.cacheHitCount,cache_miss:this.cacheMissCount,...e},completion_request:{count:this.completionRequestLatencyStats.count(),latency_avg:this.completionRequestLatencyStats.mean(),latency_p50:this.completionRequestLatencyStats.percentile(50),latency_p95:this.completionRequestLatencyStats.percentile(95),latency_p99:this.completionRequestLatencyStats.percentile(99)},completion_request_canceled:{count:this.completionRequestCanceledStats.count(),latency_avg:this.completionRequestCanceledStats.mean()},completion_request_timeout:{count:this.completionRequestTimeoutCount}}}windowed(){let e=this.recentCompletionRequestLatencies.getValues(),r=e.filter(s=>Number.isNaN(s)),n=e.filter(s=>!Number.isNaN(s)),i=n.reduce((s,o)=>s+o,0)/n.length;return {values:e,stats:{total:e.length,timeouts:r.length,responses:n.length,averageResponseTime:i}}}check(e){if(this.config.checks.disable)return null;let r=this.config.checks,{values:n,stats:{total:i,timeouts:s,responses:o,averageResponseTime:a}}=e;return n.slice(-Math.min(this.config.windowSize,r.healthy.windowSize)).every(l=>lr.highTimeoutRate.rate&&s>=r.highTimeoutRate.count?"highTimeoutRate":a>r.slowResponseTime.latency&&o>=r.slowResponseTime.count?"slowResponseTime":null}};var Wu=class t extends events.EventEmitter{constructor(){super();this.logger=ht.child({component:"TabbyAgent"});this.anonymousUsageLogger=new qu;this.config=ep;this.userConfig={};this.clientConfig={};this.status="notInitialized";this.issues=[];this.completionCache=new ku;this.completionDebounce=new Mu;this.completionProviderStats=new ju;this.tryingConnectTimer=setInterval(async()=>{this.status==="disconnected"&&(this.logger.debug("Trying to connect..."),await this.healthCheck());},t.tryConnectInterval),this.submitStatsTimer=setInterval(async()=>{await this.submitStats();},t.submitStatsInterval);}static{this.tryConnectInterval=1e3*30;}static{this.submitStatsInterval=1e3*60*60*24;}async applyConfig(){let r=this.config,n=this.status;this.config=Zv(ep,this.userConfig,this.clientConfig),ho.forEach(s=>s.level=this.config.logs.level),this.anonymousUsageLogger.disabled=this.config.anonymousUsageTracking.disable,Ae(this.config.server.token)&&this.config.server.requestHeaders.Authorization===void 0?this.config.server.endpoint!==this.auth?.endpoint&&(this.auth=new Du(this.config.server.endpoint),await this.auth.init({dataStore:this.dataStore}),this.auth.on("updated",()=>{this.setupApi();})):this.auth=void 0,(0, Hu.default)(r.server,this.config.server)||(this.serverHealthState=void 0,this.completionProviderStats.resetWindowed(),this.popIssue("slowCompletionResponseTime"),this.popIssue("highCompletionTimeoutRate"),this.popIssue("connectionFailed"),this.connectionErrorMessage=void 0),await this.setupApi(),(0, Hu.default)(r.server,this.config.server)||n==="unauthorized"&&this.status==="unauthorized"&&this.emitAuthRequired();let i={event:"configUpdated",config:this.config};this.logger.debug({event:i},"Config updated"),super.emit("configUpdated",i);}async setupApi(){let r=Ae(this.config.server.token)?this.auth?.token?`Bearer ${this.auth.token}`:void 0:`Bearer ${this.config.server.token}`;this.api=Jn({baseUrl:this.config.server.endpoint.replace(/\/+$/,""),headers:{Authorization:r,...this.config.server.requestHeaders}}),await this.healthCheck();}changeStatus(r){if(this.status!=r){this.status=r;let n={event:"statusChanged",status:r};this.logger.debug({event:n},"Status changed"),super.emit("statusChanged",n),this.status==="unauthorized"&&this.emitAuthRequired();}}issueFromName(r){switch(r){case"highCompletionTimeoutRate":return {name:"highCompletionTimeoutRate",completionResponseStats:this.completionProviderStats.windowed().stats};case"slowCompletionResponseTime":return {name:"slowCompletionResponseTime",completionResponseStats:this.completionProviderStats.windowed().stats};case"connectionFailed":return {name:"connectionFailed",message:this.connectionErrorMessage}}}pushIssue(r){this.issues.includes(r)||(this.issues.push(r),this.logger.debug({issue:r},"Issues Pushed"),this.emitIssueUpdated());}popIssue(r){let n=this.issues.indexOf(r);n>=0&&(this.issues.splice(n,1),this.logger.debug({issue:r},"Issues Popped"),this.emitIssueUpdated());}emitAuthRequired(){let r={event:"authRequired",server:this.config.server};super.emit("authRequired",r);}emitIssueUpdated(){let r={event:"issuesUpdated",issues:this.issues};super.emit("issuesUpdated",r);}async submitStats(){let r=this.completionProviderStats.stats();r.completion_request.count>0&&(await this.anonymousUsageLogger.event("AgentStats",{stats:r}),this.completionProviderStats.reset(),this.logger.debug({stats:r},"Stats submitted"));}createAbortSignal(r){let n=Math.min(2147483647,r?.timeout||this.config.server.requestTimeout);return js([AbortSignal.timeout(n),r?.signal])}async healthCheck(r){let n=Xr(),i="/v1/health",s=this.config.server.endpoint+i,o={signal:this.createAbortSignal({signal:r?.signal})};try{if(!this.api)throw new Error("http client not initialized");this.logger.debug({requestId:n,requestOptions:o,url:s},"Health check request");let a;if(r?.method==="POST"?a=await this.api.POST(i,o):a=await this.api.GET(i,o),a.error||!a.response.ok)throw new jt(a.response);this.logger.debug({requestId:n,response:a},"Health check response"),this.changeStatus("ready"),this.popIssue("connectionFailed"),this.connectionErrorMessage=void 0;let l=a.data;typeof l=="object"&&l.model!==void 0&&l.device!==void 0&&(this.serverHealthState=l,this.anonymousUsageLogger.uniqueEvent("AgentConnected",l));}catch(a){if(this.serverHealthState=void 0,a instanceof jt&&a.status==405&&r?.method!=="POST")return await this.healthCheck({method:"POST"});if(a instanceof jt&&[401,403].includes(a.status))this.logger.debug({requestId:n,error:a},"Health check error: unauthorized"),this.changeStatus("unauthorized");else {if(Hs(a))this.logger.debug({requestId:n,error:a},"Health check error: timeout"),this.connectionErrorMessage=`GET ${s}: Timed out.`;else if(en(a))this.logger.debug({requestId:n,error:a},"Health check error: canceled"),this.connectionErrorMessage=`GET ${s}: Canceled.`;else {this.logger.error({requestId:n,error:a},"Health check error: unknown error");let l=a instanceof Error?Df(a):JSON.stringify(a);this.connectionErrorMessage=`GET ${s}: Request failed: -${l}`;}this.pushIssue("connectionFailed"),this.changeStatus("disconnected");}}}createSegments(r){let n=this.config.completion.prompt.maxPrefixLines,i=this.config.completion.prompt.maxSuffixLines,{prefixLines:s,suffixLines:o}=r,a=s.slice(Math.max(s.length-n,0)).join(""),l;this.config.completion.prompt.experimentalStripAutoClosingCharacters&&r.mode!=="fill-in-line"?l=` -`+o.slice(1,i).join(""):l=o.slice(0,i).join("");let d,c=this.config.completion.prompt.clipboard;return r.clipboard.length>=c.minChars&&r.clipboard.length<=c.maxChars&&(d=r.clipboard),{prefix:a,suffix:l,clipboard:d}}async initialize(r){if(this.dataStore=r?.dataStore,await this.anonymousUsageLogger.init({dataStore:this.dataStore}),r.clientProperties){let{user:n,session:i}=r.clientProperties;ho.forEach(s=>s.setBindings?.({...i})),i&&Object.entries(i).forEach(([s,o])=>{this.anonymousUsageLogger.setSessionProperties(s,o);}),n&&Object.entries(n).forEach(([s,o])=>{this.anonymousUsageLogger.setUserProperties(s,o);});}return Zi&&(await Zi.load(),this.userConfig=Zi.config,Zi.on("updated",async n=>{this.userConfig=n,await this.applyConfig();}),Zi.watch()),r.config&&(this.clientConfig=r.config),await this.applyConfig(),await this.anonymousUsageLogger.uniqueEvent("AgentInitialized"),this.logger.debug({options:r},"Initialized"),this.status!=="notInitialized"}async finalize(){return this.status==="finalized"?!1:(await this.submitStats(),this.tryingConnectTimer&&clearInterval(this.tryingConnectTimer),this.submitStatsTimer&&clearInterval(this.submitStatsTimer),this.changeStatus("finalized"),!0)}async updateClientProperties(r,n,i){switch(r){case"session":ho.forEach(s=>s.setBindings?.(Zn({},n,i))),this.anonymousUsageLogger.setSessionProperties(n,i);break;case"user":this.anonymousUsageLogger.setUserProperties(n,i);break}return !0}async updateConfig(r,n){let i=Ta(this.clientConfig,r);return (0, Hu.default)(i,n)||(n===void 0?Aa(this.clientConfig,r):Zn(this.clientConfig,r,n),await this.applyConfig()),!0}async clearConfig(r){return await this.updateConfig(r,void 0)}getConfig(){return this.config}getStatus(){return this.status}getIssues(){return this.issues}getIssueDetail(r){let n=this.getIssues();return r.index!==void 0&&r.index({index:E.index,text:E.text,replaceRange:{start:r.position,end:r.position}}))};}catch(p){throw en(p)?(this.logger.debug({requestId:c,error:p},"Completion request canceled"),o.requestCanceled=!0,o.requestLatency=performance.now()-a):Hs(p)?(this.logger.debug({requestId:c,error:p},"Completion request timeout"),o.requestTimeout=!0,o.requestLatency=NaN):(this.logger.error({requestId:c,error:p},"Completion request failed with unknown error"),this.healthCheck()),p}if(s=await nT(l,this.config.postprocess,s),i.aborted)throw i.reason;this.completionCache.buildCache(l,JSON.parse(JSON.stringify(s)));}}if(s=await iT(l,this.config.postprocess,s),i.aborted)throw i.reason}catch(d){throw en(d)||Hs(d)?o&&(o.aborted=!0):o=void 0,d}finally{if(o&&(this.completionProviderStats.add(o),o.requestSent&&!o.requestCanceled)){let d=this.completionProviderStats.windowed();switch(this.completionProviderStats.check(d)){case"healthy":this.popIssue("slowCompletionResponseTime"),this.popIssue("highCompletionTimeoutRate");break;case"highTimeoutRate":this.popIssue("slowCompletionResponseTime"),this.pushIssue("highCompletionTimeoutRate");break;case"slowResponseTime":this.popIssue("highCompletionTimeoutRate"),this.pushIssue("slowCompletionResponseTime");break}}}return this.logger.trace({context:l,completionResponse:s},"Return from provideCompletions"),s}async postEvent(r,n){if(this.status==="notInitialized")throw new Error("Agent is not initialized");this.completionProviderStats.addEvent(r.type);let i=Xr();try{if(!this.api)throw new Error("http client not initialized");let s="/v1/events",o={body:r,params:{query:{select_kind:r.select_kind}},signal:this.createAbortSignal(n),parseAs:"text"};this.logger.debug({requestId:i,requestOptions:o,url:this.config.server.endpoint+s},"Event request");let a=await this.api.POST(s,o);if(a.error||!a.response.ok)throw new jt(a.response);return this.logger.debug({requestId:i,response:a},"Event response"),!0}catch(s){return Hs(s)?this.logger.debug({requestId:i,error:s},"Event request timeout"):en(s)?this.logger.debug({requestId:i,error:s},"Event request canceled"):this.logger.error({requestId:i,error:s},"Event request failed with unknown error"),!1}}};var fT=["statusChanged","configUpdated","authRequired","issuesUpdated"];var Bu=class{constructor(){this.process=process;this.inStream=process.stdin;this.outStream=process.stdout;this.logger=ht.child({component:"JsonLineServer"});this.abortControllers={};}async handleLine(e){let r;try{r=JSON.parse(e);}catch(i){this.logger.error({error:i},`Failed to parse request: ${e}`);return}this.logger.debug({request:r},"Received request");let n=await this.handleRequest(r);this.sendResponse(n),this.logger.debug({response:n},"Sent response");}async handleRequest(e){let r=0,n=[0,null],i=new AbortController;try{if(!this.agent)throw new Error(`Agent not bound. -`);r=e[0],n[0]=r;let s=e[1].func;if(s==="cancelRequest")n[1]=this.cancelRequest(e);else {let o=this.agent[s];if(!o)throw new Error(`Unknown function: ${s}`);let a=e[1].args;a.length>0&&typeof a[a.length-1]=="object"&&a[a.length-1].signal&&(this.abortControllers[r]=i,a[a.length-1].signal=i.signal),n[1]=await o.apply(this.agent,a);}}catch(s){en(s)?this.logger.debug({error:s,request:e},"Request canceled"):this.logger.error({error:s,request:e},"Failed to handle request");}finally{this.abortControllers[r]&&delete this.abortControllers[r];}return n}cancelRequest(e){let r=e[1].args[0],n=this.abortControllers[r];return n?(n.abort(),!0):!1}sendResponse(e){this.outStream.write(JSON.stringify(e)+` -`);}bind(e){this.agent=e;for(let r of fT)this.agent.on(r,n=>{this.sendResponse([0,n]);});}listen(){m3__default.default.createInterface({input:this.inStream}).on("line",e=>{this.handleLine(e);}),["SIGTERM","SIGINT"].forEach(e=>{this.process.on(e,async()=>{this.agent&&this.agent.getStatus()!=="finalized"&&await this.agent.finalize(),this.process.exit(0);});});}};var Jr=$t(F1());var ol=class t{constructor(e,r,n,i){this._uri=e,this._languageId=r,this._version=n,this._content=i,this._lineOffsets=void 0;}get uri(){return this._uri}get languageId(){return this._languageId}get version(){return this._version}getText(e){if(e){let r=this.offsetAt(e.start),n=this.offsetAt(e.end);return this._content.substring(r,n)}return this._content}update(e,r){for(let n of e)if(t.isIncremental(n)){let i=q1(n.range),s=this.offsetAt(i.start),o=this.offsetAt(i.end);this._content=this._content.substring(0,s)+n.text+this._content.substring(o,this._content.length);let a=Math.max(i.start.line,0),l=Math.max(i.end.line,0),d=this._lineOffsets,c=N1(n.text,!1,s);if(l-a===c.length)for(let m=0,_=c.length;m<_;m++)d[m+a+1]=c[m];else c.length<1e4?d.splice(a+1,l-a,...c):this._lineOffsets=d=d.slice(0,a+1).concat(c,d.slice(l+1));let p=n.text.length-(o-s);if(p!==0)for(let m=a+1+c.length,_=d.length;m<_;m++)d[m]=d[m]+p;}else if(t.isFull(n))this._content=n.text,this._lineOffsets=void 0;else throw new Error("Unknown change event received");this._version=r;}getLineOffsets(){return this._lineOffsets===void 0&&(this._lineOffsets=N1(this._content,!0)),this._lineOffsets}positionAt(e){e=Math.max(Math.min(e,this._content.length),0);let r=this.getLineOffsets(),n=0,i=r.length;if(i===0)return {line:0,character:e};for(;ne?i=o:n=o+1;}let s=n-1;return {line:s,character:e-r[s]}}offsetAt(e){let r=this.getLineOffsets();if(e.line>=r.length)return this._content.length;if(e.line<0)return 0;let n=r[e.line],i=e.line+1{let m=c.range.start.line-p.range.start.line;return m===0?c.range.start.character-p.range.start.character:m}),l=0,d=[];for(let c of a){let p=i.offsetAt(c.range.start);if(pl&&d.push(o.substring(l,p)),c.newText.length&&d.push(c.newText),l=i.offsetAt(c.range.end);}return d.push(o.substr(l)),d.join("")}t.applyEdits=n;})(al||(al={}));function mg(t,e){if(t.length<=1)return t;let r=t.length/2|0,n=t.slice(0,r),i=t.slice(r);mg(n,e),mg(i,e);let s=0,o=0,a=0;for(;sr.line||e.line===r.line&&e.character>r.character?{start:r,end:e}:t}function Oz(t){let e=q1(t.range);return e!==t.range?{newText:t.newText,range:e}:t}var ul=class{constructor(){this.connection=(0, Jr.createConnection)();this.documents=new Jr.TextDocuments(al);this.logger=ht.child({component:"LspServer"});this.connection.onInitialize(async e=>await this.initialize(e)),this.connection.onShutdown(async()=>await this.shutdown()),this.connection.onExit(async()=>this.exit()),this.connection.onCompletion(async e=>await this.completion(e));}bind(e){this.agent=e,this.agent.on("statusChanged",r=>{(r.status==="disconnected"||r.status==="unauthorized")&&this.showMessage({type:Jr.MessageType.Warning,message:`Tabby agent status: ${r.status}`});});}listen(){this.documents.listen(this.connection),this.connection.listen();}async initialize(e){if(this.logger.debug({params:e},"LSP: initialize: request"),!this.agent)throw new Error(`Agent not bound. -`);let{clientInfo:r,capabilities:n}=e;return await this.agent.initialize({clientProperties:{session:{client:`${r?.name} ${r?.version??""}`,ide:{name:r?.name,version:r?.version},tabby_plugin:{name:`${go} (LSP)`,version:_o}}}}),{capabilities:{textDocumentSync:{openClose:!0,change:Jr.TextDocumentSyncKind.Incremental},completionProvider:{}},serverInfo:{name:go,version:_o}}}async shutdown(){if(this.logger.debug("LSP: shutdown: request"),!this.agent)throw new Error(`Agent not bound. -`);await this.agent.finalize();}exit(){return this.logger.debug("LSP: exit: request"),process.exit(0)}async showMessage(e){this.logger.debug({params:e},"LSP server notification: window/showMessage"),await this.connection.sendNotification("window/showMessage",e);}async completion(e){if(this.logger.debug({params:e},"LSP: textDocument/completion: request"),!this.agent)throw new Error(`Agent not bound. -`);try{let r=this.buildCompletionRequest(e),n=await this.agent.provideCompletions(r),i=this.toCompletionList(n,e);return this.logger.debug({completionList:i},"LSP: textDocument/completion: response"),i}catch(r){en(r)?this.logger.debug({error:r},"LSP: textDocument/completion: canceled"):this.logger.error({error:r},"LSP: textDocument/completion: error");}return {isIncomplete:!0,items:[]}}buildCompletionRequest(e,r=!1){let{textDocument:n,position:i}=e,s=this.documents.get(n.uri);return {filepath:s.uri,language:s.languageId,text:s.getText(),position:s.offsetAt(i),manually:r}}toCompletionList(e,r){let{textDocument:n,position:i}=r,s=this.documents.get(n.uri),o=s.getText({start:{line:i.line,character:0},end:i}),a=o.match(/(\w+)$/)?.[0]??"";return {isIncomplete:!0,items:e.choices.map(l=>{let d=l.text.slice(s.offsetAt(i)-l.replaceRange.start),c=Je(d),p=c[0]||"",m=c[1]||"";return {label:a+p,labelDetails:{detail:m,description:"Tabby"},kind:Jr.CompletionItemKind.Text,documentation:{kind:"markdown",value:`\`\`\` -${o+d} -\`\`\` - --- -Suggested by Tabby.`},textEdit:{newText:a+d,range:{start:{line:i.line,character:i.character-a.length},end:s.positionAt(l.replaceRange.end)}},data:{completionId:e.id,choiceIndex:l.index}}})}}};var Iz=process.argv.slice(2),cl;Iz.indexOf("--lsp")>=0?cl=new ul:cl=new Bu;var kz=new Wu;cl.bind(kz);cl.listen(); -/*! Bundled license information: - -normalize-path/index.js: - (*! - * normalize-path - * - * Copyright (c) 2014-2018, Jon Schlinkert. - * Released under the MIT License. - *) - -is-extglob/index.js: - (*! - * is-extglob - * - * Copyright (c) 2014-2016, Jon Schlinkert. - * Licensed under the MIT License. - *) - -is-glob/index.js: - (*! - * is-glob - * - * Copyright (c) 2014-2017, Jon Schlinkert. - * Released under the MIT License. - *) - -is-number/index.js: - (*! - * is-number - * - * Copyright (c) 2014-present, Jon Schlinkert. - * Released under the MIT License. - *) - -to-regex-range/index.js: - (*! - * to-regex-range - * - * Copyright (c) 2015-present, Jon Schlinkert. - * Released under the MIT License. - *) - -fill-range/index.js: - (*! - * fill-range - * - * Copyright (c) 2014-present, Jon Schlinkert. - * Licensed under the MIT License. - *) -*/ -//# sourceMappingURL=out.js.map -//# sourceMappingURL=cli.js.map \ No newline at end of file diff --git a/clients/intellij/node_scripts/wasm/LICENSES b/clients/intellij/node_scripts/wasm/LICENSES deleted file mode 100644 index 1aec59c78271..000000000000 --- a/clients/intellij/node_scripts/wasm/LICENSES +++ /dev/null @@ -1,153 +0,0 @@ -tree-sitter.wasm (https://github.com/tree-sitter/tree-sitter) - -The MIT License (MIT) - -Copyright (c) 2018-2023 Max Brunsfeld - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ---- - -tree-sitter-go.wasm (https://github.com/tree-sitter/tree-sitter-go) - -The MIT License (MIT) - -Copyright (c) 2014 Max Brunsfeld - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ---- - -tree-sitter-python.wasm (https://github.com/tree-sitter/tree-sitter-python) - -The MIT License (MIT) - -Copyright (c) 2016 Max Brunsfeld - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ---- - -tree-sitter-ruby.wasm (https://github.com/tree-sitter/tree-sitter-ruby) - -The MIT License (MIT) - -Copyright (c) 2016 Rob Rix - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ---- - -tree-sitter-rust.wasm (https://github.com/tree-sitter/tree-sitter-rust) - -The MIT License (MIT) - -Copyright (c) 2017 Maxim Sokolov - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ---- - -tree-sitter-tsx.wasm (https://github.com/tree-sitter/tree-sitter-typescript) - -The MIT License (MIT) - -Copyright (c) 2017 GitHub - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file diff --git a/clients/intellij/node_scripts/wasm/tree-sitter-go.wasm b/clients/intellij/node_scripts/wasm/tree-sitter-go.wasm deleted file mode 100644 index 719ebf62b746..000000000000 --- a/clients/intellij/node_scripts/wasm/tree-sitter-go.wasm +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1b69c5af834fd23053238e484c7fe9ed2f121d5b1fe32242af78576d67e49f1e -size 240169 diff --git a/clients/intellij/node_scripts/wasm/tree-sitter-python.wasm b/clients/intellij/node_scripts/wasm/tree-sitter-python.wasm deleted file mode 100644 index ae2cce790da4..000000000000 --- a/clients/intellij/node_scripts/wasm/tree-sitter-python.wasm +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:72d0f97ba6c3134d7873ec5c9d0fd3c1f5137f4eac4dda0709993d92809e62b6 -size 474189 diff --git a/clients/intellij/node_scripts/wasm/tree-sitter-ruby.wasm b/clients/intellij/node_scripts/wasm/tree-sitter-ruby.wasm deleted file mode 100644 index f0e02da9bd52..000000000000 --- a/clients/intellij/node_scripts/wasm/tree-sitter-ruby.wasm +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1190cddd839b78c2aec737573399a71c23fe9a546d3543f86304c4c68ca73852 -size 990787 diff --git a/clients/intellij/node_scripts/wasm/tree-sitter-rust.wasm b/clients/intellij/node_scripts/wasm/tree-sitter-rust.wasm deleted file mode 100644 index d608d6cda48b..000000000000 --- a/clients/intellij/node_scripts/wasm/tree-sitter-rust.wasm +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:273f9ce6f2c595ad4e63b3195513b61974ae1ec513efcce39da1afa90574ef38 -size 844087 diff --git a/clients/intellij/node_scripts/wasm/tree-sitter-tsx.wasm b/clients/intellij/node_scripts/wasm/tree-sitter-tsx.wasm deleted file mode 100644 index 76509d0553d4..000000000000 --- a/clients/intellij/node_scripts/wasm/tree-sitter-tsx.wasm +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:060422a330f9c819a10e7310788d336dbcb53cc6a4be0e91d40f644564080f97 -size 1182114 diff --git a/clients/intellij/node_scripts/wasm/tree-sitter.wasm b/clients/intellij/node_scripts/wasm/tree-sitter.wasm deleted file mode 100644 index 81dd1588401a..000000000000 --- a/clients/intellij/node_scripts/wasm/tree-sitter.wasm +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:17382e1a69bd628107e8dfe37d31d57f7ba948e5f2da77e56a8aa010488dc5ae -size 186526 diff --git a/clients/intellij/package.json b/clients/intellij/package.json index 72b97bc196a3..2261dbaaaa4e 100644 --- a/clients/intellij/package.json +++ b/clients/intellij/package.json @@ -1,15 +1,8 @@ { "name": "intellij-tabby", - "version": "1.3.2", - "description": "IntelliJ plugin for Tabby AI coding assistant.", - "repository": "https://github.com/TabbyML/tabby", - "scripts": { - "preupgrade-agent": "cd ../tabby-agent && yarn build", - "upgrade-agent": "rimraf ./node_scripts && cpy ../tabby-agent/dist/cli.js ./node_scripts/ --flat --rename=tabby-agent.js && cpy ../tabby-agent/dist/wasm/* ./node_scripts/wasm/ --flat" - }, + "private": true, "devDependencies": { - "cpy-cli": "^4.2.0", - "rimraf": "^5.0.1", - "tabby-agent": "1.3.3" + "tabby-agent": "workspace:*", + "tabby-chat-panel": "workspace:*" } } diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/Icons.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/Icons.kt new file mode 100644 index 000000000000..7157bd9c02dd --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/Icons.kt @@ -0,0 +1,18 @@ +package com.tabbyml.intellijtabby + +import com.intellij.openapi.util.IconLoader +import com.intellij.ui.NewUI +import javax.swing.Icon + +private fun loadIcon(name: String): Icon { + return if (NewUI.isEnabled()) { + IconLoader.getIcon("/icons/new-ui/$name", Icons::class.java) + } else { + IconLoader.getIcon("/icons/$name", Icons::class.java) + } +} + +object Icons { + @JvmField + val Chat = loadIcon("chat.svg") +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/ProjectExt.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/ProjectExt.kt new file mode 100644 index 000000000000..b73e7133c6d1 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/ProjectExt.kt @@ -0,0 +1,75 @@ +package com.tabbyml.intellijtabby + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.runReadAction +import com.intellij.openapi.editor.Document +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.fileEditor.TextEditor +import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiManager +import com.intellij.util.messages.Topic + +fun Project.safeSyncPublisher(topic: Topic): L? { + return if (isDisposed) { + null + } else { + messageBus.let { + if (it.isDisposed) { + null + } else { + it.syncPublisher(topic) + } + } + } +} + + +fun Project.findVirtualFile(fileUri: String): VirtualFile? { + val virtualFileManager = VirtualFileManager.getInstance() + return virtualFileManager.findFileByUrl(fileUri) +} + +fun Project.findDocument(fileUri: String): Document? { + return findVirtualFile(fileUri)?.let { findDocument(it) } +} + +fun Project.findDocument(virtualFile: VirtualFile): Document? { + val fileDocumentManager = FileDocumentManager.getInstance() + return runReadAction { fileDocumentManager.getDocument(virtualFile) } +} + +fun Project.findPsiFile(fileUri: String): PsiFile? { + return findVirtualFile(fileUri)?.let { findPsiFile(it) } +} + +fun Project.findPsiFile(virtualFile: VirtualFile): PsiFile? { + val psiManager = PsiManager.getInstance(this) + return runReadAction { psiManager.findFile(virtualFile) } +} + +fun Project.findEditor(fileUri: String): TextEditor? { + return findVirtualFile(fileUri)?.let { findEditor(it) } +} + +fun Project.findEditor(virtualFile: VirtualFile): TextEditor? { + val fileEditorManager = FileEditorManagerEx.getInstanceEx(this) + + return runInEdtAndWait { + fileEditorManager.getEditors(virtualFile) + }.firstOrNull { editor -> editor is TextEditor } as? TextEditor? +} + +private fun runInEdtAndWait(runnable: () -> T): T { + val app = ApplicationManager.getApplication() + if (app.isDispatchThread) { + return runnable() + } else { + var resultRef: T? = null + app.invokeAndWait { resultRef = runnable() } + @Suppress("UNCHECKED_CAST") return resultRef as T + } +} diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/editor/EditorActionPromoter.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actionPromoter/EditorActionPromoter.kt similarity index 79% rename from clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/editor/EditorActionPromoter.kt rename to clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actionPromoter/EditorActionPromoter.kt index e925110a25d5..20879844a481 100644 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/editor/EditorActionPromoter.kt +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actionPromoter/EditorActionPromoter.kt @@ -1,12 +1,11 @@ -package com.tabbyml.intellijtabby.editor +package com.tabbyml.intellijtabby.actionPromoter import com.intellij.openapi.actionSystem.ActionPromoter import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.diagnostic.Logger -import com.tabbyml.intellijtabby.actions.HasPriority -class EditorActionPromoter: ActionPromoter { +class EditorActionPromoter : ActionPromoter { private val logger = Logger.getInstance(EditorActionPromoter::class.java) override fun promote(actions: List, context: DataContext): List { logger.debug("Promote actions: $actions") diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actionPromoter/HasPriority.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actionPromoter/HasPriority.kt new file mode 100644 index 000000000000..a2b6d8f59219 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actionPromoter/HasPriority.kt @@ -0,0 +1,5 @@ +package com.tabbyml.intellijtabby.actionPromoter + +interface HasPriority { + val priority: Int +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/AcceptCompletion.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/AcceptCompletion.kt deleted file mode 100644 index 5c071b7e20c2..000000000000 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/AcceptCompletion.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.tabbyml.intellijtabby.actions - -import com.intellij.openapi.actionSystem.DataContext -import com.intellij.openapi.components.service -import com.intellij.openapi.editor.Caret -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.editor.actionSystem.EditorAction -import com.intellij.openapi.editor.actionSystem.EditorActionHandler -import com.tabbyml.intellijtabby.editor.InlineCompletionService - -class AcceptCompletion : EditorAction(object : EditorActionHandler() { - val inlineCompletionService = service() - - override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { - inlineCompletionService.accept(InlineCompletionService.AcceptType.FULL_COMPLETION) - } - - override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean { - return editor == inlineCompletionService.shownInlineCompletion?.editor - && caret.offset == inlineCompletionService.shownInlineCompletion?.offset - } -}), HasPriority { - override val priority: Int = 0 -} diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/AcceptCompletionNextLine.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/AcceptCompletionNextLine.kt deleted file mode 100644 index 195d0e2b850e..000000000000 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/AcceptCompletionNextLine.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.tabbyml.intellijtabby.actions - -import com.intellij.openapi.actionSystem.DataContext -import com.intellij.openapi.components.service -import com.intellij.openapi.editor.Caret -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.editor.actionSystem.EditorAction -import com.intellij.openapi.editor.actionSystem.EditorActionHandler -import com.tabbyml.intellijtabby.editor.InlineCompletionService - -class AcceptCompletionNextLine : EditorAction(object : EditorActionHandler() { - val inlineCompletionService = service() - - override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { - inlineCompletionService.accept(InlineCompletionService.AcceptType.NEXT_LINE) - } - - override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean { - return editor == inlineCompletionService.shownInlineCompletion?.editor - && caret.offset == inlineCompletionService.shownInlineCompletion?.offset - } -}), HasPriority { - override val priority: Int = 1 -} diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/AcceptCompletionNextWord.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/AcceptCompletionNextWord.kt deleted file mode 100644 index 9e76f932c5d6..000000000000 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/AcceptCompletionNextWord.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.tabbyml.intellijtabby.actions - -import com.intellij.openapi.actionSystem.DataContext -import com.intellij.openapi.components.service -import com.intellij.openapi.editor.Caret -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.editor.actionSystem.EditorAction -import com.intellij.openapi.editor.actionSystem.EditorActionHandler -import com.tabbyml.intellijtabby.editor.InlineCompletionService - -class AcceptCompletionNextWord : EditorAction(object : EditorActionHandler() { - val inlineCompletionService = service() - - override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { - inlineCompletionService.accept(InlineCompletionService.AcceptType.NEXT_WORD) - } - - override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean { - return editor == inlineCompletionService.shownInlineCompletion?.editor - && caret.offset == inlineCompletionService.shownInlineCompletion?.offset - } -}), HasPriority { - override val priority: Int = 1 -} diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/CheckIssueDetail.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/CheckIssueDetail.kt index 9ef6caae1b9e..7054a76fa73a 100644 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/CheckIssueDetail.kt +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/CheckIssueDetail.kt @@ -1,177 +1,47 @@ package com.tabbyml.intellijtabby.actions import com.intellij.icons.AllIcons -import com.intellij.openapi.actionSystem.* -import com.intellij.openapi.application.invokeLater -import com.intellij.openapi.components.service -import com.intellij.openapi.diagnostic.Logger -import com.intellij.openapi.ui.Messages -import com.intellij.openapi.ui.popup.JBPopupFactory -import com.tabbyml.intellijtabby.agent.Agent -import com.tabbyml.intellijtabby.agent.AgentService -import com.tabbyml.intellijtabby.settings.ApplicationSettingsState +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.components.serviceOrNull +import com.tabbyml.intellijtabby.events.CombinedState +import com.tabbyml.intellijtabby.lsp.ConnectionService +import com.tabbyml.intellijtabby.lsp.protocol.StatusInfo +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import java.net.URL - +import org.eclipse.lsp4j.ExecuteCommandParams class CheckIssueDetail : AnAction() { - private val logger = Logger.getInstance(CheckIssueDetail::class.java) + private val scope = CoroutineScope(Dispatchers.IO) override fun actionPerformed(e: AnActionEvent) { - val settings = service() - val agentService = service() - agentService.issueNotification?.expire() - - agentService.scope.launch { - val detail = agentService.getCurrentIssueDetail() ?: return@launch - if (detail["name"] == "connectionFailed") { - invokeLater { - val messages = "" + (detail["message"] as String?)?.replace("\n", "
") + "" - val selected = Messages.showDialog( - messages, - "Cannot Connect to Tabby Server", - arrayOf("OK", "Online Help"), - 0, - Messages.getErrorIcon(), - ) - when (selected) { - 0 -> { - // OK - } - - 1 -> { - // Online Help - showOnlineHelp(e) - } - } - } - return@launch - } else { - val serverHealthState = agentService.getServerHealthState() - val agentConfig = agentService.getConfig() - logger.info("Show issue detail: $detail, $serverHealthState, $agentConfig") - val title = when (detail["name"]) { - "slowCompletionResponseTime" -> "Completion Requests Appear to Take Too Much Time" - "highCompletionTimeoutRate" -> "Most Completion Requests Timed Out" - else -> return@launch - } - val message = buildDetailMessage(detail, serverHealthState, agentConfig) - invokeLater { - val selected = Messages.showDialog( - message, - title, - arrayOf("OK", "Online Help", "Don't Show Again"), - 0, - Messages.getWarningIcon(), - ) - when (selected) { - 0 -> { - // OK - } - - 1 -> { - // Online Help - showOnlineHelp(e) - } - - 2 -> { - // Don't Show Again - settings.notificationsMuted += listOf("completionResponseTimeIssues") - } - } - } - } - } - } - - private fun showOnlineHelp(e: AnActionEvent) { - e.project?.let { - invokeLater { - val actionManager = ActionManager.getInstance() - val actionGroup = actionManager.getAction("Tabby.OpenOnlineHelp") as ActionGroup - val popup = JBPopupFactory.getInstance().createActionGroupPopup( - "Online Help", - actionGroup, - e.dataContext, - false, - null, - 10, + val project = e.getRequiredData(CommonDataKeys.PROJECT) + val combinedState = project.serviceOrNull() ?: return + val connectionService = project.serviceOrNull() ?: return + + scope.launch { + val server = connectionService.getServerAsync() ?: return@launch + val command = combinedState.state.agentStatus?.command ?: return@launch + + server.workspaceFeature.executeCommand( + ExecuteCommandParams( + command.command, + command.arguments, ) - popup.showCenteredInCurrentWindow(it) - } + ) } } - private fun buildDetailMessage( - detail: Map, - serverHealthState: Map?, - agentConfig: Agent.Config - ): String { - val stats = detail["completionResponseStats"] as Map<*, *>? - val statsMessages = when (detail["name"]) { - "slowCompletionResponseTime" -> if (stats != null && stats["responses"] is Number && stats["averageResponseTime"] is Number) { - val response = (stats["responses"] as Number).toInt() - val averageResponseTime = (stats["averageResponseTime"] as Number).toInt() - "The average response time of recent $response completion requests is $averageResponseTime ms." - } else { - "" - } - - "highCompletionTimeoutRate" -> if (stats != null && stats["total"] is Number && stats["timeouts"] is Number) { - val timeout = (stats["timeouts"] as Number).toInt() - val total = (stats["total"] as Number).toInt() - "$timeout of $total completion requests timed out." - } else { - "" - } - - else -> "" - } - - val device = serverHealthState?.get("device") as String? ?: "" - val model = serverHealthState?.get("model") as String? ?: "" - val helpMessageForRunningLargeModelOnCPU = if (device == "cpu" && model.endsWith("B")) { - """ - Your Tabby server is running model $model on CPU. - This model may be performing poorly due to its large parameter size, please consider trying smaller models or switch to GPU. - You can find a list of recommend models in the model registry. - """.trimIndent() - } else { - "" - } - var commonHelpMessage = "" - val host = URL(agentConfig.server?.endpoint).host - if (helpMessageForRunningLargeModelOnCPU.isEmpty()) { - commonHelpMessage += "
  • The running model $model may be performing poorly due to its large parameter size.
    " - commonHelpMessage += "Please consider trying smaller models. You can find a list of recommend models in the model registry.
  • " - } - if (!(host.startsWith("localhost") || host.startsWith("127.0.0.1"))) { - commonHelpMessage += "
  • A poor network connection. Please check your network and proxy settings.
  • " - commonHelpMessage += "
  • Server overload. Please contact your Tabby server administrator for assistance.
  • " - } - - var helpMessage: String - if (helpMessageForRunningLargeModelOnCPU.isNotEmpty()) { - helpMessage = "$helpMessageForRunningLargeModelOnCPU
    " - if (commonHelpMessage.isNotEmpty()) { - helpMessage += "
    Other possible causes of this issue:
      $commonHelpMessage
    " - } - } else { - // commonHelpMessage should not be empty here - helpMessage = "Possible causes of this issue:
      $commonHelpMessage
    " - } - return "$statsMessages

    $helpMessage" - } - override fun update(e: AnActionEvent) { - val settings = service() - val agentService = service() - val muted = mutableListOf() - if (settings.notificationsMuted.contains("completionResponseTimeIssues")) { - muted += listOf("slowCompletionResponseTime", "highCompletionTimeoutRate") - } - e.presentation.isVisible = agentService.currentIssue.value != null && agentService.currentIssue.value !in muted - e.presentation.icon = if (agentService.currentIssue.value == "connectionFailed") { + e.presentation.isEnabled = e.project != null && e.getData(CommonDataKeys.PROJECT) != null + val project = e.getData(CommonDataKeys.PROJECT) ?: return + val combinedState = project.serviceOrNull() ?: return + + e.presentation.isVisible = combinedState.state.agentStatus?.command != null + e.presentation.icon = if (combinedState.state.agentStatus?.status == StatusInfo.Status.DISCONNECTED) { AllIcons.General.Error } else { AllIcons.General.Warning diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/DismissCompletion.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/DismissCompletion.kt deleted file mode 100644 index 77233c1bfada..000000000000 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/DismissCompletion.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.tabbyml.intellijtabby.actions - -import com.intellij.openapi.actionSystem.DataContext -import com.intellij.openapi.components.service -import com.intellij.openapi.editor.Caret -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.editor.actionSystem.EditorAction -import com.intellij.openapi.editor.actionSystem.EditorActionHandler -import com.tabbyml.intellijtabby.editor.InlineCompletionService - -class DismissCompletion : EditorAction(object : EditorActionHandler() { - val inlineCompletionService = service() - - override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { - inlineCompletionService.dismiss() - } - - override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean { - return editor == inlineCompletionService.shownInlineCompletion?.editor - && caret.offset == inlineCompletionService.shownInlineCompletion?.offset - } -}), HasPriority { - override val priority: Int = 0 -} diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/GenerateCommitMessage.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/GenerateCommitMessage.kt new file mode 100644 index 000000000000..f996114cebc2 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/GenerateCommitMessage.kt @@ -0,0 +1,129 @@ +package com.tabbyml.intellijtabby.actions + +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.application.invokeLater +import com.intellij.openapi.components.serviceOrNull +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.Task +import com.intellij.openapi.project.guessProjectDir +import com.intellij.openapi.ui.Messages +import com.intellij.openapi.wm.WindowManager +import com.tabbyml.intellijtabby.events.FeaturesState +import com.tabbyml.intellijtabby.git.GitProvider +import com.tabbyml.intellijtabby.lsp.ConnectionService +import com.tabbyml.intellijtabby.lsp.protocol.GenerateCommitMessageParams +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.future.await +import kotlinx.coroutines.launch +import java.awt.BorderLayout +import java.awt.Toolkit +import java.awt.datatransfer.StringSelection +import javax.swing.JOptionPane +import javax.swing.JPanel +import javax.swing.JScrollPane +import javax.swing.JTextArea + +class GenerateCommitMessage : AnAction() { + private val scope = CoroutineScope(Dispatchers.IO) + + override fun actionPerformed(e: AnActionEvent) { + val project = e.getRequiredData(CommonDataKeys.PROJECT) + val projectDir = project.guessProjectDir()?.url ?: return + + val task = object : Task.Modal( + project, null, "Generate Commit Message", true + ) { + lateinit var job: Job + override fun run(indicator: ProgressIndicator) { + job = scope.launch { + indicator.isIndeterminate = true + indicator.text = "Generating commit message..." + + val parentComponent = WindowManager.getInstance().getFrame(project) + val server = project.serviceOrNull()?.getServerAsync() ?: return@launch + + val commitMessage = try { + val result = server.chatFeature.generateCommitMessage(GenerateCommitMessageParams(projectDir)).await() + if (result?.commitMessage.isNullOrBlank()) { + throw NoCommitMessageGeneratedException("No commit message generated.") + } else { + result.commitMessage + } + } catch (e: Exception) { + invokeLater { + Messages.showErrorDialog( + parentComponent, + if (e is NoCommitMessageGeneratedException) { + e.message + } else { + "Failed to generate commit message. ${e.message}" + }, + "Generate Commit Message" + ) + } + return@launch + } + + invokeLater { + val textArea = JTextArea(commitMessage, 10, 80) + textArea.lineWrap = true + val panel = JPanel(BorderLayout()) + panel.add(JScrollPane(textArea), BorderLayout.CENTER) + + val selection = JOptionPane.showOptionDialog( + parentComponent, + panel, + "Generate Commit Message", + JOptionPane.OK_CANCEL_OPTION, + JOptionPane.PLAIN_MESSAGE, + null, + arrayOf("Copy", "Cancel"), + "Copy" + ) + + if (selection == 0) { + val stringSelection = StringSelection(textArea.text) + val clipboard = Toolkit.getDefaultToolkit().systemClipboard + clipboard.setContents(stringSelection, null) + } + } + } + + while (job.isActive) { + indicator.checkCanceled() + Thread.sleep(100) + } + } + + override fun onCancel() { + job.cancel() + } + } + + ProgressManager.getInstance().run(task) + } + + class NoCommitMessageGeneratedException(message: String) : Exception(message) + + override fun update(e: AnActionEvent) { + val project = e.project ?: e.getData(CommonDataKeys.PROJECT) + val projectDir = project?.guessProjectDir()?.url + + val gitProvider = project?.serviceOrNull() + val isSupported = gitProvider?.isSupported() + e.presentation.isVisible = (isSupported ?: false) && (projectDir != null) + + val featuresState = project?.serviceOrNull() + e.presentation.isEnabled = featuresState?.features?.chat ?: false + } + + override fun getActionUpdateThread(): ActionUpdateThread { + return ActionUpdateThread.BGT + } +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/HasPriority.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/HasPriority.kt deleted file mode 100644 index 90f2ae375d54..000000000000 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/HasPriority.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.tabbyml.intellijtabby.actions - -interface HasPriority { - val priority: Int -} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/JoinTabbySlackCommunity.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/JoinTabbySlackCommunity.kt index 64fc4101326c..aed652b2f33b 100644 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/JoinTabbySlackCommunity.kt +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/JoinTabbySlackCommunity.kt @@ -4,7 +4,7 @@ import com.intellij.ide.BrowserUtil import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent -class JoinTabbySlackCommunity: AnAction() { +class JoinTabbySlackCommunity : AnAction() { override fun actionPerformed(e: AnActionEvent) { BrowserUtil.browse("https://links.tabbyml.com/join-slack-extensions") } diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/OpenAuthPage.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/OpenAuthPage.kt deleted file mode 100644 index 95360d913d6b..000000000000 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/OpenAuthPage.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.tabbyml.intellijtabby.actions - -import com.intellij.openapi.actionSystem.ActionUpdateThread -import com.intellij.openapi.actionSystem.AnAction -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.components.service -import com.intellij.openapi.diagnostic.Logger -import com.intellij.openapi.progress.ProgressIndicator -import com.intellij.openapi.progress.ProgressManager -import com.intellij.openapi.progress.Task -import com.tabbyml.intellijtabby.agent.Agent -import com.tabbyml.intellijtabby.agent.AgentService -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch - -@Deprecated("Tabby Cloud auth support will be removed.") -class OpenAuthPage : AnAction() { - private val logger = Logger.getInstance(OpenAuthPage::class.java) - - override fun actionPerformed(e: AnActionEvent) { - val agentService = service() - agentService.authNotification?.expire() - - val task = object : Task.Modal( - e.project, - "Tabby Server Authorization", - true - ) { - lateinit var job: Job - override fun run(indicator: ProgressIndicator) { - job = agentService.scope.launch { - agentService.requestAuth(indicator) - } - logger.info("Authorization task started.") - while (job.isActive) { - indicator.checkCanceled() - Thread.sleep(100) - } - } - - override fun onCancel() { - logger.info("Authorization task cancelled.") - job.cancel() - } - } - ProgressManager.getInstance().run(task) - } - - override fun update(e: AnActionEvent) { - val agentService = service() - e.presentation.isVisible = agentService.status.value == Agent.Status.UNAUTHORIZED - } - - override fun getActionUpdateThread(): ActionUpdateThread { - return ActionUpdateThread.BGT - } -} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/OpenModelRegistry.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/OpenModelRegistry.kt index 9c3d43346f8a..1c200f7a7550 100644 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/OpenModelRegistry.kt +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/OpenModelRegistry.kt @@ -4,7 +4,7 @@ import com.intellij.ide.BrowserUtil import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent -class OpenModelRegistry: AnAction() { +class OpenModelRegistry : AnAction() { override fun actionPerformed(e: AnActionEvent) { BrowserUtil.browse("https://tabby.tabbyml.com/docs/models/") } diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/OpenOnlineDocumentation.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/OpenOnlineDocumentation.kt index cb794d27f2ce..e20b98e95686 100644 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/OpenOnlineDocumentation.kt +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/OpenOnlineDocumentation.kt @@ -4,8 +4,8 @@ import com.intellij.ide.BrowserUtil import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent -class OpenOnlineDocumentation: AnAction() { +class OpenOnlineDocumentation : AnAction() { override fun actionPerformed(e: AnActionEvent) { - BrowserUtil.browse("https://tabby.tabbyml.com/") + BrowserUtil.browse("https://tabby.tabbyml.com/docs") } } \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/OpenSettings.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/OpenSettings.kt index 98e01faf69bd..471427024e2e 100644 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/OpenSettings.kt +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/OpenSettings.kt @@ -3,10 +3,10 @@ package com.tabbyml.intellijtabby.actions import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.options.ShowSettingsUtil -import com.tabbyml.intellijtabby.settings.ApplicationConfigurable +import com.tabbyml.intellijtabby.settings.Configurable -class OpenSettings: AnAction() { +class OpenSettings : AnAction() { override fun actionPerformed(e: AnActionEvent) { - ShowSettingsUtil.getInstance().showSettingsDialog(e.project, ApplicationConfigurable::class.java) + ShowSettingsUtil.getInstance().showSettingsDialog(e.project, Configurable::class.java) } } \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/ToggleInlineCompletionTriggerMode.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/ToggleInlineCompletionTriggerMode.kt index 8e29ec6e025c..d0102e0a7098 100644 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/ToggleInlineCompletionTriggerMode.kt +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/ToggleInlineCompletionTriggerMode.kt @@ -4,24 +4,26 @@ import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.components.service -import com.tabbyml.intellijtabby.settings.ApplicationSettingsState +import com.tabbyml.intellijtabby.settings.SettingsService +import com.tabbyml.intellijtabby.settings.SettingsState class ToggleInlineCompletionTriggerMode : AnAction() { + val settings = service() + override fun actionPerformed(e: AnActionEvent) { - val settings = service() settings.completionTriggerMode = when (settings.completionTriggerMode) { - ApplicationSettingsState.TriggerMode.AUTOMATIC -> ApplicationSettingsState.TriggerMode.MANUAL - ApplicationSettingsState.TriggerMode.MANUAL -> ApplicationSettingsState.TriggerMode.AUTOMATIC + SettingsState.TriggerMode.AUTOMATIC -> SettingsState.TriggerMode.MANUAL + SettingsState.TriggerMode.MANUAL -> SettingsState.TriggerMode.AUTOMATIC } + e.project?.let { settings.notifyChanges(it) } } override fun update(e: AnActionEvent) { - val settings = service() - if (settings.completionTriggerMode == ApplicationSettingsState.TriggerMode.AUTOMATIC) { - e.presentation.text = "Switch to Manual Mode" - e.presentation.description = "Manual trigger inline completion suggestions on demand." + if (settings.completionTriggerMode == SettingsState.TriggerMode.AUTOMATIC) { + e.presentation.text = "Disable Auto Inline Completion" + e.presentation.description = "You can trigger inline completion manually." } else { - e.presentation.text = "Switch to Automatic Mode" + e.presentation.text = "Enable Auto Inline Completion" e.presentation.description = "Show inline completion suggestions automatically." } } diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/TriggerCompletion.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/TriggerCompletion.kt deleted file mode 100644 index a8bfb0407688..000000000000 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/TriggerCompletion.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.tabbyml.intellijtabby.actions - -import com.intellij.openapi.actionSystem.ActionUpdateThread -import com.intellij.openapi.actionSystem.AnAction -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.CommonDataKeys -import com.intellij.openapi.components.service -import com.tabbyml.intellijtabby.editor.CompletionProvider - - -class TriggerCompletion : AnAction() { - override fun actionPerformed(e: AnActionEvent) { - val completionScheduler = service() - val editor = e.getRequiredData(CommonDataKeys.EDITOR) - val offset = editor.caretModel.primaryCaret.offset - completionScheduler.provideCompletion(editor, offset, manually = true) - } - - override fun update(e: AnActionEvent) { - e.presentation.isEnabled = e.project != null - && e.getData(CommonDataKeys.EDITOR) != null - } - - override fun getActionUpdateThread(): ActionUpdateThread { - return ActionUpdateThread.BGT - } -} diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/AddFileToChat.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/AddFileToChat.kt new file mode 100644 index 000000000000..71fb42e56c0d --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/AddFileToChat.kt @@ -0,0 +1,10 @@ +package com.tabbyml.intellijtabby.actions.chat + +import com.intellij.openapi.editor.Editor +import com.tabbyml.intellijtabby.chat.ChatBrowser + +class AddFileToChat : ChatAction(object : ChatActionHandler { + override fun doExecute(editor: Editor, chatBrowser: ChatBrowser) { + chatBrowser.addActiveEditorAsContext(ChatBrowser.RangeStrategy.FILE) + } +}) \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/AddSelectionToChat.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/AddSelectionToChat.kt new file mode 100644 index 000000000000..d0d6df5afb3d --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/AddSelectionToChat.kt @@ -0,0 +1,14 @@ +package com.tabbyml.intellijtabby.actions.chat + +import com.intellij.openapi.editor.Editor +import com.tabbyml.intellijtabby.chat.ChatBrowser + +class AddSelectionToChat : ChatAction(object : ChatActionHandler { + override fun doExecute(editor: Editor, chatBrowser: ChatBrowser) { + chatBrowser.addActiveEditorAsContext(ChatBrowser.RangeStrategy.SELECTION) + } + + override fun isEnabled(editor: Editor, chatBrowser: ChatBrowser?): Boolean { + return editor.selectionModel.let { it.hasSelection() && !it.selectedText.isNullOrBlank() } + } +}) \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/ChatAction.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/ChatAction.kt new file mode 100644 index 000000000000..26d9e7af56f0 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/ChatAction.kt @@ -0,0 +1,34 @@ +package com.tabbyml.intellijtabby.actions.chat + +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.components.serviceOrNull +import com.intellij.openapi.editor.Caret +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.actionSystem.EditorAction +import com.intellij.openapi.editor.actionSystem.EditorActionHandler +import com.intellij.openapi.project.Project +import com.tabbyml.intellijtabby.chat.ChatBrowserFactory +import com.tabbyml.intellijtabby.events.FeaturesState +import com.tabbyml.intellijtabby.widgets.openChatToolWindow + +abstract class ChatAction(private val chatActionHandler: ChatActionHandler) : + EditorAction(object : EditorActionHandler() { + override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { + val project = editor.project ?: return + openChatToolWindow(project) { + val chatBrowser = ChatBrowserFactory.findActiveChatBrowser(project) ?: return@openChatToolWindow + chatActionHandler.doExecute(editor, chatBrowser) + } + } + + override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean { + val chatBrowser = editor.project?.let { ChatBrowserFactory.findActiveChatBrowser(it) } + return isChatFeatureEnabled(editor.project) && chatActionHandler.isEnabled(editor, chatBrowser) + } + }) + +fun isChatFeatureEnabled(project: Project?): Boolean { + if (project == null) return false + val featuresState = project.serviceOrNull() + return featuresState?.features?.chat ?: false +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/ChatActionHandler.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/ChatActionHandler.kt new file mode 100644 index 000000000000..a1734cc1c233 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/ChatActionHandler.kt @@ -0,0 +1,11 @@ +package com.tabbyml.intellijtabby.actions.chat + +import com.intellij.openapi.editor.Editor +import com.tabbyml.intellijtabby.chat.ChatBrowser + +interface ChatActionHandler { + fun doExecute(editor: Editor, chatBrowser: ChatBrowser) + fun isEnabled(editor: Editor, chatBrowser: ChatBrowser?): Boolean { + return true + } +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/ChatNavigationAction.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/ChatNavigationAction.kt new file mode 100644 index 000000000000..3d527ae5e5ad --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/ChatNavigationAction.kt @@ -0,0 +1,22 @@ +package com.tabbyml.intellijtabby.actions.chat + +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.tabbyml.intellijtabby.chat.ChatBrowserFactory + +abstract class ChatNavigationAction(private val view: String) : AnAction() { + override fun actionPerformed(e: AnActionEvent) { + val chatBrowser = e.project?.let { ChatBrowserFactory.findActiveChatBrowser(it) } + chatBrowser?.navigate(view) + } + + override fun update(e: AnActionEvent) { + val chatBrowser = e.project?.let { ChatBrowserFactory.findActiveChatBrowser(it) } + e.presentation.isEnabled = chatBrowser?.isChatPanelLoaded == true + } + + override fun getActionUpdateThread(): ActionUpdateThread { + return ActionUpdateThread.BGT + } +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/CodeReview.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/CodeReview.kt new file mode 100644 index 000000000000..b03e82f430ad --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/CodeReview.kt @@ -0,0 +1,14 @@ +package com.tabbyml.intellijtabby.actions.chat + +import com.intellij.openapi.editor.Editor +import com.tabbyml.intellijtabby.chat.ChatBrowser + +class CodeReview : ChatAction(object : ChatActionHandler { + override fun doExecute(editor: Editor, chatBrowser: ChatBrowser) { + chatBrowser.codeReviewSelectedText() + } + + override fun isEnabled(editor: Editor, chatBrowser: ChatBrowser?): Boolean { + return editor.selectionModel.let { it.hasSelection() && !it.selectedText.isNullOrBlank() } + } +}) \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/Explain.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/Explain.kt new file mode 100644 index 000000000000..041e90b78903 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/Explain.kt @@ -0,0 +1,14 @@ +package com.tabbyml.intellijtabby.actions.chat + +import com.intellij.openapi.editor.Editor +import com.tabbyml.intellijtabby.chat.ChatBrowser + +class Explain : ChatAction(object : ChatActionHandler { + override fun doExecute(editor: Editor, chatBrowser: ChatBrowser) { + chatBrowser.explainSelectedText() + } + + override fun isEnabled(editor: Editor, chatBrowser: ChatBrowser?): Boolean { + return editor.selectionModel.let { it.hasSelection() && !it.selectedText.isNullOrBlank() } + } +}) \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/Fix.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/Fix.kt new file mode 100644 index 000000000000..1ba7bbbc705d --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/Fix.kt @@ -0,0 +1,14 @@ +package com.tabbyml.intellijtabby.actions.chat + +import com.intellij.openapi.editor.Editor +import com.tabbyml.intellijtabby.chat.ChatBrowser + +class Fix : ChatAction(object : ChatActionHandler { + override fun doExecute(editor: Editor, chatBrowser: ChatBrowser) { + chatBrowser.fixSelectedText() + } + + override fun isEnabled(editor: Editor, chatBrowser: ChatBrowser?): Boolean { + return editor.selectionModel.let { it.hasSelection() && !it.selectedText.isNullOrBlank() } + } +}) \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/GenerateDocs.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/GenerateDocs.kt new file mode 100644 index 000000000000..678096048e39 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/GenerateDocs.kt @@ -0,0 +1,14 @@ +package com.tabbyml.intellijtabby.actions.chat + +import com.intellij.openapi.editor.Editor +import com.tabbyml.intellijtabby.chat.ChatBrowser + +class GenerateDocs : ChatAction(object : ChatActionHandler { + override fun doExecute(editor: Editor, chatBrowser: ChatBrowser) { + chatBrowser.generateDocsForSelectedText() + } + + override fun isEnabled(editor: Editor, chatBrowser: ChatBrowser?): Boolean { + return editor.selectionModel.let { it.hasSelection() && !it.selectedText.isNullOrBlank() } + } +}) \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/GenerateTests.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/GenerateTests.kt new file mode 100644 index 000000000000..fdba03b0ce43 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/GenerateTests.kt @@ -0,0 +1,14 @@ +package com.tabbyml.intellijtabby.actions.chat + +import com.intellij.openapi.editor.Editor +import com.tabbyml.intellijtabby.chat.ChatBrowser + +class GenerateTests : ChatAction(object : ChatActionHandler { + override fun doExecute(editor: Editor, chatBrowser: ChatBrowser) { + chatBrowser.generateTestsForSelectedText() + } + + override fun isEnabled(editor: Editor, chatBrowser: ChatBrowser?): Boolean { + return editor.selectionModel.let { it.hasSelection() && !it.selectedText.isNullOrBlank() } + } +}) \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/OpenChatToolWindow.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/OpenChatToolWindow.kt new file mode 100644 index 000000000000..bb7db8f1203b --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/OpenChatToolWindow.kt @@ -0,0 +1,11 @@ +package com.tabbyml.intellijtabby.actions.chat + +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.tabbyml.intellijtabby.widgets.openChatToolWindow + +class OpenChatToolWindow : AnAction() { + override fun actionPerformed(e: AnActionEvent) { + e.project?.let { openChatToolWindow(it) } + } +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/ShowChatHistory.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/ShowChatHistory.kt new file mode 100644 index 000000000000..58ee5ba056a1 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/ShowChatHistory.kt @@ -0,0 +1,5 @@ +package com.tabbyml.intellijtabby.actions.chat + +import com.tabbyml.intellijtabby.chat.ChatView + +class ShowChatHistory : ChatNavigationAction(ChatView.HISTORY) \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/ShowNewChat.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/ShowNewChat.kt new file mode 100644 index 000000000000..2ce797ba5503 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/ShowNewChat.kt @@ -0,0 +1,5 @@ +package com.tabbyml.intellijtabby.actions.chat + +import com.tabbyml.intellijtabby.chat.ChatView + +class ShowNewChat : ChatNavigationAction(ChatView.NEW_CHAT) \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/ToggleChatToolWindow.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/ToggleChatToolWindow.kt new file mode 100644 index 000000000000..4f3e44bbef7b --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/ToggleChatToolWindow.kt @@ -0,0 +1,39 @@ +package com.tabbyml.intellijtabby.actions.chat + +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.components.service +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.wm.IdeFocusManager +import com.intellij.openapi.wm.ToolWindowManager +import com.tabbyml.intellijtabby.actionPromoter.HasPriority +import com.tabbyml.intellijtabby.chat.ChatBrowser +import com.tabbyml.intellijtabby.chat.ChatBrowserFactory +import com.tabbyml.intellijtabby.widgets.ChatToolWindowFactory + +class ToggleChatToolWindow : AnAction(), HasPriority { + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + val toolWindowManager = ToolWindowManager.getInstance(project) + val toolWindow = toolWindowManager.getToolWindow(ChatToolWindowFactory.TOOL_WINDOW_ID) ?: return + + val editor = FileEditorManager.getInstance(project).selectedTextEditor + val chatBrowserFactory = project.service() + val chatBrowser = chatBrowserFactory.getChatBrowser(toolWindow) + if (toolWindow.isActive) { + if (editor != null) { + IdeFocusManager.getInstance(project).requestFocus(editor.contentComponent, true) + } + } else { + toolWindow.show { + toolWindow.activate { + if (editor != null && chatBrowser != null && editor.selectionModel.let { it.hasSelection() && !it.selectedText.isNullOrBlank() }) { + chatBrowser.addActiveEditorAsContext(ChatBrowser.RangeStrategy.SELECTION) + } + } + } + } + } + + override val priority: Int = 1 +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/Accept.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/Accept.kt new file mode 100644 index 000000000000..493a2c4dae8f --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/Accept.kt @@ -0,0 +1,11 @@ +package com.tabbyml.intellijtabby.actions.inlineCompletion + +import com.intellij.openapi.editor.Caret +import com.intellij.openapi.editor.Editor +import com.tabbyml.intellijtabby.completion.InlineCompletionService + +class Accept : InlineCompletionAction(object : InlineCompletionActionHandler { + override fun doExecute(editor: Editor, caret: Caret?, inlineCompletionService: InlineCompletionService) { + inlineCompletionService.accept(editor, caret?.offset, InlineCompletionService.AcceptType.FULL_COMPLETION) + } +}) diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/AcceptNextLine.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/AcceptNextLine.kt new file mode 100644 index 000000000000..720acce1d5c4 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/AcceptNextLine.kt @@ -0,0 +1,11 @@ +package com.tabbyml.intellijtabby.actions.inlineCompletion + +import com.intellij.openapi.editor.Caret +import com.intellij.openapi.editor.Editor +import com.tabbyml.intellijtabby.completion.InlineCompletionService + +class AcceptNextLine : InlineCompletionAction(object : InlineCompletionActionHandler { + override fun doExecute(editor: Editor, caret: Caret?, inlineCompletionService: InlineCompletionService) { + inlineCompletionService.accept(editor, caret?.offset, InlineCompletionService.AcceptType.NEXT_LINE) + } +}) diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/AcceptNextWord.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/AcceptNextWord.kt new file mode 100644 index 000000000000..cf3b8d7dac20 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/AcceptNextWord.kt @@ -0,0 +1,11 @@ +package com.tabbyml.intellijtabby.actions.inlineCompletion + +import com.intellij.openapi.editor.Caret +import com.intellij.openapi.editor.Editor +import com.tabbyml.intellijtabby.completion.InlineCompletionService + +class AcceptNextWord : InlineCompletionAction(object : InlineCompletionActionHandler { + override fun doExecute(editor: Editor, caret: Caret?, inlineCompletionService: InlineCompletionService) { + inlineCompletionService.accept(editor, caret?.offset, InlineCompletionService.AcceptType.NEXT_WORD) + } +}) diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/CycleNext.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/CycleNext.kt new file mode 100644 index 000000000000..d3133a144de5 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/CycleNext.kt @@ -0,0 +1,11 @@ +package com.tabbyml.intellijtabby.actions.inlineCompletion + +import com.intellij.openapi.editor.Caret +import com.intellij.openapi.editor.Editor +import com.tabbyml.intellijtabby.completion.InlineCompletionService + +class CycleNext : InlineCompletionAction(object : InlineCompletionActionHandler { + override fun doExecute(editor: Editor, caret: Caret?, inlineCompletionService: InlineCompletionService) { + inlineCompletionService.cycle(editor, caret?.offset, InlineCompletionService.CycleDirection.NEXT) + } +}) diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/CyclePrevious.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/CyclePrevious.kt new file mode 100644 index 000000000000..bc5f9ec61aa8 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/CyclePrevious.kt @@ -0,0 +1,11 @@ +package com.tabbyml.intellijtabby.actions.inlineCompletion + +import com.intellij.openapi.editor.Caret +import com.intellij.openapi.editor.Editor +import com.tabbyml.intellijtabby.completion.InlineCompletionService + +class CyclePrevious : InlineCompletionAction(object : InlineCompletionActionHandler { + override fun doExecute(editor: Editor, caret: Caret?, inlineCompletionService: InlineCompletionService) { + inlineCompletionService.cycle(editor, caret?.offset, InlineCompletionService.CycleDirection.PREVIOUS) + } +}) diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/Dismiss.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/Dismiss.kt new file mode 100644 index 000000000000..91dcc4914c7f --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/Dismiss.kt @@ -0,0 +1,14 @@ +package com.tabbyml.intellijtabby.actions.inlineCompletion + +import com.intellij.openapi.editor.Caret +import com.intellij.openapi.editor.Editor +import com.tabbyml.intellijtabby.completion.InlineCompletionService + +class Dismiss : InlineCompletionAction(object : InlineCompletionActionHandler { + override fun doExecute(editor: Editor, caret: Caret?, inlineCompletionService: InlineCompletionService) { + inlineCompletionService.dismiss() + } +}) { + override val priority = -1 +} + diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/InlineCompletionAction.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/InlineCompletionAction.kt new file mode 100644 index 000000000000..70a7d5e41b27 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/InlineCompletionAction.kt @@ -0,0 +1,28 @@ +package com.tabbyml.intellijtabby.actions.inlineCompletion + +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.components.serviceOrNull +import com.intellij.openapi.editor.Caret +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.actionSystem.EditorAction +import com.intellij.openapi.editor.actionSystem.EditorActionHandler +import com.tabbyml.intellijtabby.actionPromoter.HasPriority +import com.tabbyml.intellijtabby.completion.InlineCompletionService + +abstract class InlineCompletionAction(private val inlineCompletionHandler: InlineCompletionActionHandler) : + EditorAction(object : EditorActionHandler() { + override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { + val inlineCompletionService = editor.project?.serviceOrNull() ?: return + inlineCompletionHandler.doExecute(editor, caret, inlineCompletionService) + } + + override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean { + val inlineCompletionService = editor.project?.serviceOrNull() ?: return false + return inlineCompletionService.isInlineCompletionVisibleAt( + editor, + caret.offset + ) && inlineCompletionHandler.isEnabledForCaret(editor, caret, inlineCompletionService) + } + }), HasPriority { + override val priority: Int = 1 +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/InlineCompletionActionHandler.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/InlineCompletionActionHandler.kt new file mode 100644 index 000000000000..c5caa4489127 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/InlineCompletionActionHandler.kt @@ -0,0 +1,12 @@ +package com.tabbyml.intellijtabby.actions.inlineCompletion + +import com.intellij.openapi.editor.Caret +import com.intellij.openapi.editor.Editor +import com.tabbyml.intellijtabby.completion.InlineCompletionService + +interface InlineCompletionActionHandler { + fun doExecute(editor: Editor, caret: Caret?, inlineCompletionService: InlineCompletionService) + fun isEnabledForCaret(editor: Editor, caret: Caret, inlineCompletionService: InlineCompletionService): Boolean { + return true + } +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/TabAccept.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/TabAccept.kt new file mode 100644 index 000000000000..5dfa8bf74005 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/TabAccept.kt @@ -0,0 +1,19 @@ +package com.tabbyml.intellijtabby.actions.inlineCompletion + +import com.intellij.openapi.editor.Caret +import com.intellij.openapi.editor.Editor +import com.tabbyml.intellijtabby.completion.InlineCompletionService + +class TabAccept : InlineCompletionAction(object : InlineCompletionActionHandler { + override fun doExecute(editor: Editor, caret: Caret?, inlineCompletionService: InlineCompletionService) { + inlineCompletionService.accept(editor, caret?.offset, InlineCompletionService.AcceptType.FULL_COMPLETION) + } + + override fun isEnabledForCaret( + editor: Editor, + caret: Caret, + inlineCompletionService: InlineCompletionService + ): Boolean { + return !inlineCompletionService.isInlineCompletionStartWithIndentation() + } +}) diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/Trigger.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/Trigger.kt new file mode 100644 index 000000000000..12a8948202c2 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/Trigger.kt @@ -0,0 +1,30 @@ +package com.tabbyml.intellijtabby.actions.inlineCompletion + +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.components.serviceOrNull +import com.tabbyml.intellijtabby.actionPromoter.HasPriority +import com.tabbyml.intellijtabby.completion.InlineCompletionService + +class Trigger : AnAction(), HasPriority { + override fun actionPerformed(e: AnActionEvent) { + val inlineCompletionService = + e.getRequiredData(CommonDataKeys.PROJECT).serviceOrNull() ?: return + val editor = e.getRequiredData(CommonDataKeys.EDITOR) + val offset = editor.caretModel.primaryCaret.offset + inlineCompletionService.provideInlineCompletion(editor, offset, manually = true) + } + + override fun update(e: AnActionEvent) { + e.presentation.isEnabled = + e.project != null && e.getData(CommonDataKeys.PROJECT) != null && e.getData(CommonDataKeys.EDITOR) != null + } + + override fun getActionUpdateThread(): ActionUpdateThread { + return ActionUpdateThread.BGT + } + + override val priority: Int = 1 +} diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/agent/Agent.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/agent/Agent.kt deleted file mode 100644 index 0bfc1d50ce56..000000000000 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/agent/Agent.kt +++ /dev/null @@ -1,390 +0,0 @@ -package com.tabbyml.intellijtabby.agent - -import com.google.gson.Gson -import com.google.gson.annotations.SerializedName -import com.google.gson.reflect.TypeToken -import com.intellij.execution.configurations.GeneralCommandLine -import com.intellij.execution.configurations.PathEnvironmentVariableUtil -import com.intellij.execution.process.KillableProcessHandler -import com.intellij.execution.process.ProcessAdapter -import com.intellij.execution.process.ProcessEvent -import com.intellij.execution.process.ProcessOutputTypes -import com.intellij.ide.plugins.PluginManagerCore -import com.intellij.openapi.components.service -import com.intellij.openapi.diagnostic.Logger -import com.intellij.openapi.extensions.PluginId -import com.intellij.openapi.util.Key -import com.intellij.util.EnvironmentUtil -import com.intellij.util.io.BaseOutputReader -import com.tabbyml.intellijtabby.settings.ApplicationSettingsState -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.suspendCancellableCoroutine -import java.io.BufferedReader -import java.io.File -import java.io.InputStreamReader -import java.io.OutputStreamWriter - -class Agent : ProcessAdapter() { - private val logger = Logger.getInstance(Agent::class.java) - private val gson = Gson() - private lateinit var process: KillableProcessHandler - private lateinit var streamWriter: OutputStreamWriter - - enum class Status { - NOT_INITIALIZED, - READY, - DISCONNECTED, - UNAUTHORIZED, - } - - private val statusFlow = MutableStateFlow(Status.NOT_INITIALIZED) - val status = statusFlow.asStateFlow() - private val authRequiredEventFlow = MutableSharedFlow(extraBufferCapacity = 1) - val authRequiredEvent = authRequiredEventFlow.asSharedFlow() - private val currentIssueFlow = MutableStateFlow(null) - val currentIssue = currentIssueFlow.asStateFlow() - - open class AgentException(message: String) : Exception(message) - - open class NodeBinaryException(message: String) : AgentException( - message = "$message Please install Node.js version >= 18.0, set the binary path in Tabby plugin settings or add bin path to system environment variable PATH, then restart IDE." - ) - - open class NodeBinaryNotFoundException : NodeBinaryException( - message = "Cannot find Node binary." - ) - - open class NodeBinaryInvalidVersionException(version: String) : NodeBinaryException( - message = "Node version is too old: $version." - ) - - fun open() { - val node = getNodeBinary() - val script = getNodeScript() - val options = "--dns-result-order=ipv4first" - val cmd = GeneralCommandLine(node.absolutePath, options, script.absolutePath).withCharset(Charsets.UTF_8) - process = object : KillableProcessHandler(cmd) { - override fun readerOptions(): BaseOutputReader.Options { - return BaseOutputReader.Options.forMostlySilentProcess() - } - } - process.startNotify() - process.addProcessListener(this) - streamWriter = process.processInput.writer() - } - - private fun getNodeBinary(): File { - val settings = service() - val node = if (settings.nodeBinary.isNotBlank()) { - val path = settings.nodeBinary.replaceFirst(Regex("^~"), System.getProperty("user.home")) - File(path) - } else { - logger.info("Environment variables: PATH: ${EnvironmentUtil.getValue("PATH")}") - PathEnvironmentVariableUtil.findExecutableInPathOnAnyOS("node") - } - - if (node?.exists() == true) { - logger.info("Node binary path: ${node.absolutePath}") - checkNodeVersion(node) - return node - } else { - throw NodeBinaryNotFoundException() - } - } - - private fun checkNodeVersion(node: File) { - try { - val process = GeneralCommandLine(node.absolutePath, "--version").createProcess() - val version = BufferedReader(InputStreamReader(process.inputStream)).readLine() - val regResult = Regex("v([0-9]+)\\.([0-9]+)\\.([0-9]+)").find(version) - if (regResult != null && regResult.groupValues[1].toInt() >= 18) { - return - } else { - throw NodeBinaryInvalidVersionException(version) - } - } catch (e: Exception) { - if (e is AgentException) { - throw e - } else { - throw AgentException("Failed to check node version: $e.") - } - } - } - - private fun getNodeScript(): File { - val script = - PluginManagerCore.getPlugin(PluginId.getId("com.tabbyml.intellij-tabby"))?.pluginPath?.resolve("node_scripts/tabby-agent.js") - ?.toFile() - if (script?.exists() == true) { - logger.info("Node script path: ${script.absolutePath}") - return script - } else { - throw AgentException("Node script not found. Please reinstall Tabby plugin.") - } - } - - data class Config( - val server: Server? = null, - val logs: Logs? = null, - val anonymousUsageTracking: AnonymousUsageTracking? = null, - ) { - data class Server( - val endpoint: String? = null, - val token: String? = null, - val requestHeaders: Map? = null, - ) - - data class Logs( - val level: String? = null, - ) - - data class AnonymousUsageTracking( - val disabled: Boolean? = null, - ) - } - - data class ClientProperties( - val user: Map, - val session: Map, - ) - - suspend fun initialize(config: Config, clientProperties: ClientProperties): Boolean { - return request( - "initialize", listOf( - mapOf( - "config" to config, - "clientProperties" to clientProperties, - ) - ) - ) - } - - suspend fun finalize(): Boolean { - return request("finalize", listOf()) - } - - suspend fun updateClientProperties(type: String, key: String, value: Any): Boolean { - return request("updateClientProperties", listOf(type, key, value)) - } - - suspend fun updateConfig(key: String, config: Any): Boolean { - return request("updateConfig", listOf(key, config)) - } - - suspend fun clearConfig(key: String): Boolean { - return request("clearConfig", listOf(key)) - } - - suspend fun getConfig(): Config { - return request("getConfig", listOf()) - } - - suspend fun getStatus(): Status { - return request("getStatus", listOf()) - } - - suspend fun getIssues(): List { - return request("getIssues", listOf()) - } - - data class GetIssueDetailOptions( - val index: Int? = null, - val name: String? = null, - ) - - suspend fun getIssueDetail(options: GetIssueDetailOptions): Map? { - return request("getIssueDetail", listOf(options)) - } - - suspend fun getServerHealthState(): Map? { - return request("getServerHealthState", listOf()) - } - - data class AuthUrlResponse( - val authUrl: String, - val code: String, - ) - - @Deprecated("Tabby Cloud auth support will be removed.") - suspend fun requestAuthUrl(): AuthUrlResponse? { - return request("requestAuthUrl", listOf(ABORT_SIGNAL_ENABLED)) - } - - @Deprecated("Tabby Cloud auth support will be removed.") - suspend fun waitForAuthToken(code: String) { - return request("waitForAuthToken", listOf(code, ABORT_SIGNAL_ENABLED)) - } - - data class CompletionRequest( - val filepath: String, - val language: String, - val text: String, - val position: Int, - val manually: Boolean?, - ) - - data class CompletionResponse( - val id: String, - val choices: List, - ) { - data class Choice( - val index: Int, - val text: String, - val replaceRange: Range, - ) { - data class Range( - val start: Int, - val end: Int, - ) - } - } - - suspend fun provideCompletions(request: CompletionRequest): CompletionResponse? { - return request("provideCompletions", listOf(request, ABORT_SIGNAL_ENABLED)) - } - - data class LogEventRequest( - val type: EventType, - @SerializedName("completion_id") val completionId: String, - @SerializedName("choice_index") val choiceIndex: Int, - @SerializedName("select_kind") val selectKind: SelectKind? = null, - @SerializedName("view_id") val viewId: String? = null, - val elapsed: Int? = null, - ) { - enum class EventType { - @SerializedName("view") - VIEW, - - @SerializedName("select") - SELECT, - - @SerializedName("dismiss") - DISMISS, - } - - enum class SelectKind { - @SerializedName("line") - LINE, - } - } - - suspend fun postEvent(event: LogEventRequest) { - request("postEvent", listOf(event, ABORT_SIGNAL_ENABLED)) - } - - - fun close() { - try { - streamWriter.close() - process.killProcess() - } catch (e: Exception) { - // ignore - } - } - - private var requestId = 1 - private var ongoingRequest = mutableMapOf Unit>() - - private suspend inline fun request(func: String, args: List = emptyList()): T = - suspendCancellableCoroutine { continuation -> - val id = requestId++ - ongoingRequest[id] = { response -> - logger.debug("Agent response: $response") - val result = gson.fromJson(response, object : TypeToken() {}.type) - continuation.resumeWith(Result.success(result)) - } - val data = listOf(id, mapOf("func" to func, "args" to args)) - val json = gson.toJson(data) - logger.debug("Agent request: $json") - streamWriter.write(json + "\n") - streamWriter.flush() - - continuation.invokeOnCancellation { - logger.debug("Agent request cancelled") - val cancellationId = requestId++ - ongoingRequest[cancellationId] = { response -> - logger.debug("Agent cancellation response: $response") - } - val cancellationData = listOf(cancellationId, mapOf("func" to "cancelRequest", "args" to listOf(id))) - val cancellationJson = gson.toJson(cancellationData) - logger.info("Agent cancellation request: $cancellationJson") - streamWriter.write(cancellationJson + "\n") - streamWriter.flush() - } - } - - private var outputBuffer: String = "" - - override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) { - logger.debug("Output received: $outputType: ${event.text}") - if (outputType !== ProcessOutputTypes.STDOUT) return - val lines = (outputBuffer + event.text).lines() - lines.subList(0, lines.size - 1).forEach { string -> handleOutput(string) } - outputBuffer = lines.last() - } - - private fun handleOutput(output: String) { - val data = try { - gson.fromJson(output, Array::class.java).toList() - } catch (e: Exception) { - logger.warn("Failed to parse agent output: $output") - return - } - if (data.size != 2 || data[0] !is Number) { - logger.warn("Failed to parse agent output: $output") - return - } - logger.debug("Parsed agent output: $data") - val id = (data[0] as Number).toInt() - if (id == 0) { - if (data[1] is Map<*, *>) { - handleNotification(data[1] as Map<*, *>) - } - } else { - ongoingRequest[id]?.let { callback -> - callback(gson.toJson(data[1])) - } - ongoingRequest.remove(id) - } - } - - private fun handleNotification(event: Map<*, *>) { - when (event["event"]) { - "statusChanged" -> { - logger.debug("Agent notification $event") - statusFlow.value = when (event["status"]) { - "notInitialized" -> Status.NOT_INITIALIZED - "ready" -> Status.READY - "disconnected" -> Status.DISCONNECTED - "unauthorized" -> Status.UNAUTHORIZED - else -> Status.NOT_INITIALIZED - } - } - - "configUpdated" -> { - logger.debug("Agent notification $event") - } - - "authRequired" -> { - logger.debug("Agent notification $event") - authRequiredEventFlow.tryEmit(Unit) - } - - "issuesUpdated" -> { - logger.debug("Agent notification $event") - currentIssueFlow.value = (event["issues"] as List<*>).firstOrNull() as String? - } - - else -> { - logger.warn("Agent notification, unknown event name: ${event["event"]}") - } - } - } - - companion object { - private val ABORT_SIGNAL_ENABLED = mapOf("signal" to true) - } -} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/agent/AgentService.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/agent/AgentService.kt deleted file mode 100644 index 108620ead7ab..000000000000 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/agent/AgentService.kt +++ /dev/null @@ -1,420 +0,0 @@ -package com.tabbyml.intellijtabby.agent - -import com.intellij.ide.BrowserUtil -import com.intellij.ide.plugins.PluginManagerCore -import com.intellij.lang.Language -import com.intellij.notification.Notification -import com.intellij.notification.NotificationType -import com.intellij.notification.Notifications -import com.intellij.openapi.Disposable -import com.intellij.openapi.actionSystem.ActionManager -import com.intellij.openapi.actionSystem.AnAction -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.application.ApplicationInfo -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.application.ReadAction -import com.intellij.openapi.application.invokeLater -import com.intellij.openapi.components.Service -import com.intellij.openapi.components.service -import com.intellij.openapi.diagnostic.Logger -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.extensions.PluginId -import com.intellij.openapi.keymap.Keymap -import com.intellij.openapi.keymap.KeymapManagerListener -import com.intellij.openapi.options.ShowSettingsUtil -import com.intellij.openapi.progress.ProgressIndicator -import com.intellij.psi.PsiDocumentManager -import com.intellij.psi.PsiFile -import com.tabbyml.intellijtabby.settings.ApplicationConfigurable -import com.tabbyml.intellijtabby.settings.ApplicationSettingsState -import com.tabbyml.intellijtabby.settings.KeymapSettings -import io.ktor.util.* -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -@Service -class AgentService : Disposable { - private val logger = Logger.getInstance(AgentService::class.java) - private var agent: Agent = Agent() - val scope: CoroutineScope = CoroutineScope(Dispatchers.IO) - - private var initFailedNotification: Notification? = null - - @Deprecated("Tabby Cloud auth support will be removed.") - var authNotification: Notification? = null - private set - - var issueNotification: Notification? = null - private set - - enum class Status { - INITIALIZING, - INITIALIZATION_FAILED, - } - - private var initResultFlow: MutableStateFlow = MutableStateFlow(null) - val status = initResultFlow.combine(agent.status) { initResult, agentStatus -> - if (initResult == null) { - Status.INITIALIZING - } else if (initResult) { - agentStatus - } else { - Status.INITIALIZATION_FAILED - } - }.stateIn(scope, SharingStarted.Eagerly, Status.INITIALIZING) - - val currentIssue get() = agent.currentIssue - - init { - val settings = service() - val keymapSettings = service() - scope.launch { - val config = createAgentConfig(settings.data) - val clientProperties = createClientProperties(settings.data) - try { - agent.open() - agent.initialize(config, clientProperties) - initResultFlow.value = true - logger.info("Agent init done.") - } catch (e: Exception) { - initResultFlow.value = false - logger.warn("Agent init failed: $e") - - val notification = Notification( - "com.tabbyml.intellijtabby.notification.warning", - "Tabby initialization failed", - "${e.message}", - NotificationType.ERROR, - ) - notification.addAction( - object : AnAction("Open Online Documentation") { - override fun actionPerformed(e: AnActionEvent) { - BrowserUtil.browse("https://tabby.tabbyml.com/docs/extensions/troubleshooting/#tabby-initialization-failed") - } - } - ) - invokeLater { - initFailedNotification?.expire() - initFailedNotification = notification - Notifications.Bus.notify(notification) - } - } - } - - scope.launch { - settings.serverEndpointState.collect { - setEndpoint(it) - } - } - - scope.launch { - settings.serverTokenState.collect { - setToken(it) - } - } - - scope.launch { - settings.completionTriggerModeState.collect { - updateClientProperties("user", "intellij.triggerMode", it) - } - } - - scope.launch { - settings.isAnonymousUsageTrackingDisabledState.collect { - updateConfig("anonymousUsageTracking.disable", it) - } - } - - var keymapAttributeUpdateJob: Job? = null - ApplicationManager.getApplication().messageBus.connect().subscribe( - KeymapManagerListener.TOPIC, - object : KeymapManagerListener { - override fun shortcutChanged(keymap: Keymap, actionId: String) { - if (actionId.startsWith("Tabby.")) { - keymapAttributeUpdateJob?.cancel() - keymapAttributeUpdateJob = scope.launch { - // there will be many shortcutChanged events at once, so we debounce them - delay(1000) - val style = keymapSettings.getCurrentKeymapStyle() - logger.info("Updated keymap style: $style") - updateClientProperties("user", "intellij.keymapStyle", style) - } - } - } - } - ) - - scope.launch { - agent.authRequiredEvent.collect { - logger.info("Will show auth required notification.") - val currentToken = getConfig().server?.token - val message = if (currentToken?.isNotBlank() == true) { - "Tabby server requires authentication, but the current token is invalid." - } else { - "Tabby server requires authentication, please set your personal token." - } - val notification = Notification( - "com.tabbyml.intellijtabby.notification.warning", - message, - NotificationType.WARNING, - ) - notification.addAction(object : AnAction("Open Settings...") { - override fun actionPerformed(e: AnActionEvent) { - issueNotification?.expire() - ShowSettingsUtil.getInstance().showSettingsDialog(e.project, ApplicationConfigurable::class.java) - } - }) - invokeLater { - issueNotification?.expire() - issueNotification = notification - Notifications.Bus.notify(notification) - } - } - } - - scope.launch { - agent.status.collect { status -> - if (status == Agent.Status.READY) { - invokeLater { - issueNotification?.expire() - } - } - } - } - - scope.launch { - agent.currentIssue.collect { issueName -> - val notification = when (issueName) { - "connectionFailed" -> Notification( - "com.tabbyml.intellijtabby.notification.warning", - "Cannot connect to Tabby server", - NotificationType.ERROR, - ).apply { - addAction(ActionManager.getInstance().getAction("Tabby.CheckIssueDetail")) - } - - else -> { - invokeLater { - issueNotification?.expire() - } - return@collect - } - } - invokeLater { - issueNotification?.expire() - issueNotification = notification - Notifications.Bus.notify(notification) - } - } - } - } - - private fun createAgentConfig(state: ApplicationSettingsState.State): Agent.Config { - return Agent.Config( - server = if (state.serverEndpoint.isNotBlank() || state.serverToken.isNotBlank()) { - Agent.Config.Server( - endpoint = state.serverEndpoint.ifBlank { null }, - token = state.serverToken.ifBlank { null }, - ) - } else { - null - }, - anonymousUsageTracking = if (state.isAnonymousUsageTrackingDisabled) { - Agent.Config.AnonymousUsageTracking( - disabled = true, - ) - } else { - null - }, - ) - } - - private fun createClientProperties(state: ApplicationSettingsState.State): Agent.ClientProperties { - val appInfo = ApplicationInfo.getInstance() - val appVersion = appInfo.fullVersion - val appName = appInfo.fullApplicationName.replace(appVersion, "").trim() - val pluginId = "com.tabbyml.intellij-tabby" - val pluginVersion = PluginManagerCore.getPlugin(PluginId.getId(pluginId))?.version - val client = "$appName $pluginId $pluginVersion" - return Agent.ClientProperties( - user = mapOf( - "intellij" to mapOf( - "triggerMode" to state.completionTriggerMode, - "keymapStyle" to "unknown", // FIXME: At initialization, we cannot get the correct keymap style. It will be updated later. - ), - ), - session = mapOf( - "client" to client, - "ide" to mapOf("name" to appName, "version" to appVersion), - "tabby_plugin" to mapOf("name" to pluginId, "version" to pluginVersion), - ), - ) - } - - private suspend fun waitForInitialized() { - agent.status.first { it != Agent.Status.NOT_INITIALIZED } - } - - private suspend fun updateClientProperties(type: String, key: String, config: Any) { - waitForInitialized() - agent.updateClientProperties(type, key, config) - } - - private suspend fun updateConfig(key: String, config: Any) { - waitForInitialized() - agent.updateConfig(key, config) - } - - private suspend fun clearConfig(key: String) { - waitForInitialized() - agent.clearConfig(key) - } - - private suspend fun setEndpoint(endpoint: String) { - if (endpoint.isNotBlank()) { - updateConfig("server.endpoint", endpoint) - } else { - clearConfig("server.endpoint") - } - } - - private suspend fun setToken(token: String) { - if (token.isNotBlank()) { - updateConfig("server.token", token) - } else { - clearConfig("server.token") - } - } - - suspend fun getConfig(): Agent.Config { - waitForInitialized() - return agent.getConfig() - } - - suspend fun provideCompletion(editor: Editor, offset: Int, manually: Boolean = false): Agent.CompletionResponse? { - waitForInitialized() - return ReadAction.compute { - editor.project?.let { project -> - PsiDocumentManager.getInstance(project).getPsiFile(editor.document) - } - }?.let { file -> - agent.provideCompletions( - Agent.CompletionRequest( - file.virtualFile.path, - file.getLanguageId(), - editor.document.text, - offset, - manually, - ) - ) - } - } - - suspend fun postEvent(event: Agent.LogEventRequest) { - waitForInitialized() - agent.postEvent(event) - } - - @Deprecated("Tabby Cloud auth support will be removed.") - suspend fun requestAuth(progress: ProgressIndicator) { - waitForInitialized() - progress.isIndeterminate = true - progress.text = "Generating authorization url..." - val authUrlResponse = agent.requestAuthUrl() - val notification = if (authUrlResponse != null) { - BrowserUtil.browse(authUrlResponse.authUrl) - progress.text = "Waiting for authorization from browser..." - agent.waitForAuthToken(authUrlResponse.code) - if (agent.status.value == Agent.Status.READY) { - Notification( - "com.tabbyml.intellijtabby.notification.info", - "Congrats, you're authorized, start to use Tabby now.", - NotificationType.INFORMATION - ) - } else { - Notification( - "com.tabbyml.intellijtabby.notification.warning", - "Connection error, please check settings and try again.", - NotificationType.WARNING - ) - } - } else { - Notification( - "com.tabbyml.intellijtabby.notification.info", "You are already authorized.", NotificationType.INFORMATION - ) - } - invokeLater { - authNotification?.expire() - authNotification = notification - Notifications.Bus.notify(notification) - } - } - - suspend fun getCurrentIssueDetail(): Map? { - waitForInitialized() - return agent.getIssueDetail(Agent.GetIssueDetailOptions(name = currentIssue.value)) - } - - suspend fun getServerHealthState(): Map? { - waitForInitialized() - return agent.getServerHealthState() - } - - override fun dispose() { - runBlocking { - runCatching { - agent.finalize() - } - } - agent.close() - } - - companion object { - // Language id: https://code.visualstudio.com/docs/languages/identifiers - private fun PsiFile.getLanguageId(): String { - return if (this.language != Language.ANY && - this.language.id.isNotBlank() && - this.language.id.toLowerCasePreservingASCIIRules() !in arrayOf("txt", "text", "textmate") - ) { - languageIdMap[this.language.id] ?: this.language.id - .toLowerCasePreservingASCIIRules() - .replace("#", "sharp") - .replace("++", "pp") - .replace(" ", "") - } else { - val ext = this.fileType.defaultExtension.ifBlank { - this.virtualFile.name.substringAfterLast(".") - } - if (ext.isNotBlank()) { - filetypeMap[ext] ?: ext.toLowerCasePreservingASCIIRules() - } else { - "plaintext" - } - } - } - - private val languageIdMap = mapOf( - "ObjectiveC" to "objective-c", - "ObjectiveC++" to "objective-cpp", - ) - private val filetypeMap = mapOf( - "py" to "python", - "js" to "javascript", - "cjs" to "javascript", - "mjs" to "javascript", - "jsx" to "javascriptreact", - "ts" to "typescript", - "tsx" to "typescriptreact", - "kt" to "kotlin", - "md" to "markdown", - "cc" to "cpp", - "cs" to "csharp", - "m" to "objective-c", - "mm" to "objective-cpp", - "sh" to "shellscript", - "zsh" to "shellscript", - "bash" to "shellscript", - "txt" to "plaintext", - ) - } -} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/chat/ChatBrowser.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/chat/ChatBrowser.kt new file mode 100644 index 000000000000..f2e1c3035a22 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/chat/ChatBrowser.kt @@ -0,0 +1,989 @@ +package com.tabbyml.intellijtabby.chat + +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.intellij.ide.BrowserUtil +import com.intellij.ide.plugins.PluginManagerCore +import com.intellij.openapi.application.invokeLater +import com.intellij.openapi.application.runReadAction +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.components.service +import com.intellij.openapi.components.serviceOrNull +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.editor.Document +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.colors.EditorColors +import com.intellij.openapi.editor.colors.EditorColorsListener +import com.intellij.openapi.editor.colors.EditorColorsManager +import com.intellij.openapi.editor.colors.EditorFontType +import com.intellij.openapi.editor.event.SelectionEvent +import com.intellij.openapi.extensions.PluginId +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.fileEditor.FileEditorManagerEvent +import com.intellij.openapi.fileEditor.FileEditorManagerListener +import com.intellij.openapi.fileEditor.OpenFileDescriptor +import com.intellij.openapi.progress.util.BackgroundTaskUtil +import com.intellij.openapi.project.Project +import com.intellij.openapi.project.guessProjectDir +import com.intellij.openapi.util.SystemInfo +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.ui.JBColor +import com.intellij.ui.jcef.JBCefBrowser +import com.intellij.ui.jcef.JBCefBrowserBase +import com.intellij.ui.jcef.JBCefJSQuery +import com.tabbyml.intellijtabby.events.CombinedState +import com.tabbyml.intellijtabby.events.SelectionListener +import com.tabbyml.intellijtabby.findVirtualFile +import com.tabbyml.intellijtabby.git.GitProvider +import com.tabbyml.intellijtabby.lsp.ConnectionService +import com.tabbyml.intellijtabby.lsp.ConnectionService.InitializationException +import com.tabbyml.intellijtabby.lsp.positionInDocument +import com.tabbyml.intellijtabby.lsp.protocol.Config +import com.tabbyml.intellijtabby.lsp.protocol.StatusInfo +import com.tabbyml.intellijtabby.lsp.protocol.StatusRequestParams +import io.github.z4kn4fein.semver.Version +import io.github.z4kn4fein.semver.constraints.Constraint +import io.github.z4kn4fein.semver.constraints.satisfiedBy +import io.ktor.http.* +import kotlinx.coroutines.* +import org.cef.browser.CefBrowser +import org.cef.handler.CefLoadHandlerAdapter +import java.awt.Color +import java.awt.Toolkit +import java.awt.datatransfer.StringSelection +import java.io.File +import java.util.* +import java.util.concurrent.CompletableFuture + + +class ChatBrowser(private val project: Project) : JBCefBrowser( + createBuilder() + .setOffScreenRendering( + when { + SystemInfo.isWindows -> false + SystemInfo.isMac -> false + SystemInfo.isLinux -> true + else -> false + } + ) + .setEnableOpenDevToolsMenuItem(true) +) { + private val logger = logger() + private val gson = Gson() + private val combinedState = project.service() + private val gitProvider = project.service() + private val fileEditorManager = FileEditorManager.getInstance(project) + private val messageBusConnection = project.messageBus.connect() + + private val scope = CoroutineScope(Dispatchers.IO) + private var syncChatPanelActiveSelectionJob: Job? = null + + private suspend fun getServer() = project.serviceOrNull()?.getServerAsync() + + private var currentConfig: Config.ServerConfig? = null + var isChatPanelLoaded = false + private set + var chatPanelAPIVersion: String? = null + private set + private val pendingScripts: MutableList = mutableListOf() + + init { + component.isVisible = false + val bgColor = calcComponentBgColor() + component.background = bgColor + setPageBackgroundColor("hsl(${bgColor.toHsl()})") + + val tabbyChatPanelScript = loadTabbyChatPanelScript() + val htmlContent = loadHtmlContent(tabbyChatPanelScript) + loadHTML(htmlContent) + + jbCefClient.addLoadHandler(object : CefLoadHandlerAdapter() { + override fun onLoadingStateChange( + browser: CefBrowser?, + isLoading: Boolean, + canGoBack: Boolean, + canGoForward: Boolean + ) { + if (browser != null && !isLoading) { + handleLoaded() + } + } + }, cefBrowser) + + messageBusConnection.subscribe(CombinedState.Listener.TOPIC, object : CombinedState.Listener { + override fun stateChanged(state: CombinedState.State) { + reloadContent() + } + }) + + messageBusConnection.subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, object : FileEditorManagerListener { + override fun selectionChanged(event: FileEditorManagerEvent) { + syncChatPanelActiveSelectionJob?.cancel() + syncChatPanelActiveSelectionJob = scope.launch { + BackgroundTaskUtil.executeOnPooledThread(this@ChatBrowser) { + val context = getActiveEditorFileContext() + chatPanelUpdateActiveSelection(context) + } + } + } + }) + + messageBusConnection.subscribe(SelectionListener.TOPIC, object : SelectionListener { + override fun selectionChanged(editor: Editor, event: SelectionEvent) { + if (editor == fileEditorManager.selectedTextEditor) { + syncChatPanelActiveSelectionJob?.cancel() + syncChatPanelActiveSelectionJob = scope.launch { + delay(100) + BackgroundTaskUtil.executeOnPooledThread(this@ChatBrowser) { + val context = getActiveEditorFileContext() + chatPanelUpdateActiveSelection(context) + } + } + } + } + }) + + messageBusConnection.subscribe(EditorColorsManager.TOPIC, EditorColorsListener { + BackgroundTaskUtil.executeOnPooledThread(this) { + logger.debug("EditorColorsManager globalSchemeChange received, updating style.") + Thread.sleep(100) + jsApplyStyle() + chatPanelUpdateTheme() + } + }) + } + + fun navigate(view: String) { + chatPanelNavigate(view) + } + + fun explainSelectedText() { + BackgroundTaskUtil.executeOnPooledThread(this) { + chatPanelExecuteCommand(ChatCommand.EXPLAIN) + } + } + + fun fixSelectedText() { + BackgroundTaskUtil.executeOnPooledThread(this) { + // FIXME(@icycodes): collect the diagnostic message provided by IDE + chatPanelExecuteCommand(ChatCommand.FIX) + } + } + + fun codeReviewSelectedText() { + BackgroundTaskUtil.executeOnPooledThread(this) { + chatPanelExecuteCommand(ChatCommand.CODE_REVIEW) + } + } + + fun generateDocsForSelectedText() { + BackgroundTaskUtil.executeOnPooledThread(this) { + chatPanelExecuteCommand(ChatCommand.GENERATE_DOCS) + } + } + + fun generateTestsForSelectedText() { + BackgroundTaskUtil.executeOnPooledThread(this) { + chatPanelExecuteCommand(ChatCommand.GENERATE_TESTS) + } + } + + // default: use selection if available, otherwise use the whole file + // selection: use selection if available, otherwise return null + // file: use the whole file + enum class RangeStrategy { + DEFAULT, SELECTION, FILE + } + + fun addActiveEditorAsContext(rangeStrategy: RangeStrategy = RangeStrategy.DEFAULT) { + BackgroundTaskUtil.executeOnPooledThread(this) { + val context = getActiveEditorFileContext(rangeStrategy) ?: return@executeOnPooledThread + chatPanelAddRelevantContext(context) + } + } + + private fun virtualFileToFilepath(virtualFile: VirtualFile): Filepath { + val uri = virtualFile.url + + val workspaceDir = project.guessProjectDir()?.url + val gitRepo = gitProvider.getRepository(uri) + val gitUrl = gitRepo?.let { getDefaultRemoteUrl(it) } + + return if (gitUrl != null && uri.startsWith(gitRepo.root)) { + val relativePath = uri.substringAfter(gitRepo.root).trimStart(File.separatorChar) + FilepathInGitRepository( + filepath = relativePath, + gitUrl = gitUrl, + ) + } else if (workspaceDir != null && uri.startsWith(workspaceDir)) { + FilepathInWorkspace( + filepath = uri.substringAfter(workspaceDir).trimStart(File.separatorChar), + baseDir = workspaceDir, + ) + } else { + FilepathUri(uri = uri) + } + } + + private fun findVirtualFile(filepath: Filepath): VirtualFile? { + return when (filepath.kind) { + Filepath.Kind.URI -> { + val filepathUri = filepath as FilepathUri + project.findVirtualFile(filepathUri.uri) ?: project.guessProjectDir()?.url?.let { + project.findVirtualFile(it.appendUrlPathSegments(filepathUri.uri)) + } + } + + Filepath.Kind.WORKSPACE -> { + val filepathInWorkspace = filepath as FilepathInWorkspace + filepathInWorkspace.baseDir.let { + project.findVirtualFile(it.appendUrlPathSegments(filepathInWorkspace.filepath)) + } ?: project.guessProjectDir()?.url?.let { + project.findVirtualFile(it.appendUrlPathSegments(filepathInWorkspace.filepath)) + } + } + + Filepath.Kind.GIT -> { + val filepathInGit = filepath as FilepathInGitRepository + gitRemoteUrlToLocalRoot[filepathInGit.gitUrl]?.let { + project.findVirtualFile(it.appendUrlPathSegments(filepathInGit.filepath)) + } ?: project.guessProjectDir()?.url?.let { + project.findVirtualFile(it.appendUrlPathSegments(filepathInGit.filepath)) + } + } + + else -> { + null + } + } + } + + private fun getActiveEditorFileContext(rangeStrategy: RangeStrategy = RangeStrategy.DEFAULT): EditorFileContext? { + val editor = fileEditorManager.selectedTextEditor ?: return null + val virtualFile = editor.virtualFile ?: return null + + val context = runReadAction { + val document = editor.document + val selectionModel = editor.selectionModel + val useSelectedText = rangeStrategy == RangeStrategy.SELECTION + || (rangeStrategy == RangeStrategy.DEFAULT && selectionModel.hasSelection()) + if (useSelectedText) { + val text = selectionModel.selectedText.takeUnless { it.isNullOrBlank() } ?: return@runReadAction null + Pair( + text, + PositionRange( + positionOneBasedInDocument(document, selectionModel.selectionStart), + positionOneBasedInDocument(document, selectionModel.selectionEnd), + ) + ) + } else { + val text = document.text.takeUnless { it.isBlank() } ?: return@runReadAction null + Pair( + text, + null, + ) + } + } ?: return null + + val filepath = virtualFileToFilepath(virtualFile) + val editorFileContext = EditorFileContext( + filepath = filepath, + range = context.second, + content = context.first, + ) + + logger.debug("Collected active editor file context: $editorFileContext") + return editorFileContext + } + + private fun openInEditor(fileLocation: FileLocation): Boolean { + val filepath = fileLocation.filepath + val virtualFile = findVirtualFile(filepath) ?: return false + + val position = when (val location = fileLocation.location) { + is Number -> { + Position(location.toInt() - 1, 0) + } + + is Position -> { + Position(location.line - 1, location.character - 1) + } + + is LineRange -> { + Position(location.start - 1, 0) + } + + is PositionRange -> { + Position(location.start.line - 1, location.start.character - 1) + } + + else -> { + null + } + } + + invokeLater { + val descriptor = OpenFileDescriptor( + project, + virtualFile, + position?.line?.coerceAtLeast(0) ?: -1, + position?.character?.coerceAtLeast(0) ?: -1, + ) + fileEditorManager.openTextEditor(descriptor, true) + } + return true + } + + private fun handleLoaded() { + jsInjectFunctions() + jsApplyStyle() + reloadContent() + component.isVisible = true + } + + private val isDarkTheme get() = EditorColorsManager.getInstance().isDarkEditor + + private fun calcComponentBgColor(): Color { + val editorColorsScheme = EditorColorsManager.getInstance().schemeForCurrentUITheme + return editorColorsScheme.getColor(EditorColors.CARET_ROW_COLOR) + ?: if (isDarkTheme) editorColorsScheme.defaultBackground.brighter() else editorColorsScheme.defaultBackground.darker() + } + + @Suppress("UnnecessaryVariable", "UseJBColor") + private fun buildCss(): String { + val editorColorsScheme = EditorColorsManager.getInstance().schemeForCurrentUITheme + val componentBgColor = calcComponentBgColor() + val bgColor = editorColorsScheme.defaultBackground + val fgColor = editorColorsScheme.defaultForeground + val borderColor = if (isDarkTheme) JBColor.DARK_GRAY.darker() else JBColor.LIGHT_GRAY + val inputColor = componentBgColor + val inputBorderColor = borderColor + + val primaryColor = editorColorsScheme.getAttributes(EditorColors.REFERENCE_HYPERLINK_COLOR).foregroundColor + ?: if (isDarkTheme) Color(55, 148, 255) else Color(26, 133, 255) + val primaryFgColor = Color.WHITE + val popoverColor = componentBgColor + val popoverFgColor = fgColor + val accentColor = + if (isDarkTheme) Color(4, 57, 94) else componentBgColor.interpolate(componentBgColor.darker(), 0.2) + val accentFgColor = fgColor + + val ringColor = primaryColor + + val font = editorColorsScheme.getFont(EditorFontType.PLAIN).fontName + val fontSize = editorColorsScheme.editorFontSize + val css = String.format("--sidebar-background: %s;", componentBgColor.toHsl()) + + String.format("--background: %s;", bgColor.toHsl()) + + String.format("--foreground: %s;", fgColor.toHsl()) + + String.format("--border: %s;", borderColor.toHsl()) + + String.format("--input: %s;", inputColor.toHsl()) + + String.format("--input-border: %s;", inputBorderColor.toHsl()) + + String.format("--ring: %s;", ringColor.toHsl()) + + String.format("--primary: %s;", primaryColor.toHsl()) + + String.format("--primary-foreground: %s;", primaryFgColor.toHsl()) + + String.format("--popover: %s;", popoverColor.toHsl()) + + String.format("--popover-foreground: %s;", popoverFgColor.toHsl()) + + String.format("--accent: %s;", accentColor.toHsl()) + + String.format("--accent-foreground: %s;", accentFgColor.toHsl()) + + String.format("font: %s;", font) + + String.format("font-size: %spx;", fontSize) + logger.debug("CSS: $css") + return css + } + + private fun reloadContent(force: Boolean = false) { + if (force) { + scope.launch { + val server = getServer() ?: return@launch + reloadContentInternal(StatusInfo(StatusInfo.Status.CONNECTING)) + delay(500) // make the loading indicator visible for a bit + server.statusFeature.getStatus(StatusRequestParams(recheckConnection = true)).thenAccept { + reloadContentInternal(it, true) + } + } + } else { + reloadContentInternal(combinedState.state.agentStatus) + } + } + + private fun reloadContentInternal(statusInfo: StatusInfo?, force: Boolean = false) { + if (statusInfo == null) { + currentConfig = null + showContent("Initializing...") + } else { + when (statusInfo.status) { + StatusInfo.Status.CONNECTING -> { + currentConfig = null + showContent("Connecting to Tabby server...") + } + + StatusInfo.Status.UNAUTHORIZED -> { + currentConfig = null + showContent("Authorization required, please set your token in settings.") + } + + StatusInfo.Status.DISCONNECTED -> { + currentConfig = null + showContent("Cannot connect to Tabby server, please check your settings.") + } + + else -> { + val health = statusInfo.serverHealth + val error = checkServerHealth(health) + if (error != null) { + currentConfig = null + showContent(error) + } else { + val config = combinedState.state.agentConfig?.server + if (config == null) { + currentConfig = null + showContent("Initializing...") + } else if (force || currentConfig != config) { + showContent("Loading chat panel...") + isChatPanelLoaded = false + currentConfig = config + jsLoadChatPanel() + jsCreateChatPanelClient() + } + } + } + } + } + } + + private fun showContent(message: String? = null) { + if (message != null) { + jsShowMessage(message) + jsShowChatPanel(false) + } else { + jsShowMessage(null) + jsShowChatPanel(true) + } + } + + // chat panel api functions + + private fun chatPanelInit() { + val params = + listOf( + mapOf( + "fetcherOptions" to mapOf( + "authorization" to currentConfig?.token, + ) + ) + ) + logger.debug("chatPanelInit: $params") + jsChatPanelClientInvoke(ChatPanelApiVersion.V0_8_0, "init", params) + } + + private fun chatPanelExecuteCommand(command: String) { + val params = listOf(command) + logger.debug("chatPanelExecuteCommand: $params") + jsChatPanelClientInvoke(ChatPanelApiVersion.V0_8_0, "executeCommand", params) + } + + private fun chatPanelAddRelevantContext(context: EditorFileContext) { + val params = listOf(context) + + logger.debug("chatPanelAddRelevantContext: $params") + jsChatPanelClientInvoke(ChatPanelApiVersion.V0_8_0, "addRelevantContext", params) + } + + private fun chatPanelUpdateTheme() { + val params = + listOf( + buildCss(), + if (isDarkTheme) "dark" else "light", + ) + logger.debug("chatPanelUpdateTheme: $params") + jsChatPanelClientInvoke(ChatPanelApiVersion.V0_8_0, "updateTheme", params) + } + + private fun chatPanelUpdateActiveSelection(context: EditorFileContext?) { + val params = listOf(context) + logger.debug("chatPanelUpdateActiveSelection: $params") + jsChatPanelClientInvoke(ChatPanelApiVersion.V0_8_0, "updateActiveSelection", params) + } + + private fun chatPanelNavigate(view: String) { + val params = listOf(view) + logger.debug("chatPanelNavigate: $params") + jsChatPanelClientInvoke(ChatPanelApiVersion.V0_8_0, "navigate", params) + } + + // js handler functions to inject + + private fun createJsFunction(handler: (List) -> Any?): String { + val jsQuery = JBCefJSQuery.create(this as JBCefBrowserBase) + jsQuery.addHandler { paramsJson -> + val params = gson.fromJson(paramsJson, object : TypeToken>() {}) + val result = handler(params) + val resultJson = gson.toJson(result) + return@addHandler JBCefJSQuery.Response(resultJson) + } + val injection = jsQuery.inject( + "paramsJson", + "function(response) { resolve(JSON.parse(response)); }", + "function(error_code, error_message) { reject(new Error(error_message)); }", + ) + return """ + function(...args) { + return new Promise((resolve, reject) => { + const paramsJson = JSON.stringify(args); + $injection + }); + } + """.trimIndent().trimStart() + } + + private val jsHandleChatPanelClientCreated = createJsFunction { params -> + logger.debug("chatPanelClientCreated: $params") + isChatPanelLoaded = true + chatPanelAPIVersion = params.getOrNull(0) as String? + pendingScripts.forEach { executeJs(it) } + pendingScripts.clear() + chatPanelInit() + chatPanelUpdateTheme() + showContent() + } + + private val jsReloadContent = createJsFunction { reloadContent(true) } + + private val jsHandleChatPanelRefresh = createJsFunction { + logger.debug("refresh") + reloadContent(true) + } + + private val jsHandleChatPanelOnApplyInEditor = createJsFunction { params -> + logger.debug("onApplyInEditor: $params") + val content = params.getOrNull(0) as String? ?: return@createJsFunction Unit + val editor = fileEditorManager.selectedTextEditor ?: return@createJsFunction Unit + invokeLater { + WriteCommandAction.runWriteCommandAction(project) { + val start = editor.selectionModel.selectionStart + val end = editor.selectionModel.selectionEnd + editor.document.replaceString(start, end, content) + editor.caretModel.moveToOffset(start + content.length) + } + } + } + + private val jsHandleChatPanelOnCopy = createJsFunction { params -> + logger.debug("onCopy: request: $params") + val content = params.getOrNull(0) as String? ?: return@createJsFunction Unit + val stringSelection = StringSelection(content) + val clipboard = Toolkit.getDefaultToolkit().systemClipboard + clipboard.setContents(stringSelection, null) + } + + private val jsHandleChatPanelOpenInEditor = createJsFunction { params -> + logger.debug("openInEditor: request: $params") + val fileLocation = params.getOrNull(0)?.asFileLocation() ?: return@createJsFunction false + return@createJsFunction openInEditor(fileLocation) + } + + private val jsHandleChatPanelOpenExternal = createJsFunction { params -> + logger.debug("openExternal: request: $params") + val url = params.getOrNull(0) as String? ?: return@createJsFunction Unit + BrowserUtil.browse(url) + } + + private val jsHandleChatPanelReadWorkspaceGitRepositories = createJsFunction { params -> + logger.debug("readWorkspaceGitRepositories: request: $params") + val activeTextEditorUri = fileEditorManager.selectedTextEditor?.virtualFile?.url + val projectDir = project.guessProjectDir()?.url + val pathToCheck = activeTextEditorUri ?: projectDir ?: return@createJsFunction null + val gitRepo = gitProvider.getRepository(pathToCheck)?.let { + getDefaultRemoteUrl(it) + }?.let { + GitRepository(it) + } + return@createJsFunction listOfNotNull(gitRepo) + } + + private val jsHandleChatPanelGetActiveEditorSelection = createJsFunction { params -> + logger.debug("getActiveEditorSelection: request: $params") + return@createJsFunction getActiveEditorFileContext() + } + + // functions to execute js scripts + + private fun executeJs(script: String) { + cefBrowser.executeJavaScript(script, cefBrowser.url, 0) + } + + private fun jsInjectFunctions() { + val script = """ + if (!window.handleReload) { + window.handleReload = $jsReloadContent + } + """.trimIndent().trimStart() + executeJs(script) + } + + private fun jsApplyStyle() { + val script = String.format( + "applyStyle('%s')", + gson.toJson(mapOf("theme" to if (isDarkTheme) "dark" else "light", "css" to buildCss())) + ) + executeJs(script) + } + + private fun jsShowMessage(message: String?) { + val script = if (message != null) "showMessage('${message}')" else "showMessage()" + executeJs(script) + } + + private fun jsShowChatPanel(visible: Boolean) { + val script = String.format("showChatPanel(%s)", if (visible) "true" else "false") + executeJs(script) + } + + private fun jsLoadChatPanel() { + val config = currentConfig ?: return + val chatUrl = "${config.endpoint}/chat?client=intellij" + val script = "loadChatPanel('$chatUrl')" + executeJs(script) + } + + private fun jsCreateChatPanelClient() { + val script = """ + TabbyChatPanel.createClient(getChatPanel(), { + refresh: $jsHandleChatPanelRefresh, + onApplyInEditor: $jsHandleChatPanelOnApplyInEditor, + onCopy: $jsHandleChatPanelOnCopy, + openInEditor: $jsHandleChatPanelOpenInEditor, + openExternal: $jsHandleChatPanelOpenExternal, + readWorkspaceGitRepositories: $jsHandleChatPanelReadWorkspaceGitRepositories, + getActiveEditorSelection: $jsHandleChatPanelGetActiveEditorSelection, + }).then((client) => { + window.tabbyChatPanelClient = client; + const getVersion = client && client['0.9.0'] && client['0.9.0']['getVersion']; + if (getVersion && typeof getVersion === 'function') { + return getVersion(); + } else { + return undefined; + } + }).then((version) => { + console.log('Tabby Chat Panel API version: ' + version); + const callback = $jsHandleChatPanelClientCreated; + callback(version); + }); + """.trimIndent().trimStart() + executeJs(script) + } + + private val pendingChatPanelRequest = mutableMapOf>() + private val jsChatPanelResponseHandlerInjection = JBCefJSQuery.create(this as JBCefBrowserBase).apply { + addHandler { results -> + logger.debug("Response from chat panel: $results") + val parsedResult = gson.fromJson(results, object : TypeToken>() {}) + val future = pendingChatPanelRequest.remove(parsedResult[0] as String) + if (parsedResult[1] is String) { + future?.completeExceptionally(Exception(parsedResult[1] as String)) + } else { + future?.complete(parsedResult[2]) + } + return@addHandler JBCefJSQuery.Response("") + } + }.inject("results") + + private fun jsChatPanelClientInvoke( + version: ChatPanelApiVersion, + method: String, + params: List + ): CompletableFuture { + val future = CompletableFuture() + val uuid = UUID.randomUUID().toString() + pendingChatPanelRequest[uuid] = future + val paramsJson = escapeCharacters(gson.toJson(params)) + val script = """ + (function() { + const client = window.tabbyChatPanelClient; + if (client && typeof client === 'object') { + const func = client['${version.value}'] && client['${version.value}']['$method']; + if (func && typeof func === 'function') { + const params = JSON.parse('$paramsJson') + const resultPromise = func(...params) + if (resultPromise && typeof resultPromise.then === 'function') { + resultPromise.then(result => { + const results = JSON.stringify(['$uuid', null, result]) + $jsChatPanelResponseHandlerInjection + }).catch(error => { + const results = JSON.stringify(['$uuid', error.message, null]) + $jsChatPanelResponseHandlerInjection + }) + } else { + const results = JSON.stringify(['$uuid', null, resultPromise]) + $jsChatPanelResponseHandlerInjection + } + } else { + const results = JSON.stringify(['$uuid', 'Function not found: $version $method', null]) + $jsChatPanelResponseHandlerInjection + } + } else { + const results = JSON.stringify(['$uuid', 'Tabby chat panel client is not connected.', null]) + $jsChatPanelResponseHandlerInjection + } + })() + """.trimIndent().trimStart() + + logger.debug("Request to chat panel: $uuid, $version, $method, $paramsJson") + if (isChatPanelLoaded) { + executeJs(script) + } else { + pendingScripts.add(script) + } + return future + } + + companion object { + private const val TABBY_SERVER_VERSION_RANGE = ">=0.27.0" + + enum class ChatPanelApiVersion(val value: String) { + V0_8_0("0.8.0"), + V0_9_0("0.9.0"), + } + + private fun parseVersion(versionString: String): Version? { + return try { + val version = versionString.removePrefix("v").substringBefore("-") + Version.parse(version) + } catch (e: Exception) { + null + } + } + + private fun checkServerHealth(serverHealth: Map?): String? { + if (serverHealth == null) { + return "Connecting to Tabby server..." + } + if (serverHealth["webserver"] == null || serverHealth["chat_model"] == null) { + return "You need to launch the server with the chat model enabled; for example, use `--chat-model Qwen2-1.5B-Instruct`." + } + + if (serverHealth.containsKey("version")) { + val versionObj = serverHealth["version"] + val version: Version? = if (versionObj is String) { + parseVersion(versionObj) + } else if (versionObj is Map<*, *> && versionObj.containsKey("git_describe")) { + val gitDescribe = versionObj["git_describe"] + if (gitDescribe is String) { + parseVersion(gitDescribe) + } else { + null + } + } else { + null + } + if (version != null && !Constraint.parse(TABBY_SERVER_VERSION_RANGE).satisfiedBy(version)) { + return String.format( + "Tabby Chat requires Tabby server version %s. Your server is running version %s.", + TABBY_SERVER_VERSION_RANGE, version.toString() + ) + } + } + return null + } + + private fun escapeCharacters(input: String): String { + return input.replace("\\", "\\\\") + .replace("'", "\\'") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t") + .replace("\b", "\\b") + } + + private fun positionOneBasedInDocument(document: Document, offset: Int): Position { + val position = positionInDocument(document, offset) + return Position(position.line + 1, position.character + 1) + } + + @Suppress("UseJBColor") + private fun Color.interpolate(other: Color, fraction: Double): Color { + val r = (red + (other.red - red) * fraction).toInt().coerceIn(0..255) + val g = (green + (other.green - green) * fraction).toInt().coerceIn(0..255) + val b = (blue + (other.blue - blue) * fraction).toInt().coerceIn(0..255) + return Color(r, g, b) + } + + private fun Color.toHsl(): String { + val r = red / 255.0 + val g = green / 255.0 + val b = blue / 255.0 + val max = maxOf(r, g, b) + val min = minOf(r, g, b) + var l = (max + min) / 2.0 + var h: Double + var s: Double + + if (max == min) { + h = 0.0 + s = 0.0 + } else { + val delta = max - min + s = if (l > 0.5) delta / (2.0 - max - min) else delta / (max + min) + h = when (max) { + r -> (g - b) / delta + if (g < b) 6 else 0 + g -> (b - r) / delta + 2 + else -> (r - g) / delta + 4 + } + h /= 6.0 + } + + h *= 360 + s *= 100 + l *= 100 + + return String.format("%.0f %.0f%% %.0f%%", h, s, l) + } + + // FIXME: extract this to git provider + private val gitRemoteUrlToLocalRoot = mutableMapOf() + + private fun getDefaultRemoteUrl(repo: GitProvider.Repository): String? { + if (repo.remotes.isNullOrEmpty()) { + return null + } + val remoteUrl = repo.remotes.firstOrNull { it.name == "origin" }?.url + ?: repo.remotes.firstOrNull { it.name == "upstream" }?.url + ?: repo.remotes.firstOrNull()?.url + if (remoteUrl != null) { + gitRemoteUrlToLocalRoot[remoteUrl] = repo.root + } + return remoteUrl + } + + private fun String.appendUrlPathSegments(path: String): String { + return URLBuilder(this).appendPathSegments(path).toString() + } + + private fun loadTabbyChatPanelScript(): String { + val script = + PluginManagerCore.getPlugin(PluginId.getId("com.tabbyml.intellij-tabby")) + ?.pluginPath + ?.resolve("tabby-chat-panel/iife/tabby-chat-panel.min.js") + ?.toFile() + if (script?.exists() == true) { + logger().info("Tabby-chat-panel script path: ${script.absolutePath}") + return script.readText() + } else { + throw InitializationException("Tabby-chat-panel script not found. Please reinstall Tabby plugin.") + } + } + + private fun loadHtmlContent(script: String) = """ + + + + + + + + + + +
    +
    +

    Welcome to Tabby Chat

    +

    + Reload +
    +
    + + + + + + + """.trimIndent().trimStart() + } +} diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/chat/ChatBrowserFactory.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/chat/ChatBrowserFactory.kt new file mode 100644 index 000000000000..492c3d9f687c --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/chat/ChatBrowserFactory.kt @@ -0,0 +1,40 @@ +package com.tabbyml.intellijtabby.chat + +import com.intellij.openapi.Disposable +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.intellij.openapi.wm.ToolWindow +import com.intellij.openapi.wm.ToolWindowManager +import com.tabbyml.intellijtabby.widgets.ChatToolWindowFactory + +@Service(Service.Level.PROJECT) +class ChatBrowserFactory(private val project: Project) : Disposable { + private val registry: MutableMap = mutableMapOf() + + fun createChatBrowser(toolWindow: ToolWindow): ChatBrowser { + val chatBrowser = ChatBrowser(project) + registry[toolWindow] = chatBrowser + return chatBrowser + } + + fun getChatBrowser(toolWindow: ToolWindow): ChatBrowser? { + return registry[toolWindow] + } + + override fun dispose() { + registry.forEach { + it.value.dispose() + } + registry.clear() + } + + companion object { + fun findActiveChatBrowser(project: Project): ChatBrowser? { + val toolWindowManager = ToolWindowManager.getInstance(project) + val toolWindow = toolWindowManager.getToolWindow(ChatToolWindowFactory.TOOL_WINDOW_ID) ?: return null + val chatBrowserFactory = project.service() + return chatBrowserFactory.getChatBrowser(toolWindow) + } + } +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/chat/ChatPanelData.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/chat/ChatPanelData.kt new file mode 100644 index 000000000000..632f55aca134 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/chat/ChatPanelData.kt @@ -0,0 +1,128 @@ +package com.tabbyml.intellijtabby.chat + +import com.google.gson.Gson + +abstract class Filepath( + val kind: String +) { + sealed class Kind { + companion object { + const val GIT = "git" + const val WORKSPACE = "workspace" + const val URI = "uri" + } + } +} + +data class FilepathInGitRepository( + val filepath: String, + val gitUrl: String, + val revision: String? = null, +) : Filepath(Kind.GIT) + +data class FilepathInWorkspace( + val filepath: String, + val baseDir: String, +) : Filepath(Kind.WORKSPACE) + +data class FilepathUri( + val uri: String, +) : Filepath(Kind.URI) + +data class Position( + // 1-based + val line: Int, + val character: Int, +) + +abstract class Range +data class LineRange( + // 1-based + val start: Int, + val end: Int, +) : Range() + +data class PositionRange( + val start: Position, + val end: Position, +) : Range() + +data class EditorFileContext( + val kind: String = "file", + val filepath: Filepath, + val range: Range?, + val content: String, +) + +sealed class ChatCommand { + companion object { + const val EXPLAIN = "explain" + const val FIX = "fix" + const val GENERATE_DOCS = "generate-docs" + const val GENERATE_TESTS = "generate-tests" + const val CODE_REVIEW = "code-review" + } +} + +sealed class ChatView { + companion object { + const val NEW_CHAT = "new-chat" + const val HISTORY = "history" + } +} + +data class FileLocation( + val filepath: Filepath, + // Int, LineRange, Position, or PositionRange + val location: Any?, +) + +data class GitRepository( + val url: String, +) + +private val gson = Gson() + +fun Any.asFileLocation(): FileLocation? { + val filepath = if (this is Map<*, *> && this.containsKey("filepath")) { + val filepathValue = this["filepath"] + if (filepathValue is Map<*, *> && filepathValue.containsKey("kind")) { + if (filepathValue["kind"] == Filepath.Kind.GIT) { + gson.fromJson(gson.toJson(filepathValue), FilepathInGitRepository::class.java) + } else if (filepathValue["kind"] == Filepath.Kind.WORKSPACE) { + gson.fromJson(gson.toJson(filepathValue), FilepathInWorkspace::class.java) + } else if (filepathValue["kind"] == Filepath.Kind.URI) { + gson.fromJson(gson.toJson(filepathValue), FilepathUri::class.java) + } else { + null + } + } else { + null + } + } else { + null + } ?: return null + + val location = if (this is Map<*, *> && containsKey("location")) { + val locationValue = this["location"] + if (locationValue is Number) { + locationValue + } else if (locationValue is Map<*, *>) { + if (locationValue.containsKey("line")) { + gson.fromJson(gson.toJson(locationValue), Position::class.java) + } else if (locationValue.containsKey("start") && locationValue["start"] is Number) { + gson.fromJson(gson.toJson(locationValue), LineRange::class.java) + } else if (locationValue.containsKey("start") && locationValue["start"] is Map<*, *>) { + gson.fromJson(gson.toJson(locationValue), PositionRange::class.java) + } else { + null + } + } else { + null + } + } else { + null + } + + return FileLocation(filepath, location) +} diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/completion/InlineCompletionData.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/completion/InlineCompletionData.kt new file mode 100644 index 000000000000..6381ee76af6e --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/completion/InlineCompletionData.kt @@ -0,0 +1,23 @@ +package com.tabbyml.intellijtabby.completion + +import com.tabbyml.intellijtabby.lsp.protocol.CompletionEventId + +data class InlineCompletionList( + val isIncomplete: Boolean, + val items: List, +) + +data class InlineCompletionItem( + val insertText: String, + val replaceRange: Range, + val data: Data? = null, +) { + data class Range( + val start: Int, + val end: Int, + ) + + data class Data( + val eventId: CompletionEventId? = null + ) +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/completion/InlineCompletionRenderer.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/completion/InlineCompletionRenderer.kt new file mode 100644 index 000000000000..ef20a1bc9d0a --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/completion/InlineCompletionRenderer.kt @@ -0,0 +1,212 @@ +package com.tabbyml.intellijtabby.completion + +import com.intellij.openapi.application.invokeLater +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.EditorCustomElementRenderer +import com.intellij.openapi.editor.Inlay +import com.intellij.openapi.editor.colors.EditorFontType +import com.intellij.openapi.editor.impl.FontInfo +import com.intellij.openapi.editor.markup.HighlighterLayer +import com.intellij.openapi.editor.markup.HighlighterTargetArea +import com.intellij.openapi.editor.markup.RangeHighlighter +import com.intellij.openapi.editor.markup.TextAttributes +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.TextRange +import com.intellij.ui.JBColor +import com.intellij.util.ui.UIUtil +import java.awt.Font +import java.awt.Graphics +import java.awt.Rectangle + +class InlineCompletionRenderer { + private val logger = Logger.getInstance(InlineCompletionRenderer::class.java) + + data class RenderingContext( + val id: String, + val editor: Editor, + val offset: Int, + val completionItem: InlineCompletionItem, + val inlays: List>, + val markups: List, + val displayAt: Long, + ) { + fun calcElapsed(): Long { + return System.currentTimeMillis() - displayAt + } + } + + var current: RenderingContext? = null + private set + + fun show( + editor: Editor, offset: Int, completion: InlineCompletionItem, callback: (context: RenderingContext) -> Unit = {} + ) { + invokeLater { + current?.let { + it.inlays.forEach(Disposer::dispose) + it.markups.forEach { markup -> + it.editor.markupModel.removeHighlighter(markup) + } + current = null + } + + if (editor.caretModel.offset != offset) { + return@invokeLater + } + + logger.debug("Showing inline completion at $offset: $completion") + + val cmplId = completion.data?.eventId?.completionId?.replace("cmpl-", "") ?: "noCmplId" + val displayAt = System.currentTimeMillis() + val id = "view-${cmplId}-at-${displayAt}" + + val prefixReplaceLength = offset - completion.replaceRange.start + val suffixReplaceLength = completion.replaceRange.end - offset + val text = completion.insertText.substring(prefixReplaceLength) + if (text.isEmpty()) { + // Nothing to display + return@invokeLater + } + val currentLineNumber = editor.document.getLineNumber(offset) + val currentLineEndOffset = editor.document.getLineEndOffset(currentLineNumber) + val currentLineSuffix = editor.document.getText(TextRange(offset, currentLineEndOffset)) + + val textLines = text.lines().toMutableList() + + val inlays = mutableListOf>() + val markups = mutableListOf() + if (suffixReplaceLength == 0) { + // No replace range to handle + createInlayText(editor, textLines[0], offset, 0)?.let { inlays.add(it) } + if (textLines.size > 1) { + if (currentLineSuffix.isNotEmpty()) { + markupReplaceText(editor, offset, currentLineEndOffset).let { markups.add(it) } + textLines[textLines.lastIndex] += currentLineSuffix + } + textLines.forEachIndexed { index, line -> + if (index > 0) { + createInlayText(editor, line, offset, index)?.let { inlays.add(it) } + } + } + } + } else if (suffixReplaceLength == 1) { + // Replace range contains one char + val replaceChar = currentLineSuffix[0] + // Insert part is substring of first line that before the char + // Append part is substring of first line that after the char + // If first line doesn't contain the char, insert part is full first line, append part is empty + val insertPart = if (textLines[0].startsWith(replaceChar)) { + "" + } else { + textLines[0].split(replaceChar).first() + } + val appendPart = if (insertPart.length < textLines[0].length) { + textLines[0].substring(insertPart.length + 1) + } else { + "" + } + if (insertPart.isNotEmpty()) { + createInlayText(editor, insertPart, offset, 0)?.let { inlays.add(it) } + } + if (appendPart.isNotEmpty()) { + createInlayText(editor, appendPart, offset + 1, 0)?.let { inlays.add(it) } + } + if (textLines.size > 1) { + if (currentLineSuffix.isNotEmpty()) { + val startOffset = if (insertPart.length < textLines[0].length) { + // First line contains the char + offset + 1 + } else { + // First line doesn't contain the char + offset + } + markupReplaceText(editor, startOffset, currentLineEndOffset).let { markups.add(it) } + textLines[textLines.lastIndex] += currentLineSuffix.substring(1) + } + textLines.forEachIndexed { index, line -> + if (index > 0) { + createInlayText(editor, line, offset, index)?.let { inlays.add(it) } + } + } + } + } else { + // Replace range contains multiple chars + // It's hard to match these chars in the insertion text, we just mark them up + createInlayText(editor, textLines[0], offset, 0)?.let { inlays.add(it) } + markupReplaceText(editor, offset, offset + suffixReplaceLength).let { markups.add(it) } + if (textLines.size > 1) { + if (currentLineSuffix.length > suffixReplaceLength) { + markupReplaceText(editor, offset + suffixReplaceLength, currentLineEndOffset).let { markups.add(it) } + textLines[textLines.lastIndex] += currentLineSuffix.substring(suffixReplaceLength) + } + textLines.forEachIndexed { index, line -> + if (index > 0) { + createInlayText(editor, line, offset, index)?.let { inlays.add(it) } + } + } + } + } + val context = RenderingContext(id, editor, offset, completion, inlays, markups, displayAt) + current = context + callback(context) + } + } + + fun hide() { + current?.let { + invokeLater { + it.inlays.forEach(Disposer::dispose) + it.markups.forEach { markup -> + it.editor.markupModel.removeHighlighter(markup) + } + } + current = null + } + } + + private fun createInlayText(editor: Editor, text: String, offset: Int, lineOffset: Int): Inlay<*>? { + val renderer = object : EditorCustomElementRenderer { + override fun getContextMenuGroupId(inlay: Inlay<*>): String { + return "Tabby.InlineCompletionContextMenu" + } + + override fun calcWidthInPixels(inlay: Inlay<*>): Int { + return maxOf(getWidth(inlay.editor, text), 1) + } + + override fun paint(inlay: Inlay<*>, graphics: Graphics, targetRect: Rectangle, textAttributes: TextAttributes) { + graphics.font = getFont(inlay.editor) + graphics.color = JBColor.GRAY + graphics.drawString(text, targetRect.x, targetRect.y + inlay.editor.ascent) + } + + private fun getFont(editor: Editor): Font { + return editor.colorsScheme.getFont(EditorFontType.ITALIC).let { + UIUtil.getFontWithFallbackIfNeeded(it, text).deriveFont(editor.colorsScheme.editorFontSize) + } + } + + private fun getWidth(editor: Editor, line: String): Int { + val font = getFont(editor) + val metrics = FontInfo.getFontMetrics(font, FontInfo.getFontRenderContext(editor.contentComponent)) + return metrics.stringWidth(line) + } + } + return if (lineOffset == 0) { + editor.inlayModel.addInlineElement(offset, true, renderer) + } else { + editor.inlayModel.addBlockElement(offset, true, false, -lineOffset, renderer) + } + } + + private fun markupReplaceText(editor: Editor, startOffset: Int, endOffset: Int): RangeHighlighter { + val textAttributes = TextAttributes().apply { + foregroundColor = JBColor.background() + backgroundColor = JBColor.background() + } + return editor.markupModel.addRangeHighlighter( + startOffset, endOffset, HighlighterLayer.LAST + 1000, textAttributes, HighlighterTargetArea.EXACT_RANGE + ) + } +} diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/completion/InlineCompletionService.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/completion/InlineCompletionService.kt new file mode 100644 index 000000000000..71f730e99342 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/completion/InlineCompletionService.kt @@ -0,0 +1,499 @@ +package com.tabbyml.intellijtabby.completion + +import com.intellij.codeInsight.lookup.LookupEvent +import com.intellij.codeInsight.lookup.LookupListener +import com.intellij.codeInsight.lookup.LookupManagerListener +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.invokeLater +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.components.serviceOrNull +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.editor.Document +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.event.CaretEvent +import com.intellij.openapi.editor.event.DocumentEvent +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.fileEditor.FileEditorManagerEvent +import com.intellij.openapi.fileEditor.FileEditorManagerListener +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.TextRange +import com.intellij.util.messages.Topic +import com.tabbyml.intellijtabby.events.CaretListener +import com.tabbyml.intellijtabby.events.DocumentListener +import com.tabbyml.intellijtabby.lsp.ConnectionService +import com.tabbyml.intellijtabby.lsp.offsetInDocument +import com.tabbyml.intellijtabby.lsp.positionInDocument +import com.tabbyml.intellijtabby.lsp.protocol.EventParams +import com.tabbyml.intellijtabby.lsp.protocol.InlineCompletionParams +import com.tabbyml.intellijtabby.safeSyncPublisher +import com.tabbyml.intellijtabby.settings.SettingsService +import com.tabbyml.intellijtabby.settings.SettingsState +import kotlinx.coroutines.* +import kotlinx.coroutines.future.await +import org.eclipse.lsp4j.Range +import org.eclipse.lsp4j.TextDocumentIdentifier + +@Service(Service.Level.PROJECT) +class InlineCompletionService(private val project: Project) : Disposable { + private val logger = Logger.getInstance(InlineCompletionService::class.java) + private val messageBusConnection = project.messageBus.connect() + private val editorManager = FileEditorManager.getInstance(project) + private val scope = CoroutineScope(Dispatchers.IO) + private val settings = service() + private val renderer = InlineCompletionRenderer() + + private suspend fun getServer() = project.serviceOrNull()?.getServerAsync() + + data class InlineCompletionContext( + val request: Request, + val job: Job, + val response: Response? = null, + val partialAccepted: Boolean = false, + ) { + + data class CompletionInfo (val text: String, val prefixLength: Int) + + data class Request( + val editor: Editor, + val document: Document, + val modificationStamp: Long, + val offset: Int, + val manually: Boolean, + val selectedCompletion: CompletionInfo? = null, + ) { + fun withManually(manually: Boolean = true): Request { + return Request(editor, document, modificationStamp, offset, manually) + } + + fun withCompletionInfo(selectedCompletion: CompletionInfo?): Request { + return Request(editor, document, modificationStamp, offset, manually, selectedCompletion) + } + + companion object { + fun from(editor: Editor, offset: Int? = null): Request { + val document = editor.document + return Request( + editor, document, document.modificationStamp, offset ?: editor.caretModel.primaryCaret.offset, false + ) + } + } + } + + data class Response( + val completionList: InlineCompletionList, + val itemIndex: Int, + ) + + fun isMatch(editor: Editor, offset: Int?): Boolean { + return this.request.editor == editor && this.request.document == editor.document && (this.partialAccepted || this.request.modificationStamp == editor.document.modificationStamp) && offset?.let { this.request.offset == it } != false + } + + fun withResponse(completionList: InlineCompletionList?, itemIndex: Int = 0): InlineCompletionContext { + return InlineCompletionContext(request, job, completionList?.let { Response(it, itemIndex) }) + } + + fun withUpdatedItemIndex(itemIndex: Int): InlineCompletionContext { + val response = this.response ?: return this + return withResponse(response.completionList, itemIndex) + } + + fun withPartialAccepted(acceptedItem: InlineCompletionItem, acceptedLength: Int): InlineCompletionContext { + val forwardRequest = Request( + request.editor, request.document, request.modificationStamp, request.offset + acceptedLength, request.manually + ) + val forwardResponse = Response( + completionList = InlineCompletionList( + true, listOf( + InlineCompletionItem( + insertText = acceptedItem.insertText, replaceRange = InlineCompletionItem.Range( + acceptedItem.replaceRange.start, acceptedItem.replaceRange.end + acceptedLength + ), data = acceptedItem.data + ) + ) + ), itemIndex = 0 + ) + return InlineCompletionContext(forwardRequest, job, forwardResponse, true) + } + } + + private var current: InlineCompletionContext? = null + private var currentContextWriteLock = Object() + private var shouldAutoTrigger: Boolean + private var documentChangedListenerJob: Job? = null + private var completionChangedListenerJob: Job? = null + + fun isInlineCompletionVisibleAt(editor: Editor, offset: Int): Boolean = + renderer.current?.editor == editor && renderer.current?.offset == offset + + fun isInlineCompletionStartWithIndentation(): Boolean { + val renderingContext = renderer.current ?: return false + val document = renderingContext.editor.document + val completionItem = renderingContext.completionItem + val offset = renderingContext.offset + val lineStart = document.getLineStartOffset(document.getLineNumber(offset)) + val linePrefix = document.getText(TextRange(lineStart, offset)) + val visibleText = completionItem.insertText.substring(offset - completionItem.replaceRange.start) + return linePrefix.isBlank() && visibleText.matches(Regex("(\\t+| {2,}).*", RegexOption.DOT_MATCHES_ALL)) + } + + init { + logger.debug("Initialize InlineCompletionService.") + val triggerMode = settings.settings().completionTriggerMode + logger.debug("TriggerMode: $triggerMode") + shouldAutoTrigger = triggerMode == SettingsState.TriggerMode.AUTOMATIC + messageBusConnection.subscribe(SettingsService.Listener.TOPIC, object : SettingsService.Listener { + override fun settingsChanged(settings: SettingsService.Settings) { + logger.debug("TriggerMode updated: ${settings.completionTriggerMode}") + shouldAutoTrigger = settings.completionTriggerMode == SettingsState.TriggerMode.AUTOMATIC + } + }) + messageBusConnection.subscribe(DocumentListener.TOPIC, object : DocumentListener { + override fun documentChanged(document: Document, editor: Editor, event: DocumentEvent) { + if (editorManager.selectedTextEditor == editor) { + documentChangedListenerJob?.cancel() + documentChangedListenerJob = scope.launch { + // debounce for multiple documentChanged events triggered at one stroke + // such as input `(`, and `)` will be auto added, this will trigger two events + delay(10) + handleDocumentChanged(document, editor, event) + } + } + } + }) + messageBusConnection.subscribe(CaretListener.TOPIC, object : CaretListener { + override fun caretPositionChanged(editor: Editor, event: CaretEvent) { + if (editorManager.selectedTextEditor == editor) { + val offset = editor.caretModel.offset + val context = current + if (context != null && !context.isMatch(editor, offset)) { + dismiss() + } + } + } + }) + messageBusConnection.subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, object : FileEditorManagerListener { + override fun selectionChanged(event: FileEditorManagerEvent) { + dismiss() + } + }) + messageBusConnection.subscribe(LookupManagerListener.TOPIC, LookupManagerListener { oldLookup, newLookup -> + newLookup?.addLookupListener(object: LookupListener { + override fun currentItemChanged(event: LookupEvent) { + val lookupItem = event.item + if (lookupItem == null) { + return + } + val lookup = event.lookup + val prefixLength = lookup.itemMatcher(lookupItem).prefix.length + completionChangedListenerJob?.cancel() + completionChangedListenerJob = scope.launch { + delay(10) + invokeLater { + handleLookupItemChanged(event.lookup.editor, InlineCompletionContext.CompletionInfo( + lookupItem.lookupString, prefixLength + )) + } + } + } + }) + }) + } + + private fun handleDocumentChanged(document: Document, editor: Editor, event: DocumentEvent) { + if (event.newFragment.isEmpty()) { + // newFragment is empty, so this is a delete or backspace, which do not trigger caret position changed, + // so we should handle it here + val offset = event.offset + val context = current + if (context != null && !context.isMatch(editor, offset)) { + dismiss() + } + } + if (shouldAutoTrigger) { + invokeLater { + val offset = editor.caretModel.offset + provideInlineCompletion(editor, offset) + } + } + } + + private fun handleLookupItemChanged(editor: Editor, completionInfo: InlineCompletionContext.CompletionInfo) { + if (shouldAutoTrigger) { + val offset = editor.caretModel.offset + provideInlineCompletion(editor, offset, false, completionInfo) + } + } + + private fun buildInlineCompletionParams(requestContext: InlineCompletionContext.Request): InlineCompletionParams { + val documentUri = requestContext.editor.virtualFile.url + val position = positionInDocument(requestContext.document, requestContext.offset) + return InlineCompletionParams( + context = InlineCompletionParams.InlineCompletionContext( + triggerKind = if (requestContext.manually) { + InlineCompletionParams.InlineCompletionContext.InlineCompletionTriggerKind.Invoked + } else { + InlineCompletionParams.InlineCompletionContext.InlineCompletionTriggerKind.Automatic + }, + selectedCompletionInfo = if (requestContext.selectedCompletion != null) InlineCompletionParams.InlineCompletionContext.SelectedCompletionInfo( + requestContext.selectedCompletion.text, + Range( + positionInDocument( + requestContext.document, + requestContext.offset - requestContext.selectedCompletion.prefixLength + ), position + ) + ) else null + ), + textDocument = TextDocumentIdentifier(documentUri), + position = position, + ) + } + + private fun convertInlineCompletionList( + inlineCompletionList: com.tabbyml.intellijtabby.lsp.protocol.InlineCompletionList?, + requestContext: InlineCompletionContext.Request + ): InlineCompletionList? { + return inlineCompletionList?.let { list -> + InlineCompletionList( + isIncomplete = list.isIncomplete, + items = list.items.map { item -> + InlineCompletionItem(insertText = item.insertText, replaceRange = item.range?.let { + InlineCompletionItem.Range( + start = offsetInDocument(requestContext.document, it.start), + end = offsetInDocument(requestContext.document, it.end), + ) + } ?: InlineCompletionItem.Range( + start = requestContext.offset, + end = requestContext.offset, + ), data = InlineCompletionItem.Data( + eventId = item.data?.eventId + )) + }, + ) + } + } + + private fun launchJobForInlineCompletion( + requestContext: InlineCompletionContext.Request, + finishedCallback: (inlineCompletionList: com.tabbyml.intellijtabby.lsp.protocol.InlineCompletionList?) -> Unit = {} + ): Job { + return scope.launch { + val params = buildInlineCompletionParams(requestContext) + val server = getServer() ?: return@launch + logger.debug("Request inline completion: $params") + project.safeSyncPublisher(Listener.TOPIC)?.loadingStateChanged(true) + val inlineCompletionList = try { + server.textDocumentFeature.inlineCompletion(params).await() + } catch (e: Exception) { + if (e is CancellationException) { + null + } else { + logger.warn("Error while requesting inline completion", e) + null + } + } + val context = current + if (requestContext == context?.request) { + logger.debug("Request inline completion done: $inlineCompletionList") + project.safeSyncPublisher(Listener.TOPIC)?.loadingStateChanged(false) + finishedCallback(inlineCompletionList) + } else if (context == null) { + project.safeSyncPublisher(Listener.TOPIC)?.loadingStateChanged(false) + } + } + } + + private fun renderCurrentResponse() { + val context = current ?: return + val editor = context.request.editor + val offset = context.request.offset + val completionItem = context.response?.let { + it.completionList.items.getOrNull(it.itemIndex) + } + if (completionItem == null) { + renderer.hide() + } else { + renderer.show(editor, offset, completionItem) { + telemetryEvent(EventParams.EventType.VIEW, it) + } + } + } + + private fun calcCycleIndex(index: Int, size: Int, direction: CycleDirection): Int { + if (size <= 1) { + return index + } + return when (direction) { + CycleDirection.NEXT -> (index + 1).mod(size) + CycleDirection.PREVIOUS -> (index - 1).mod(size) + } + } + + private fun telemetryEvent( + type: String, renderingContext: InlineCompletionRenderer.RenderingContext, acceptType: AcceptType? = null + ) { + val eventId = renderingContext.completionItem.data?.eventId ?: return + val eventParams = EventParams( + type = type, + selectKind = when (acceptType) { + AcceptType.NEXT_WORD, AcceptType.NEXT_LINE -> EventParams.SelectKind.LINE + else -> null + }, + eventId = eventId, + viewId = renderingContext.id, + elapsed = when (type) { + EventParams.EventType.VIEW -> null + else -> renderingContext.calcElapsed().toInt() + }, + ) + scope.launch { + getServer()?.telemetryFeature?.event(eventParams) + } + } + + fun provideInlineCompletion(editor: Editor, offset: Int, manually: Boolean = false, completionInfo: InlineCompletionContext.CompletionInfo? = null) { + synchronized(currentContextWriteLock) { + logger.debug("Provide inline completion at $editor $offset $manually $completionInfo") + var partialAcceptedResponse: InlineCompletionContext.Response? = null + current?.let { + it.job.cancel() + if (it.partialAccepted) { + partialAcceptedResponse = it.response + } else { + renderer.hide() + } + current = null + } + val requestContext = InlineCompletionContext.Request.from(editor, offset).withManually(manually).withCompletionInfo(completionInfo) + val job = launchJobForInlineCompletion(requestContext) { inlineCompletionList -> + synchronized(currentContextWriteLock) { + current = current?.withResponse(convertInlineCompletionList(inlineCompletionList, requestContext)) + renderCurrentResponse() + } + } + current = InlineCompletionContext(requestContext, job, partialAcceptedResponse) + } + } + + enum class CycleDirection { + NEXT, PREVIOUS, + } + + fun cycle(editor: Editor, offset: Int?, direction: CycleDirection) { + synchronized(currentContextWriteLock) { + logger.debug("Cycle inline completion at $editor $offset $direction") + val context = current ?: return + if (!context.isMatch(editor, offset)) { + return + } + val responseContext = context.response ?: return + val itemIndex = responseContext.itemIndex + if (responseContext.completionList.isIncomplete) { + val requestContext = context.request.withManually() + val job = launchJobForInlineCompletion(requestContext) { inlineCompletionList -> + inlineCompletionList?.let { + synchronized(currentContextWriteLock) { + current = current?.withResponse(convertInlineCompletionList(inlineCompletionList, requestContext)) + ?.withUpdatedItemIndex(calcCycleIndex(itemIndex, it.items.size, direction)) + renderCurrentResponse() + } + } + } + current = InlineCompletionContext(requestContext, job, responseContext) + } else { + current = + current?.withUpdatedItemIndex(calcCycleIndex(itemIndex, responseContext.completionList.items.size, direction)) + renderCurrentResponse() + } + } + } + + enum class AcceptType { + FULL_COMPLETION, NEXT_WORD, NEXT_LINE, + } + + fun accept(editor: Editor, offset: Int?, type: AcceptType) { + synchronized(currentContextWriteLock) { + logger.debug("Accept inline completion at $editor $offset $type") + val context = current ?: return + if (!context.isMatch(editor, offset)) { + return + } + val originOffset = context.request.offset + val completionItem = context.response?.let { + it.completionList.items.getOrNull(it.itemIndex) + } + if (completionItem == null) { + return + } + val prefixReplaceLength = originOffset - completionItem.replaceRange.start + val completionText = completionItem.insertText.substring(prefixReplaceLength) + val text = when (type) { + AcceptType.FULL_COMPLETION -> completionText + AcceptType.NEXT_WORD -> { + Regex("\\w+|\\W+").find(completionText)?.value ?: "" + } + + AcceptType.NEXT_LINE -> { + val lines = completionText.lines() + if (lines.size <= 1) { + completionText + } else lines.first().ifEmpty { + lines.subList(0, 2).joinToString("\n") + } + } + } + renderer.current?.let { + telemetryEvent(EventParams.EventType.SELECT, it, type) + } + if (type == AcceptType.FULL_COMPLETION) { + current = null + renderer.hide() + } else { + current = context.withPartialAccepted(completionItem, text.length) + } + invokeLater { + WriteCommandAction.runWriteCommandAction(editor.project) { + editor.document.replaceString(originOffset, completionItem.replaceRange.end, text) + editor.caretModel.moveToOffset(originOffset + text.length) + } + renderCurrentResponse() + } + } + } + + fun dismiss() { + synchronized(currentContextWriteLock) { + logger.debug("Dismiss inline completion.") + renderer.current?.let { + telemetryEvent(EventParams.EventType.DISMISS, it) + renderer.hide() + } + current?.let { + it.job.cancel() + project.safeSyncPublisher(Listener.TOPIC)?.loadingStateChanged(false) + current = null + } + } + } + + override fun dispose() { + dismiss() + messageBusConnection.dispose() + } + + /** + * @deprecated + * This is not used anymore and should be removed. + */ + interface Listener { + fun loadingStateChanged(loading: Boolean) {} + + companion object { + @Topic.ProjectLevel + val TOPIC = Topic(Listener::class.java, Topic.BroadcastDirection.NONE) + } + } +} diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/editor/CompletionProvider.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/editor/CompletionProvider.kt deleted file mode 100644 index 2219f8380ed8..000000000000 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/editor/CompletionProvider.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.tabbyml.intellijtabby.editor - -import com.intellij.openapi.components.Service -import com.intellij.openapi.components.service -import com.intellij.openapi.diagnostic.Logger -import com.intellij.openapi.editor.Editor -import com.tabbyml.intellijtabby.agent.AgentService -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch - -@Service -class CompletionProvider { - private val logger = Logger.getInstance(CompletionProvider::class.java) - - data class CompletionContext(val editor: Editor, val offset: Int, val job: Job) - - private val ongoingCompletionFlow: MutableStateFlow = MutableStateFlow(null) - val ongoingCompletion = ongoingCompletionFlow.asStateFlow() - - fun provideCompletion(editor: Editor, offset: Int, manually: Boolean = false) { - val agentService = service() - val inlineCompletionService = service() - clear() - val job = agentService.scope.launch { - logger.info("Trigger completion at $offset") - agentService.provideCompletion(editor, offset, manually).let { - ongoingCompletionFlow.value = null - if (it != null) { - logger.info("Show completion at $offset: $it") - inlineCompletionService.show(editor, offset, it) - } - } - } - ongoingCompletionFlow.value = CompletionContext(editor, offset, job) - } - - fun clear() { - val inlineCompletionService = service() - inlineCompletionService.dismiss() - ongoingCompletionFlow.value?.let { - it.job.cancel() - ongoingCompletionFlow.value = null - } - } -} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/editor/EditorListener.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/editor/EditorListener.kt deleted file mode 100644 index cc54de97d9ba..000000000000 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/editor/EditorListener.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.tabbyml.intellijtabby.editor - -import com.intellij.openapi.application.invokeLater -import com.intellij.openapi.components.service -import com.intellij.openapi.diagnostic.Logger -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.editor.event.* -import com.intellij.openapi.fileEditor.FileEditorManager -import com.intellij.openapi.fileEditor.FileEditorManagerEvent -import com.intellij.openapi.fileEditor.FileEditorManagerListener -import com.intellij.util.messages.MessageBusConnection -import com.tabbyml.intellijtabby.settings.ApplicationSettingsState - -class EditorListener : EditorFactoryListener { - private val logger = Logger.getInstance(EditorListener::class.java) - private val messagesConnection = mutableMapOf() - - override fun editorCreated(event: EditorFactoryEvent) { - val editor = event.editor - val editorManager = editor.project?.let { FileEditorManager.getInstance(it) } ?: return - val settings = service() - val completionProvider = service() - - editor.caretModel.addCaretListener(object : CaretListener { - override fun caretPositionChanged(event: CaretEvent) { - logger.debug("CaretListener: caretPositionChanged $event") - if (editorManager.selectedTextEditor == editor) { - completionProvider.ongoingCompletion.value.let { - if (it != null && it.editor == editor && it.offset == editor.caretModel.primaryCaret.offset) { - // keep ongoing completion - logger.debug("Keep ongoing completion.") - } else { - completionProvider.clear() - } - } - } - } - }) - - editor.document.addDocumentListener(object : DocumentListener { - override fun documentChanged(event: DocumentEvent) { - logger.debug("DocumentListener: documentChanged $event") - if (editorManager.selectedTextEditor == editor) { - if (settings.completionTriggerMode == ApplicationSettingsState.TriggerMode.AUTOMATIC) { - completionProvider.ongoingCompletion.value.let { - if (it != null && it.editor == editor && it.offset == editor.caretModel.primaryCaret.offset) { - // keep ongoing completion - logger.debug("Keep ongoing completion.") - } else { - invokeLater { - completionProvider.provideCompletion(editor, editor.caretModel.primaryCaret.offset) - } - } - } - } - } - } - }) - - editor.project?.messageBus?.connect()?.let { - it.subscribe( - FileEditorManagerListener.FILE_EDITOR_MANAGER, - object : FileEditorManagerListener { - override fun selectionChanged(event: FileEditorManagerEvent) { - logger.debug("FileEditorManagerListener: selectionChanged.") - completionProvider.clear() - } - } - ) - messagesConnection[editor] = it - } - } - - override fun editorReleased(event: EditorFactoryEvent) { - messagesConnection[event.editor]?.disconnect() - } -} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/editor/InlineCompletionService.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/editor/InlineCompletionService.kt deleted file mode 100644 index a767213f32a7..000000000000 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/editor/InlineCompletionService.kt +++ /dev/null @@ -1,302 +0,0 @@ -package com.tabbyml.intellijtabby.editor - -import com.intellij.openapi.application.invokeLater -import com.intellij.openapi.command.WriteCommandAction -import com.intellij.openapi.components.Service -import com.intellij.openapi.components.service -import com.intellij.openapi.diagnostic.Logger -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.editor.EditorCustomElementRenderer -import com.intellij.openapi.editor.Inlay -import com.intellij.openapi.editor.colors.EditorFontType -import com.intellij.openapi.editor.impl.FontInfo -import com.intellij.openapi.editor.markup.HighlighterLayer -import com.intellij.openapi.editor.markup.HighlighterTargetArea -import com.intellij.openapi.editor.markup.RangeHighlighter -import com.intellij.openapi.editor.markup.TextAttributes -import com.intellij.openapi.util.Disposer -import com.intellij.openapi.util.TextRange -import com.intellij.ui.JBColor -import com.intellij.util.ui.UIUtil -import com.tabbyml.intellijtabby.agent.Agent -import com.tabbyml.intellijtabby.agent.AgentService -import kotlinx.coroutines.launch -import java.awt.Font -import java.awt.Graphics -import java.awt.Rectangle - -@Service -class InlineCompletionService { - private val logger = Logger.getInstance(InlineCompletionService::class.java) - - data class InlineCompletion( - val editor: Editor, - val offset: Int, - val completion: Agent.CompletionResponse, - val inlays: List>, - val markups: List, - val id: String, - val displayAt: Long, - ) - - var shownInlineCompletion: InlineCompletion? = null - private set - - fun show(editor: Editor, offset: Int, completion: Agent.CompletionResponse) { - dismiss() - if (completion.choices.isEmpty()) { - return - } - invokeLater { - if (editor.caretModel.offset != offset) { - return@invokeLater - } - - // only support multiple choices for now - val choice = completion.choices.first() - logger.info("Showing inline completion at $offset: $choice") - - val prefixReplaceLength = offset - choice.replaceRange.start - val suffixReplaceLength = choice.replaceRange.end - offset - val text = choice.text.substring(prefixReplaceLength) - if (text.isEmpty()) { - return@invokeLater - } - val currentLineNumber = editor.document.getLineNumber(offset) - val currentLineEndOffset = editor.document.getLineEndOffset(currentLineNumber) - if (currentLineEndOffset - offset < suffixReplaceLength) { - return@invokeLater - } - val currentLineSuffix = editor.document.getText(TextRange(offset, currentLineEndOffset)) - - val textLines = text.lines().toMutableList() - - val inlays = mutableListOf>() - val markups = mutableListOf() - if (suffixReplaceLength == 0) { - // No replace range to handle - createInlayText(editor, textLines[0], offset, 0)?.let { inlays.add(it) } - if (textLines.size > 1) { - if (currentLineSuffix.isNotEmpty()) { - markupReplaceText(editor, offset, currentLineEndOffset).let { markups.add(it) } - textLines[textLines.lastIndex] += currentLineSuffix - } - textLines.forEachIndexed { index, line -> - if (index > 0) { - createInlayText(editor, line, offset, index)?.let { inlays.add(it) } - } - } - } - } else if (suffixReplaceLength == 1) { - // Replace range contains one char - val replaceChar = currentLineSuffix[0] - // Insert part is substring of first line that before the char - // Append part is substring of first line that after the char - // If first line doesn't contain the char, insert part is full first line, append part is empty - val insertPart = if (textLines[0].startsWith(replaceChar)) { - "" - } else { - textLines[0].split(replaceChar).first() - } - val appendPart = if (insertPart.length < textLines[0].length) { - textLines[0].substring(insertPart.length + 1) - } else { - "" - } - if (insertPart.isNotEmpty()) { - createInlayText(editor, insertPart, offset, 0)?.let { inlays.add(it) } - } - if (appendPart.isNotEmpty()) { - createInlayText(editor, appendPart, offset + 1, 0)?.let { inlays.add(it) } - } - if (textLines.size > 1) { - if (currentLineSuffix.isNotEmpty()) { - val startOffset = if (insertPart.length < textLines[0].length) { - // First line contains the char - offset + 1 - } else { - // First line doesn't contain the char - offset - } - markupReplaceText(editor, startOffset, currentLineEndOffset).let { markups.add(it) } - textLines[textLines.lastIndex] += currentLineSuffix.substring(1) - } - textLines.forEachIndexed { index, line -> - if (index > 0) { - createInlayText(editor, line, offset, index)?.let { inlays.add(it) } - } - } - } - } else { - // Replace range contains multiple chars - // It's hard to match these chars in the insertion text, we just mark them up - createInlayText(editor, textLines[0], offset, 0)?.let { inlays.add(it) } - markupReplaceText(editor, offset, offset + suffixReplaceLength).let { markups.add(it) } - if (textLines.size > 1) { - if (currentLineSuffix.length > suffixReplaceLength) { - markupReplaceText(editor, offset + suffixReplaceLength, currentLineEndOffset).let { markups.add(it) } - textLines[textLines.lastIndex] += currentLineSuffix.substring(suffixReplaceLength) - } - textLines.forEachIndexed { index, line -> - if (index > 0) { - createInlayText(editor, line, offset, index)?.let { inlays.add(it) } - } - } - } - } - - val cmplId = completion.id.replace("cmpl-", "") - val displayAt = System.currentTimeMillis() - val id = "view-${cmplId}-at-${displayAt}" - shownInlineCompletion = InlineCompletion(editor, offset, completion, inlays, markups, id, displayAt) - - val agentService = service() - agentService.scope.launch { - agentService.postEvent( - Agent.LogEventRequest( - type = Agent.LogEventRequest.EventType.VIEW, - completionId = completion.id, - choiceIndex = completion.choices.first().index, - viewId = id, - ) - ) - } - } - } - - enum class AcceptType { - FULL_COMPLETION, - NEXT_WORD, - NEXT_LINE, - } - - fun accept(type: AcceptType) { - shownInlineCompletion?.let { - val choice = it.completion.choices.first() - logger.info("Accept inline completion at ${it.offset}: $type: $choice") - - val prefixReplaceLength = it.offset - choice.replaceRange.start - val completionText = choice.text.substring(prefixReplaceLength) - val text = when (type) { - AcceptType.FULL_COMPLETION -> completionText - AcceptType.NEXT_WORD -> { - Regex("\\w+|\\W+").find(completionText)?.value ?: "" - } - - AcceptType.NEXT_LINE -> { - val lines = completionText.lines() - if (lines.size <= 1) { - completionText - } else if (lines.first().isEmpty()) { - lines.subList(0, 2).joinToString("\n") - } else { - lines.first() - } - } - } - invokeLater { - WriteCommandAction.runWriteCommandAction(it.editor.project) { - it.editor.document.deleteString(it.offset, choice.replaceRange.end) - it.editor.document.insertString(it.offset, text) - it.editor.caretModel.moveToOffset(it.offset + text.length) - } - it.inlays.forEach(Disposer::dispose) - } - val agentService = service() - agentService.scope.launch { - agentService.postEvent( - Agent.LogEventRequest( - type = Agent.LogEventRequest.EventType.SELECT, - completionId = it.completion.id, - choiceIndex = choice.index, - selectKind = when (type) { - AcceptType.FULL_COMPLETION -> null - AcceptType.NEXT_WORD, AcceptType.NEXT_LINE -> Agent.LogEventRequest.SelectKind.LINE - }, - viewId = it.id, - elapsed = (System.currentTimeMillis() - it.displayAt).toInt(), - ) - ) - } - shownInlineCompletion = null - - // Update inline completion after partial completion - if (type == AcceptType.NEXT_LINE || type == AcceptType.NEXT_WORD) { - invokeLater { - val completionProvider = service() - completionProvider.provideCompletion(it.editor, it.offset + text.length, true) - } - } - } - } - - fun dismiss() { - shownInlineCompletion?.let { - invokeLater { - it.inlays.forEach(Disposer::dispose) - it.markups.forEach { markup -> - it.editor.markupModel.removeHighlighter(markup) - } - } - val choice = it.completion.choices.first() - val agentService = service() - agentService.scope.launch { - agentService.postEvent( - Agent.LogEventRequest( - type = Agent.LogEventRequest.EventType.DISMISS, - completionId = it.completion.id, - choiceIndex = choice.index, - viewId = it.id, - elapsed = (System.currentTimeMillis() - it.displayAt).toInt(), - ) - ) - } - shownInlineCompletion = null - } - } - - private fun createInlayText(editor: Editor, text: String, offset: Int, lineOffset: Int): Inlay<*>? { - val renderer = object : EditorCustomElementRenderer { - override fun getContextMenuGroupId(inlay: Inlay<*>): String { - return "Tabby.InlineCompletionContextMenu" - } - - override fun calcWidthInPixels(inlay: Inlay<*>): Int { - return maxOf(getWidth(inlay.editor, text), 1) - } - - override fun paint(inlay: Inlay<*>, graphics: Graphics, targetRect: Rectangle, textAttributes: TextAttributes) { - graphics.font = getFont(inlay.editor) - graphics.color = JBColor.GRAY - graphics.drawString(text, targetRect.x, targetRect.y + inlay.editor.ascent) - } - - private fun getFont(editor: Editor): Font { - return editor.colorsScheme.getFont(EditorFontType.PLAIN).let { - UIUtil.getFontWithFallbackIfNeeded(it, text).deriveFont(editor.colorsScheme.editorFontSize) - } - } - - private fun getWidth(editor: Editor, line: String): Int { - val font = getFont(editor) - val metrics = FontInfo.getFontMetrics(font, FontInfo.getFontRenderContext(editor.contentComponent)) - return metrics.stringWidth(line) - } - } - return if (lineOffset == 0) { - editor.inlayModel.addInlineElement(offset, true, renderer) - } else { - editor.inlayModel.addBlockElement(offset, true, false, -lineOffset, renderer) - } - } - - private fun markupReplaceText(editor: Editor, startOffset: Int, endOffset: Int): RangeHighlighter { - val textAttributes = TextAttributes().apply { - foregroundColor = JBColor.background() - backgroundColor = JBColor.background() - } - return editor.markupModel.addRangeHighlighter( - startOffset, endOffset, HighlighterLayer.LAST + 1000, textAttributes, HighlighterTargetArea.EXACT_RANGE - ) - } -} diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/events/CaretListener.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/events/CaretListener.kt new file mode 100644 index 000000000000..44769563c2be --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/events/CaretListener.kt @@ -0,0 +1,14 @@ +package com.tabbyml.intellijtabby.events + +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.event.CaretEvent +import com.intellij.util.messages.Topic + +interface CaretListener { + fun caretPositionChanged(editor: Editor, event: CaretEvent) {} + + companion object { + @Topic.ProjectLevel + val TOPIC = Topic(CaretListener::class.java, Topic.BroadcastDirection.NONE) + } +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/events/CombinedState.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/events/CombinedState.kt new file mode 100644 index 000000000000..2344daff3b3c --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/events/CombinedState.kt @@ -0,0 +1,97 @@ +package com.tabbyml.intellijtabby.events + +import com.intellij.openapi.Disposable +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.intellij.util.messages.Topic +import com.tabbyml.intellijtabby.lsp.ConnectionService +import com.tabbyml.intellijtabby.lsp.LanguageClient +import com.tabbyml.intellijtabby.lsp.protocol.Config +import com.tabbyml.intellijtabby.lsp.protocol.StatusInfo +import com.tabbyml.intellijtabby.notifications.hideAuthRequiredNotification +import com.tabbyml.intellijtabby.notifications.notifyAuthRequired +import com.tabbyml.intellijtabby.safeSyncPublisher +import com.tabbyml.intellijtabby.settings.SettingsService + +@Service(Service.Level.PROJECT) +class CombinedState(private val project: Project) : Disposable { + private val messageBusConnection = project.messageBus.connect() + + data class State( + val settings: SettingsService.Settings, + val connectionState: ConnectionService.State, + val agentStatus: StatusInfo?, + val agentConfig: Config?, + ) { + fun withSettings(settings: SettingsService.Settings): State { + return State(settings, connectionState, agentStatus, agentConfig) + } + + fun withConnectionState(connectionState: ConnectionService.State): State { + return State(settings, connectionState, agentStatus, agentConfig) + } + + fun withStatus(status: StatusInfo): State { + return State(settings, connectionState, status, agentConfig) + } + + fun withConfig(config: Config): State { + return State(settings, connectionState, agentStatus, config) + } + } + + var state = State( + service().settings(), + ConnectionService.State.INITIALIZING, + null, + null, + ) + private set + + init { + messageBusConnection.subscribe(SettingsService.Listener.TOPIC, object : SettingsService.Listener { + override fun settingsChanged(settings: SettingsService.Settings) { + state = state.withSettings(settings) + project.safeSyncPublisher(Listener.TOPIC)?.stateChanged(state) + } + }) + messageBusConnection.subscribe(ConnectionService.Listener.TOPIC, object : ConnectionService.Listener { + override fun connectionStateChanged(state: ConnectionService.State) { + this@CombinedState.state = this@CombinedState.state.withConnectionState(state) + project.safeSyncPublisher(Listener.TOPIC)?.stateChanged(this@CombinedState.state) + } + }) + messageBusConnection.subscribe(LanguageClient.StatusListener.TOPIC, object : LanguageClient.StatusListener { + override fun statusChanged(status: StatusInfo) { + state = state.withStatus(status) + project.safeSyncPublisher(Listener.TOPIC)?.stateChanged(state) + + if (status.status == StatusInfo.Status.UNAUTHORIZED) { + notifyAuthRequired() + } else { + hideAuthRequiredNotification() + } + } + }) + messageBusConnection.subscribe(LanguageClient.ConfigListener.TOPIC, object : LanguageClient.ConfigListener { + override fun configChanged(config: Config) { + state = state.withConfig(config) + project.safeSyncPublisher(Listener.TOPIC)?.stateChanged(state) + } + }) + } + + override fun dispose() { + messageBusConnection.dispose() + } + + interface Listener { + fun stateChanged(state: State) {} + + companion object { + @Topic.ProjectLevel + val TOPIC = Topic(Listener::class.java, Topic.BroadcastDirection.NONE) + } + } +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/events/DocumentListener.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/events/DocumentListener.kt new file mode 100644 index 000000000000..0d3965eb8ca9 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/events/DocumentListener.kt @@ -0,0 +1,17 @@ +package com.tabbyml.intellijtabby.events + +import com.intellij.openapi.editor.Document +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.event.DocumentEvent +import com.intellij.util.messages.Topic + +interface DocumentListener { + fun documentOpened(document: Document, editor: Editor) {} + fun documentClosed(document: Document, editor: Editor) {} + fun documentChanged(document: Document, editor: Editor, event: DocumentEvent) {} + + companion object { + @Topic.ProjectLevel + val TOPIC = Topic(DocumentListener::class.java, Topic.BroadcastDirection.NONE) + } +} diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/events/EditorFactoryListener.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/events/EditorFactoryListener.kt new file mode 100644 index 000000000000..be45f95d7e54 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/events/EditorFactoryListener.kt @@ -0,0 +1,63 @@ +package com.tabbyml.intellijtabby.events + +import com.intellij.openapi.Disposable +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.event.CaretEvent +import com.intellij.openapi.editor.event.DocumentEvent +import com.intellij.openapi.editor.event.EditorFactoryEvent +import com.intellij.openapi.editor.event.SelectionEvent +import com.tabbyml.intellijtabby.safeSyncPublisher + +class EditorFactoryListener : com.intellij.openapi.editor.event.EditorFactoryListener { + private val logger = Logger.getInstance(EditorFactoryListener::class.java) + private val listeners = mutableMapOf() + + override fun editorCreated(event: EditorFactoryEvent) { + logger.debug("EditorFactoryListener: editorCreated $event") + val editor = event.editor + val project = editor.project ?: return + project.safeSyncPublisher(DocumentListener.TOPIC)?.documentOpened(editor.document, editor) + + val caretListener = object : com.intellij.openapi.editor.event.CaretListener { + override fun caretPositionChanged(event: CaretEvent) { + logger.debug("CaretListener: caretPositionChanged $editor $event") + project.safeSyncPublisher(CaretListener.TOPIC)?.caretPositionChanged(editor, event) + } + } + + val selectionListener = object : com.intellij.openapi.editor.event.SelectionListener { + override fun selectionChanged(event: SelectionEvent) { + logger.debug("SelectionListener: selectionChanged $editor $event") + project.safeSyncPublisher(SelectionListener.TOPIC)?.selectionChanged(editor, event) + } + } + + val documentListener = object : com.intellij.openapi.editor.event.DocumentListener { + override fun documentChanged(event: DocumentEvent) { + logger.debug("DocumentListener: documentChanged $editor $event") + project.safeSyncPublisher(DocumentListener.TOPIC)?.documentChanged(editor.document, editor, event) + } + } + + editor.caretModel.addCaretListener(caretListener) + editor.selectionModel.addSelectionListener(selectionListener) + editor.document.addDocumentListener(documentListener) + + listeners[editor] = Disposable { + editor.caretModel.removeCaretListener(caretListener) + editor.selectionModel.removeSelectionListener(selectionListener) + editor.document.removeDocumentListener(documentListener) + } + } + + override fun editorReleased(event: EditorFactoryEvent) { + logger.debug("EditorFactoryListener: editorReleased $event") + val editor = event.editor + val project = editor.project ?: return + project.safeSyncPublisher(DocumentListener.TOPIC)?.documentClosed(editor.document, editor) + + listeners[event.editor]?.dispose() + listeners.remove(event.editor) + } +} diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/events/FeaturesState.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/events/FeaturesState.kt new file mode 100644 index 000000000000..f1a895f835d3 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/events/FeaturesState.kt @@ -0,0 +1,54 @@ +package com.tabbyml.intellijtabby.events + +import com.intellij.openapi.Disposable +import com.intellij.openapi.components.Service +import com.intellij.openapi.project.Project +import com.intellij.util.messages.Topic +import com.tabbyml.intellijtabby.lsp.LanguageClient +import com.tabbyml.intellijtabby.safeSyncPublisher + +@Service(Service.Level.PROJECT) +class FeaturesState(private val project: Project) : Disposable { + private val messageBusConnection = project.messageBus.connect() + private val registrations = mutableMapOf>() + + data class Features( + val inlineCompletion: Boolean, + val chat: Boolean, + ) + + val features: Features + get() = Features( + inlineCompletion = registrations.containsKey("textDocument/inlineCompletion"), + chat = registrations.containsKey("tabby/chat"), + ) + + init { + messageBusConnection.subscribe( + LanguageClient.CapabilityRegistrationListener.TOPIC, + object : LanguageClient.CapabilityRegistrationListener { + override fun onRegisterCapability(id: String, method: String, options: Any) { + registrations[method] = Pair(id, options) + project.safeSyncPublisher(Listener.TOPIC)?.stateChanged(features) + } + + override fun onUnregisterCapability(id: String, method: String) { + registrations.remove(method) + project.safeSyncPublisher(Listener.TOPIC)?.stateChanged(features) + } + }) + } + + override fun dispose() { + messageBusConnection.dispose() + } + + interface Listener { + fun stateChanged(features: Features) {} + + companion object { + @Topic.ProjectLevel + val TOPIC = Topic(Listener::class.java, Topic.BroadcastDirection.NONE) + } + } +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/events/SelectionListener.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/events/SelectionListener.kt new file mode 100644 index 000000000000..179ccd12cc28 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/events/SelectionListener.kt @@ -0,0 +1,14 @@ +package com.tabbyml.intellijtabby.events + +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.event.SelectionEvent +import com.intellij.util.messages.Topic + +interface SelectionListener { + fun selectionChanged(editor: Editor, event: SelectionEvent) {} + + companion object { + @Topic.ProjectLevel + val TOPIC = Topic(SelectionListener::class.java, Topic.BroadcastDirection.NONE) + } +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/events/StartupActivity.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/events/StartupActivity.kt new file mode 100644 index 000000000000..f0bf9b14d5e3 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/events/StartupActivity.kt @@ -0,0 +1,24 @@ +package com.tabbyml.intellijtabby.events + +import com.intellij.openapi.components.serviceOrNull +import com.intellij.openapi.project.Project +import com.intellij.openapi.startup.ProjectActivity +import com.tabbyml.intellijtabby.completion.InlineCompletionService +import com.tabbyml.intellijtabby.lsp.ConnectionService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class StartupActivity : ProjectActivity { + override suspend fun execute(project: Project) { + // initialize services + project.serviceOrNull() + project.serviceOrNull() + project.serviceOrNull() + + val connectionService = project.serviceOrNull() + CoroutineScope(Dispatchers.IO).launch { + connectionService?.getServerAsync() + } + } +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/git/DummyGitProvider.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/git/DummyGitProvider.kt new file mode 100644 index 000000000000..14c33fe855dd --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/git/DummyGitProvider.kt @@ -0,0 +1,7 @@ +package com.tabbyml.intellijtabby.git + +class DummyGitProvider : GitProvider { + override fun isSupported(): Boolean { + return false + } +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/git/Git4IdeaProvider.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/git/Git4IdeaProvider.kt new file mode 100644 index 000000000000..418db67396d1 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/git/Git4IdeaProvider.kt @@ -0,0 +1,47 @@ +package com.tabbyml.intellijtabby.git + +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFileManager +import com.tabbyml.intellijtabby.git.GitProvider.Repository +import git4idea.commands.Git +import git4idea.commands.GitCommand +import git4idea.commands.GitLineHandler +import git4idea.repo.GitRepositoryManager + + +class Git4IdeaProvider(private val project: Project) : GitProvider { + private val git = Git.getInstance() + private val virtualFileManager = VirtualFileManager.getInstance() + private val gitRepositoryManger = GitRepositoryManager.getInstance(project) + + override fun isSupported(): Boolean { + return true + } + + override fun getRepository(fileUri: String): Repository? { + val repo = gitRepositoryManger.getRepositoryForFile(virtualFileManager.findFileByUrl(fileUri)) ?: return null + return Repository( + root = repo.root.url, + remotes = repo.remotes.mapNotNull { remote -> + remote.firstUrl?.let { + Repository.Remote( + name = remote.name, + url = it, + ) + } + } + ) + } + + override fun diff(rootUri: String, cached: Boolean): List? { + val root = virtualFileManager.findFileByUrl(rootUri) ?: return null + val handler = GitLineHandler(project, root, GitCommand.DIFF).apply { + if (cached) { + addParameters("--cached") + } + } + val output = git.runCommand(handler).output + // FIXME: sort the diffs + return output.joinToString("\n").split(Regex("\\n(?=diff)")) + } +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/git/GitProvider.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/git/GitProvider.kt new file mode 100644 index 000000000000..2023d2800fb3 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/git/GitProvider.kt @@ -0,0 +1,23 @@ +package com.tabbyml.intellijtabby.git + +interface GitProvider { + fun isSupported(): Boolean + + data class Repository( + val root: String, + val remotes: List?, + ) { + data class Remote( + val name: String, + val url: String, + ) + } + + fun getRepository(fileUri: String): Repository? { + return null + } + + fun diff(rootUri: String, cached: Boolean = false): List? { + return null + } +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/inlineChat/CodeVisionProvider.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/inlineChat/CodeVisionProvider.kt new file mode 100644 index 000000000000..4d7ae5de446b --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/inlineChat/CodeVisionProvider.kt @@ -0,0 +1,155 @@ +package com.tabbyml.intellijtabby.inlineChat + +import com.google.gson.JsonObject +import com.intellij.codeInsight.codeVision.* +import com.intellij.codeInsight.codeVision.CodeVisionState.Companion.READY_EMPTY +import com.intellij.codeInsight.codeVision.settings.CodeVisionGroupSettingProvider +import com.intellij.codeInsight.codeVision.ui.model.TextCodeVisionEntry +import com.intellij.icons.AllIcons +import com.intellij.ide.DataManager +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.ex.ActionUtil +import com.intellij.openapi.components.serviceOrNull +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.keymap.KeymapUtil +import com.intellij.openapi.project.DumbAware +import com.intellij.openapi.util.TextRange +import com.intellij.ui.SpinningProgressIcon +import org.eclipse.lsp4j.Location +import org.eclipse.lsp4j.Position +import org.eclipse.lsp4j.Range +import javax.swing.Icon + +abstract class InlineChatCodeVisionProvider : CodeVisionProvider, DumbAware { + private val logger = Logger.getInstance(InlineChatCodeVisionProvider::class.java) + override val defaultAnchor: CodeVisionAnchorKind = CodeVisionAnchorKind.Top + + // provider id + abstract override val id: String + + override val groupId = "Tabby.InlineEdit" + + // command name + abstract val command: String + + // action name + abstract val action: String? + + // execute action id + abstract val actionId: String? + abstract val icon: Icon + + override fun precomputeOnUiThread(editor: Editor): Any { + return Any() + } + + override fun computeCodeVision(editor: Editor, uiData: Any): CodeVisionState { + val project = editor.project ?: return READY_EMPTY + val inlineChatService = project.serviceOrNull() ?: return READY_EMPTY + val document = editor.document + val virtualFile = FileDocumentManager.getInstance() + .getFile(editor.document) + val uri = virtualFile?.url ?: return READY_EMPTY + val codeLenses = try { + getCodeLenses(project, uri).get() ?: return READY_EMPTY + } catch (e: Exception) { + logger.warn("Failed to get code lenses", e) + return READY_EMPTY + } + val codelens = codeLenses.firstOrNull() { + val codeLensCommand = it.command?.command + val codeLensAction = (it.command?.arguments?.firstOrNull() as JsonObject?)?.get("action")?.asString + codeLensCommand == command && codeLensAction == action + } + if (codelens == null) { + inlineChatService.inlineChatDiffActionState[id] = false + return READY_EMPTY + } + inlineChatService.inlineChatDiffActionState[id] = true + inlineChatService.location = Location( + uri, + Range( + Position(codelens.range.start.line, codelens.range.start.character), + Position(codelens.range.end.line, codelens.range.end.character) + ) + ) + val prefixRegex = Regex("""^\$\(.*?\)""") + val title = codelens.command.title.replace( + prefixRegex, + "" + ) + if (actionId != null) " (${KeymapUtil.getFirstKeyboardShortcutText(getAction(actionId!!))})" else "" + val startOffset = document.getLineStartOffset(codelens.range.start.line) + codelens.range.start.character + val endOffset = document.getLineStartOffset(codelens.range.end.line) + codelens.range.end.character + val entry = + TextCodeVisionEntry(title, id, icon) + val textRange = TextRange(startOffset, endOffset) + textRange to entry + // CodeVisionProvider can only display one entry for each line + return CodeVisionState.Ready(listOf(textRange to entry)) + } + + override fun handleClick(editor: Editor, textRange: TextRange, entry: CodeVisionEntry) { + if (actionId == null) { + return + } + val editorDataContext = DataManager.getInstance().getDataContext(editor.component) + ActionUtil.invokeAction(getAction(actionId!!), editorDataContext, "", null, null) + } + + private fun getAction(actionId: String) = ActionManager.getInstance().getAction(actionId) +} + +class InlineChatLoadingCodeVisionProvider : InlineChatCodeVisionProvider() { + override val id: String = "Tabby.InlineChat.Loading" + override val name ="Tabby Inline Edit Loading" + override val command: String = " " + override val action: String? = null + override val actionId: String? = null + override val icon: Icon = SpinningProgressIcon() + override val relativeOrderings: List = + listOf(CodeVisionRelativeOrdering.CodeVisionRelativeOrderingBefore("Tabby.InlineChat.Cancel")) +} + +class InlineChatCancelCodeVisionProvider : InlineChatCodeVisionProvider() { + override val id: String = "Tabby.InlineChat.Cancel" + override val name ="Tabby Inline Edit Cancel" + override val command: String = "tabby/chat/edit/resolve" + override val action: String = "cancel" + override val actionId: String = "Tabby.InlineChat.Resolve.Cancel" + override val icon: Icon = AllIcons.Actions.Cancel + override val relativeOrderings: List = + emptyList() +} + +class InlineChatAcceptCodeVisionProvider : InlineChatCodeVisionProvider() { + override val id: String = "Tabby.InlineChat.Accept" + override val name ="Tabby Inline Edit Accept" + override val command: String = "tabby/chat/edit/resolve" + override val action: String? = "accept" + override val actionId: String = "Tabby.InlineChat.Resolve.Accept" + override val icon: Icon = AllIcons.Actions.Checked + override val relativeOrderings: List = + listOf(CodeVisionRelativeOrdering.CodeVisionRelativeOrderingBefore("Tabby.InlineChat.Discard")) +} + +class InlineChatDiscardCodeVisionProvider : InlineChatCodeVisionProvider() { + override val id: String = "Tabby.InlineChat.Discard" + override val name ="Tabby Inline Edit Discard" + override val command: String = "tabby/chat/edit/resolve" + override val action: String = "discard" + override val actionId: String = "Tabby.InlineChat.Resolve.Discard" + override val icon: Icon = AllIcons.Actions.Close + override val relativeOrderings: List = + emptyList() +} + +class InlineChatCodeVisionSettingProvider: CodeVisionGroupSettingProvider { + override val groupId: String + get() = "Tabby.InlineEdit" + override val groupName: String + get() = "Tabby Inline Edit" + override val description: String + get() = "Tabby Inline Edit" +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/inlineChat/CommandHistory.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/inlineChat/CommandHistory.kt new file mode 100644 index 000000000000..ca81ed328d32 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/inlineChat/CommandHistory.kt @@ -0,0 +1,42 @@ +package com.tabbyml.intellijtabby.inlineChat + +import com.intellij.openapi.components.* + +class CommandHistoryState : BaseState() { + var history by list() +} + +@Service +@State( + name = "com.tabbyml.intellijtabby.inlineChat.CommandHistory", + storages = [Storage("intellij-tabby-command-history.xml")] +) +class CommandHistory : SimplePersistentStateComponent(CommandHistoryState()) { + private val maxHistorySize = 30 + + fun getHistory(): List { + return state.history.toList() + } + + fun addCommand(command: String) { + val existingIndex = state.history.indexOfFirst { it == command } + + if (existingIndex != -1) { + state.history.removeAt(existingIndex) + } + + state.history.add(0, command) + + if (state.history.size > maxHistorySize) { + state.history = state.history.take(maxHistorySize).toMutableList() + } + } + + fun deleteCommand(value: String) { + state.history.removeAll { it == value } + } + + fun clearHistory() { + state.history.clear() + } +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/inlineChat/CommandListComponent.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/inlineChat/CommandListComponent.kt new file mode 100644 index 000000000000..edf49b94273a --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/inlineChat/CommandListComponent.kt @@ -0,0 +1,240 @@ +package com.tabbyml.intellijtabby.inlineChat + +import com.intellij.icons.AllIcons +import com.intellij.openapi.application.ApplicationManager +import com.intellij.ui.CollectionListModel +import com.intellij.ui.components.IconLabelButton +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBList +import com.intellij.ui.components.JBScrollPane +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.UIUtil +import java.awt.* +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent +import java.util.function.Consumer +import javax.swing.* + + +data class CommandListItem( + var label: String, + var value: String, + var icon: Icon, + var description: String?, + val canDelete: Boolean +) + +class CommandListComponent( + private val title: String = "Commands", + initialData: List?, + private val onItemSelected: Consumer?, + private val onItemDeleted: Consumer?, + private val onClearAll: () -> Unit, +) { + val list: JBList + private val model: CollectionListModel = CollectionListModel(initialData ?: emptyList()) + private val scrollPane: JBScrollPane + private val mainPanel: JPanel = JPanel(BorderLayout()) + + private var hoveredIndex: Int = -1 + + init { + list = JBList(model) + list.cellRenderer = CustomListItemRenderer { hoveredIndex } + list.selectionMode = ListSelectionModel.SINGLE_SELECTION + list.setEmptyText("No items") + + list.addMouseListener(object : MouseAdapter() { + override fun mouseClicked(e: MouseEvent) { + handleMouseAction(e) + } + + override fun mouseExited(e: MouseEvent) { + if (hoveredIndex != -1) { + hoveredIndex = -1 + list.repaint() + } + } + }) + + list.addMouseMotionListener(object : MouseAdapter() { + override fun mouseMoved(e: MouseEvent) { + val index = list.locationToIndex(e.point) + if (index != hoveredIndex) { + hoveredIndex = index + list.repaint() + } + } + }) + val toolbar = createToolbar() + scrollPane = JBScrollPane(list) + mainPanel.add(toolbar, BorderLayout.NORTH) + mainPanel.add(scrollPane, BorderLayout.CENTER) + } + + private fun createToolbar(): JPanel { + val toolbar = JPanel(BorderLayout()).apply { + preferredSize = Dimension(730, 20) + } + toolbar.border = JBUI.Borders.empty(3, 5) + val titleLabel = JBLabel(title) + titleLabel.font = JBUI.Fonts.label().deriveFont(JBUI.Fonts.label().size + 1.0f) +// val clearAllButton = IconLabelButton(AllIcons.Actions.GC) { +// onClearAll() +// } +// clearAllButton.toolTipText = "Clear all commands" +// clearAllButton.cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) +// clearAllButton.border = BorderFactory.createEmptyBorder(4, 8, 4, 8) + + toolbar.add(titleLabel, BorderLayout.WEST) +// toolbar.add(clearAllButton, BorderLayout.EAST) + + return toolbar + } + + private fun handleMouseAction(e: MouseEvent) { + val index = list.locationToIndex(e.point) + if (index != -1) { + val item = model.getElementAt(index) + val deleteBounds = CustomListItemRenderer.getDeleteButtonBounds(list, index, item) + + if (deleteBounds != null && deleteBounds.contains(e.point)) { + e.consume() // Prevent list selection change on delete click + onItemDeleted?.accept(item) + } else { + val selectedValue = list.selectedValue + if (selectedValue != null) { + onItemSelected?.accept(selectedValue) + } + } + } + } + + fun setData(newData: List, onUpdated: (() -> Unit)? = null) { + ApplicationManager.getApplication().invokeLater { + val selectedValue = list.selectedValue + model.replaceAll(newData) + if (selectedValue != null) { + val newIndex = model.getElementIndex(selectedValue) + if (newIndex != -1) { + list.setSelectedIndex(newIndex) + } else { + list.clearSelection() + } + } else { + list.clearSelection() + } + onUpdated?.invoke() + } + } + + val component: JComponent + get() = mainPanel + + fun dispose() { + mainPanel.removeAll() + scrollPane.viewport.view = null + list.model = CollectionListModel() + } +} + + +class CustomListItemRenderer(private val getHoveredIndex: () -> Int) : JPanel(), ListCellRenderer { + private val iconLabel: JBLabel + private val contentPanel: JPanel + private val label: JLabel + private val desc: JLabel + private val deleteButton: JLabel + + init { + layout = BorderLayout(JBUI.scale(5), 0) + border = JBUI.Borders.empty(2, 5) + + iconLabel = JBLabel() + label = JLabel() + desc = JLabel() + contentPanel = JPanel(BorderLayout()) + contentPanel.add(desc, BorderLayout.CENTER) + contentPanel.add(label, BorderLayout.WEST) + deleteButton = JLabel(DELETE_ICON).apply { + isOpaque = false + toolTipText = "delete command" + preferredSize = + Dimension(DELETE_ICON.iconWidth, DELETE_ICON.iconHeight) + } + + add(deleteButton, BorderLayout.EAST) + add(iconLabel, BorderLayout.WEST) + add(contentPanel, BorderLayout.CENTER) + } + + override fun getListCellRendererComponent( + list: JList, + value: CommandListItem, + index: Int, + isSelected: Boolean, + cellHasFocus: Boolean + ): Component { + iconLabel.icon = value.icon + label.text = value.label + desc.text = value.description + label.border = BorderFactory.createEmptyBorder(0, 10, 0, 10) + desc.foreground = UIUtil.getContextHelpForeground() + contentPanel.isOpaque = false + + val isHovered = index == getHoveredIndex() + + if (isSelected) { + background = UIUtil.getListSelectionBackground(true) // Use focus-aware color + iconLabel.foreground = UIUtil.getListSelectionForeground(true) + } else if (isHovered) { + background = UIUtil.getListSelectionBackground(false) + iconLabel.foreground = UIUtil.getListForeground() + cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) + } else { + background = UIUtil.getListBackground() + iconLabel.foreground = UIUtil.getListForeground() + cursor = Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR) + } + + isOpaque = true + + if (value.canDelete) { + deleteButton.isVisible = true + if (isHovered) { + deleteButton.setIcon(DELETE_ICON_HOVERED); + } else { + deleteButton.setIcon(DELETE_ICON); + } + } else { + deleteButton.isVisible = false + } + + return this + } + + companion object { + val DELETE_ICON: Icon = AllIcons.Actions.Close + val DELETE_ICON_HOVERED: Icon = AllIcons.Actions.CloseHovered + + fun getDeleteButtonBounds(list: JList, index: Int, item: CommandListItem): Rectangle? { + if (index < 0 || index >= list.model.size) { + return null + } + val cellBounds = list.getCellBounds(index, index) ?: return null + val renderer = CustomListItemRenderer { index } + renderer.getListCellRendererComponent(list, item, index, true, false) + val prefSize = renderer.preferredSize + renderer.setBounds(0, 0, cellBounds.width, prefSize.height) + renderer.doLayout() + val deleteBoundsRelativeToPanel = renderer.deleteButton.bounds + return Rectangle( + cellBounds.x + deleteBoundsRelativeToPanel.x, + cellBounds.y + deleteBoundsRelativeToPanel.y, + deleteBoundsRelativeToPanel.width, + deleteBoundsRelativeToPanel.height + ) + } + } +} + diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/inlineChat/DiffHighLightingPass.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/inlineChat/DiffHighLightingPass.kt new file mode 100644 index 000000000000..9d7383800e68 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/inlineChat/DiffHighLightingPass.kt @@ -0,0 +1,150 @@ +package com.tabbyml.intellijtabby.inlineChat + +import com.google.gson.JsonObject +import com.intellij.codeHighlighting.* +import com.intellij.codeInsight.daemon.impl.HighlightInfo +import com.intellij.codeInsight.daemon.impl.HighlightInfoType +import com.intellij.codeInsight.daemon.impl.UpdateHighlightersUtil +import com.intellij.lang.annotation.HighlightSeverity +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.editor.Document +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.colors.EditorColors +import com.intellij.openapi.editor.colors.EditorColorsManager +import com.intellij.openapi.editor.markup.TextAttributes +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.project.DumbAware +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiFile +import org.eclipse.lsp4j.CodeLens +import java.awt.Color +import java.awt.Font + +class DiffHighlighterRegister : TextEditorHighlightingPassFactoryRegistrar { + override fun registerHighlightingPassFactory(register: TextEditorHighlightingPassRegistrar, project: Project) { + register.registerTextEditorHighlightingPass( + DiffHighlightingPassFactory(), TextEditorHighlightingPassRegistrar.Anchor.FIRST, + Pass.EXTERNAL_TOOLS, false, false + ) + } +} + +class DiffHighlightingPassFactory : TextEditorHighlightingPassFactory { + override fun createHighlightingPass(file: PsiFile, editor: Editor): TextEditorHighlightingPass? { + if (!file.isValid) { + return null + } + return DiffHighLightingPass(file.project, editor.document, editor) + } +} + +class DiffHighLightingPass(project: Project, document: Document, val editor: Editor) : + TextEditorHighlightingPass(project, document, true), DumbAware { + + private val logger = Logger.getInstance(DiffHighLightingPass::class.java) + + private var lenses = emptyList() + private val file = FileDocumentManager.getInstance().getFile(myDocument) + private val highlights = mutableListOf() + private var lineAttributesMap = emptyMap() + private var textAttributesMap = emptyMap() + + init { + colorsScheme = EditorColorsManager.getInstance().globalScheme + val headerColor = Color(64f / 255, 166f / 255, 1f, 0.5f) + val insertColor = Color((155f / 255), 185f / 255, 85f / 255, 0.2f) + lineAttributesMap = mapOf( + "header" to TextAttributes( + null, + headerColor, + null, + null, + 0 + ), + "footer" to TextAttributes( + null, + headerColor, + null, + null, + 0 + ), + "commentsFirstLine" to TextAttributes( + null, + colorsScheme?.getColor(EditorColors.DOCUMENTATION_COLOR), + null, + null, + Font.ITALIC + ), + "comments" to TextAttributes( + null, + colorsScheme?.getColor(EditorColors.DOCUMENTATION_COLOR), + null, + null, + Font.ITALIC + ), + "waiting" to TextAttributes( + null, + colorsScheme?.getColor(EditorColors.TAB_UNDERLINE_INACTIVE), + null, + null, + 0 + ), + "inProgress" to TextAttributes(null, insertColor, null, null, 0), + "unchanged" to TextAttributes(null, null, null, null, 0), + "inserted" to TextAttributes(null, insertColor, null, null, 0), + "deleted" to TextAttributes(null, Color(1f, 0f, 0f, 0.2f), null, null, 0), + ) + + textAttributesMap = mapOf( + "inserted" to TextAttributes(null, Color(156f / 255, 204f / 255, 44f / 255, 0.2f), null, null, 0), + "deleted" to TextAttributes( + null, + Color(1f, 0f, 0f, 0.2f), + null, + null, + 0 + ), + ) + } + + override fun doCollectInformation(progress: ProgressIndicator) { + val uri = file?.url ?: return + lenses = getCodeLenses(myProject, uri).get() ?: emptyList() + logger.debug("Lens: $lenses") + for (lens in lenses) { + if ((lens.data as JsonObject?)?.get("type")?.asString != "previewChanges") continue + val range = lens.range + val startOffset = myDocument.getLineStartOffset(range.start.line) + range.start.character + val lineType = (lens.data as JsonObject?)?.get("line")?.asString + var endOffset = myDocument.getLineStartOffset(range.end.line) + range.end.character + if (lineType != null) { + endOffset = myDocument.getLineStartOffset(range.end.line + 1) + } + val textRange = TextRange(startOffset, endOffset) + var attributes = TextAttributes(null, null, null, null, 0) + if (lineType != null) { + attributes = lineAttributesMap.get(lineType) ?: continue + } + val textType = (lens.data as JsonObject?)?.get("text")?.asString + if (textType != null) { + attributes = textAttributesMap.get(textType) ?: continue + } + val builder = HighlightInfo.newHighlightInfo(HighlightInfoType.INFORMATION) + .range(textRange) + .textAttributes(attributes) + .descriptionAndTooltip("Tabby inline diff") + .severity(HighlightSeverity.TEXT_ATTRIBUTES) + val highlight = builder.create() ?: continue + highlights.add(highlight) + } + } + + override fun doApplyInformationToEditor() { + UpdateHighlightersUtil.setHighlightersToEditor( + myProject, myDocument, 0, myDocument.textLength, + highlights, colorsScheme, id + ) + } +} diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/inlineChat/InlineChatAction.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/inlineChat/InlineChatAction.kt new file mode 100644 index 000000000000..e6a8a02887ae --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/inlineChat/InlineChatAction.kt @@ -0,0 +1,69 @@ +package com.tabbyml.intellijtabby.inlineChat + +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.components.serviceOrNull +import com.intellij.openapi.project.DumbAwareAction +import com.tabbyml.intellijtabby.actions.chat.isChatFeatureEnabled +import com.tabbyml.intellijtabby.lsp.ConnectionService +import com.tabbyml.intellijtabby.lsp.protocol.ChatEditResolveParams +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +abstract class BaseInlineChatAction : DumbAwareAction() { + override fun update(e: AnActionEvent) { + val project = e.project + e.presentation.isEnabled = isChatFeatureEnabled(project) + } +} + +class InlineChatAction : BaseInlineChatAction() { + override fun actionPerformed(e: AnActionEvent) { + val editor = e.getRequiredData(CommonDataKeys.EDITOR) + val project = e.project ?: return + InlineChatIntentionAction().invoke(project, editor, null) + } +} + +class InlineChatAcceptAction : BaseInlineChatAction() { + private val scope = CoroutineScope(Dispatchers.IO) + + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + val inlineChatService = project.serviceOrNull() ?: return + val location = inlineChatService.location ?: return + scope.launch { + val server = project.serviceOrNull()?.getServerAsync() ?: return@launch + server.chatFeature.resolveEdit(ChatEditResolveParams(location = location, action = "accept")) + } + } +} + +class InlineChatDiscardAction : BaseInlineChatAction() { + private val scope = CoroutineScope(Dispatchers.IO) + + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + val inlineChatService = project.serviceOrNull() ?: return + val location = inlineChatService.location ?: return + scope.launch { + val server = project.serviceOrNull()?.getServerAsync() ?: return@launch + server.chatFeature.resolveEdit(ChatEditResolveParams(location = location, action = "discard")) + } + } +} + +class InlineChatCancelAction : BaseInlineChatAction() { + private val scope = CoroutineScope(Dispatchers.IO) + + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + val inlineChatService = project.serviceOrNull() ?: return + val location = inlineChatService.location ?: return + scope.launch { + val server = project.serviceOrNull()?.getServerAsync() ?: return@launch + server.chatFeature.resolveEdit(ChatEditResolveParams(location = location, action = "cancel")) + } + } +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/inlineChat/InlineChatIntentionAction.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/inlineChat/InlineChatIntentionAction.kt new file mode 100644 index 000000000000..6d03117cb904 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/inlineChat/InlineChatIntentionAction.kt @@ -0,0 +1,451 @@ +package com.tabbyml.intellijtabby.inlineChat + +import com.intellij.codeInsight.intention.impl.BaseIntentionAction +import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo +import com.intellij.icons.AllIcons +import com.intellij.ide.ui.LafManagerListener +import com.intellij.openapi.components.serviceOrNull +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.EditorCustomElementRenderer +import com.intellij.openapi.editor.Inlay +import com.intellij.openapi.editor.colors.EditorColorsManager +import com.intellij.openapi.editor.colors.EditorColorsScheme +import com.intellij.openapi.editor.markup.TextAttributes +import com.intellij.openapi.project.DumbAware +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.Messages +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.psi.PsiFile +import com.intellij.ui.components.IconLabelButton +import com.intellij.ui.components.JBTextArea +import com.intellij.util.ui.UIUtil +import com.tabbyml.intellijtabby.lsp.ConnectionService +import com.tabbyml.intellijtabby.lsp.protocol.ChatEditParams +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.eclipse.lsp4j.Range +import java.awt.* +import java.awt.event.FocusAdapter +import java.awt.event.FocusEvent +import java.awt.event.KeyEvent +import java.awt.event.KeyListener +import javax.swing.BorderFactory +import javax.swing.JLabel +import javax.swing.JPanel +import javax.swing.JTextArea +import com.intellij.codeInsight.codeVision.settings.CodeVisionSettings +import com.intellij.codeInsight.hints.settings.InlaySettingsConfigurable +import com.intellij.openapi.options.ShowSettingsUtil +import com.tabbyml.intellijtabby.actions.chat.isChatFeatureEnabled + +class InlineChatIntentionAction : BaseIntentionAction(), DumbAware { + private var inlay: Inlay? = null + private var inlayRender: InlineChatInlayRenderer? = null + private var project: Project? = null + private var editor: Editor? = null + override fun getFamilyName(): String { + return "Tabby" + } + + override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean { + return editor != null && isChatFeatureEnabled(project); + } + + override fun invoke(project: Project, editor: Editor?, file: PsiFile?) { + val inlineChatService = project.serviceOrNull() ?: return + if (inlineChatService.inlineChatInputVisible || inlineChatService.hasDiffAction) return + this.project = project + this.editor = editor + if (editor != null) { + val locationInfo = getCurrentLocation(editor = editor) + inlineChatService.location = locationInfo.location + inlineChatService.inlineChatInputVisible = true + addInputToEditor(project, editor, locationInfo.startOffset); + } + + // listen for theme change + project.messageBus.connect().subscribe(LafManagerListener.TOPIC, LafManagerListener { + inlayRender?.repaint() // FIXME + }) + } + + override fun getText(): String { + return "Open Tabby inline edit"; + } + + override fun generatePreview(project: Project, editor: Editor, file: PsiFile): IntentionPreviewInfo { + return IntentionPreviewInfo.EMPTY + } + + private fun addInputToEditor(project: Project, editor: Editor, offset: Int) { + val inlayModel = editor.inlayModel + inlayRender = InlineChatInlayRenderer(project, editor, this::onClose, this::onInputSubmit) + inlay = inlayModel.addBlockElement(offset, true, true, 0, inlayRender!!) + } + + private fun onClose() { + inlay?.dispose() + val inlineChatService = project?.serviceOrNull() ?: return + inlineChatService.inlineChatInputVisible = false + } + + private fun onInputSubmit(value: String) { + chatEdit(command = value) + editor?.selectionModel?.removeSelection() + project?.serviceOrNull()?.addCommand(value) + } + + private fun chatEdit(command: String) { + val scope = CoroutineScope(Dispatchers.IO) + val inlineChatService = project?.serviceOrNull() ?: return + scope.launch { + val server = project?.serviceOrNull()?.getServerAsync() ?: return@launch + val location = inlineChatService.location ?: return@launch + val param = ChatEditParams( + location = location, + command = command + ) + server.chatFeature.chatEdit(params = param) + } + } +} + +class InlineChatInlayRenderer( + private val project: Project, + private val editor: Editor, + private val onClose: () -> Unit, + private val onSubmit: (value: String) -> Unit +) : + EditorCustomElementRenderer { + private val inlineChatComponent = InlineChatComponent(project, this::handleClose, onSubmit) + private var targetRegion: Rectangle? = null + private var disposed = false + + private val keyEventHandler = object : KeyEventDispatcher { + override fun dispatchKeyEvent(e: KeyEvent): Boolean { + if (e.id == KeyEvent.KEY_PRESSED && (e.keyCode == KeyEvent.VK_BACK_SPACE || e.keyCode == KeyEvent.VK_DELETE)) { + if (e.component is JBTextArea) { + // Return true to consume the event (prevent default handling) + return true + } + } + if (e.id == KeyEvent.KEY_PRESSED && e.keyCode == KeyEvent.VK_ESCAPE) { + handleClose() + return true + } + return false + } + } + + init { + KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(keyEventHandler) + } + + + override fun calcWidthInPixels(inlay: Inlay<*>): Int { + return inlineChatComponent.preferredSize.width + } + + override fun calcHeightInPixels(inlay: Inlay<*>): Int { + return inlineChatComponent.preferredSize.height + } + + override fun paint(inlay: Inlay<*>, g: Graphics, targetRegion: Rectangle, textAttributes: TextAttributes) { + if (disposed) { + return + } + val visibleArea = editor.scrollingModel.visibleArea + if (this.targetRegion == null) { + this.targetRegion = targetRegion + this.targetRegion?.y = targetRegion.y + visibleArea.y + } + val firstTargetRegion = this.targetRegion ?: targetRegion + inlineChatComponent.setSize(firstTargetRegion.width, firstTargetRegion.height) + inlineChatComponent.setLocation(firstTargetRegion.x, firstTargetRegion.y) + + if (inlineChatComponent.parent == null) { + editor.contentComponent.add(inlineChatComponent) + inlineChatComponent.requestFocus() + } + } + + fun repaint() { + inlineChatComponent.repaint() + } + + private fun handleClose() { + this.dispose() + onClose() + } + + + private fun dispose() { + if (disposed) { + return + } + KeyboardFocusManager.getCurrentKeyboardFocusManager().removeKeyEventDispatcher(keyEventHandler) + inlineChatComponent.parent?.remove(inlineChatComponent) + editor.contentComponent.remove(inlineChatComponent) + editor.contentComponent.repaint(); + this.disposed = true + } +} + +class InlineChatComponent( + private val project: Project, + private val onClose: () -> Unit, + private val onSubmit: (value: String) -> Unit +) : JPanel() { + private val closeButton = createCloseButton() + private val inlineInput = InlineInputComponent(project, this::handleSubmit, this::handleClose) + + override fun isOpaque(): Boolean { + return false; + } + + private fun getTheme(): EditorColorsScheme { + return EditorColorsManager.getInstance().globalScheme; + } + + init { + layout = BorderLayout() + putClientProperty(UIUtil.HIDE_EDITOR_FROM_DATA_CONTEXT_PROPERTY, true) + + val contentPanel = JPanel(BorderLayout()) + contentPanel.add(inlineInput, BorderLayout.CENTER) + contentPanel.add(closeButton, BorderLayout.EAST) + contentPanel.border = BorderFactory.createEmptyBorder(6, 6, 6, 6) + + add(contentPanel, BorderLayout.CENTER) + + border = BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(getTheme().defaultBackground, 3, true), + BorderFactory.createEmptyBorder(4, 8, 4, 8) + ) + + minimumSize = Dimension(200, 40) + preferredSize = Dimension(800, 60) + } + + private fun handleClose(): Unit { + onClose() + } + + private fun createCloseButton(): JLabel { + val closeButton = IconLabelButton(AllIcons.Actions.Close) { handleClose() } + closeButton.toolTipText = "Close Tabby inline edit" + closeButton.cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) + closeButton.border = BorderFactory.createEmptyBorder(8, 8, 8, 8) + return closeButton + } + + private fun handleSubmit(value: String) { + onClose() + onSubmit(value) + } + + override fun requestFocus() { + inlineInput.requestFocus() + } +} + +data class ChatEditFileContext(val referrer: String, val uri: String, val range: Range) +data class InlineEditCommand(val command: String, val context: List?) + +class InlineInputComponent( + private var project: Project, + private var onSubmit: (value: String) -> Unit, + private var onCancel: () -> Unit +) : JPanel() { + private val logger = Logger.getInstance(InlineInputComponent::class.java) + private val history: CommandHistory? = project.serviceOrNull() + private val textArea: JTextArea = createTextArea() + private val submitButton: JLabel = createSubmitButton() + private val historyButton: JLabel = createHistoryButton() + private var commandListComponent: CommandListComponent? = null + + init { + putClientProperty(UIUtil.HIDE_EDITOR_FROM_DATA_CONTEXT_PROPERTY, true) + layout = BorderLayout() + add(historyButton, BorderLayout.WEST) + add(textArea, BorderLayout.CENTER) + add(submitButton, BorderLayout.EAST) + border = BorderFactory.createLineBorder(UIUtil.getHeaderInactiveColor(), 2, true) + + addKeyListener(object : KeyListener { + override fun keyPressed(e: KeyEvent) { + e.consume() + } + + override fun keyReleased(e: KeyEvent) { + if (e.keyCode == KeyEvent.VK_BACK_SPACE || e.keyCode == KeyEvent.VK_DELETE) { + e.consume() + } + } + + override fun keyTyped(e: KeyEvent) { + e.consume() + } + }) + } + + private fun createTextArea(): JBTextArea { + val textArea = JBTextArea().apply { + font = Font(font.family, font.style, 14) + } + textArea.lineWrap = false + textArea.rows = 1 + textArea.columns = 30 + textArea.emptyText.text = "Enter the command to editing" + textArea.border = BorderFactory.createEmptyBorder(6, 4, 4, 4) + // To prevent keystrokes(backspace, delete) being handled by the host editor, https://intellij-support.jetbrains.com/hc/en-us/community/posts/360010505760-Issues-embedding-editor-in-block-inlay + textArea.putClientProperty(UIUtil.HIDE_EDITOR_FROM_DATA_CONTEXT_PROPERTY, true) + textArea.addKeyListener(object : KeyListener { + override fun keyPressed(e: KeyEvent) { + // + } + + override fun keyReleased(e: KeyEvent) { + if (e.keyCode == KeyEvent.VK_BACK_SPACE || e.keyCode == KeyEvent.VK_DELETE) { + e.consume() + } + + if (e.keyCode == KeyEvent.VK_ENTER) { + handleConfirm() + } + + if (e.keyCode == KeyEvent.VK_ESCAPE) { + // Handle escape + textArea.text = "" + onCancel() + } + } + + override fun keyTyped(e: KeyEvent) { + // + } + }) + textArea.addFocusListener(object : FocusAdapter() { + override fun focusGained(e: FocusEvent) { + border = BorderFactory.createLineBorder(UIUtil.getFocusedBorderColor(), 2, true) + } + + override fun focusLost(e: FocusEvent) { + border = BorderFactory.createLineBorder(UIUtil.getHeaderInactiveColor(), 2, true) + } + }) + return textArea + } + + override fun requestFocus() { + textArea.requestFocus() + } + + private fun handleConfirm() { + val codeVisionSettings = CodeVisionSettings.instance() + if (!codeVisionSettings.isProviderEnabled("Tabby.InlineEdit")) { + val result = Messages.showOkCancelDialog( + project, + "Tabby Inline Edit Code Vision feature is not enabled. Please enable it in Settings > Editor > Inlay Hint > Code Vision > Tabby Inline Edit.", + "Tabby Inline Edit Code Vision Disabled", + "Open Settings", + "Cancel", + Messages.getWarningIcon() + ) + if (result == Messages.OK) { + ShowSettingsUtil.getInstance().showSettingsDialog(project, InlaySettingsConfigurable::class.java) + } + textArea.text = textArea.text.trim() + return + } + onSubmit(textArea.text.trim()) + textArea.text = "" + } + + private fun createSubmitButton(): JLabel { + val submitButton = IconLabelButton(AllIcons.Chooser.Right) { handleConfirm() } + submitButton.toolTipText = "Submit the command" + submitButton.cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) + submitButton.border = BorderFactory.createEmptyBorder(4, 8, 4, 8) + return submitButton + } + + private fun createHistoryButton(): JLabel { + val historyButton = IconLabelButton(AllIcons.Actions.SearchWithHistory) { handleOpenHistory() } + historyButton.toolTipText = "Select suggested / history Command" + historyButton.cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) + historyButton.border = BorderFactory.createEmptyBorder(4, 8, 4, 8) + return historyButton + } + + private fun handleOpenHistory() { + val commandItems = getCommandList() + var popup: com.intellij.openapi.ui.popup.JBPopup? = null + commandListComponent = CommandListComponent("Select Command", commandItems, { + textArea.text = it.value + popup?.cancel() + }, { + history?.deleteCommand(it.value) + refreshCommandList() { + popup?.pack(true, true) + } + }, { + history?.clearHistory() + refreshCommandList() { + popup?.pack(true, true) + } + }) + popup = + JBPopupFactory.getInstance().createComponentPopupBuilder(commandListComponent?.component!!, JPanel()) + .createPopup() + popup.showUnderneathOf(this) + } + + private fun refreshCommandList(onUpdated: (() -> Unit)? = null) { + val commandItems = getCommandList() + commandListComponent?.setData(commandItems, onUpdated) + } + + private fun getHistoryCommand(): List { + return history?.getHistory()?.map { + InlineEditCommand(it, null) + } ?: emptyList() + } + + private fun getCommandList(): List { + val location = project.serviceOrNull()?.location ?: return emptyList() + val suggestedItems = try { + getSuggestedCommands(project, location).get()?.map { + CommandListItem( + label = it.label, + value = it.command, + icon = AllIcons.Actions.IntentionBulbGrey, + description = it.command, + canDelete = false + ) + } ?: emptyList() + } catch (e: Exception) { + logger.warn("Error getting suggested commands", e) + emptyList() + } + val historyItems = getHistoryCommand().filter { historyCommand -> + suggestedItems.find { + it.value == historyCommand.command.replace( + "\n", + "" + ) + } == null + }.map { + CommandListItem( + label = it.command.replace("\n", ""), + value = it.command.replace("\n", ""), + icon = AllIcons.Vcs.History, + description = null, + canDelete = true + ) + } + return suggestedItems + historyItems + } +} diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/inlineChat/InlineChatService.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/inlineChat/InlineChatService.kt new file mode 100644 index 000000000000..092313abe4fe --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/inlineChat/InlineChatService.kt @@ -0,0 +1,23 @@ +package com.tabbyml.intellijtabby.inlineChat + +import com.intellij.openapi.Disposable +import com.intellij.openapi.components.Service +import com.intellij.openapi.project.Project +import org.eclipse.lsp4j.Location + +@Service(Service.Level.PROJECT) +class InlineChatService(private val project: Project) : Disposable { + + var inlineChatInputVisible = false + var inlineChatDiffActionState = mutableMapOf() + var location: Location? = null + + val hasDiffAction: Boolean + get() = inlineChatDiffActionState.any { it.value } + + override fun dispose() { + inlineChatInputVisible = false + inlineChatDiffActionState.clear() + location = null + } +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/inlineChat/SelectionGutterIconManager.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/inlineChat/SelectionGutterIconManager.kt new file mode 100644 index 000000000000..e4d03fb6a409 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/inlineChat/SelectionGutterIconManager.kt @@ -0,0 +1,143 @@ +package com.tabbyml.intellijtabby.inlineChat + +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.application.invokeLater +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.event.SelectionEvent +import com.intellij.openapi.editor.event.SelectionListener +import com.intellij.openapi.editor.markup.GutterIconRenderer +import com.intellij.openapi.editor.markup.HighlighterLayer +import com.intellij.openapi.editor.markup.HighlighterTargetArea +import com.intellij.openapi.editor.markup.RangeHighlighter +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.fileEditor.FileEditorManagerEvent +import com.intellij.openapi.fileEditor.FileEditorManagerListener +import com.intellij.openapi.fileEditor.TextEditor +import com.intellij.openapi.project.Project +import com.intellij.openapi.startup.ProjectActivity +import com.intellij.openapi.util.IconLoader +import com.intellij.openapi.vfs.VirtualFile +import com.tabbyml.intellijtabby.actions.chat.isChatFeatureEnabled +import javax.swing.Icon + +class SelectionGutterIconManager : ProjectActivity { + private val selectionIcon: Icon by lazy { + IconLoader.getIcon("/icons/chat.svg", SelectionGutterIconManager::class.java) + } + + private val editorToHighlighter = mutableMapOf() + private var activeEditor: Editor? = null + private var currentSelectionListener: SelectionListener? = null + + override suspend fun execute(project: Project) { + project.messageBus.connect().subscribe( + FileEditorManagerListener.FILE_EDITOR_MANAGER, + object : FileEditorManagerListener { + override fun fileOpened(source: FileEditorManager, file: VirtualFile) { + val editor = source.selectedTextEditor + editor?.let { setActiveEditor(it) } + } + + override fun fileClosed(source: FileEditorManager, file: VirtualFile) { + val editors = source.getEditors(file) + for (fileEditor in editors) { + if (fileEditor is TextEditor) { + val editor = fileEditor.editor + if (editor == activeEditor) { + removeSelectionListener(editor) + removeHighlighter(editor) + activeEditor = null + } + } + } + } + + override fun selectionChanged(event: FileEditorManagerEvent) { + if (event.newEditor != null) { + val editor = FileEditorManager.getInstance(project).selectedTextEditor + if (editor != null && editor != activeEditor) { + activeEditor?.let { + removeSelectionListener(it) + removeHighlighter(it) + } + setActiveEditor(editor) + } + } + } + } + ) + + FileEditorManager.getInstance(project).selectedTextEditor?.let { + setActiveEditor(it) + } + } + + private fun setActiveEditor(editor: Editor) { + activeEditor = editor + addSelectionListener(editor) + updateSelectionGutterIcon(editor) + } + + private fun addSelectionListener(editor: Editor) { + currentSelectionListener = object : SelectionListener { + override fun selectionChanged(e: SelectionEvent) { + updateSelectionGutterIcon(editor) + } + } + editor.selectionModel.addSelectionListener(currentSelectionListener!!) + } + + private fun removeSelectionListener(editor: Editor) { + currentSelectionListener?.let { + editor.selectionModel.removeSelectionListener(it) + currentSelectionListener = null + } + } + + private fun updateSelectionGutterIcon(editor: Editor) { + if (!isChatFeatureEnabled(editor.project)) return + invokeLater { + removeHighlighter(editor) + + if (!editor.selectionModel.hasSelection()) return@invokeLater + + val selectionStart = editor.selectionModel.selectionStart + val lineNumber = editor.document.getLineNumber(selectionStart) + val lineStart = editor.document.getLineStartOffset(lineNumber) + + val markupModel = editor.markupModel + val highlighter = markupModel.addRangeHighlighter( + lineStart, lineStart + 1, + HighlighterLayer.LAST, + null, + HighlighterTargetArea.LINES_IN_RANGE + ) + + highlighter.gutterIconRenderer = object : GutterIconRenderer() { + override fun getIcon(): Icon = selectionIcon + + override fun equals(other: Any?): Boolean { + return other is GutterIconRenderer && other.javaClass == this.javaClass + } + + override fun hashCode(): Int = javaClass.hashCode() + + override fun getTooltipText(): String = "Open Tabby inline edit" + + override fun getClickAction(): AnAction { + return InlineChatAction() + } + } + + editorToHighlighter[editor] = highlighter + } + } + + private fun removeHighlighter(editor: Editor) { + editorToHighlighter.remove(editor)?.let { highlighter -> + if (highlighter.isValid) { + editor.markupModel.removeHighlighter(highlighter) + } + } + } +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/inlineChat/util.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/inlineChat/util.kt new file mode 100644 index 000000000000..3fc97ec6a954 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/inlineChat/util.kt @@ -0,0 +1,74 @@ +package com.tabbyml.intellijtabby.inlineChat + +import com.intellij.openapi.components.serviceOrNull +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.tabbyml.intellijtabby.lsp.ConnectionService +import com.tabbyml.intellijtabby.lsp.protocol.ChatEditCommand +import com.tabbyml.intellijtabby.lsp.protocol.ChatEditCommandParams +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.eclipse.lsp4j.* +import java.util.concurrent.CompletableFuture + +data class LocationInfo(val location: Location, val startOffset: Int) + +fun getCurrentLocation(editor: Editor): LocationInfo { + val fileUri = editor.virtualFile.url + val location = Location() + location.uri = fileUri + val selectionModel = editor.selectionModel + val document = editor.document + val caretOffset = editor.caretModel.offset + var startOffset = caretOffset + var endOffset = caretOffset + if (selectionModel.hasSelection()) { + startOffset = selectionModel.selectionStart + endOffset = selectionModel.selectionEnd + } + val startPosition = Position(document.getLineNumber(startOffset), 0) + val endChar = endOffset - document.getLineStartOffset(document.getLineNumber(endOffset)) + val endLine = if (endChar == 0) document.getLineNumber(endOffset) else document.getLineNumber(endOffset) + 1 + val endPosition = Position(endLine, 0) + location.range = Range(startPosition, endPosition) + return LocationInfo(location, startOffset) +} + +fun getCodeLenses(project: Project, uri: String): CompletableFuture?> { + val scope = CoroutineScope(Dispatchers.IO) + val params = CodeLensParams(TextDocumentIdentifier(uri)) + return CompletableFuture?>().also { future -> + scope.launch { + try { + val server = project.serviceOrNull()?.getServerAsync() ?: run { + future.complete(null) + return@launch + } + val result = server.textDocumentFeature.codeLens(params) + future.complete(result.get()) + } catch (e: Exception) { + future.completeExceptionally(e) + } + } + } +} + +fun getSuggestedCommands(project: Project, location: Location): CompletableFuture?> { + val scope = CoroutineScope(Dispatchers.IO) + val params = ChatEditCommandParams(location) + return CompletableFuture?>().also { future -> + scope.launch { + try { + val server = project.serviceOrNull()?.getServerAsync() ?: run { + future.complete(null) + return@launch + } + val result = server.chatFeature.editCommand(params) + future.complete(result.get()) + } catch (e: Exception) { + future.completeExceptionally(e) + } + } + } +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/languageSupport/DefaultLanguageSupportProvider.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/languageSupport/DefaultLanguageSupportProvider.kt new file mode 100644 index 000000000000..27d4f13a2c54 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/languageSupport/DefaultLanguageSupportProvider.kt @@ -0,0 +1,115 @@ +package com.tabbyml.intellijtabby.languageSupport + +import com.intellij.codeInsight.TargetElementUtil +import com.intellij.openapi.application.ReadAction +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiRecursiveElementWalkingVisitor +import com.intellij.util.concurrency.AppExecutorUtil +import com.tabbyml.intellijtabby.findEditor +import com.tabbyml.intellijtabby.languageSupport.LanguageSupportProvider.* +import org.eclipse.lsp4j.SemanticTokenTypes +import java.util.concurrent.Callable +import java.util.concurrent.CompletableFuture + +/** + * The default implementation of [LanguageSupportProvider]. + * This implementation relies on [TargetElementUtil] and tries to find the navigation target at each position in the + * editor to provide semantic tokens and declarations. + * This implementation may not work effectively for all languages. + */ +open class DefaultLanguageSupportProvider : LanguageSupportProvider { + private val targetElementUtil = TargetElementUtil.getInstance() + private val executor = AppExecutorUtil.getAppExecutorService() + + override fun provideSemanticTokensRange( + project: Project, + fileRange: FileRange + ): CompletableFuture?> { + val psiFile = fileRange.file + val editor = project.findEditor(psiFile.virtualFile) ?: return CompletableFuture.completedFuture(null) + + val future = CompletableFuture?>() + + ReadAction.nonBlocking(Callable { + val leafElements = mutableListOf() + psiFile.accept(object : PsiRecursiveElementWalkingVisitor() { + override fun visitElement(element: PsiElement) { + if (future.isCancelled) { + return + } + if (element.children.isEmpty() && + element.text.matches(Regex("\\w+")) && + fileRange.range.contains(element.textRange) && + leafElements.none { it.textRange.intersects(element.textRange) } + ) { + leafElements.add(element) + } + if (element.textRange.intersects(fileRange.range)) { + super.visitElement(element) + } + } + }) + + if (future.isCancelled) { + return@Callable + } + + val result = leafElements.mapNotNull { + if (future.isCancelled) { + return@mapNotNull null + } + val target = try { + targetElementUtil.findTargetElement( + editor.editor, + TargetElementUtil.ELEMENT_NAME_ACCEPTED or TargetElementUtil.REFERENCED_ELEMENT_ACCEPTED, + it.textRange.startOffset + ) + } catch (e: Exception) { + null + } + if (target == it || target == null || target.text == null) { + null + } else { + SemanticToken( + text = it.text, + range = it.textRange, + type = SemanticTokenTypes.Type, // Default to use `Type` as the token type as we don't know the actual type + ) + } + } + + future.complete(result) + }).inSmartMode(project).submit(executor) + + return future + } + + override fun provideDeclaration(project: Project, filePosition: FilePosition): CompletableFuture?> { + val psiFile = filePosition.file + val editor = project.findEditor(psiFile.virtualFile) ?: return CompletableFuture.completedFuture(null) + + val future = CompletableFuture?>() + + ReadAction.nonBlocking(Callable { + val target = try { + targetElementUtil.findTargetElement( + editor.editor, + TargetElementUtil.ELEMENT_NAME_ACCEPTED or TargetElementUtil.REFERENCED_ELEMENT_ACCEPTED, + filePosition.offset + ) + } catch (e: Exception) { + null + } + val file = target?.containingFile + val range = target?.textRange + if (file == null || range == null) { + future.complete(listOf()) + } else { + future.complete(listOf(FileRange(file, range))) + } + }).inSmartMode(project).submit(executor) + + return future + } +} diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/languageSupport/KotlinLanguageSupportProvider.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/languageSupport/KotlinLanguageSupportProvider.kt new file mode 100644 index 000000000000..a929b1ca9c00 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/languageSupport/KotlinLanguageSupportProvider.kt @@ -0,0 +1,91 @@ +package com.tabbyml.intellijtabby.languageSupport + +import com.intellij.openapi.application.ReadAction.nonBlocking +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.tabbyml.intellijtabby.languageSupport.LanguageSupportProvider.* +import io.ktor.util.* +import org.eclipse.lsp4j.SemanticTokenTypes +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.forEachDescendantOfType +import java.util.concurrent.Callable +import java.util.concurrent.CompletableFuture + +class KotlinLanguageSupportProvider : LanguageSupportProvider { + override fun provideSemanticTokensRange( + project: Project, + fileRange: FileRange + ): CompletableFuture?> { + val psiFile = fileRange.file + if (psiFile.language.id.toUpperCasePreservingASCIIRules() != "KOTLIN") { + return CompletableFuture.completedFuture(null) + } + + val future = CompletableFuture?>() + + nonBlocking(Callable { + val semanticTokens = mutableListOf() + psiFile.forEachDescendantOfType { element -> + if (future.isCancelled) { + return@forEachDescendantOfType + } + if (element.children.isEmpty() && fileRange.range.contains(element.textRange)) { + val referenceTarget = + element.references.map { it.resolve() }.firstNotNullOfOrNull { it } ?: return@forEachDescendantOfType + val type = parseReferenceType(referenceTarget) + semanticTokens.add( + SemanticToken( + text = element.text, + range = element.textRange, + type = type, + ) + ) + } + } + future.complete(semanticTokens.toList()) + }) + + return future + } + + override fun provideDeclaration(project: Project, filePosition: FilePosition): CompletableFuture?> { + val psiFile = filePosition.file + if (psiFile.language.id.toUpperCasePreservingASCIIRules() != "KOTLIN") { + return CompletableFuture.completedFuture(null) + } + + val future = CompletableFuture?>() + + nonBlocking(Callable { + val element = psiFile.findElementAt(filePosition.offset) + val referenceExpression = + element as? KtReferenceExpression ?: element?.parent as? KtReferenceExpression + val referenceTarget = + referenceExpression?.references?.map { it.resolve() }?.firstNotNullOfOrNull { it } + + val file = referenceTarget?.containingFile + val range = referenceTarget?.textRange + if (file == null || range == null) { + future.complete(listOf()) + } else { + future.complete(listOf(FileRange(file, range))) + } + }) + + return future + } + + private fun parseReferenceType(referenceTarget: PsiElement): String { + return when (referenceTarget) { + is KtClassOrObject -> SemanticTokenTypes.Class + is KtFunction -> SemanticTokenTypes.Function + is KtProperty -> SemanticTokenTypes.Property + is KtParameter -> SemanticTokenTypes.Parameter + is KtVariableDeclaration -> SemanticTokenTypes.Variable + + // 1. Fallback to `Type` for other kotlin element declarations + // 2. The reference target may be declared in Java, fallback to `Type` for now + else -> SemanticTokenTypes.Type + } + } +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/languageSupport/LanguageSupportProvider.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/languageSupport/LanguageSupportProvider.kt new file mode 100644 index 000000000000..1bbcd5a1a8e7 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/languageSupport/LanguageSupportProvider.kt @@ -0,0 +1,52 @@ +package com.tabbyml.intellijtabby.languageSupport + +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiFile +import java.util.concurrent.CompletableFuture + +interface LanguageSupportProvider { + data class FilePosition( + val file: PsiFile, + val offset: Int, + ) + + data class FileRange( + val file: PsiFile, + val range: TextRange, + ) + + data class SemanticToken( + val text: String, + val range: TextRange, + /** + * See [org.eclipse.lsp4j.SemanticTokenTypes] + */ + val type: String, + /** + * See [org.eclipse.lsp4j.SemanticTokenModifiers] + */ + val modifiers: List = emptyList(), + ) + + /** + * Find all semantic tokens in the given [fileRange]. + * For now, this function is only used to find tokens that reference a declaration, which will be used to invoke [provideDeclaration] later. + * So it is safe to only contain these tokens in the result, like class names, function names, etc. + * + * If no tokens are found, return an empty list. + * If the provider does not support the given document, return null. + */ + fun provideSemanticTokensRange(project: Project, fileRange: FileRange): CompletableFuture?> { + return CompletableFuture.completedFuture(null) + } + + /** + * Get the declaration location for the token at the given [filePosition]. + * If no declaration is found, return an empty list. + * If the provider does not support the given document, return null. + */ + fun provideDeclaration(project: Project, filePosition: FilePosition): CompletableFuture?> { + return CompletableFuture.completedFuture(null) + } +} diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/languageSupport/LanguageSupportService.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/languageSupport/LanguageSupportService.kt new file mode 100644 index 000000000000..e3530d22b35c --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/languageSupport/LanguageSupportService.kt @@ -0,0 +1,117 @@ +package com.tabbyml.intellijtabby.languageSupport + +import com.intellij.openapi.components.Service +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.extensions.ExtensionPointName +import com.intellij.openapi.project.Project +import com.tabbyml.intellijtabby.languageSupport.LanguageSupportProvider.* +import java.util.concurrent.CompletableFuture +import java.util.concurrent.TimeUnit + + +@Service(Service.Level.PROJECT) +class LanguageSupportService(private val project: Project) { + private val logger = logger() + private val languageSupportProviderExtensionPoint: ExtensionPointName = + ExtensionPointName.create("com.tabbyml.intellij-tabby.languageSupportProvider") + private val defaultLanguageSupportProvider = DefaultLanguageSupportProvider() + + fun provideSemanticTokensRange(fileRange: FileRange): CompletableFuture?> { + val providers = languageSupportProviderExtensionPoint.extensionList.iterator() + val future = CompletableFuture?>() + future.completeOnTimeout(null, TIMEOUT_SEMANTIC_TOKENS_RANGE_PROVIDER, TimeUnit.MILLISECONDS) + computeSemanticTokensRangeFromProviders(future, providers, fileRange) + return future + } + + private fun computeSemanticTokensRangeFromProviders( + future: CompletableFuture?>, + providers: Iterator, + fileRange: FileRange + ) { + if (future.isDone) { + return + } + val pair = if (providers.hasNext()) { + val provider = providers.next() + Pair(provider) { result: List? -> + if (result != null) { + logger.trace("Semantic tokens provided by ${provider.javaClass.name}: $result") + future.complete(result) + } else { + // next provider + computeSemanticTokensRangeFromProviders(future, providers, fileRange) + } + } + } else { + Pair(defaultLanguageSupportProvider) { result: List? -> + if (result != null) { + logger.trace("Semantic tokens provided by default provider: $result") + future.complete(result) + } else { + future.complete(null) + } + } + } + + val request = pair.first.provideSemanticTokensRange(project, fileRange) + future.whenComplete { _, _ -> + request.cancel(true) + } + request.thenAccept { result -> + pair.second(result) + } + } + + fun provideDeclaration(position: FilePosition): CompletableFuture?> { + val providers = languageSupportProviderExtensionPoint.extensionList.iterator() + val future = CompletableFuture?>() + future.completeOnTimeout(null, TIMEOUT_DECLARATION_PROVIDER, TimeUnit.MILLISECONDS) + computeDeclarationFromProviders(future, providers, position) + return future + } + + private fun computeDeclarationFromProviders( + future: CompletableFuture?>, + providers: Iterator, + position: FilePosition + ) { + if (future.isDone) { + return + } + val pair = if (providers.hasNext()) { + val provider = providers.next() + Pair(provider) { result: List? -> + if (result != null) { + logger.trace("Declaration provided by ${provider.javaClass.name}: $result") + future.complete(result) + } else { + // next provider + computeDeclarationFromProviders(future, providers, position) + } + } + } else { + Pair(defaultLanguageSupportProvider) { result: List? -> + if (result != null) { + logger.trace("Declaration provided by default provider: $result") + future.complete(result) + } else { + future.complete(null) + } + } + } + + val request = pair.first.provideDeclaration(project, position) + future.whenComplete { _, _ -> + request.cancel(true) + } + request.thenAccept { result -> + pair.second(result) + } + } + + companion object { + private const val TIMEOUT_SEMANTIC_TOKENS_RANGE_PROVIDER = 100L // ms + private const val TIMEOUT_DECLARATION_PROVIDER = 10L // ms + } +} diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/ConfigurationSync.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/ConfigurationSync.kt new file mode 100644 index 000000000000..9632373fc76d --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/ConfigurationSync.kt @@ -0,0 +1,127 @@ +package com.tabbyml.intellijtabby.lsp + +import com.intellij.openapi.Disposable +import com.intellij.openapi.components.service +import com.intellij.openapi.components.serviceOrNull +import com.intellij.openapi.keymap.Keymap +import com.intellij.openapi.keymap.KeymapManagerListener +import com.intellij.openapi.project.Project +import com.tabbyml.intellijtabby.lsp.protocol.ClientProvidedConfig +import com.tabbyml.intellijtabby.lsp.protocol.DidChangeConfigurationParams +import com.tabbyml.intellijtabby.lsp.protocol.server.LanguageServer +import com.tabbyml.intellijtabby.settings.KeymapSettings +import com.tabbyml.intellijtabby.settings.SettingsService +import com.tabbyml.intellijtabby.settings.SettingsState +import java.util.Base64 + +class ConfigurationSync(private val project: Project) : Disposable { + private val messageBusConnection = project.messageBus.connect() + private val settings = service() + private val keymapSettings = project.serviceOrNull() + + data class SettingsData( + val settings: SettingsService.Settings, + val keymap: KeymapSettings.KeymapStyle?, + ) { + fun withSettings(settings: SettingsService.Settings): SettingsData { + return SettingsData(settings, keymap) + } + + fun withKeymap(keymap: KeymapSettings.KeymapStyle): SettingsData { + return SettingsData(settings, keymap) + } + } + + private var cached: SettingsData = SettingsData( + settings.settings(), + keymapSettings?.getCurrentKeymapStyle(), + ) + + fun getConfiguration(): ClientProvidedConfig { + cached = SettingsData( + settings.settings(), + keymapSettings?.getCurrentKeymapStyle(), + ) + return buildClientProvidedConfig(cached) + } + private fun getProxyUrl(): String? { + val proxySettings = com.intellij.util.net.HttpConfigurable.getInstance() + return if (proxySettings.USE_HTTP_PROXY) { + "http://${proxySettings.PROXY_HOST}:${proxySettings.PROXY_PORT}" + } else { + null + } + } + + private fun getProxyAuthorization(): String? { + val proxySettings = com.intellij.util.net.HttpConfigurable.getInstance() + return if (!proxySettings.proxyLogin.isNullOrEmpty() && !proxySettings.plainProxyPassword.isNullOrEmpty()) { + "Basic " + Base64.getEncoder().encodeToString("${proxySettings.proxyLogin}:${proxySettings.plainProxyPassword}".toByteArray()) + } else { + null + } + } + + fun startSync(server: LanguageServer) { + messageBusConnection.subscribe(SettingsService.Listener.TOPIC, object : SettingsService.Listener { + override fun settingsChanged(settings: SettingsService.Settings) { + cached = cached.withSettings(settings) + notifyServer(server) + } + }) + messageBusConnection.subscribe(KeymapManagerListener.TOPIC, object : KeymapManagerListener { + override fun shortcutChanged(keymap: Keymap, actionId: String, fromSettings: Boolean) { + keymapSettings?.getCurrentKeymapStyle()?.let { + if (cached.keymap !== it) { + cached = cached.withKeymap(it) + notifyServer(server) + } + } + } + }) + } + + private fun notifyServer(server: LanguageServer) { + server.workspaceFeature.didChangeConfiguration( + DidChangeConfigurationParams( + settings = buildClientProvidedConfig(cached) + ) + ) + } + + private fun buildClientProvidedConfig(data: SettingsData): ClientProvidedConfig { + val settings = data.settings + val keymap = data.keymap + val proxyUrl = getProxyUrl() + val proxyAuthorization = getProxyAuthorization() + return ClientProvidedConfig( + server = ClientProvidedConfig.ServerConfig( + endpoint = settings.serverEndpoint, + token = settings.serverToken, + ), + proxy = ClientProvidedConfig.ProxyConfig( + url = proxyUrl, + authorization = proxyAuthorization, + ), + inlineCompletion = ClientProvidedConfig.InlineCompletionConfig( + triggerMode = when (settings.completionTriggerMode) { + SettingsState.TriggerMode.AUTOMATIC -> ClientProvidedConfig.InlineCompletionConfig.TriggerMode.AUTO + SettingsState.TriggerMode.MANUAL -> ClientProvidedConfig.InlineCompletionConfig.TriggerMode.MANUAL + } + ), + keybindings = when (keymap) { + KeymapSettings.KeymapStyle.DEFAULT -> ClientProvidedConfig.Keybindings.DEFAULT + KeymapSettings.KeymapStyle.TABBY_STYLE -> ClientProvidedConfig.Keybindings.TABBY_STYLE + KeymapSettings.KeymapStyle.CUSTOMIZE -> ClientProvidedConfig.Keybindings.CUSTOMIZE + null -> ClientProvidedConfig.Keybindings.DEFAULT + }, + anonymousUsageTracking = ClientProvidedConfig.AnonymousUsageTrackingConfig( + disable = settings.isAnonymousUsageTrackingDisabled, + ), + ) + } + + override fun dispose() { + messageBusConnection.dispose() + } +} diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/ConnectionService.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/ConnectionService.kt new file mode 100644 index 000000000000..dc2e492b124c --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/ConnectionService.kt @@ -0,0 +1,201 @@ +package com.tabbyml.intellijtabby.lsp + +import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.execution.configurations.PathEnvironmentVariableUtil +import com.intellij.ide.plugins.PluginManagerCore +import com.intellij.openapi.Disposable +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.extensions.PluginId +import com.intellij.openapi.project.Project +import com.intellij.util.EnvironmentUtil +import com.intellij.util.messages.Topic +import com.tabbyml.intellijtabby.lsp.protocol.server.LanguageServer +import com.tabbyml.intellijtabby.notifications.notifyInitializationFailed +import com.tabbyml.intellijtabby.safeSyncPublisher +import com.tabbyml.intellijtabby.settings.SettingsService +import kotlinx.coroutines.delay +import kotlinx.coroutines.future.await +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.eclipse.lsp4j.InitializedParams +import org.eclipse.lsp4j.jsonrpc.Launcher +import java.io.BufferedReader +import java.io.File +import java.io.InputStreamReader +import java.io.PrintWriter +import java.util.concurrent.Future +import java.util.concurrent.TimeUnit + +@Service(Service.Level.PROJECT) +class ConnectionService(private val project: Project) : Disposable { + private val logger = Logger.getInstance(ConnectionService::class.java) + private val settings = service() + private val client = LanguageClient(project) + private var process: Process? = null + private var listening: Future? = null + private var server: LanguageServer? = null + private val initializeMutex = Mutex() + + suspend fun getServerAsync(): LanguageServer? { + if (server == null || listening == null || (process?.isAlive != true)) { + initializeMutex.withLock { + if (server == null || listening == null || (process?.isAlive != true)) { + initialize() + } + } + } + return server + } + + open class InitializationException(message: String) : Exception(message) + + open class NodeBinaryException(message: String) : InitializationException( + message = "$message Please install Node.js version >= 18.0, set the binary path in Tabby plugin settings or add bin path to system environment variable PATH, then restart IDE." + ) + + open class NodeBinaryNotFoundException : NodeBinaryException( + message = "Cannot find Node binary." + ) + + open class NodeBinaryInvalidVersionException(version: String) : NodeBinaryException( + message = "Node version is too old: $version." + ) + + private suspend fun initialize(retry: Int = 0) { + try { + logger.info("Creating tabby-agent process...") + project.safeSyncPublisher(Listener.TOPIC)?.connectionStateChanged(State.INITIALIZING) + val node = getNodeBinary() + val script = getNodeScript() + val options = "--stdio" + val cmd = GeneralCommandLine(node.absolutePath, script.absolutePath, options).withCharset(Charsets.UTF_8) + val process = cmd.createProcess() + if (!process.isAlive) { + throw InitializationException("Failed to create agent process.") + } + val launcher = + Launcher.Builder().setLocalService(client).setRemoteInterface(LanguageServer::class.java) + .setInput(process.inputStream).setOutput(process.outputStream).traceMessages(PrintWriter(Tracer())).create() + val server = launcher.remoteProxy + logger.info("Created tabby-agent process with PID: ${process.pid()}, listening to stdio.") + + this.process = process + this.server = server + this.listening = launcher.startListening() + + val initializeParams = client.buildInitializeParams() + val initializeResult = server.initialize(initializeParams).await() + client.processInitializeResult(server, initializeResult) + server.initialized(InitializedParams()) + project.safeSyncPublisher(Listener.TOPIC)?.connectionStateChanged(State.READY) + } catch (e: InitializationException) { + logger.warn("Failed to initialize connection.", e) + if (retry < 5) { + val initRetryDelay: Long = 1000 + delay(initRetryDelay) + initialize(retry + 1) + } else { + project.safeSyncPublisher(Listener.TOPIC)?.connectionStateChanged(State.INITIALIZATION_FAILED) + notifyInitializationFailed(e) + } + } + } + + private suspend fun shutdown() { + try { + server?.let { server -> + server.shutdown().orTimeout(3, TimeUnit.SECONDS).await() + server.exit() + this.server = null + + listening?.let { + it.cancel(true) + this.listening = null + } + + process?.let { + if (it.isAlive) { + it.destroy() + } + this.process = null + } + } + } catch (e: Exception) { + logger.warn("Failed to shutdown.", e) + } + } + + private fun getNodeBinary(): File { + val node = settings.nodeBinary.let { + if (it.isNotBlank()) { + val path = it.replaceFirst(Regex("^~"), System.getProperty("user.home")) + File(path) + } else { + logger.info("Environment variables: PATH: ${EnvironmentUtil.getValue("PATH")}") + PathEnvironmentVariableUtil.findExecutableInPathOnAnyOS("node") + } + } + + if (node?.exists() == true) { + logger.info("Node binary path: ${node.absolutePath}") + checkNodeVersion(node) + return node + } else { + throw NodeBinaryNotFoundException() + } + } + + private fun checkNodeVersion(node: File) { + try { + val process = GeneralCommandLine(node.absolutePath, "--version").createProcess() + val version = BufferedReader(InputStreamReader(process.inputStream)).readLine() + val regResult = Regex("v([0-9]+)\\.([0-9]+)\\.([0-9]+)").find(version) + if (regResult != null && regResult.groupValues[1].toInt() >= 18) { + return + } else { + throw NodeBinaryInvalidVersionException(version) + } + } catch (e: Exception) { + if (e is InitializationException) { + throw e + } else { + throw InitializationException("Failed to check node version: $e.") + } + } + } + + private fun getNodeScript(): File { + val script = + PluginManagerCore.getPlugin(PluginId.getId("com.tabbyml.intellij-tabby"))?.pluginPath?.resolve("tabby-agent/node/index.js") + ?.toFile() + if (script?.exists() == true) { + logger.info("Node script path: ${script.absolutePath}") + return script + } else { + throw InitializationException("Node script not found. Please reinstall Tabby plugin.") + } + } + + override fun dispose() { + runBlocking { + shutdown() + client.dispose() + } + } + + enum class State { + INITIALIZING, READY, INITIALIZATION_FAILED, + } + + interface Listener { + fun connectionStateChanged(state: State) {} + + companion object { + @Topic.ProjectLevel + val TOPIC = Topic(Listener::class.java, Topic.BroadcastDirection.NONE) + } + } +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/LanguageClient.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/LanguageClient.kt new file mode 100644 index 000000000000..b6d69ede3a7f --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/LanguageClient.kt @@ -0,0 +1,461 @@ +package com.tabbyml.intellijtabby.lsp + +import com.intellij.ide.plugins.PluginManagerCore +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.ApplicationInfo +import com.intellij.openapi.application.invokeLater +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.command.WriteCommandAction.runWriteCommandAction +import com.intellij.openapi.components.serviceOrNull +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.editor.Document +import com.intellij.openapi.extensions.PluginId +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.project.guessProjectDir +import com.intellij.openapi.ui.Messages +import com.intellij.openapi.util.TextRange +import com.intellij.psi.codeStyle.CodeStyleSettingsManager +import com.intellij.util.messages.Topic +import com.tabbyml.intellijtabby.findDocument +import com.tabbyml.intellijtabby.findPsiFile +import com.tabbyml.intellijtabby.findVirtualFile +import com.tabbyml.intellijtabby.git.GitProvider +import com.tabbyml.intellijtabby.languageSupport.LanguageSupportProvider +import com.tabbyml.intellijtabby.languageSupport.LanguageSupportService +import com.tabbyml.intellijtabby.lsp.protocol.* +import com.tabbyml.intellijtabby.lsp.protocol.ClientCapabilities +import com.tabbyml.intellijtabby.lsp.protocol.ClientInfo +import com.tabbyml.intellijtabby.lsp.protocol.InitializeParams +import com.tabbyml.intellijtabby.lsp.protocol.TextDocumentClientCapabilities +import com.tabbyml.intellijtabby.lsp.protocol.server.LanguageServer +import com.tabbyml.intellijtabby.safeSyncPublisher +import org.eclipse.lsp4j.* +import java.util.concurrent.CompletableFuture + +class LanguageClient(private val project: Project) : com.tabbyml.intellijtabby.lsp.protocol.client.LanguageClient(), + Disposable { + private val logger = Logger.getInstance(LanguageClient::class.java) + private val gitProvider = project.serviceOrNull() + private val languageSupportService = project.serviceOrNull() + private val configurationSync = ConfigurationSync(project) + private val textDocumentSync = TextDocumentSync(project) + private val documentStopUndoMap = mutableMapOf() + + override fun buildInitializeParams(): InitializeParams { + val appInfo = ApplicationInfo.getInstance() + val appVersion = appInfo.fullVersion + val appName = appInfo.fullApplicationName.replace(appVersion, "").trim() + val pluginId = "com.tabbyml.intellij-tabby" + val pluginVersion = PluginManagerCore.getPlugin(PluginId.getId(pluginId))?.version + val params = InitializeParams( + processId = ProcessHandle.current().pid().toInt(), clientInfo = ClientInfo( + name = appName, + version = appVersion, + tabbyPlugin = ClientInfo.TabbyPluginInfo( + name = pluginId, + version = pluginVersion, + ), + ), initializationOptions = InitializationOptions( + config = configurationSync.getConfiguration() + ), capabilities = ClientCapabilities( + textDocument = TextDocumentClientCapabilities( + synchronization = SynchronizationCapabilities(), + inlineCompletion = InlineCompletionCapabilities( + dynamicRegistration = true, + ), + codeLens = CodeLensCapabilities( + true, + ), + ), + workspace = WorkspaceClientCapabilities().apply { + workspaceFolders = true + configuration = true + didChangeConfiguration = DidChangeConfigurationCapabilities() + }, + tabby = TabbyClientCapabilities( + configDidChangeListener = true, + statusDidChangeListener = true, + gitProvider = gitProvider?.isSupported(), + workspaceFileSystem = true, + languageSupport = languageSupportService != null, + editorOptions = true, + ), + ), workspaceFolders = getWorkspaceFolders() + ) + logger.info("Initialize params: $params") + return params + } + + override fun processInitializeResult(server: LanguageServer, result: InitializeResult?) { + configurationSync.startSync(server) + textDocumentSync.startSync(server) + } + + override fun configDidChange(params: Config) { + project.safeSyncPublisher(ConfigListener.TOPIC)?.configChanged(params) + } + + override fun statusDidChange(params: StatusInfo) { + project.safeSyncPublisher(StatusListener.TOPIC)?.statusChanged(params) + } + + override fun editorOptions(params: EditorOptionsParams): CompletableFuture { + val codeStyleSettingsManager = CodeStyleSettingsManager.getInstance(project) + val indentation = project.findPsiFile(params.uri)?.language?.let { + codeStyleSettingsManager.mainProjectCodeStyle?.getCommonSettings(it)?.indentOptions + }?.let { + if (it.USE_TAB_CHARACTER) { + "\t" + } else { + " ".repeat(it.INDENT_SIZE) + } + } + return CompletableFuture().apply { + complete(EditorOptions(indentation = indentation)) + } + } + + override fun readFile(params: ReadFileParams): CompletableFuture { + val file = project.findVirtualFile(params.uri) ?: return CompletableFuture.completedFuture(null) + when (params.format) { + ReadFileParams.Format.TEXT -> { + val document = project.findDocument(file) ?: return CompletableFuture.completedFuture(null) + val text = if (params.range != null) { + document.getText( + TextRange( + offsetInDocument(document, params.range.start), + offsetInDocument(document, params.range.end) + ) + ) + } else { + document.text + } + return CompletableFuture.completedFuture(ReadFileResult(text)) + } + + else -> { + return CompletableFuture.completedFuture(null) + } + } + } + + override fun declaration(params: DeclarationParams): CompletableFuture?> { + val future = CompletableFuture?>() + val virtualFile = project.findVirtualFile(params.textDocument.uri) + val document = virtualFile?.let { project.findDocument(it) } + val psiFile = virtualFile?.let { project.findPsiFile(it) } + val languageSupport = languageSupportService + + if (virtualFile == null || document == null || psiFile == null || languageSupport == null) { + future.complete(null) + return future + } + + val request = languageSupport.provideDeclaration( + LanguageSupportProvider.FilePosition( + psiFile, + offsetInDocument(document, params.position) + ) + ) + + future.whenComplete { _, _ -> + request.cancel(true) + } + request.thenAccept { result -> + future.complete(result?.mapNotNull { + val targetUri = it.file.virtualFile.url + val targetDocument = project.findDocument(it.file.virtualFile) ?: return@mapNotNull null + val range = Range( + positionInDocument(targetDocument, it.range.startOffset), + positionInDocument(targetDocument, it.range.endOffset) + ) + LocationLink(targetUri, range, range) + }) + } + return future + } + + override fun semanticTokensRange(params: SemanticTokensRangeParams): CompletableFuture { + val future = CompletableFuture() + val virtualFile = project.findVirtualFile(params.textDocument.uri) + val document = virtualFile?.let { project.findDocument(it) } + val psiFile = virtualFile?.let { project.findPsiFile(it) } + val languageSupport = languageSupportService + + if (virtualFile == null || document == null || psiFile == null || languageSupport == null) { + future.complete(null) + return future + } + + val request = languageSupport.provideSemanticTokensRange( + LanguageSupportProvider.FileRange( + psiFile, + TextRange( + offsetInDocument(document, params.range.start), + offsetInDocument(document, params.range.end) + ) + ) + ) + + future.whenComplete { _, _ -> + request.cancel(true) + } + request.thenAccept { result -> + future.complete(result?.let { + encodeSemanticTokens(document, it) + }) + } + return future + } + + override fun gitRepository(params: GitRepositoryParams): CompletableFuture { + val repository = gitProvider?.getRepository(params.uri)?.let { repo -> + GitRepository(root = repo.root, remotes = repo.remotes?.map { + GitRepository.Remote( + name = it.name, + url = it.url, + ) + }) + } + return CompletableFuture().apply { + complete(repository) + } + } + + override fun gitDiff(params: GitDiffParams): CompletableFuture { + val result = gitProvider?.diff(params.repository, params.cached)?.let { + GitDiffResult(diff = it) + } + return CompletableFuture().apply { + complete(result) + } + } + + override fun registerCapability(params: RegistrationParams): CompletableFuture { + params.registrations.forEach { + project.safeSyncPublisher(CapabilityRegistrationListener.TOPIC) + ?.onRegisterCapability(it.id, it.method, it.registerOptions) + } + return CompletableFuture().apply { complete(null) } + } + + override fun unregisterCapability(params: UnregistrationParams): CompletableFuture { + params.unregisterations.forEach { + project.safeSyncPublisher(CapabilityRegistrationListener.TOPIC) + ?.onUnregisterCapability(it.id, it.method) + } + return CompletableFuture().apply { complete(null) } + } + + override fun configuration(params: Any): CompletableFuture?> { + return CompletableFuture?>().apply { + complete(listOf(configurationSync.getConfiguration())) + } + } + + override fun workspaceFolders(): CompletableFuture> { + return CompletableFuture>().apply { + complete(getWorkspaceFolders()) + } + } + + override fun applyEdit(params: ApplyWorkspaceEditParams): CompletableFuture { + val future = CompletableFuture() + invokeLater { + try { + val edit = params.edit + runWriteCommandAction(project) { + edit.changes?.forEach { (uri, edits) -> + val virtualFile = project.findVirtualFile(uri) ?: return@forEach + val document = project.findDocument(virtualFile) ?: return@forEach + edits.forEach { textEdit -> + val startOffset = offsetInDocument(document, textEdit.range.start).coerceIn(0, document.textLength) + val endOffset = offsetInDocument(document, textEdit.range.end).coerceIn(0, document.textLength) + document.replaceString(startOffset, endOffset, textEdit.newText) + } + } + } + future.complete(ApplyWorkspaceEditResponse(true)) + } catch (e: Exception) { + logger.warn("Failed to apply workspace edit", e) + future.complete(ApplyWorkspaceEditResponse(false).apply { failureReason = "Failed to apply workspace edit ${e.message}" }) + } + } + return future + } + + override fun applyWorkspaceEdit(params: TabbyApplyWorkspaceEditParams): CompletableFuture { + val future = CompletableFuture() + invokeLater { + try { + val edit = params.edit + edit.changes?.forEach { (uri, edits) -> + val virtualFile = project.findVirtualFile(uri) ?: return@forEach + val document = project.findDocument(virtualFile) ?: return@forEach + val url = FileDocumentManager.getInstance() + .getFile(document)?.url ?: return@forEach + + logger.info("url $url") + if (params.options?.undoStopBefore == true) { + documentStopUndoMap[url] = true + } + + if (documentStopUndoMap[url] == true) { + // continued command action with same group id will be combined into one undo history + WriteCommandAction.writeCommandAction(project).withGroupId("tabby").run { + writeDocument(document, edits) + } + } else { + runWriteCommandAction(project) { + writeDocument(document, edits) + } + } + + if (params.options?.undoStopAfter == true) { + documentStopUndoMap[url] = false + } + } + future.complete(true) + } catch (e: Exception) { + logger.warn("Failed to apply workspace edit", e) + future.complete(false) + } + } + return future + } + + private fun writeDocument(document: Document, edits: List) { + edits.forEach { textEdit -> + val startOffset = offsetInDocument(document, textEdit.range.start).coerceIn(0, document.textLength) + val endOffset = offsetInDocument(document, textEdit.range.end).coerceIn(0, document.textLength) + document.replaceString(startOffset, endOffset, textEdit.newText) + } + } + + override fun showMessageRequest(params: ShowMessageRequestParams): CompletableFuture { + return CompletableFuture().apply { + invokeLater { + val actions = params.actions.map { it.title }.toTypedArray() + val selected = Messages.showDialog( + params.message, + "Tabby", + actions, + 0, + when (params.type) { + MessageType.Error -> Messages.getErrorIcon() + MessageType.Warning -> Messages.getWarningIcon() + else -> Messages.getInformationIcon() + }, + ) + complete(actions.getOrNull(selected)?.let { MessageActionItem(it) }) + } + } + } + + override fun logMessage(params: MessageParams) { + when (params.type) { + MessageType.Error -> logger.warn(params.message) + MessageType.Warning -> logger.warn(params.message) + MessageType.Info -> logger.info(params.message) + MessageType.Log -> logger.debug(params.message) + null -> return + } + } + + override fun logTrace(params: LogTraceParams) { + logger.trace("${params.message}\n${params.verbose}") + } + + override fun dispose() { + configurationSync.dispose() + textDocumentSync.dispose() + documentStopUndoMap.clear() + } + + private fun getWorkspaceFolders(): List { + return project.guessProjectDir()?.let { + listOf(WorkspaceFolder(it.url, project.name)) + } ?: listOf() + } + + private fun encodeSemanticTokens( + document: Document, + tokens: List + ): SemanticTokensRangeResult { + val tokenTypesLegend = mutableListOf() + val tokenModifiersLegend = mutableListOf() + val data = mutableListOf() + var line = 0 + var character = 0 + for (token in tokens.sortedBy { it.range.startOffset }) { + val position = positionInDocument(document, token.range.startOffset) + val deltaLine = position.line - line + line = position.line + if (deltaLine != 0) { + character = 0 + } + val deltaCharacter = position.character - character + character = position.character + val length = token.range.endOffset - token.range.startOffset + val tokenType = tokenTypesLegend.indexOf(token.type).let { + if (it == -1) { + tokenTypesLegend.add(token.type) + tokenTypesLegend.size - 1 + } else { + it + } + } + val tokenModifiers = token.modifiers.map { modifier -> + tokenModifiersLegend.indexOf(modifier).let { + if (it == -1) { + tokenModifiersLegend.add(modifier) + tokenModifiersLegend.size - 1 + } else { + it + } + } + }.fold(0) { acc, i -> + acc or (1 shl i) + } + + data.add(deltaLine) + data.add(deltaCharacter) + data.add(length) + data.add(tokenType) + data.add(tokenModifiers) + } + return SemanticTokensRangeResult( + legend = SemanticTokensRangeResult.SemanticTokensLegend(tokenTypesLegend, tokenModifiersLegend), + tokens = SemanticTokensRangeResult.SemanticTokens(data = data) + ) + } + + interface ConfigListener { + fun configChanged(config: Config) {} + + companion object { + @Topic.ProjectLevel + val TOPIC = Topic(ConfigListener::class.java, Topic.BroadcastDirection.NONE) + } + } + + interface StatusListener { + fun statusChanged(status: StatusInfo) {} + + companion object { + @Topic.ProjectLevel + val TOPIC = Topic(StatusListener::class.java, Topic.BroadcastDirection.NONE) + } + } + + interface CapabilityRegistrationListener { + fun onRegisterCapability(id: String, method: String, options: Any) {} + fun onUnregisterCapability(id: String, method: String) {} + + companion object { + @Topic.ProjectLevel + val TOPIC = Topic(CapabilityRegistrationListener::class.java, Topic.BroadcastDirection.NONE) + } + } +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/LanguageIdExt.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/LanguageIdExt.kt new file mode 100644 index 000000000000..ab90a006ac1a --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/LanguageIdExt.kt @@ -0,0 +1,54 @@ +package com.tabbyml.intellijtabby.lsp + +import com.intellij.lang.Language +import com.intellij.psi.PsiFile +import io.ktor.util.* + + +private val languageIdMap = mapOf( + "ObjectiveC" to "objective-c", + "ObjectiveC++" to "objective-cpp", +) +private val filetypeMap = mapOf( + "py" to "python", + "js" to "javascript", + "cjs" to "javascript", + "mjs" to "javascript", + "jsx" to "javascriptreact", + "ts" to "typescript", + "tsx" to "typescriptreact", + "kt" to "kotlin", + "md" to "markdown", + "cc" to "cpp", + "cs" to "csharp", + "m" to "objective-c", + "mm" to "objective-cpp", + "sh" to "shellscript", + "zsh" to "shellscript", + "bash" to "shellscript", + "txt" to "plaintext", +) + +// Language id: https://code.visualstudio.com/docs/languages/identifiers +fun PsiFile.getLanguageId(): String { + return if (language != Language.ANY && + language.id.isNotBlank() && + language.id.toLowerCasePreservingASCIIRules() !in arrayOf("txt", "text", "textmate") + ) { + languageIdMap[language.id] ?: language.id + .toLowerCasePreservingASCIIRules() + .replace("#", "sharp") + .replace("++", "pp") + .replace(" ", "") + } else { + val ext = this.fileType.defaultExtension.ifBlank { + this.virtualFile.name.substringAfterLast(".") + } + if (ext.isNotBlank()) { + filetypeMap[ext] ?: ext.toLowerCasePreservingASCIIRules() + } else { + "plaintext" + } + } +} + diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/PositionExt.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/PositionExt.kt new file mode 100644 index 000000000000..3a1946822ceb --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/PositionExt.kt @@ -0,0 +1,20 @@ +package com.tabbyml.intellijtabby.lsp + +import com.intellij.openapi.editor.Document +import org.eclipse.lsp4j.Position + +fun positionInDocument(document: Document, offset: Int): Position { + val line = document.getLineNumber(offset) + val character = offset - document.getLineStartOffset(line) + return Position(line, character) +} + +fun offsetInDocument(document: Document, position: Position): Int { + if (position.line < 0) { + return position.character + } + if (position.line >= document.lineCount) { + return document.getLineEndOffset(document.lineCount - 1) + } + return document.getLineStartOffset(position.line) + position.character +} diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/TextDocumentSync.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/TextDocumentSync.kt new file mode 100644 index 000000000000..8468ebc39ca5 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/TextDocumentSync.kt @@ -0,0 +1,91 @@ +package com.tabbyml.intellijtabby.lsp + +import com.intellij.openapi.Disposable +import com.intellij.openapi.editor.Document +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.EditorFactory +import com.intellij.openapi.editor.event.DocumentEvent +import com.intellij.openapi.project.Project +import com.tabbyml.intellijtabby.events.DocumentListener +import com.tabbyml.intellijtabby.findPsiFile +import com.tabbyml.intellijtabby.lsp.protocol.server.LanguageServer +import org.eclipse.lsp4j.* + +class TextDocumentSync(private val project: Project) : Disposable { + private val messageBusConnection = project.messageBus.connect() + + fun startSync(server: LanguageServer) { + initSync(server) + registerListeners(server) + } + + private fun initSync(server: LanguageServer) { + val editorFactory = EditorFactory.getInstance() + editorFactory.allEditors.forEach { editor -> + buildDidOpenTextDocumentParams(editor)?.let { server.textDocumentFeature.didOpen(it) } + } + } + + private fun registerListeners(server: LanguageServer) { + messageBusConnection.subscribe(DocumentListener.TOPIC, object : DocumentListener { + override fun documentOpened(document: Document, editor: Editor) { + buildDidOpenTextDocumentParams(editor)?.let { server.textDocumentFeature.didOpen(it) } + } + + override fun documentClosed(document: Document, editor: Editor) { + buildDidCloseTextDocumentParams(editor)?.let { server.textDocumentFeature.didClose(it) } + } + + override fun documentChanged(document: Document, editor: Editor, event: DocumentEvent) { + buildDidChangeTextDocumentParams(editor, event)?.let { server.textDocumentFeature.didChange(it) } + } + }) + } + + override fun dispose() { + messageBusConnection.dispose() + } + + companion object { + fun buildDidOpenTextDocumentParams(editor: Editor): DidOpenTextDocumentParams? { + val project = editor.project ?: return null + val virtualFile = editor.virtualFile ?: return null + return DidOpenTextDocumentParams( + TextDocumentItem( + virtualFile.url, + project.findPsiFile(virtualFile)?.getLanguageId() ?: "plaintext", + editor.document.modificationStamp.toInt(), + editor.document.text, + ) + ) + } + + fun buildDidCloseTextDocumentParams(editor: Editor): DidCloseTextDocumentParams? { + val virtualFile = editor.virtualFile ?: return null + return DidCloseTextDocumentParams( + TextDocumentIdentifier(virtualFile.url) + ) + } + + fun buildDidChangeTextDocumentParams(editor: Editor, event: DocumentEvent): DidChangeTextDocumentParams? { + val virtualFile = editor.virtualFile ?: return null + val oldLines = event.oldFragment.lines() + val startPosition = positionInDocument(editor.document, event.offset) + val endPosition = Position( + startPosition.line + oldLines.size - 1, + (if (oldLines.size == 1) startPosition.character else 0) + oldLines.last().length, + ) + return DidChangeTextDocumentParams( + VersionedTextDocumentIdentifier( + virtualFile.url, + editor.document.modificationStamp.toInt(), + ), listOf( + TextDocumentContentChangeEvent( + Range(startPosition, endPosition), event.newFragment.toString() + ) + ) + ) + + } + } +} diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/Tracer.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/Tracer.kt new file mode 100644 index 000000000000..4a5ff929ed5e --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/Tracer.kt @@ -0,0 +1,20 @@ +package com.tabbyml.intellijtabby.lsp + +import com.intellij.openapi.diagnostic.Logger +import java.io.Writer + +class Tracer : Writer() { + private val logger = Logger.getInstance(Tracer::class.java) + + override fun write(cbuf: CharArray, off: Int, len: Int) { + logger.trace(String(cbuf.sliceArray(IntRange(off, off + len - 1)))) + } + + override fun flush() { + // nothing + } + + override fun close() { + // nothing + } +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/protocol/ProtocolData.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/protocol/ProtocolData.kt new file mode 100644 index 000000000000..77717c232f9b --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/protocol/ProtocolData.kt @@ -0,0 +1,385 @@ +package com.tabbyml.intellijtabby.lsp.protocol + +import com.tabbyml.intellijtabby.lsp.protocol.ClientProvidedConfig.InlineCompletionConfig.TriggerMode +import com.tabbyml.intellijtabby.lsp.protocol.ClientProvidedConfig.Keybindings +import com.tabbyml.intellijtabby.lsp.protocol.EventParams.EventType +import com.tabbyml.intellijtabby.lsp.protocol.EventParams.SelectKind +import com.tabbyml.intellijtabby.lsp.protocol.ReadFileParams.Format +import com.tabbyml.intellijtabby.lsp.protocol.StatusIgnoredIssuesEditParams.Operation +import com.tabbyml.intellijtabby.lsp.protocol.StatusIgnoredIssuesEditParams.StatusIssuesName +import com.tabbyml.intellijtabby.lsp.protocol.StatusInfo.Status +import org.eclipse.lsp4j.* + +data class InitializeParams( + val processId: Int? = null, + val clientInfo: ClientInfo? = null, + val initializationOptions: InitializationOptions? = null, + val capabilities: ClientCapabilities, + /** + * [TraceValue] + */ + val trace: String? = null, + val workspaceFolders: List? = null +) + +data class ClientInfo( + val name: String, + val version: String? = null, + val tabbyPlugin: TabbyPluginInfo? = null, +) { + data class TabbyPluginInfo( + val name: String, + val version: String? = null, + ) +} + +data class InitializationOptions( + val config: ClientProvidedConfig? = null +) + +data class ClientCapabilities( + val workspace: WorkspaceClientCapabilities? = null, + val textDocument: TextDocumentClientCapabilities? = null, + val notebookDocument: NotebookDocumentClientCapabilities? = null, + val tabby: TabbyClientCapabilities? = null, +) + +data class TextDocumentClientCapabilities( + val synchronization: SynchronizationCapabilities? = null, + val completion: CompletionCapabilities? = null, + val inlineCompletion: InlineCompletionCapabilities? = null, + var codeLens: CodeLensCapabilities ? = null, +) + +data class InlineCompletionCapabilities( + val dynamicRegistration: Boolean? = null, +) + +data class TabbyClientCapabilities( + val configDidChangeListener: Boolean? = null, + val statusDidChangeListener: Boolean? = null, + val workspaceFileSystem: Boolean? = null, + val dataStore: Boolean? = null, + val languageSupport: Boolean? = null, + val gitProvider: Boolean? = null, + val editorOptions: Boolean? = null, +) + +data class ClientProvidedConfig( + val server: ServerConfig? = null, + val proxy: ProxyConfig? = null, + val inlineCompletion: InlineCompletionConfig? = null, + /** + * [Keybindings] + */ + val keybindings: String? = null, + val anonymousUsageTracking: AnonymousUsageTrackingConfig? = null, +) { + data class ServerConfig( + val endpoint: String? = null, + val token: String? = null, + ) + + data class ProxyConfig( + val url: String? = null, + val authorization: String? = null, + ) + + data class InlineCompletionConfig( + /** + * [TriggerMode] + */ + val triggerMode: String? = null, + ) { + sealed class TriggerMode { + companion object { + const val AUTO = "auto" + const val MANUAL = "manual" + } + } + } + + sealed class Keybindings { + companion object { + const val DEFAULT = "default" + const val TABBY_STYLE = "tabby-style" + const val CUSTOMIZE = "customize" + } + } + + data class AnonymousUsageTrackingConfig( + val disable: Boolean? = null, + ) +} + +data class DidChangeConfigurationParams( + val settings: ClientProvidedConfig? = null, +) + +data class CompletionList( + val isIncomplete: Boolean, + val items: List, +) + +data class CompletionItem( + val label: String, + val labelDetails: CompletionItemLabelDetails? = null, + val kind: CompletionItemKind? = null, + val tags: List? = null, + val detail: String? = null, + val documentation: MarkupContent? = null, + val preselect: Boolean? = null, + val sortText: String? = null, + val filterText: String? = null, + val insertText: String? = null, + val insertTextFormat: InsertTextFormat? = null, + val insertTextMode: InsertTextMode? = null, + val textEdit: TextEdit? = null, + val textEditText: String? = null, + val additionalTextEdits: List? = null, + val commitCharacters: List? = null, + val command: Command? = null, + val data: Data? = null, +) { + data class Data( + val eventId: CompletionEventId? = null + ) +} + +data class InlineCompletionParams( + val context: InlineCompletionContext? = null, + val textDocument: TextDocumentIdentifier, + val position: Position, +) { + data class InlineCompletionContext( + val triggerKind: InlineCompletionTriggerKind, + val selectedCompletionInfo: SelectedCompletionInfo? = null, + ) { + enum class InlineCompletionTriggerKind(val value: Int) { + Invoked(0), Automatic(1); + } + + data class SelectedCompletionInfo( + val text: String, + val range: Range, + ) + } +} + +data class InlineCompletionList( + val isIncomplete: Boolean, + val items: List, +) + +data class InlineCompletionItem( + val insertText: String, + val filterText: String? = null, + val range: Range? = null, + val command: Command? = null, + val data: Data? = null, +) { + data class Data( + val eventId: CompletionEventId? = null + ) +} + +data class CompletionEventId( + val completionId: String, + val choiceIndex: Int, +) + +data class DidChangeActiveEditorParams( + val activeEditor: Location, + val visibleEditors: List? = null, +) + +data class EventParams( + /** + * [EventType] + */ + val type: String, + /** + * [SelectKind] + */ + val selectKind: String? = null, + val eventId: CompletionEventId, + val viewId: String? = null, + val elapsed: Int? = null, +) { + sealed class EventType { + companion object { + const val VIEW = "view" + const val SELECT = "select" + const val DISMISS = "dismiss" + } + } + + sealed class SelectKind { + companion object { + const val LINE = "line" + } + } +} + +data class Config( + val server: ServerConfig, +) { + data class ServerConfig( + val endpoint: String, + val token: String, + val requestHeaders: Map, + ) +} + +data class StatusRequestParams( + val recheckConnection: Boolean? = null, +) + +data class StatusInfo( + /** + * [Status] + */ + val status: String, + val tooltip: String? = null, + val serverHealth: Map? = null, + val command: Command? = null, + val helpMessage: String? = null, +) { + sealed class Status { + companion object { + const val CONNECTING = "connecting" + const val UNAUTHORIZED = "unauthorized" + const val DISCONNECTED = "disconnected" + const val READY = "ready" + const val READY_FOR_AUTO_TRIGGER = "readyForAutoTrigger" + const val READY_FOR_MANUAL_TRIGGER = "readyForManualTrigger" + const val FETCHING = "fetching" + const val COMPLETION_RESPONSE_SLOW = "completionResponseSlow" + } + } +} + +data class StatusIgnoredIssuesEditParams( + /** + * [Operation] + */ + val operation: String, + /** + * [StatusIssuesName] + */ + val issues: List? = null, +) { + sealed class Operation { + companion object { + const val ADD = "add" + const val REMOVE = "remove" + const val REMOVE_ALL = "removeAll" + } + } + + sealed class StatusIssuesName { + companion object { + const val COMPLETION_RESPONSE_SLOW = "completionResponseSlow" + } + } +} + +data class ReadFileParams( + val uri: String, + /** + * [Format] + */ + val format: String, + val range: Range? = null, +) { + sealed class Format { + companion object { + const val TEXT = "text" + } + } +} + +data class ReadFileResult( + val text: String? = null +) + +data class SemanticTokensRangeResult( + val legend: SemanticTokensLegend, + val tokens: SemanticTokens, +) { + data class SemanticTokensLegend( + val tokenTypes: List, + val tokenModifiers: List, + ) + + data class SemanticTokens( + val resultId: String? = null, + val data: List, + ) +} + +data class GitRepositoryParams( + val uri: String +) + +data class GitRepository( + val root: String, + val remoteUrl: String? = null, + val remotes: List? = null, +) { + data class Remote( + val name: String, + val url: String, + ) +} + +data class GitDiffParams( + val repository: String, + val cached: Boolean, +) + +data class GitDiffResult( + val diff: List, +) + +data class EditorOptionsParams( + val uri: String, +) + +data class EditorOptions( + val indentation: String? = null, +) + +data class GenerateCommitMessageParams( + val repository: String, +) + +data class GenerateCommitMessageResult( + val commitMessage: String, +) + +data class ChatEditParams( + val location: Location, + val command: String, + val format: String = "previewChanges", + val context: List? = null, +) + +data class ChatEditFileContext( + val referrer: String, + val uri: String, + val range: Range, +) + +data class ChatEditResolveParams( + val location: Location, + var action: String, +) + +data class ChatEditCommandParams(var location: Location) + +data class ChatEditCommand(var label: String, var command: String, var source: String = "preset" ) + +data class TabbyApplyWorkspaceEditOptions(val undoStopBefore: Boolean = false, val undoStopAfter: Boolean = false) + +data class TabbyApplyWorkspaceEditParams(val label: String?, val edit: WorkspaceEdit, val options: TabbyApplyWorkspaceEditOptions? = null) \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/protocol/client/LanguageClient.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/protocol/client/LanguageClient.kt new file mode 100644 index 000000000000..4b38f8111b16 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/protocol/client/LanguageClient.kt @@ -0,0 +1,155 @@ +package com.tabbyml.intellijtabby.lsp.protocol.client + +import com.tabbyml.intellijtabby.lsp.protocol.* +import com.tabbyml.intellijtabby.lsp.protocol.InitializeParams +import com.tabbyml.intellijtabby.lsp.protocol.server.LanguageServer +import org.eclipse.lsp4j.* +import org.eclipse.lsp4j.jsonrpc.services.JsonNotification +import org.eclipse.lsp4j.jsonrpc.services.JsonRequest +import java.util.concurrent.CompletableFuture + +abstract class LanguageClient { + abstract fun buildInitializeParams(): InitializeParams + + open fun processInitializeResult(server: LanguageServer, result: InitializeResult?) {} + + @JsonNotification("tabby/config/didChange") + open fun configDidChange(params: Config) { + throw UnsupportedOperationException() + } + + @JsonNotification("tabby/status/didChange") + open fun statusDidChange(params: StatusInfo) { + throw UnsupportedOperationException() + } + + @JsonRequest("tabby/workspaceFileSystem/readFile") + open fun readFile(params: ReadFileParams): CompletableFuture { + throw UnsupportedOperationException() + } + + @JsonRequest("tabby/languageSupport/textDocument/declaration") + open fun declaration(params: DeclarationParams): CompletableFuture?> { + throw UnsupportedOperationException() + } + + @JsonRequest("tabby/languageSupport/textDocument/semanticTokens/range") + open fun semanticTokensRange(params: SemanticTokensRangeParams): CompletableFuture { + throw UnsupportedOperationException() + } + + @JsonRequest("tabby/git/repository") + open fun gitRepository(params: GitRepositoryParams): CompletableFuture { + throw UnsupportedOperationException() + } + + @JsonRequest("tabby/git/diff") + open fun gitDiff(params: GitDiffParams): CompletableFuture { + throw UnsupportedOperationException() + } + + @JsonRequest("tabby/editorOptions") + open fun editorOptions(params: EditorOptionsParams): CompletableFuture { + throw UnsupportedOperationException() + } + + @JsonRequest("workspace/applyEdit") + open fun applyEdit(params: ApplyWorkspaceEditParams): CompletableFuture { + throw UnsupportedOperationException() + } + + @JsonRequest("client/registerCapability") + open fun registerCapability(params: RegistrationParams): CompletableFuture { + throw UnsupportedOperationException() + } + + @JsonRequest("client/unregisterCapability") + open fun unregisterCapability(params: UnregistrationParams): CompletableFuture { + throw UnsupportedOperationException() + } + + @JsonNotification("telemetry/event") + open fun telemetryEvent(params: Any) { + throw UnsupportedOperationException() + } + + @JsonNotification("textDocument/publishDiagnostics") + open fun publishDiagnostics(params: PublishDiagnosticsParams) { + throw UnsupportedOperationException() + } + + @JsonNotification("window/showMessage") + open fun showMessage(params: MessageParams) { + throw UnsupportedOperationException() + } + + @JsonRequest("window/showMessageRequest") + open fun showMessageRequest(params: ShowMessageRequestParams): CompletableFuture { + throw UnsupportedOperationException() + } + + @JsonRequest("window/showDocument") + open fun showDocument(params: ShowDocumentParams): CompletableFuture { + throw UnsupportedOperationException() + } + + @JsonNotification("window/logMessage") + open fun logMessage(params: MessageParams) { + throw UnsupportedOperationException() + } + + @JsonRequest("workspace/workspaceFolders") + open fun workspaceFolders(): CompletableFuture> { + throw UnsupportedOperationException() + } + + @JsonRequest("workspace/configuration") + open fun configuration(params: Any): CompletableFuture?> { + throw UnsupportedOperationException() + } + + @JsonRequest("window/workDoneProgress/create") + open fun createProgress(params: WorkDoneProgressCreateParams): CompletableFuture { + throw UnsupportedOperationException() + } + + @JsonNotification("$/progress") + open fun notifyProgress(params: ProgressParams) { + throw UnsupportedOperationException() + } + + @JsonNotification("$/logTrace") + open fun logTrace(params: LogTraceParams) { + throw UnsupportedOperationException() + } + + @JsonRequest("workspace/semanticTokens/refresh") + open fun refreshSemanticTokens(): CompletableFuture { + throw UnsupportedOperationException() + } + + @JsonRequest("workspace/codeLens/refresh") + open fun refreshCodeLenses(): CompletableFuture { + throw UnsupportedOperationException() + } + + @JsonRequest("workspace/inlayHint/refresh") + open fun refreshInlayHints(): CompletableFuture { + throw UnsupportedOperationException() + } + + @JsonRequest("workspace/inlineValue/refresh") + open fun refreshInlineValues(): CompletableFuture { + throw UnsupportedOperationException() + } + + @JsonRequest("workspace/diagnostic/refresh") + open fun refreshDiagnostics(): CompletableFuture { + throw UnsupportedOperationException() + } + + @JsonRequest("tabby/workspace/applyEdit") + open fun applyWorkspaceEdit(edit: TabbyApplyWorkspaceEditParams): CompletableFuture { + throw UnsupportedOperationException() + } +} diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/protocol/server/ChatFeature.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/protocol/server/ChatFeature.kt new file mode 100644 index 000000000000..0c45a1c74e1f --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/protocol/server/ChatFeature.kt @@ -0,0 +1,23 @@ +package com.tabbyml.intellijtabby.lsp.protocol.server + +import com.jetbrains.rd.generator.nova.PredefinedType +import com.tabbyml.intellijtabby.lsp.protocol.* +import org.eclipse.lsp4j.jsonrpc.services.JsonRequest +import org.eclipse.lsp4j.jsonrpc.services.JsonSegment +import java.util.concurrent.CompletableFuture + +@JsonSegment("tabby/chat") +interface ChatFeature { + @JsonRequest("edit") + fun chatEdit(params: ChatEditParams): CompletableFuture + + @JsonRequest("edit/resolve") + fun resolveEdit(params: ChatEditResolveParams): CompletableFuture + + + @JsonRequest("edit/command") + fun editCommand(params: ChatEditCommandParams): CompletableFuture?> + + @JsonRequest + fun generateCommitMessage(params: GenerateCommitMessageParams): CompletableFuture +} diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/protocol/server/ConfigFeature.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/protocol/server/ConfigFeature.kt new file mode 100644 index 000000000000..23d9c1cd5c98 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/protocol/server/ConfigFeature.kt @@ -0,0 +1,12 @@ +package com.tabbyml.intellijtabby.lsp.protocol.server + +import com.tabbyml.intellijtabby.lsp.protocol.Config +import org.eclipse.lsp4j.jsonrpc.services.JsonRequest +import org.eclipse.lsp4j.jsonrpc.services.JsonSegment +import java.util.concurrent.CompletableFuture + +@JsonSegment("tabby") +interface ConfigFeature { + @JsonRequest("config") + fun getConfig(): CompletableFuture +} diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/protocol/server/EditorsFeature.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/protocol/server/EditorsFeature.kt new file mode 100644 index 000000000000..383e0bc20f88 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/protocol/server/EditorsFeature.kt @@ -0,0 +1,11 @@ +package com.tabbyml.intellijtabby.lsp.protocol.server + +import com.tabbyml.intellijtabby.lsp.protocol.DidChangeActiveEditorParams +import org.eclipse.lsp4j.jsonrpc.services.JsonNotification +import org.eclipse.lsp4j.jsonrpc.services.JsonSegment + +@JsonSegment("tabby/editors") +interface EditorsFeature { + @JsonNotification + fun didChangeActiveEditor(params: DidChangeActiveEditorParams) +} diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/protocol/server/LanguageServer.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/protocol/server/LanguageServer.kt new file mode 100644 index 000000000000..fd0e4fcf75cc --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/protocol/server/LanguageServer.kt @@ -0,0 +1,51 @@ +package com.tabbyml.intellijtabby.lsp.protocol.server + +import com.tabbyml.intellijtabby.lsp.protocol.InitializeParams +import org.eclipse.lsp4j.InitializeResult +import org.eclipse.lsp4j.InitializedParams +import org.eclipse.lsp4j.SetTraceParams +import org.eclipse.lsp4j.jsonrpc.services.JsonDelegate +import org.eclipse.lsp4j.jsonrpc.services.JsonNotification +import org.eclipse.lsp4j.jsonrpc.services.JsonRequest +import java.util.concurrent.CompletableFuture + +interface LanguageServer { + @JsonRequest + fun initialize(params: InitializeParams): CompletableFuture + + @JsonNotification + fun initialized(params: InitializedParams) + + @JsonRequest + fun shutdown(): CompletableFuture + + @JsonNotification + fun exit() + + @get:JsonDelegate + val notebookDocumentFeature: NotebookDocumentFeature + + @get:JsonDelegate + val textDocumentFeature: TextDocumentFeature + + @get:JsonDelegate + val workspaceFeature: WorkspaceFeature + + @get:JsonDelegate + val telemetryFeature: TelemetryFeature + + @get:JsonDelegate + val configFeature: ConfigFeature + + @get:JsonDelegate + val statusFeature: StatusFeature + + @get:JsonDelegate + val chatFeature: ChatFeature + + @get:JsonDelegate + val editorsFeature: EditorsFeature + + @JsonNotification("$/setTrace") + fun setTrace(params: SetTraceParams) +} diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/protocol/server/NotebookDocumentFeature.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/protocol/server/NotebookDocumentFeature.kt new file mode 100644 index 000000000000..2f249da449c1 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/protocol/server/NotebookDocumentFeature.kt @@ -0,0 +1,22 @@ +package com.tabbyml.intellijtabby.lsp.protocol.server + +import org.eclipse.lsp4j.DidChangeNotebookDocumentParams +import org.eclipse.lsp4j.DidCloseNotebookDocumentParams +import org.eclipse.lsp4j.DidOpenNotebookDocumentParams +import org.eclipse.lsp4j.DidSaveNotebookDocumentParams +import org.eclipse.lsp4j.jsonrpc.services.JsonNotification +import org.eclipse.lsp4j.jsonrpc.services.JsonSegment + +@JsonSegment("notebookDocument") +interface NotebookDocumentFeature { + fun didOpen(params: DidOpenNotebookDocumentParams) + + @JsonNotification + fun didChange(params: DidChangeNotebookDocumentParams) + + @JsonNotification + fun didSave(params: DidSaveNotebookDocumentParams) + + @JsonNotification + fun didClose(params: DidCloseNotebookDocumentParams) +} diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/protocol/server/StatusFeature.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/protocol/server/StatusFeature.kt new file mode 100644 index 000000000000..9ce9aab4511e --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/protocol/server/StatusFeature.kt @@ -0,0 +1,20 @@ +package com.tabbyml.intellijtabby.lsp.protocol.server + +import com.tabbyml.intellijtabby.lsp.protocol.StatusIgnoredIssuesEditParams +import com.tabbyml.intellijtabby.lsp.protocol.StatusInfo +import com.tabbyml.intellijtabby.lsp.protocol.StatusRequestParams +import org.eclipse.lsp4j.jsonrpc.services.JsonRequest +import org.eclipse.lsp4j.jsonrpc.services.JsonSegment +import java.util.concurrent.CompletableFuture + +@JsonSegment("tabby") +interface StatusFeature { + @JsonRequest("status") + fun getStatus(params: StatusRequestParams): CompletableFuture + + @JsonRequest("status/showHelpMessage") + fun showHelpMessage(params: Any): CompletableFuture + + @JsonRequest("status/ignoredIssues/edit") + fun editIgnoredIssues(params: StatusIgnoredIssuesEditParams): CompletableFuture +} diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/protocol/server/TelemetryFeature.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/protocol/server/TelemetryFeature.kt new file mode 100644 index 000000000000..fcbf7e276fbe --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/protocol/server/TelemetryFeature.kt @@ -0,0 +1,11 @@ +package com.tabbyml.intellijtabby.lsp.protocol.server + +import com.tabbyml.intellijtabby.lsp.protocol.EventParams +import org.eclipse.lsp4j.jsonrpc.services.JsonNotification +import org.eclipse.lsp4j.jsonrpc.services.JsonSegment + +@JsonSegment("tabby/telemetry") +interface TelemetryFeature { + @JsonNotification + fun event(params: EventParams) +} diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/protocol/server/TextDocumentFeature.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/protocol/server/TextDocumentFeature.kt new file mode 100644 index 000000000000..a9eabed45df7 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/protocol/server/TextDocumentFeature.kt @@ -0,0 +1,37 @@ +package com.tabbyml.intellijtabby.lsp.protocol.server + +import com.tabbyml.intellijtabby.lsp.protocol.CompletionList +import com.tabbyml.intellijtabby.lsp.protocol.InlineCompletionList +import com.tabbyml.intellijtabby.lsp.protocol.InlineCompletionParams +import org.eclipse.lsp4j.* +import org.eclipse.lsp4j.jsonrpc.services.JsonNotification +import org.eclipse.lsp4j.jsonrpc.services.JsonRequest +import org.eclipse.lsp4j.jsonrpc.services.JsonSegment +import java.util.concurrent.CompletableFuture + +@JsonSegment("textDocument") +interface TextDocumentFeature { + @JsonRequest + fun completion(params: CompletionParams): CompletableFuture + + @JsonRequest + fun inlineCompletion(params: InlineCompletionParams): CompletableFuture + + @JsonNotification + fun didOpen(params: DidOpenTextDocumentParams) + + @JsonNotification + fun didChange(params: DidChangeTextDocumentParams) + + @JsonNotification + fun didClose(params: DidCloseTextDocumentParams) + + @JsonNotification + fun didSave(params: DidSaveTextDocumentParams) + + @JsonNotification + fun willSave(params: WillSaveTextDocumentParams) + + @JsonRequest + fun codeLens(params: CodeLensParams): CompletableFuture?> +} diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/protocol/server/WorkspaceFeature.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/protocol/server/WorkspaceFeature.kt new file mode 100644 index 000000000000..059b9d9a2079 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/lsp/protocol/server/WorkspaceFeature.kt @@ -0,0 +1,21 @@ +package com.tabbyml.intellijtabby.lsp.protocol.server + +import com.tabbyml.intellijtabby.lsp.protocol.DidChangeConfigurationParams +import org.eclipse.lsp4j.DidChangeWorkspaceFoldersParams +import org.eclipse.lsp4j.ExecuteCommandParams +import org.eclipse.lsp4j.jsonrpc.services.JsonNotification +import org.eclipse.lsp4j.jsonrpc.services.JsonRequest +import org.eclipse.lsp4j.jsonrpc.services.JsonSegment +import java.util.concurrent.CompletableFuture + +@JsonSegment("workspace") +interface WorkspaceFeature { + @JsonNotification + fun didChangeConfiguration(params: DidChangeConfigurationParams) + + @JsonNotification + fun didChangeWorkspaceFolders(params: DidChangeWorkspaceFoldersParams) + + @JsonRequest + fun executeCommand(params: ExecuteCommandParams): CompletableFuture +} diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/notifications/Notifications.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/notifications/Notifications.kt new file mode 100644 index 000000000000..85cd688bb301 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/notifications/Notifications.kt @@ -0,0 +1,87 @@ +package com.tabbyml.intellijtabby.notifications + +import com.intellij.ide.BrowserUtil +import com.intellij.notification.Notification +import com.intellij.notification.NotificationType +import com.intellij.notification.Notifications +import com.intellij.openapi.actionSystem.ActionGroup +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.application.invokeLater +import com.intellij.openapi.options.ShowSettingsUtil +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.tabbyml.intellijtabby.lsp.ConnectionService +import com.tabbyml.intellijtabby.settings.Configurable + +var initializationFailedNotification: Notification? = null + +fun notifyInitializationFailed(exception: ConnectionService.InitializationException) { + initializationFailedNotification?.expire() + + val notification = Notification( + "com.tabbyml.intellijtabby.notifications.warning", + "Tabby initialization failed", + "${exception.message}", + NotificationType.ERROR, + ) + notification.addAction(object : AnAction("Open Online Documentation") { + override fun actionPerformed(e: AnActionEvent) { + notification.expire() + BrowserUtil.browse("https://tabby.tabbyml.com/docs/extensions/troubleshooting/#tabby-initialization-failed") + } + }) + initializationFailedNotification = notification + invokeLater { + Notifications.Bus.notify(notification) + } +} + +var authRequiredNotification: Notification? = null + +fun notifyAuthRequired() { + authRequiredNotification?.expire() + val notification = Notification( + "com.tabbyml.intellijtabby.notifications.warning", + "Tabby server requires authentication, please set your personal token.", + NotificationType.WARNING, + ) + notification.addAction(object : AnAction("Open Settings...") { + override fun actionPerformed(e: AnActionEvent) { + notification.expire() + ShowSettingsUtil.getInstance().showSettingsDialog(e.project, Configurable::class.java) + } + }) + notification.addAction(object : AnAction("Open Online Help") { + override fun actionPerformed(e: AnActionEvent) { + notification.expire() + BrowserUtil.browse("https://tabby.tabbyml.com/docs/quick-start/register-account/") + } + }) + authRequiredNotification = notification + invokeLater { + Notifications.Bus.notify(notification) + } +} + +fun hideAuthRequiredNotification() { + authRequiredNotification?.expire() +} + +fun showOnlineHelp(e: AnActionEvent) { + e.project?.let { + invokeLater { + val actionManager = ActionManager.getInstance() + val actionGroup = actionManager.getAction("Tabby.OpenOnlineHelp") as ActionGroup + val popup = JBPopupFactory.getInstance().createActionGroupPopup( + "Online Help", + actionGroup, + e.dataContext, + false, + null, + 10, + ) + popup.showCenteredInCurrentWindow(it) + } + } +} diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/ApplicationConfigurable.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/ApplicationConfigurable.kt deleted file mode 100644 index 6abb76e41c9f..000000000000 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/ApplicationConfigurable.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.tabbyml.intellijtabby.settings - -import com.intellij.openapi.components.service -import com.intellij.openapi.options.Configurable -import javax.swing.JComponent - -class ApplicationConfigurable : Configurable { - private lateinit var settingsPanel: ApplicationSettingsPanel - - override fun getDisplayName(): String { - return "Tabby" - } - - override fun createComponent(): JComponent { - settingsPanel = ApplicationSettingsPanel() - return settingsPanel.mainPanel - } - - override fun isModified(): Boolean { - val settings = service() - val keymapSettings = service() - return settingsPanel.completionTriggerMode != settings.completionTriggerMode || - (settingsPanel.keymapStyle != keymapSettings.getCurrentKeymapStyle() && - settingsPanel.keymapStyle != KeymapSettings.KeymapStyle.CUSTOMIZE) || - settingsPanel.serverEndpoint != settings.serverEndpoint || - settingsPanel.serverToken != settings.serverToken || - settingsPanel.nodeBinary != settings.nodeBinary || - settingsPanel.isAnonymousUsageTrackingDisabled != settings.isAnonymousUsageTrackingDisabled - } - - override fun apply() { - val settings = service() - val keymapSettings = service() - settings.completionTriggerMode = settingsPanel.completionTriggerMode - keymapSettings.applyKeymapStyle(settingsPanel.keymapStyle) - settings.serverEndpoint = settingsPanel.serverEndpoint - settings.serverToken = settingsPanel.serverToken - settings.nodeBinary = settingsPanel.nodeBinary - settings.isAnonymousUsageTrackingDisabled = settingsPanel.isAnonymousUsageTrackingDisabled - } - - override fun reset() { - val settings = service() - val keymapSettings = service() - settingsPanel.completionTriggerMode = settings.completionTriggerMode - settingsPanel.keymapStyle = keymapSettings.getCurrentKeymapStyle() - settingsPanel.serverEndpoint = settings.serverEndpoint - settingsPanel.serverToken = settings.serverToken - settingsPanel.nodeBinary = settings.nodeBinary - settingsPanel.isAnonymousUsageTrackingDisabled = settings.isAnonymousUsageTrackingDisabled - } -} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/ApplicationSettingsPanel.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/ApplicationSettingsPanel.kt deleted file mode 100644 index 2a191f8ef285..000000000000 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/ApplicationSettingsPanel.kt +++ /dev/null @@ -1,299 +0,0 @@ -package com.tabbyml.intellijtabby.settings - -import com.intellij.openapi.application.ModalityState -import com.intellij.openapi.application.invokeLater -import com.intellij.openapi.components.service -import com.intellij.openapi.keymap.impl.ui.KeymapPanel -import com.intellij.openapi.options.ShowSettingsUtil -import com.intellij.openapi.progress.ProgressIndicator -import com.intellij.openapi.progress.ProgressManager -import com.intellij.openapi.progress.Task -import com.intellij.openapi.ui.Messages -import com.intellij.ui.components.* -import com.intellij.util.ui.FormBuilder -import com.intellij.util.ui.JBUI -import com.intellij.util.ui.UIUtil -import com.tabbyml.intellijtabby.agent.Agent -import com.tabbyml.intellijtabby.agent.AgentService -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch -import javax.swing.ButtonGroup -import javax.swing.JButton -import javax.swing.JPanel - - -private fun FormBuilder.addCopyableTooltip(text: String): FormBuilder { - return this.addComponentToRightColumn( - JBLabel( - text, - UIUtil.ComponentStyle.SMALL, - UIUtil.FontColor.BRIGHTER - ).apply { - setBorder(JBUI.Borders.emptyLeft(10)) - setCopyable(true) - }, - 1, - ) -} - -class ApplicationSettingsPanel { - private val serverEndpointTextField = JBTextField() - private val serverTokenPasswordField = JBPasswordField() - private val serverEndpointCheckConnectionButton = JButton("Check connection").apply { - addActionListener { - val parentComponent = this@ApplicationSettingsPanel.mainPanel - val agentService = service() - val settings = service() - - val task = object : Task.Modal( - null, - parentComponent, - "Check Connection", - true - ) { - lateinit var job: Job - override fun run(indicator: ProgressIndicator) { - job = agentService.scope.launch { - indicator.isIndeterminate = true - indicator.text = "Checking connection..." - settings.serverEndpoint = serverEndpoint - settings.serverToken = serverToken - var serverConfig = agentService.getConfig().server - while ( - (serverEndpoint.isNotBlank() && serverConfig?.endpoint != serverEndpoint) || - (serverToken.isNotBlank() && serverConfig?.token != serverToken) - ) { - Thread.sleep(200) - serverConfig = agentService.getConfig().server - } - when (agentService.status.value) { - Agent.Status.READY -> { - invokeLater(ModalityState.stateForComponent(parentComponent)) { - Messages.showInfoMessage( - parentComponent, - "Successfully connected to the Tabby server.", - "Check Connection Completed" - ) - } - } - - Agent.Status.UNAUTHORIZED -> { - val currentToken = agentService.getConfig().server?.token - val message = if (currentToken?.isNotBlank() == true) { - "Tabby server requires authentication, but the current token is invalid." - } else { - "Tabby server requires authentication, please set your personal token." - } - invokeLater(ModalityState.stateForComponent(parentComponent)) { - Messages.showErrorDialog( - parentComponent, - message, - "Check Connection Failed" - ) - } - } - - else -> { - val detail = agentService.getCurrentIssueDetail() - if (detail?.get("name") == "connectionFailed") { - invokeLater(ModalityState.stateForComponent(parentComponent)) { - val errorMessage = (detail["message"] as String?)?.replace("\n", "
    ") ?: "" - val messages = "Failed to connect to the Tabby server:
    ${errorMessage}" - Messages.showErrorDialog(parentComponent, messages, "Check Connection Failed") - } - } - } - } - } - while (job.isActive) { - indicator.checkCanceled() - Thread.sleep(100) - } - } - - override fun onCancel() { - job.cancel() - } - } - ProgressManager.getInstance().run(task) - } - } - private val serverEndpointPanel = FormBuilder.createFormBuilder() - .addLabeledComponent("Endpoint", serverEndpointTextField, 0, false) - .addCopyableTooltip( - """ - - A http or https URL of Tabby server endpoint.
    - If leave empty, server endpoint config in ~/.tabby-client/agent/config.toml will be used.
    - Default to http://localhost:8080. - - """.trimIndent() - ) - .addSeparator() - .addLabeledComponent("Token", serverTokenPasswordField, 0, false) - .addCopyableTooltip( - """ - - Set token here if your Tabby server requires authentication. - - """.trimIndent() - ) - .addSeparator() - .addComponent(serverEndpointCheckConnectionButton) - .panel - - private val nodeBinaryTextField = JBTextField() - private val nodeBinaryPanel = FormBuilder.createFormBuilder() - .addComponent(nodeBinaryTextField) - .addCopyableTooltip( - """ - - Path to the Node binary for running the Tabby agent. The Node version must be >= 18.0.
    - If left empty, Tabby will attempt to find the Node binary in the PATH environment variable.
    - - """.trimIndent() - ) - .panel - - private val completionTriggerModeAutomaticRadioButton = JBRadioButton("Automatic") - private val completionTriggerModeManualRadioButton = JBRadioButton("Manual") - private val completionTriggerModeRadioGroup = ButtonGroup().apply { - add(completionTriggerModeAutomaticRadioButton) - add(completionTriggerModeManualRadioButton) - } - private val completionTriggerModePanel: JPanel = FormBuilder.createFormBuilder() - .addComponent(completionTriggerModeAutomaticRadioButton) - .addCopyableTooltip("Trigger automatically when you stop typing") - .addComponent(completionTriggerModeManualRadioButton) - .addCopyableTooltip("Trigger on-demand by pressing a shortcut") - .panel - - private val keymapStyleDefaultRadioButton = JBRadioButton("Default") - private val keymapStyleTabbyStyleRadioButton = JBRadioButton("Tabby style") - private val keymapStyleCustomRadioButton = JBRadioButton("Customize...").apply { - addActionListener { - ShowSettingsUtil.getInstance().showSettingsDialog(null, KeymapPanel::class.java) { panel -> - CoroutineScope(Dispatchers.IO).launch { - Thread.sleep(500) // FIXME: It seems that we need to wait for the KeymapPanel to be ready? - invokeLater(ModalityState.stateForComponent(panel)) { - panel.showOption("Tabby") - } - } - } - } - border = JBUI.Borders.emptyLeft(1) - } - private val keymapStyleRadioGroup = ButtonGroup().apply { - add(keymapStyleDefaultRadioButton) - add(keymapStyleTabbyStyleRadioButton) - add(keymapStyleCustomRadioButton) - } - private val keymapStylePanel: JPanel = FormBuilder.createFormBuilder() - .addComponent(keymapStyleDefaultRadioButton) - .addCopyableTooltip("Use Tab to accept full completion, and use Ctrl+Tab to accept next line.") - .addComponent(keymapStyleTabbyStyleRadioButton) - .addCopyableTooltip("Use Ctrl+Tab to accept full completion, and use Tab to accept next line.") - .addComponent(keymapStyleCustomRadioButton) - .panel - - private val isAnonymousUsageTrackingDisabledCheckBox = JBCheckBox("Disable anonymous usage tracking") - private val isAnonymousUsageTrackingPanel: JPanel = FormBuilder.createFormBuilder() - .addComponent(isAnonymousUsageTrackingDisabledCheckBox) - .addCopyableTooltip( - """ - - Tabby collects aggregated anonymous usage data and sends it to the Tabby team to help improve our products.
    - Your code, generated completions, or any identifying information is never tracked or transmitted.
    - For more details on data collection, please check our online documentation.
    - - """ - ) - .panel - - private val resetMutedNotificationsButton = JButton("Reset \"Don't Show Again\" Notifications").apply { - addActionListener { - val settings = service() - settings.notificationsMuted = listOf() - invokeLater(ModalityState.stateForComponent(this@ApplicationSettingsPanel.mainPanel)) { - Messages.showInfoMessage("Reset \"Don't Show Again\" notifications successfully.", "Reset Notifications") - } - } - } - private val resetMutedNotificationsPanel: JPanel = FormBuilder.createFormBuilder() - .addComponent(resetMutedNotificationsButton) - .panel - - val mainPanel: JPanel = FormBuilder.createFormBuilder() - .addLabeledComponent("Server", serverEndpointPanel, 5, false) - .addSeparator(5) - .addLabeledComponent("Inline completion trigger", completionTriggerModePanel, 5, false) - .addSeparator(5) - .addLabeledComponent("Keymap", keymapStylePanel, 5, false) - .addSeparator(5) - .addLabeledComponent("Node binary
    (Requires restart IDE)", nodeBinaryPanel, 5, false) - .addSeparator(5) - .addLabeledComponent("Anonymous usage tracking", isAnonymousUsageTrackingPanel, 5, false) - .apply { - if (service().notificationsMuted.isNotEmpty()) { - addSeparator(5) - addLabeledComponent("Notifications", resetMutedNotificationsPanel, 5, false) - } - } - .addComponentFillVertically(JPanel(), 0) - .panel - - var serverEndpoint: String - get() = serverEndpointTextField.text - set(value) { - serverEndpointTextField.text = value - } - - var serverToken: String - get() = String(serverTokenPasswordField.password) - set(value) { - serverTokenPasswordField.text = value - } - - var nodeBinary: String - get() = nodeBinaryTextField.text - set(value) { - nodeBinaryTextField.text = value - } - - var keymapStyle: KeymapSettings.KeymapStyle - get() = if (keymapStyleDefaultRadioButton.isSelected) { - KeymapSettings.KeymapStyle.DEFAULT - } else if (keymapStyleTabbyStyleRadioButton.isSelected) { - KeymapSettings.KeymapStyle.TABBY_STYLE - } else { - KeymapSettings.KeymapStyle.CUSTOMIZE - } - set(value) { - when (value) { - KeymapSettings.KeymapStyle.DEFAULT -> keymapStyleDefaultRadioButton.isSelected = true - KeymapSettings.KeymapStyle.TABBY_STYLE -> keymapStyleTabbyStyleRadioButton.isSelected = true - KeymapSettings.KeymapStyle.CUSTOMIZE -> keymapStyleCustomRadioButton.isSelected = true - } - } - - var completionTriggerMode: ApplicationSettingsState.TriggerMode - get() = if (completionTriggerModeAutomaticRadioButton.isSelected) { - ApplicationSettingsState.TriggerMode.AUTOMATIC - } else { - ApplicationSettingsState.TriggerMode.MANUAL - } - set(value) { - when (value) { - ApplicationSettingsState.TriggerMode.AUTOMATIC -> completionTriggerModeAutomaticRadioButton.isSelected = true - ApplicationSettingsState.TriggerMode.MANUAL -> completionTriggerModeManualRadioButton.isSelected = true - } - } - - var isAnonymousUsageTrackingDisabled: Boolean - get() = isAnonymousUsageTrackingDisabledCheckBox.isSelected - set(value) { - isAnonymousUsageTrackingDisabledCheckBox.isSelected = value - } -} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/ApplicationSettingsState.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/ApplicationSettingsState.kt deleted file mode 100644 index 961cd41c7e90..000000000000 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/ApplicationSettingsState.kt +++ /dev/null @@ -1,119 +0,0 @@ -package com.tabbyml.intellijtabby.settings - -import com.google.gson.annotations.SerializedName -import com.intellij.openapi.components.PersistentStateComponent -import com.intellij.openapi.components.Service -import com.intellij.openapi.components.State -import com.intellij.openapi.components.Storage -import com.intellij.util.xmlb.XmlSerializerUtil -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.* - -@Service -@State( - name = "com.tabbyml.intellijtabby.settings.ApplicationSettingsState", - storages = [Storage("intellij-tabby.xml")] -) -class ApplicationSettingsState : PersistentStateComponent { - enum class TriggerMode { - @SerializedName("manual") - MANUAL, - - @SerializedName("automatic") - AUTOMATIC, - } - - private val completionTriggerModeFlow = MutableStateFlow(TriggerMode.AUTOMATIC) - val completionTriggerModeState = completionTriggerModeFlow.asStateFlow() - var completionTriggerMode: TriggerMode = TriggerMode.AUTOMATIC - set(value) { - field = value - completionTriggerModeFlow.value = value - } - - private val serverEndpointFlow = MutableStateFlow("") - val serverEndpointState = serverEndpointFlow.asStateFlow() - var serverEndpoint: String = "" - set(value) { - field = value - serverEndpointFlow.value = value - } - - private val serverTokenFlow = MutableStateFlow("") - val serverTokenState = serverTokenFlow.asStateFlow() - var serverToken: String = "" - set(value) { - field = value - serverTokenFlow.value = value - } - - private val nodeBinaryFlow = MutableStateFlow("") - val nodeBinaryState = nodeBinaryFlow.asStateFlow() - var nodeBinary: String = "" - set(value) { - field = value - nodeBinaryFlow.value = value - } - - private val isAnonymousUsageTrackingDisabledFlow = MutableStateFlow(false) - val isAnonymousUsageTrackingDisabledState = isAnonymousUsageTrackingDisabledFlow.asStateFlow() - var isAnonymousUsageTrackingDisabled: Boolean = false - set(value) { - field = value - isAnonymousUsageTrackingDisabledFlow.value = value - } - - private val notificationsMutedFlow = MutableStateFlow(listOf()) - val notificationsMutedState = notificationsMutedFlow.asStateFlow() - var notificationsMuted: List = listOf() - set(value) { - field = value - notificationsMutedFlow.value = value - } - - data class State( - val completionTriggerMode: TriggerMode, - val serverEndpoint: String, - val serverToken: String, - val nodeBinary: String, - val isAnonymousUsageTrackingDisabled: Boolean, - val notificationsMuted: List, - ) - - val data: State - get() = State( - completionTriggerMode = completionTriggerMode, - serverEndpoint = serverEndpoint, - serverToken = serverToken, - nodeBinary = nodeBinary, - isAnonymousUsageTrackingDisabled = isAnonymousUsageTrackingDisabled, - notificationsMuted = notificationsMuted, - ) - - val state = combine( - completionTriggerModeState, - serverEndpointState, - serverTokenState, - nodeBinaryState, - isAnonymousUsageTrackingDisabledState, - notificationsMutedState, - ) { args -> - State( - completionTriggerMode = args[0] as TriggerMode, - serverEndpoint = args[1] as String, - serverToken = args[2] as String, - nodeBinary = args[3] as String, - isAnonymousUsageTrackingDisabled = args[4] as Boolean, - notificationsMuted = args[5] as List, - ) - }.stateIn(CoroutineScope(Dispatchers.IO), SharingStarted.Eagerly, this.data) - - override fun getState(): ApplicationSettingsState { - return this - } - - override fun loadState(state: ApplicationSettingsState) { - XmlSerializerUtil.copyBean(state, this) - } -} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/Configurable.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/Configurable.kt new file mode 100644 index 000000000000..7d685f9600c9 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/Configurable.kt @@ -0,0 +1,60 @@ +package com.tabbyml.intellijtabby.settings + +import com.intellij.openapi.components.service +import com.intellij.openapi.components.serviceOrNull +import com.intellij.openapi.options.Configurable +import com.intellij.openapi.project.Project +import javax.swing.JComponent + +class Configurable(private val project: Project) : Configurable { + private val settings = service() + private var settingsPanel: SettingsPanel? = null + private val keymapSettings = project.serviceOrNull() + + override fun getDisplayName(): String { + return "Tabby" + } + + override fun createComponent(): JComponent { + val panel = SettingsPanel(project) + settingsPanel = panel + return panel.mainPanel + } + + override fun isModified(): Boolean { + val panel = settingsPanel ?: return false + return panel.completionTriggerMode != settings.completionTriggerMode || + panel.serverEndpoint != settings.serverEndpoint || + panel.serverToken != settings.serverToken || + panel.nodeBinary != settings.nodeBinary || + panel.isAnonymousUsageTrackingDisabled != settings.isAnonymousUsageTrackingDisabled || + (panel.keymapStyle != keymapSettings?.getCurrentKeymapStyle() && panel.keymapStyle != KeymapSettings.KeymapStyle.CUSTOMIZE) + } + + override fun apply() { + val panel = settingsPanel ?: return + settings.completionTriggerMode = panel.completionTriggerMode + settings.serverEndpoint = panel.serverEndpoint + settings.serverToken = panel.serverToken + settings.nodeBinary = panel.nodeBinary + settings.isAnonymousUsageTrackingDisabled = panel.isAnonymousUsageTrackingDisabled + settings.notifyChanges(project) + + keymapSettings?.applyKeymapStyle(panel.keymapStyle) + } + + override fun reset() { + val panel = settingsPanel ?: return + panel.completionTriggerMode = settings.completionTriggerMode + panel.serverEndpoint = settings.serverEndpoint + panel.serverToken = settings.serverToken + panel.nodeBinary = settings.nodeBinary + panel.isAnonymousUsageTrackingDisabled = settings.isAnonymousUsageTrackingDisabled + + keymapSettings?.let { panel.keymapStyle = it.getCurrentKeymapStyle() } + } + + override fun disposeUIResources() { + settingsPanel = null + } +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/KeymapSettings.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/KeymapSettings.kt index 5d8dd9a0bc98..ac49e47a3726 100644 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/KeymapSettings.kt +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/KeymapSettings.kt @@ -1,26 +1,19 @@ package com.tabbyml.intellijtabby.settings -import com.google.gson.annotations.SerializedName import com.intellij.openapi.actionSystem.KeyboardShortcut import com.intellij.openapi.components.Service import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.keymap.Keymap import com.intellij.openapi.keymap.ex.KeymapManagerEx +import com.intellij.openapi.project.Project -@Service -class KeymapSettings { +@Service(Service.Level.PROJECT) +class KeymapSettings(private val project: Project) { private val logger = Logger.getInstance(KeymapSettings::class.java) private val manager = KeymapManagerEx.getInstanceEx() enum class KeymapStyle { - @SerializedName("default") - DEFAULT, - - @SerializedName("tabby-style") - TABBY_STYLE, - - @SerializedName("customize") - CUSTOMIZE, + DEFAULT, TABBY_STYLE, CUSTOMIZE, } fun getCurrentKeymapStyle(): KeymapStyle { @@ -81,24 +74,30 @@ class KeymapSettings { companion object { private val DEFAULT_KEYMAP_SCHEMA = mapOf( - "Tabby.TriggerCompletion" to listOf( - KeyboardShortcut.fromString("ctrl BACK_SLASH"), - KeyboardShortcut.fromString("alt BACK_SLASH") + "Tabby.InlineCompletion.Trigger" to listOf( + KeyboardShortcut.fromString("ctrl BACK_SLASH"), KeyboardShortcut.fromString("alt BACK_SLASH") ), - "Tabby.AcceptCompletion" to listOf(KeyboardShortcut.fromString("TAB")), - "Tabby.AcceptCompletionNextLine" to listOf(KeyboardShortcut.fromString("ctrl TAB")), - "Tabby.AcceptCompletionNextWord" to listOf(KeyboardShortcut.fromString("ctrl RIGHT")), - "Tabby.DismissCompletion" to listOf(KeyboardShortcut.fromString("ESCAPE")), + "Tabby.InlineCompletion.TabAccept" to listOf(KeyboardShortcut.fromString("TAB")), + "Tabby.InlineCompletion.AcceptNextLine" to listOf(KeyboardShortcut.fromString("ctrl TAB")), + "Tabby.InlineCompletion.AcceptNextWord" to listOf(KeyboardShortcut.fromString("ctrl RIGHT")), + "Tabby.InlineCompletion.Dismiss" to listOf(KeyboardShortcut.fromString("ESCAPE")), + "Tabby.Chat.ToggleChatToolWindow" to listOf(KeyboardShortcut.fromString("ctrl L")), + "Tabby.InlineChat.Open" to listOf(KeyboardShortcut.fromString("ctrl I")), + "Tabby.InlineChat.Resolve.Accept" to listOf(KeyboardShortcut.fromString("ctrl shift D")), + "Tabby.InlineChat.Resolve.Discard" to listOf(KeyboardShortcut.fromString("ctrl ESCAPE")), ) private val TABBY_STYLE_KEYMAP_SCHEMA = mapOf( - "Tabby.TriggerCompletion" to listOf( - KeyboardShortcut.fromString("ctrl BACK_SLASH"), - KeyboardShortcut.fromString("alt BACK_SLASH") + "Tabby.InlineCompletion.Trigger" to listOf( + KeyboardShortcut.fromString("ctrl BACK_SLASH"), KeyboardShortcut.fromString("alt BACK_SLASH") ), - "Tabby.AcceptCompletion" to listOf(KeyboardShortcut.fromString("ctrl TAB")), - "Tabby.AcceptCompletionNextLine" to listOf(KeyboardShortcut.fromString("TAB")), - "Tabby.AcceptCompletionNextWord" to listOf(KeyboardShortcut.fromString("ctrl RIGHT")), - "Tabby.DismissCompletion" to listOf(KeyboardShortcut.fromString("ESCAPE")), + "Tabby.InlineCompletion.TabAccept" to listOf(KeyboardShortcut.fromString("ctrl TAB")), + "Tabby.InlineCompletion.AcceptNextLine" to listOf(KeyboardShortcut.fromString("TAB")), + "Tabby.InlineCompletion.AcceptNextWord" to listOf(KeyboardShortcut.fromString("ctrl RIGHT")), + "Tabby.InlineCompletion.Dismiss" to listOf(KeyboardShortcut.fromString("ESCAPE")), + "Tabby.Chat.ToggleChatToolWindow" to listOf(KeyboardShortcut.fromString("ctrl L")), + "Tabby.InlineChat.Open" to listOf(KeyboardShortcut.fromString("ctrl I")), + "Tabby.InlineChat.Resolve.Accept" to listOf(KeyboardShortcut.fromString("ctrl shift D")), + "Tabby.InlineChat.Resolve.Discard" to listOf(KeyboardShortcut.fromString("ctrl ESCAPE")), ) } } \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/SettingsPanel.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/SettingsPanel.kt new file mode 100644 index 000000000000..797a57b9f43a --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/SettingsPanel.kt @@ -0,0 +1,276 @@ +package com.tabbyml.intellijtabby.settings + +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.application.invokeLater +import com.intellij.openapi.components.service +import com.intellij.openapi.components.serviceOrNull +import com.intellij.openapi.keymap.impl.ui.KeymapPanel +import com.intellij.openapi.options.ShowSettingsUtil +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.Task +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.Messages +import com.intellij.ui.components.* +import com.intellij.util.ui.FormBuilder +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.UIUtil +import com.tabbyml.intellijtabby.lsp.ConnectionService +import com.tabbyml.intellijtabby.lsp.protocol.StatusIgnoredIssuesEditParams +import com.tabbyml.intellijtabby.lsp.protocol.StatusInfo +import com.tabbyml.intellijtabby.lsp.protocol.StatusRequestParams +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.future.await +import kotlinx.coroutines.launch +import javax.swing.ButtonGroup +import javax.swing.JButton +import javax.swing.JPanel + +class SettingsPanel(private val project: Project) { + private val settings = service() + private val scope = CoroutineScope(Dispatchers.IO) + private suspend fun getServer() = project.serviceOrNull()?.getServerAsync() + + private val serverEndpointTextField = JBTextField() + private val serverTokenPasswordField = JBPasswordField() + private val serverEndpointCheckConnectionButton = JButton("Check connection").apply { + addActionListener { + val parentComponent = this@SettingsPanel.mainPanel + + val task = object : Task.Modal( + project, parentComponent, "Check Connection", true + ) { + lateinit var job: Job + override fun run(indicator: ProgressIndicator) { + job = scope.launch { + indicator.isIndeterminate = true + indicator.text = "Checking connection..." + settings.serverEndpoint = serverEndpoint + settings.serverToken = serverToken + settings.notifyChanges(project) + + val server = getServer() ?: return@launch + val statusInfo = server.statusFeature.getStatus(StatusRequestParams(recheckConnection = true)).await() + when (statusInfo.status) { + StatusInfo.Status.CONNECTING -> { + // Do nothing + } + + StatusInfo.Status.UNAUTHORIZED -> { + invokeLater(ModalityState.stateForComponent(parentComponent)) { + Messages.showErrorDialog( + parentComponent, + "Tabby server requires authentication, please set your personal token.", + "Check Connection Failed" + ) + } + } + + StatusInfo.Status.DISCONNECTED -> { + invokeLater(ModalityState.stateForComponent(parentComponent)) { + val errorMessage = statusInfo.helpMessage ?: "Unknown error." + val messages = "Failed to connect to the Tabby server:
    ${errorMessage}" + Messages.showErrorDialog(parentComponent, messages, "Check Connection Failed") + } + } + + else -> { + invokeLater(ModalityState.stateForComponent(parentComponent)) { + Messages.showInfoMessage( + parentComponent, "Successfully connected to the Tabby server.", "Check Connection Completed" + ) + } + } + } + } + + while (job.isActive) { + indicator.checkCanceled() + Thread.sleep(100) + } + } + + override fun onCancel() { + job.cancel() + } + } + + ProgressManager.getInstance().run(task) + } + } + private val serverEndpointPanel = + FormBuilder.createFormBuilder().addLabeledComponent("Endpoint", serverEndpointTextField, 0, false) + .addCopyableTooltip( + """ + + A http or https URL of Tabby server endpoint.
    + If left empty, the server endpoint config in ~/.tabby-client/agent/config.toml will be used.
    + Default to http://localhost:8080. + + """.trimIndent() + ).addSeparator().addLabeledComponent("Token", serverTokenPasswordField, 0, false).addCopyableTooltip( + """ + + Set token here if your Tabby server requires authentication. + + """.trimIndent() + ).addSeparator().addComponent(serverEndpointCheckConnectionButton).panel + + private val nodeBinaryTextField = JBTextField() + private val nodeBinaryPanel = FormBuilder.createFormBuilder().addComponent(nodeBinaryTextField).addCopyableTooltip( + """ + + Path to the Node binary for running the Tabby agent. The Node version must be >= 18.0.
    + If left empty, Tabby will attempt to find the Node binary in the PATH environment variable.
    + + """.trimIndent() + ).panel + + private val completionTriggerModeAutomaticRadioButton = JBRadioButton("Automatic") + private val completionTriggerModeManualRadioButton = JBRadioButton("Manual") + private val completionTriggerModeRadioGroup = ButtonGroup().apply { + add(completionTriggerModeAutomaticRadioButton) + add(completionTriggerModeManualRadioButton) + } + private val completionTriggerModePanel: JPanel = + FormBuilder.createFormBuilder().addComponent(completionTriggerModeAutomaticRadioButton) + .addCopyableTooltip("Trigger automatically when you stop typing") + .addComponent(completionTriggerModeManualRadioButton) + .addCopyableTooltip("Trigger on-demand by pressing a shortcut").panel + + private val keymapStyleDefaultRadioButton = JBRadioButton("Default") + private val keymapStyleTabbyStyleRadioButton = JBRadioButton("Tabby style") + private val keymapStyleCustomRadioButton = JBRadioButton("Customize...").apply { + addActionListener { + ShowSettingsUtil.getInstance().showSettingsDialog(project, KeymapPanel::class.java) { panel -> + CoroutineScope(Dispatchers.IO).launch { + Thread.sleep(500) // FIXME: It seems that we need to wait for the KeymapPanel to be ready? + invokeLater(ModalityState.stateForComponent(panel)) { + panel.showOption("Tabby") + } + } + } + } + border = JBUI.Borders.emptyLeft(1) + } + private val keymapStyleRadioGroup = ButtonGroup().apply { + add(keymapStyleDefaultRadioButton) + add(keymapStyleTabbyStyleRadioButton) + add(keymapStyleCustomRadioButton) + } + private val keymapStylePanel: JPanel = FormBuilder.createFormBuilder().addComponent(keymapStyleDefaultRadioButton) + .addCopyableTooltip("Use Tab to accept full completion, and use Ctrl+Tab to accept next line.") + .addComponent(keymapStyleTabbyStyleRadioButton) + .addCopyableTooltip("Use Ctrl+Tab to accept full completion, and use Tab to accept next line.") + .addComponent(keymapStyleCustomRadioButton).panel + + private val isAnonymousUsageTrackingDisabledCheckBox = JBCheckBox("Disable anonymous usage tracking") + private val isAnonymousUsageTrackingPanel: JPanel = + FormBuilder.createFormBuilder().addComponent(isAnonymousUsageTrackingDisabledCheckBox).addCopyableTooltip( + """ + + Tabby collects aggregated anonymous usage data and sends it to the Tabby team to help improve our products.
    + Your code, generated completions, or any identifying information is never tracked or transmitted.
    + For more details on data collection, please check our online documentation.
    + + """ + ).panel + + private val resetMutedNotificationsButton = JButton("Reset \"Don't Show Again\" Notifications").apply { + addActionListener { + scope.launch { + val server = getServer() ?: return@launch + server.statusFeature.editIgnoredIssues(StatusIgnoredIssuesEditParams(operation = StatusIgnoredIssuesEditParams.Operation.REMOVE_ALL)) + .thenAccept { + invokeLater(ModalityState.stateForComponent(this@SettingsPanel.mainPanel)) { + Messages.showInfoMessage("Reset \"Don't Show Again\" notifications successfully.", "Reset Notifications") + } + } + } + } + } + private val resetMutedNotificationsPanel: JPanel = + FormBuilder.createFormBuilder().addComponent(resetMutedNotificationsButton).panel + + val mainPanel: JPanel = + FormBuilder.createFormBuilder().addLabeledComponent("Server", serverEndpointPanel, 5, false) + .addSeparator(5) + .addLabeledComponent("Inline completion trigger", completionTriggerModePanel, 5, false) + .addSeparator(5) + .addLabeledComponent("Keymap", keymapStylePanel, 5, false) + .addSeparator(5) + .addLabeledComponent("Node binary
    (Requires restart IDE)", nodeBinaryPanel, 5, false) + .addSeparator(5) + .addLabeledComponent("Anonymous usage tracking", isAnonymousUsageTrackingPanel, 5, false) + .addSeparator(5) + .addLabeledComponent("Notifications", resetMutedNotificationsPanel, 5, false) + .addComponentFillVertically(JPanel(), 0) + .panel + + var serverEndpoint: String + get() = serverEndpointTextField.text + set(value) { + serverEndpointTextField.text = value + } + + var serverToken: String + get() = String(serverTokenPasswordField.password) + set(value) { + serverTokenPasswordField.text = value + } + + var nodeBinary: String + get() = nodeBinaryTextField.text + set(value) { + nodeBinaryTextField.text = value + } + + var keymapStyle: KeymapSettings.KeymapStyle + get() = if (keymapStyleDefaultRadioButton.isSelected) { + KeymapSettings.KeymapStyle.DEFAULT + } else if (keymapStyleTabbyStyleRadioButton.isSelected) { + KeymapSettings.KeymapStyle.TABBY_STYLE + } else { + KeymapSettings.KeymapStyle.CUSTOMIZE + } + set(value) { + when (value) { + KeymapSettings.KeymapStyle.DEFAULT -> keymapStyleDefaultRadioButton.isSelected = true + KeymapSettings.KeymapStyle.TABBY_STYLE -> keymapStyleTabbyStyleRadioButton.isSelected = true + KeymapSettings.KeymapStyle.CUSTOMIZE -> keymapStyleCustomRadioButton.isSelected = true + } + } + + var completionTriggerMode: SettingsState.TriggerMode + get() = if (completionTriggerModeAutomaticRadioButton.isSelected) { + SettingsState.TriggerMode.AUTOMATIC + } else { + SettingsState.TriggerMode.MANUAL + } + set(value) { + when (value) { + SettingsState.TriggerMode.AUTOMATIC -> completionTriggerModeAutomaticRadioButton.isSelected = true + SettingsState.TriggerMode.MANUAL -> completionTriggerModeManualRadioButton.isSelected = true + } + } + + var isAnonymousUsageTrackingDisabled: Boolean + get() = isAnonymousUsageTrackingDisabledCheckBox.isSelected + set(value) { + isAnonymousUsageTrackingDisabledCheckBox.isSelected = value + } +} + +private fun FormBuilder.addCopyableTooltip(text: String): FormBuilder { + return this.addComponentToRightColumn( + JBLabel( + text, UIUtil.ComponentStyle.SMALL, UIUtil.FontColor.BRIGHTER + ).apply { + setBorder(JBUI.Borders.emptyLeft(10)) + setCopyable(true) + }, + 1, + ) +} diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/SettingsService.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/SettingsService.kt new file mode 100644 index 000000000000..320f7fbacacc --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/SettingsService.kt @@ -0,0 +1,72 @@ +package com.tabbyml.intellijtabby.settings + +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.SimplePersistentStateComponent +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import com.intellij.openapi.project.Project +import com.intellij.util.messages.Topic +import com.tabbyml.intellijtabby.safeSyncPublisher + +@Service +@State( + name = "com.tabbyml.intellijtabby.settings.SettingsService", storages = [Storage("intellij-tabby.xml")] +) +class SettingsService : SimplePersistentStateComponent(SettingsState()) { + var completionTriggerMode + get() = state.completionTriggerMode + set(value) { + state.completionTriggerMode = value + } + var serverEndpoint + get() = state.serverEndpoint ?: "" + set(value) { + state.serverEndpoint = value + } + var serverToken + get() = state.serverToken ?: "" + set(value) { + state.serverToken = value + } + var nodeBinary + get() = state.nodeBinary ?: "" + set(value) { + state.nodeBinary = value + } + var isAnonymousUsageTrackingDisabled + get() = state.isAnonymousUsageTrackingDisabled + set(value) { + state.isAnonymousUsageTrackingDisabled = value + } + + fun notifyChanges(project: Project) { + project.safeSyncPublisher(Listener.TOPIC)?.settingsChanged(settings()) + } + + data class Settings( + val completionTriggerMode: SettingsState.TriggerMode, + val serverEndpoint: String, + val serverToken: String, + val nodeBinary: String, + val isAnonymousUsageTrackingDisabled: Boolean, + ) + + fun settings(): Settings { + return Settings( + completionTriggerMode, + serverEndpoint, + serverToken, + nodeBinary, + isAnonymousUsageTrackingDisabled, + ) + } + + interface Listener { + fun settingsChanged(settings: Settings) {} + + companion object { + @Topic.ProjectLevel + val TOPIC = Topic(Listener::class.java, Topic.BroadcastDirection.NONE) + } + } +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/SettingsState.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/SettingsState.kt new file mode 100644 index 000000000000..9daac6821597 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/SettingsState.kt @@ -0,0 +1,16 @@ +package com.tabbyml.intellijtabby.settings + +import com.intellij.openapi.components.BaseState + + +class SettingsState : BaseState() { + enum class TriggerMode { + MANUAL, AUTOMATIC, + } + + var completionTriggerMode by enum(TriggerMode.AUTOMATIC) + var serverEndpoint by string() + var serverToken by string() + var nodeBinary by string() + var isAnonymousUsageTrackingDisabled by property(false) +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/status/StatusBarWidgetFactory.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/status/StatusBarWidgetFactory.kt deleted file mode 100644 index 3e03c612908d..000000000000 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/status/StatusBarWidgetFactory.kt +++ /dev/null @@ -1,164 +0,0 @@ -package com.tabbyml.intellijtabby.status - -import com.intellij.icons.AllIcons -import com.intellij.openapi.actionSystem.* -import com.intellij.openapi.application.invokeLater -import com.intellij.openapi.components.service -import com.intellij.openapi.project.Project -import com.intellij.openapi.ui.popup.JBPopupFactory -import com.intellij.openapi.ui.popup.ListPopup -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.openapi.wm.StatusBarWidget -import com.intellij.openapi.wm.impl.status.EditorBasedStatusBarPopup -import com.intellij.openapi.wm.impl.status.widget.StatusBarEditorBasedWidgetFactory -import com.intellij.ui.AnimatedIcon -import com.tabbyml.intellijtabby.agent.Agent -import com.tabbyml.intellijtabby.agent.AgentService -import com.tabbyml.intellijtabby.editor.CompletionProvider -import com.tabbyml.intellijtabby.settings.ApplicationSettingsState -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.launch -import javax.swing.Icon - -class StatusBarWidgetFactory : StatusBarEditorBasedWidgetFactory() { - override fun getId(): String { - return StatusBarWidgetFactory::class.java.name - } - - override fun getDisplayName(): String { - return "Tabby" - } - - override fun createWidget(project: Project): StatusBarWidget { - data class CombinedState( - val settings: ApplicationSettingsState.State, - val agentStatus: Enum<*>, - val currentIssue: String?, - val ongoingCompletion: CompletionProvider.CompletionContext?, - ) - - return object : EditorBasedStatusBarPopup(project, false) { - val updateStatusScope: CoroutineScope = CoroutineScope(Dispatchers.Main) - val text = "Tabby" - var icon: Icon = AnimatedIcon.Default() - var tooltip = "Tabby: Initializing" - - init { - val settings = service() - val agentService = service() - val completionProvider = service() - updateStatusScope.launch { - combine( - settings.state, - agentService.status, - agentService.currentIssue, - completionProvider.ongoingCompletion - ) { settings, agentStatus, currentIssue, ongoingCompletion -> - CombinedState(settings, agentStatus, currentIssue, ongoingCompletion) - }.collect { - updateStatus(it) - } - } - } - - override fun ID(): String { - return "${StatusBarWidgetFactory::class.java.name}.widget" - } - - override fun createInstance(project: Project): StatusBarWidget { - return createWidget(project) - } - - override fun getWidgetState(file: VirtualFile?): WidgetState { - return WidgetState(tooltip, text, true).also { - it.icon = icon - } - } - - override fun createPopup(context: DataContext?): ListPopup? { - if (context == null) { - return null - } - return JBPopupFactory.getInstance().createActionGroupPopup( - tooltip, - ActionManager.getInstance().getAction("Tabby.StatusBarPopupMenu") as ActionGroup, - context, - false, - null, - 10, - ) - } - - private fun updateStatus(state: CombinedState) { - when (state.agentStatus) { - AgentService.Status.INITIALIZING, Agent.Status.NOT_INITIALIZED -> { - icon = AnimatedIcon.Default() - tooltip = "Tabby: Initializing" - } - - AgentService.Status.INITIALIZATION_FAILED -> { - icon = AllIcons.General.Error - tooltip = "Tabby: Initialization failed" - } - - Agent.Status.READY -> { - val muted = mutableListOf() - if (state.settings.notificationsMuted.contains("completionResponseTimeIssues")) { - muted += listOf("slowCompletionResponseTime", "highCompletionTimeoutRate") - } - if (state.currentIssue != null && state.currentIssue !in muted) { - icon = AllIcons.General.Warning - tooltip = when (state.currentIssue) { - "slowCompletionResponseTime" -> "Tabby: Completion requests appear to take too much time" - "highCompletionTimeoutRate" -> "Tabby: Most completion requests timed out" - else -> "Tabby: Issues exist" - } - } else { - when (state.settings.completionTriggerMode) { - ApplicationSettingsState.TriggerMode.AUTOMATIC -> { - if (state.ongoingCompletion == null) { - icon = AllIcons.Actions.Checked - tooltip = "Tabby: Automatic code completion is enabled" - } else { - icon = AnimatedIcon.Default() - tooltip = "Tabby: Generating code completions" - } - } - - ApplicationSettingsState.TriggerMode.MANUAL -> { - if (state.ongoingCompletion == null) { - icon = AllIcons.General.ChevronRight - tooltip = "Tabby: Standing by, please manually trigger code completion." - } else { - icon = AnimatedIcon.Default() - tooltip = "Tabby: Generating code completions" - } - } - } - } - } - - Agent.Status.DISCONNECTED -> { - icon = AllIcons.General.Error - tooltip = "Tabby: Cannot connect to Server, please check settings" - } - - Agent.Status.UNAUTHORIZED -> { - icon = AllIcons.General.Warning - tooltip = "Tabby: Authorization required, please set your personal token in settings" - } - } - invokeLater { - update { myStatusBar?.updateWidget(ID()) } - } - } - } - } - - override fun disposeWidget(widget: StatusBarWidget) { - // Nothing to do - } -} diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/widgets/ChatToolWindowFactory.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/widgets/ChatToolWindowFactory.kt new file mode 100644 index 000000000000..62290390a450 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/widgets/ChatToolWindowFactory.kt @@ -0,0 +1,61 @@ +package com.tabbyml.intellijtabby.widgets + +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.project.DumbAware +import com.intellij.openapi.project.Project +import com.intellij.openapi.wm.ToolWindow +import com.intellij.openapi.wm.ToolWindowFactory +import com.intellij.openapi.wm.ToolWindowManager +import com.intellij.ui.components.JBLabel +import com.intellij.ui.content.ContentFactory +import com.intellij.util.ui.JBUI +import com.tabbyml.intellijtabby.chat.ChatBrowserFactory +import java.awt.GridBagLayout +import javax.swing.JPanel + +class ChatToolWindowFactory : ToolWindowFactory, DumbAware { + private val logger = logger() + + override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { + try { + val chatBrowserFactory = project.service() + val browser = chatBrowserFactory.createChatBrowser(toolWindow) + + val content = ContentFactory.getInstance().createContent(browser.component, "", false) + toolWindow.contentManager.addContent(content) + toolWindow.setTitleActions(listOf(ActionManager.getInstance().getAction("Tabby.ChatToolWindowToolbar"))) + } catch (e: Exception) { + logger.warn("Failed to create chat tool window", e) + + val helpMessage = + """ + + Failed to create the chat panel.
    + Please check the online documentation for trouble shooting. + + """.trimIndent() + val label = JBLabel(helpMessage).apply { + border = JBUI.Borders.emptyLeft(20) + setCopyable(true) + } + val panel = JPanel(GridBagLayout()).apply { + add(label) + } + + val content = ContentFactory.getInstance().createContent(panel, "", false) + toolWindow.contentManager.addContent(content) + } + } + + companion object { + const val TOOL_WINDOW_ID = "Tabby" + } +} + +fun openChatToolWindow(project: Project, callback: Runnable? = null) { + val toolWindowManager = ToolWindowManager.getInstance(project) + val toolWindow = toolWindowManager.getToolWindow(ChatToolWindowFactory.TOOL_WINDOW_ID) ?: return + toolWindow.show(callback) +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/widgets/StatusBarWidgetFactory.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/widgets/StatusBarWidgetFactory.kt new file mode 100644 index 000000000000..f4fba94b1b96 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/widgets/StatusBarWidgetFactory.kt @@ -0,0 +1,130 @@ +package com.tabbyml.intellijtabby.widgets + +import com.intellij.icons.AllIcons +import com.intellij.openapi.actionSystem.ActionGroup +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.application.invokeLater +import com.intellij.openapi.components.serviceOrNull +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.openapi.ui.popup.ListPopup +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.wm.StatusBarWidget +import com.intellij.openapi.wm.impl.status.EditorBasedStatusBarPopup +import com.intellij.openapi.wm.impl.status.widget.StatusBarEditorBasedWidgetFactory +import com.intellij.ui.AnimatedIcon +import com.tabbyml.intellijtabby.events.CombinedState +import com.tabbyml.intellijtabby.lsp.ConnectionService +import com.tabbyml.intellijtabby.lsp.protocol.StatusInfo +import javax.swing.Icon + +class StatusBarWidgetFactory : StatusBarEditorBasedWidgetFactory() { + override fun getId(): String { + return StatusBarWidgetFactory::class.java.name + } + + override fun getDisplayName(): String { + return "Tabby" + } + + override fun createWidget(project: Project): StatusBarWidget { + return object : EditorBasedStatusBarPopup(project, false) { + private val messageBusConnection = project.messageBus.connect() + val text = "Tabby" + var icon: Icon = AnimatedIcon.Default() + var tooltip = "Tabby: Initializing" + + init { + project.serviceOrNull()?.state?.let { updateRendering(it) } + messageBusConnection.subscribe(CombinedState.Listener.TOPIC, object : CombinedState.Listener { + override fun stateChanged(state: CombinedState.State) { + updateRendering(state) + } + }) + } + + override fun ID(): String { + return "${StatusBarWidgetFactory::class.java.name}.widget" + } + + override fun createInstance(project: Project): StatusBarWidget { + return createWidget(project) + } + + override fun getWidgetState(file: VirtualFile?): WidgetState { + return WidgetState(tooltip, text, true).also { + it.icon = icon + } + } + + override fun createPopup(context: DataContext): ListPopup { + return JBPopupFactory.getInstance().createActionGroupPopup( + tooltip, + ActionManager.getInstance().getAction("Tabby.StatusBarPopupMenu") as ActionGroup, + context, + false, + null, + 10, + ) + } + + override fun dispose() { + messageBusConnection.dispose() + super.dispose() + } + + private fun updateRendering(combinedState: CombinedState.State) { + when (combinedState.connectionState) { + ConnectionService.State.INITIALIZING -> { + icon = AnimatedIcon.Default() + tooltip = "Tabby: Initializing" + } + + ConnectionService.State.INITIALIZATION_FAILED -> { + icon = AllIcons.General.Error + tooltip = "Tabby: Initialization failed" + } + + ConnectionService.State.READY -> { + val statusInfo = combinedState.agentStatus + if (statusInfo == null) { + icon = AnimatedIcon.Default() + tooltip = "Tabby: Updating status" + } else { + icon = when (statusInfo.status) { + StatusInfo.Status.CONNECTING, StatusInfo.Status.FETCHING -> { + AnimatedIcon.Default() + } + + StatusInfo.Status.UNAUTHORIZED, StatusInfo.Status.COMPLETION_RESPONSE_SLOW -> { + AllIcons.General.Warning + } + + StatusInfo.Status.DISCONNECTED -> { + AllIcons.General.Error + } + + StatusInfo.Status.READY, StatusInfo.Status.READY_FOR_AUTO_TRIGGER -> { + AllIcons.Actions.Checked + } + + StatusInfo.Status.READY_FOR_MANUAL_TRIGGER -> { + AllIcons.General.ChevronRight + } + + else -> { + AnimatedIcon.Default() + } + } + tooltip = statusInfo.tooltip ?: "Tabby: ${statusInfo.status}" + } + } + } + invokeLater { + update { myStatusBar?.updateWidget(ID()) } + } + } + } + } +} diff --git a/clients/intellij/src/main/resources/META-INF/plugin-Git4Idea.xml b/clients/intellij/src/main/resources/META-INF/plugin-Git4Idea.xml new file mode 100644 index 000000000000..910104137a0d --- /dev/null +++ b/clients/intellij/src/main/resources/META-INF/plugin-Git4Idea.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/clients/intellij/src/main/resources/META-INF/plugin-kotlin.xml b/clients/intellij/src/main/resources/META-INF/plugin-kotlin.xml new file mode 100644 index 000000000000..d4903d30c413 --- /dev/null +++ b/clients/intellij/src/main/resources/META-INF/plugin-kotlin.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/clients/intellij/src/main/resources/META-INF/plugin.xml b/clients/intellij/src/main/resources/META-INF/plugin.xml index d8b915ff8e74..edd9c2e906ed 100644 --- a/clients/intellij/src/main/resources/META-INF/plugin.xml +++ b/clients/intellij/src/main/resources/META-INF/plugin.xml @@ -19,8 +19,6 @@

    For more information, please check out our Website and GitHub. If you encounter any problem or have any suggestion, please open an issue, or join our Slack community for more support!

    -

    Demo

    -

    Try our online demo here.

    Requirements

    Tabby plugin requires Node.js v18+ installed.

    ]]> @@ -28,50 +26,115 @@ com.intellij.modules.platform + Git4Idea + org.jetbrains.kotlin + + + + + + + + + - - - - + - + + + + com.tabbyml.intellijtabby.inlineChat.InlineChatIntentionAction + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + class="com.tabbyml.intellijtabby.actions.ToggleInlineCompletionTriggerMode" + text="Toggle Auto Inline Completion" + description="Enable or disable the automatic inline completion."> - + + + - - - + + + + + + + + + - - + + - - + - - + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/clients/intellij/src/main/resources/META-INF/pluginIcon.svg b/clients/intellij/src/main/resources/META-INF/pluginIcon.svg index 4ea2a9f18df1..955e0d66d3a6 100644 --- a/clients/intellij/src/main/resources/META-INF/pluginIcon.svg +++ b/clients/intellij/src/main/resources/META-INF/pluginIcon.svg @@ -1 +1,651 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/clients/intellij/src/main/resources/icons/chat.svg b/clients/intellij/src/main/resources/icons/chat.svg new file mode 100644 index 000000000000..b01d1bb81432 --- /dev/null +++ b/clients/intellij/src/main/resources/icons/chat.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/clients/intellij/src/main/resources/icons/chat_dark.svg b/clients/intellij/src/main/resources/icons/chat_dark.svg new file mode 100644 index 000000000000..318e5864d912 --- /dev/null +++ b/clients/intellij/src/main/resources/icons/chat_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/clients/intellij/src/main/resources/icons/new-ui/chat.svg b/clients/intellij/src/main/resources/icons/new-ui/chat.svg new file mode 100644 index 000000000000..eb680ea7b326 --- /dev/null +++ b/clients/intellij/src/main/resources/icons/new-ui/chat.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/clients/intellij/src/main/resources/icons/new-ui/chat_dark.svg b/clients/intellij/src/main/resources/icons/new-ui/chat_dark.svg new file mode 100644 index 000000000000..389f2c4e979f --- /dev/null +++ b/clients/intellij/src/main/resources/icons/new-ui/chat_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/clients/tabby-agent/.eslintrc b/clients/tabby-agent/.eslintrc index b55ee0f3e786..944e18276a6f 100644 --- a/clients/tabby-agent/.eslintrc +++ b/clients/tabby-agent/.eslintrc @@ -10,5 +10,6 @@ "rules": { "@typescript-eslint/no-explicit-any": 0, "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }] - } + }, + "ignorePatterns": ["dist", "node_modules"] } diff --git a/clients/tabby-agent/.gitignore b/clients/tabby-agent/.gitignore index 752acdd443ca..f06235c460c2 100644 --- a/clients/tabby-agent/.gitignore +++ b/clients/tabby-agent/.gitignore @@ -1,3 +1,2 @@ node_modules -generated dist diff --git a/clients/tabby-agent/.prettierignore b/clients/tabby-agent/.prettierignore index cab767407a33..b6235e984d55 100644 --- a/clients/tabby-agent/.prettierignore +++ b/clients/tabby-agent/.prettierignore @@ -1,2 +1 @@ -tests/golden/** -tests/bad_cases/** \ No newline at end of file +**/prompts/** diff --git a/clients/tabby-agent/.vscode/extensions.json b/clients/tabby-agent/.vscode/extensions.json new file mode 100644 index 000000000000..14a71fbbdce0 --- /dev/null +++ b/clients/tabby-agent/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. + // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp + + // List of extensions which should be recommended for users of this workspace. + "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "segevfiner.tsup-problem-matcher"], + // List of extensions recommended by VS Code that should not be recommended for users of this workspace. + "unwantedRecommendations": [] +} diff --git a/clients/tabby-agent/.vscode/launch.json b/clients/tabby-agent/.vscode/launch.json new file mode 100644 index 000000000000..0914d5db6425 --- /dev/null +++ b/clients/tabby-agent/.vscode/launch.json @@ -0,0 +1,24 @@ +// A launch configuration that compiles the extension and then opens it inside a new window +// Use IntelliSense to learn about possible attributes. +// Hover to view descriptions of existing attributes. +// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run LSP Server (Node.js)", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/dist/node/index.js", + "args": ["--stdio"], + "outFiles": ["${workspaceFolder}/dist/**/*.js"], + "preLaunchTask": "watch" + }, + { + "name": "Run Tests", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha" + } + ] +} diff --git a/clients/tabby-agent/.vscode/tasks.json b/clients/tabby-agent/.vscode/tasks.json new file mode 100644 index 000000000000..96b8d5c45c47 --- /dev/null +++ b/clients/tabby-agent/.vscode/tasks.json @@ -0,0 +1,22 @@ +// See https://go.microsoft.com/fwlink/?LinkId=733558 +// for the documentation about the tasks.json format +{ + "version": "2.0.0", + "tasks": [ + { + "label": "watch", + "type": "npm", + "script": "watch", + "problemMatcher": ["$tsup-watch", "$tsup-tsc-watch"], + "isBackground": true, + "presentation": { + "reveal": "never", + "group": "watchers" + }, + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} diff --git a/clients/tabby-agent/CHANGELOG.md b/clients/tabby-agent/CHANGELOG.md index c08efa1f24d0..bc384eec6552 100644 --- a/clients/tabby-agent/CHANGELOG.md +++ b/clients/tabby-agent/CHANGELOG.md @@ -1,3 +1,68 @@ +## 1.8.0 + +### Breaking Changes + +- Removed the deprecated `TabbyAgent` interface and related types. + +### Features + +- Added support for HTTP proxy configuration, defaulting to using HTTP proxy settings in environment variables. +- Included a default git context provider based on the system git command for collecting git repo context. +- Introduced `tabby/status` and `tabby/config` methods, deprecating `tabby/agent` methods. +- Added a method to sync all visible editor ranges for collecting code snippet context to enhance code completion generation. + +### Fixes & Improvements + +- Added more controls in the initialization options for better compatibility. +- Added a configurable minimal text length threshold to display the completion item. + +## 1.7.0 + +### Breaking Changes + +- The tabby-agent will only support running as a language server starting from version 1.7.0. + +### Features + +- Added support for collecting relative code snippets to enhance code completion. +- Extended the protocol by adding a new method to support inline chat editing. + +### Fixes & Improvements + +- Fixed a bug that caused unexpected logging output and the generation of an audit.json file in the working directory. + +## 1.6.0 + +### Features + +- Added support for multiple choices in inline completion. +- Introduced an experimental feature to generate commit messages. + +### Fixes & Improvements + +- Improved logging, logging levels can now be set to `silent`, `error`, `info`, `debug` or `verbose`. + +## 1.5.0 + +### Features + +- Added support for sending additional context for completion requests, including: + - filepath + - git repository information + - relevant declaration code snippets + - relevant recently edited code snippets + +### Fixes + +- Corrected server-side config retrieval behavior for connections to Tabby servers with version < 0.9. + +## 1.4.1 + +### Features + +- Added support for loading system-wide CA certificates. Previously, only Node.js bundled CA certificates were used. +- Added support for loading configurations from Tabby server, including `Disabling Client-side Telemetry`. + ## 1.3.3 ### Features diff --git a/clients/tabby-agent/README.md b/clients/tabby-agent/README.md index d904337bb02c..2fc50559e234 100644 --- a/clients/tabby-agent/README.md +++ b/clients/tabby-agent/README.md @@ -1,84 +1,94 @@ # Tabby Agent -The Tabby Agent is an agent used for communication with the Tabby server. It is based on Node.js v18 and implements several common features to enhance code completion, including caching, debouncing, and post-processing. +The [tabby-agent](https://www.npmjs.com/package/tabby-agent) is an agent used for communication with the [Tabby](https://www.tabbyml.com) server. It is based on Node.js v18 and runs as a language server. -This package exports both libraries and a CLI tool. +**Breaking Changes**: The tabby-agent will only support running as a language server since version 1.7.0. -## Language Server +The tabby-agent mainly supports the following features of LSP: -To run the agent as a language server, you can pass the argument `--lsp` and an additional IO option to the CLI tool. The available IO options are `--stdio`, `--node-ipc` or `--socket=`. +- Completion (textDocument/completion) +- Inline Completion (textDocument/inlineCompletion, since LSP v3.18.0) -```bash -npx tabby-agent --lsp --stdio -``` +For collecting more context to enhance the completion quality or providing more features like inline chat editing, the tabby-agent extends the protocol with some custom methods starting with `tabby/*`. These methods are used in Tabby-provided editor extensions. -The language server provides the following features: +## Usage -- Completion -- Inline Completion (WIP, upcoming LSP v3.18 feature) +**Note**: For VSCode, IntelliJ Platform IDEs, and Vim/NeoVim, it is recommended to use the Tabby-provided extensions, which run the Tabby Agent underlying. -Since most text editors have their built-in LSP clients or popular LSP client plugins, you can easily connect to the Tabby agent language server from your editor. Here are some example configurations for popular editors. +- [VSCode](https://marketplace.visualstudio.com/items?itemName=TabbyML.vscode-tabby) +- [IntelliJ Platform IDEs](https://plugins.jetbrains.com/plugin/22379-tabby) +- [Vim/NeoVim](https://github.com/TabbyML/vim-tabby) -### VSCode +The following guide is only for users who want to set up the tabby-agent as a language server manually. -A common way to add a new language server to VSCode is by creating a new extension. We provide an example extension for VSCode in [example-vscode-lsp](https://github.com/tabbyml/tabby/blob/master/clients/example-vscode-lsp). +### Start the Language Server -**Note**: Tabby provides an official extension for VSCode, you can install it from [marketplace](https://marketplace.visualstudio.com/items?itemName=tabbyml.vscode-tabby). The example provided here is just for reference on how to use the Tabby agent as a language server. - -### Emacs +```bash +npx tabby-agent --stdio +``` -The package [lsp-mode](https://github.com/emacs-lsp/lsp-mode) provides an LSP client for Emacs. You can add the following code to your Emacs configuration script to use the Tabby agent as a language server. +### Connect the IDE to the tabby-agent -```emacs-lisp -(with-eval-after-load 'lsp-mode - (lsp-register-client - (make-lsp-client :new-connection (lsp-stdio-connection '("npx" "tabby-agent" "--lsp" "--stdio")) - ;; you can select languages to enable Tabby language server - :activation-fn (lsp-activate-on "typescript" "javascript" "toml") - :priority 1 - :add-on? t - :server-id 'tabby-agent))) -``` +Since most text editors have their built-in LSP clients or popular LSP client plugins, you can easily connect to the tabby-agent from your editor. Here are some example configurations for popular editors. -### Vim/Neovim +#### Vim/Neovim (coc.nvim) -There are several Vim/Neovim plugins that provide LSP support. One of them is [coc.nvim](https://github.com/neoclide/coc.nvim). To use the Tabby agent as a language server, you can add the following code to your :CocConfig. +There are several Vim plugins that provide LSP support. One of them is [coc.nvim](https://github.com/neoclide/coc.nvim). To use the tabby-agent as a language server, you can add the following code to your `:CocConfig`. ```json { "languageserver": { "tabby-agent": { "command": "npx", - "args": ["tabby-agent", "--lsp", "--stdio"], + "args": ["tabby-agent", "--stdio"], "filetypes": ["*"] } } } ``` -### Helix +#### Emacs -[Helix](https://helix-editor.com/) has built-in LSP support. To use the Tabby agent as a language server, you can add the following code to your `languages.toml`. +The package [lsp-mode](https://github.com/emacs-lsp/lsp-mode) provides an LSP client for Emacs. You can add the following code to your Emacs configuration script to use the tabby-agent as a language server. + +```emacs-lisp +(with-eval-after-load 'lsp-mode + (lsp-register-client + (make-lsp-client :new-connection (lsp-stdio-connection '("npx" "tabby-agent" "--stdio")) + ;; you can select languages to enable Tabby language server + :activation-fn (lsp-activate-on "typescript" "javascript" "toml") + :priority 1 + :add-on? t + :server-id 'tabby-agent))) +``` + +#### Helix + +[Helix](https://helix-editor.com/) has built-in LSP support. To use the tabby-agent as a language server, you can add the following code to your `languages.toml`. ```toml [language-server.tabby] command = "npx" -args = ["tabby-agent", "--lsp", "--stdio"] +args = ["tabby-agent", "--stdio"] # Add Tabby as the second language server for your specific languages -[[languages]] +[[language]] name = "typescript" language-servers = ["typescript-language-server", "tabby"] -[[languages]] +[[language]] name = "toml" language-servers = ["taplo", "tabby"] ``` -### More Editors +#### More Editors You are welcome to contribute by adding example configurations for your favorite editor. Please submit a PR with your additions. +### Configurations + +Please refer to the [configuration documentation](https://tabby.tabbyml.com/docs/extensions/configurations/) for more details. + ## License Copyright (c) 2023-2024 TabbyML, Inc. diff --git a/clients/tabby-agent/openapi/tabby.json b/clients/tabby-agent/openapi/tabby.json deleted file mode 100644 index f1e154c5504f..000000000000 --- a/clients/tabby-agent/openapi/tabby.json +++ /dev/null @@ -1,326 +0,0 @@ -{ - "openapi": "3.0.3", - "info": { - "title": "Tabby Server", - "description": "\n[![tabby stars](https://img.shields.io/github/stars/TabbyML/tabby)](https://github.com/TabbyML/tabby)\n[![Join Slack](https://shields.io/badge/Join-Tabby%20Slack-red?logo=slack)](https://links.tabbyml.com/join-slack)\n\nInstall following IDE / Editor extensions to get started with [Tabby](https://github.com/TabbyML/tabby).\n* [VSCode Extension](https://github.com/TabbyML/tabby/tree/main/clients/vscode) – Install from the [marketplace](https://marketplace.visualstudio.com/items?itemName=TabbyML.vscode-tabby), or [open-vsx.org](https://open-vsx.org/extension/TabbyML/vscode-tabby)\n* [VIM Extension](https://github.com/TabbyML/tabby/tree/main/clients/vim)\n* [IntelliJ Platform Plugin](https://github.com/TabbyML/tabby/tree/main/clients/intellij) – Install from the [marketplace](https://plugins.jetbrains.com/plugin/22379-tabby)\n", - "license": { "name": "Apache 2.0", "url": "https://github.com/TabbyML/tabby/blob/main/LICENSE" }, - "version": "0.7.0" - }, - "servers": [{ "url": "/", "description": "Server" }], - "paths": { - "/v1/completions": { - "post": { - "tags": ["v1"], - "operationId": "completion", - "requestBody": { - "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CompletionRequest" } } }, - "required": true - }, - "responses": { - "200": { - "description": "Success", - "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CompletionResponse" } } } - }, - "400": { "description": "Bad Request" } - }, - "security": [{ "token": [] }] - } - }, - "/v1/events": { - "post": { - "tags": ["v1"], - "operationId": "event", - "requestBody": { - "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LogEventRequest" } } }, - "required": true - }, - "responses": { "200": { "description": "Success" }, "400": { "description": "Bad Request" } }, - "security": [{ "token": [] }] - } - }, - "/v1/health": { - "get": { - "tags": ["v1"], - "operationId": "health", - "responses": { - "200": { - "description": "Success", - "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HealthState" } } } - } - }, - "security": [{ "token": [] }] - } - }, - "/v1beta/chat/completions": { - "post": { - "tags": ["v1beta"], - "operationId": "chat_completions", - "requestBody": { - "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChatCompletionRequest" } } }, - "required": true - }, - "responses": { - "200": { - "description": "Success", - "content": { "text/event-stream": { "schema": { "$ref": "#/components/schemas/ChatCompletionChunk" } } } - }, - "405": { "description": "When chat model is not specified, the endpoint returns 405 Method Not Allowed" }, - "422": { "description": "When the prompt is malformed, the endpoint returns 422 Unprocessable Entity" } - }, - "security": [{ "token": [] }] - } - }, - "/v1beta/search": { - "get": { - "tags": ["v1beta"], - "operationId": "search", - "parameters": [ - { "name": "q", "in": "query", "required": true, "schema": { "type": "string", "default": "get" } }, - { - "name": "limit", - "in": "query", - "required": false, - "schema": { "type": "integer", "default": 20, "nullable": true, "minimum": 0.0 } - }, - { - "name": "offset", - "in": "query", - "required": false, - "schema": { "type": "integer", "default": 0, "nullable": true, "minimum": 0.0 } - } - ], - "responses": { - "200": { - "description": "Success", - "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SearchResponse" } } } - }, - "501": { "description": "When code search is not enabled, the endpoint will returns 501 Not Implemented" } - }, - "security": [{ "token": [] }] - } - } - }, - "components": { - "schemas": { - "ChatCompletionChoice": { - "type": "object", - "required": ["index", "delta"], - "properties": { - "index": { "type": "integer", "minimum": 0.0 }, - "logprobs": { "type": "string", "nullable": true }, - "finish_reason": { "type": "string", "nullable": true }, - "delta": { "$ref": "#/components/schemas/ChatCompletionDelta" } - } - }, - "ChatCompletionChunk": { - "type": "object", - "required": ["id", "created", "system_fingerprint", "object", "model", "choices"], - "properties": { - "id": { "type": "string" }, - "created": { "type": "integer", "format": "int64", "minimum": 0.0 }, - "system_fingerprint": { "type": "string" }, - "object": { "type": "string" }, - "model": { "type": "string" }, - "choices": { "type": "array", "items": { "$ref": "#/components/schemas/ChatCompletionChoice" } } - } - }, - "ChatCompletionDelta": { - "type": "object", - "required": ["content"], - "properties": { "content": { "type": "string" } } - }, - "ChatCompletionRequest": { - "type": "object", - "required": ["messages"], - "properties": { - "messages": { "type": "array", "items": { "$ref": "#/components/schemas/Message" } }, - "temperature": { "type": "number", "format": "float", "nullable": true }, - "seed": { "type": "integer", "format": "int64", "nullable": true, "minimum": 0.0 } - }, - "example": { - "messages": [ - { "role": "user", "content": "What is tail recursion?" }, - { "role": "assistant", "content": "It's a kind of optimization in compiler?" }, - { "role": "user", "content": "Could you share more details?" } - ] - } - }, - "Choice": { - "type": "object", - "required": ["index", "text"], - "properties": { - "index": { "type": "integer", "format": "int32", "minimum": 0.0 }, - "text": { "type": "string" } - } - }, - "CompletionRequest": { - "type": "object", - "properties": { - "language": { - "type": "string", - "description": "Language identifier, full list is maintained at\nhttps://code.visualstudio.com/docs/languages/identifiers", - "example": "python", - "nullable": true - }, - "segments": { "allOf": [{ "$ref": "#/components/schemas/Segments" }], "nullable": true }, - "user": { - "type": "string", - "description": "A unique identifier representing your end-user, which can help Tabby to monitor & generating\nreports.", - "nullable": true - }, - "debug_options": { "allOf": [{ "$ref": "#/components/schemas/DebugOptions" }], "nullable": true }, - "temperature": { - "type": "number", - "format": "float", - "description": "The temperature parameter for the model, used to tune variance and \"creativity\" of the model output", - "nullable": true - }, - "seed": { - "type": "integer", - "format": "int64", - "description": "The seed used for randomly selecting tokens", - "nullable": true, - "minimum": 0.0 - } - }, - "example": { - "language": "python", - "segments": { "prefix": "def fib(n):\n ", "suffix": "\n return fib(n - 1) + fib(n - 2)" } - } - }, - "CompletionResponse": { - "type": "object", - "required": ["id", "choices"], - "properties": { - "id": { "type": "string" }, - "choices": { "type": "array", "items": { "$ref": "#/components/schemas/Choice" } }, - "debug_data": { "allOf": [{ "$ref": "#/components/schemas/DebugData" }], "nullable": true } - }, - "example": { "id": "string", "choices": [{ "index": 0, "text": "string" }] } - }, - "DebugData": { - "type": "object", - "properties": { - "snippets": { "type": "array", "items": { "$ref": "#/components/schemas/Snippet" }, "nullable": true }, - "prompt": { "type": "string", "nullable": true } - } - }, - "DebugOptions": { - "type": "object", - "properties": { - "raw_prompt": { - "type": "string", - "description": "When `raw_prompt` is specified, it will be passed directly to the inference engine for completion. `segments` field in `CompletionRequest` will be ignored.\n\nThis is useful for certain requests that aim to test the tabby's e2e quality.", - "nullable": true - }, - "return_snippets": { "type": "boolean", "description": "When true, returns `snippets` in `debug_data`." }, - "return_prompt": { "type": "boolean", "description": "When true, returns `prompt` in `debug_data`." }, - "disable_retrieval_augmented_code_completion": { - "type": "boolean", - "description": "When true, disable retrieval augmented code completion." - } - } - }, - "HealthState": { - "type": "object", - "required": ["device", "arch", "cpu_info", "cpu_count", "cuda_devices", "version"], - "properties": { - "model": { "type": "string", "nullable": true }, - "chat_model": { "type": "string", "nullable": true }, - "device": { "type": "string" }, - "arch": { "type": "string" }, - "cpu_info": { "type": "string" }, - "cpu_count": { "type": "integer", "minimum": 0.0 }, - "cuda_devices": { "type": "array", "items": { "type": "string" } }, - "version": { "$ref": "#/components/schemas/Version" } - } - }, - "Hit": { - "type": "object", - "required": ["score", "doc", "id"], - "properties": { - "score": { "type": "number", "format": "float" }, - "doc": { "$ref": "#/components/schemas/HitDocument" }, - "id": { "type": "integer", "format": "int32", "minimum": 0.0 } - } - }, - "HitDocument": { - "type": "object", - "required": ["body", "filepath", "git_url", "kind", "language", "name"], - "properties": { - "body": { "type": "string" }, - "filepath": { "type": "string" }, - "git_url": { "type": "string" }, - "kind": { "type": "string" }, - "language": { "type": "string" }, - "name": { "type": "string" } - } - }, - "LogEventRequest": { - "type": "object", - "required": ["type", "completion_id", "choice_index"], - "properties": { - "type": { - "type": "string", - "description": "Event type, should be `view`, `select` or `dismiss`.", - "example": "view" - }, - "completion_id": { "type": "string" }, - "choice_index": { "type": "integer", "format": "int32", "minimum": 0.0 }, - "view_id": { "type": "string", "nullable": true }, - "elapsed": { "type": "integer", "format": "int32", "nullable": true, "minimum": 0.0 } - } - }, - "Message": { - "type": "object", - "required": ["role", "content"], - "properties": { "role": { "type": "string" }, "content": { "type": "string" } } - }, - "SearchResponse": { - "type": "object", - "required": ["num_hits", "hits"], - "properties": { - "num_hits": { "type": "integer", "minimum": 0.0 }, - "hits": { "type": "array", "items": { "$ref": "#/components/schemas/Hit" } } - } - }, - "Segments": { - "type": "object", - "required": ["prefix"], - "properties": { - "prefix": { "type": "string", "description": "Content that appears before the cursor in the editor window." }, - "suffix": { - "type": "string", - "description": "Content that appears after the cursor in the editor window.", - "nullable": true - }, - "clipboard": { - "type": "string", - "description": "Clipboard content when requesting code completion.", - "nullable": true - } - } - }, - "Snippet": { - "type": "object", - "required": ["filepath", "body", "score"], - "properties": { - "filepath": { "type": "string" }, - "body": { "type": "string" }, - "score": { "type": "number", "format": "float" } - } - }, - "Version": { - "type": "object", - "required": ["build_date", "build_timestamp", "git_sha", "git_describe"], - "properties": { - "build_date": { "type": "string" }, - "build_timestamp": { "type": "string" }, - "git_sha": { "type": "string" }, - "git_describe": { "type": "string" } - } - } - }, - "securitySchemes": { "token": { "type": "http", "scheme": "bearer", "bearerFormat": "token" } } - } -} diff --git a/clients/tabby-agent/package.json b/clients/tabby-agent/package.json index 2fb0a03cf39d..cc5ffbbf2553 100644 --- a/clients/tabby-agent/package.json +++ b/clients/tabby-agent/package.json @@ -1,8 +1,8 @@ { "name": "tabby-agent", - "version": "1.3.3", + "version": "1.9.0-dev", "description": "Generic client agent for Tabby AI coding assistant IDE extensions.", - "homepage": "https://tabby.tabbyml.com/", + "homepage": "https://www.tabbyml.com/", "repository": "https://github.com/TabbyML/tabby", "bugs": "https://github.com/TabbyML/tabby/issues", "keywords": [ @@ -17,72 +17,85 @@ "node": ">=18" }, "files": [ - "./dist/cli.js", - "./dist/index.js", - "./dist/index.mjs", - "./dist/index.d.ts", - "./dist/wasm/**" + "./dist/**" ], "bin": { - "tabby-agent": "./dist/cli.js" + "tabby-agent": "./dist/node/index.js" }, - "main": "./dist/index.js", - "browser": "./dist/index.mjs", - "types": "./dist/index.d.ts", + "main": "./dist/protocol.js", + "types": "./dist/protocol.d.ts", "scripts": { + "build": "tsup --minify", + "watch": "tsup --watch --onSuccess \"echo '' >> dist/protocol.d.ts\"", + "vscode:dev": "pnpm run watch", "openapi-codegen": "openapi-typescript ./openapi/tabby.json -o ./src/types/tabbyApi.d.ts", - "dev": "tsup --watch --no-minify --no-treeshake", - "build": "tsc --noEmit && tsup", "test": "mocha", - "test:watch": "env TEST_LOG_DEBUG=1 mocha --watch", - "lint": "eslint --fix --ext .ts ./src && prettier --write .", - "lint:check": "eslint --ext .ts ./src && prettier --check ." + "lint": "eslint --ext .ts ./src && prettier --check .", + "lint:fix": "eslint --fix --ext .ts ./src && prettier --write ." }, "devDependencies": { + "@orama/orama": "^2.0.18", "@types/chai": "^4.3.5", "@types/dedent": "^0.7.2", "@types/deep-equal": "^1.0.4", + "@types/diff": "^5.2.1", "@types/fast-levenshtein": "^0.0.4", "@types/fs-extra": "^11.0.1", "@types/glob": "^7.2.0", + "@types/js-levenshtein": "^1.1.3", "@types/mocha": "^10.0.1", "@types/node": "18.x", "@types/object-hash": "^3.0.0", + "@types/readable-stream": "^4.0.14", + "@types/semver": "^7.5.8", "@types/uuid": "^9.0.0", + "@types/win-ca": "^3.5.4", "@typescript-eslint/eslint-plugin": "^6.13.1", "@typescript-eslint/parser": "^6.13.1", "chai": "^4.3.7", - "dedent": "^0.7.0", - "esbuild-plugin-copy": "^2.1.1", - "esbuild-plugin-polyfill-node": "^0.3.0", - "eslint": "^8.55.0", - "eslint-config-prettier": "^9.0.0", - "glob": "^7.2.0", - "mocha": "^10.2.0", - "openapi-typescript": "^6.6.1", - "prettier": "^3.0.0", - "ts-node": "^10.9.1", - "tsup": "^7.1.0", - "typescript": "^5.3.2" - }, - "dependencies": { "chokidar": "^3.5.3", + "codiff": "^0.1.2", + "crypto-random-string": "^5.0.0", + "dedent": "^0.7.0", "deep-equal": "^2.2.1", "deepmerge-ts": "^5.1.0", + "diff": "^5.2.0", "dot-prop": "^8.0.2", + "esbuild": "^0.19.12", + "esbuild-plugin-polyfill-node": "^0.3.0", + "eslint": "^8.55.0", + "eslint-config-prettier": "^9.0.0", + "eventsource-parser": "^1.1.2", "fast-levenshtein": "^3.0.0", "file-stream-rotator": "^1.0.0", "fs-extra": "^11.1.1", + "glob": "^7.2.0", + "js-levenshtein": "^1.1.6", "jwt-decode": "^3.1.2", "lru-cache": "^9.1.1", + "mac-ca": "^2.0.3", + "mocha": "^10.2.0", "object-hash": "^3.0.0", "openapi-fetch": "^0.7.6", "pino": "^8.14.1", - "stats-logscale": "^1.0.7", + "prettier": "^3.0.0", + "readable-from-web": "^1.0.0", + "readable-stream": "^4.5.2", + "semver": "^7.6.0", + "stats-logscale": "^1.0.9", + "tabby-openapi": "workspace:*", "toml": "^3.0.0", + "ts-node": "^10.9.1", + "tsc-watch": "^6.2.0", + "tsup": "^8.0.2", + "typescript": "^5.3.2", + "undici": "^6.19.2", + "uri-js": "^4.4.1", "uuid": "^9.0.0", "vscode-languageserver": "^9.0.1", + "vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-textdocument": "^1.0.11", - "web-tree-sitter": "^0.20.8" + "web-tree-sitter": "^0.20.8", + "win-ca": "^3.5.1" } } diff --git a/clients/tabby-agent/register-loader.js b/clients/tabby-agent/register-loader.js new file mode 100644 index 000000000000..1e17785b957c --- /dev/null +++ b/clients/tabby-agent/register-loader.js @@ -0,0 +1,21 @@ +const path = require("path"); +const fs = require("fs"); +const REQUIRE_PATH_TEST = /\.md$/; + +function register() { + const Module = require("module"); + const originalLoad = Module._load; + const cwd = process.cwd(); + Module._load = function (request, _parent) { + if (request.match(REQUIRE_PATH_TEST)) { + return fs.readFileSync(path.join(path.dirname(_parent ? _parent.filename : cwd), request), "utf8"); + } + return originalLoad.apply(this, arguments); + }; + + return () => { + Module._load = originalLoad; + }; +} + +register(); diff --git a/clients/tabby-agent/src/Agent.ts b/clients/tabby-agent/src/Agent.ts deleted file mode 100644 index 849a5bfa8aa2..000000000000 --- a/clients/tabby-agent/src/Agent.ts +++ /dev/null @@ -1,185 +0,0 @@ -import type { components as ApiComponents } from "./types/tabbyApi"; -import type { AgentConfig, PartialAgentConfig } from "./AgentConfig"; -import type { DataStore } from "./dataStore"; -import type { CompletionRequest, CompletionResponse } from "./CompletionContext"; - -export type { CompletionRequest, CompletionResponse }; - -export type ClientProperties = Partial<{ - user: Record; - session: Record; -}>; - -export type AgentInitOptions = Partial<{ - config: PartialAgentConfig; - clientProperties: ClientProperties; - dataStore: DataStore; -}>; - -export type ServerHealthState = ApiComponents["schemas"]["HealthState"]; - -export type LogEventRequest = ApiComponents["schemas"]["LogEventRequest"] & { - select_kind?: "line"; -}; - -export type AbortSignalOption = { signal: AbortSignal }; - -export type SlowCompletionResponseTimeIssue = { - name: "slowCompletionResponseTime"; - completionResponseStats: Record; -}; -export type HighCompletionTimeoutRateIssue = { - name: "highCompletionTimeoutRate"; - completionResponseStats: Record; -}; -export type ConnectionFailedIssue = { - name: "connectionFailed"; - message?: string; -}; -export type AgentIssue = SlowCompletionResponseTimeIssue | HighCompletionTimeoutRateIssue | ConnectionFailedIssue; - -/** - * Represents the status of the agent. - * @enum - * @property {string} notInitialized - When the agent has not been initialized. - * @property {string} ready - When the agent gets a valid response from the server. - * @property {string} disconnected - When the agent fails to connect to the server. - * @property {string} unauthorized - When the server requires authentication. - * @property {string} finalized - When the agent is finalized. - */ -export type AgentStatus = "notInitialized" | "ready" | "disconnected" | "unauthorized" | "finalized"; - -export interface AgentFunction { - /** - * Initialize agent. Client should call this method before calling any other methods. - * @param options - */ - initialize(options?: AgentInitOptions): Promise; - - /** - * Finalize agent. Client should call this method before exiting. - */ - finalize(): Promise; - - /** - * Update client properties. - * Client properties are mostly used for logging and anonymous usage statistics. - */ - updateClientProperties(type: keyof ClientProperties, key: string, value: any): Promise; - - /** - * The agent configuration has the following levels, will be deep merged in the order: - * 1. Default config - * 2. User config file `~/.tabby-client/agent/config.toml` (not available in browser) - * 3. Agent `initialize` and `updateConfig` methods - * - * This method will update the 3rd level config. - * @param key the key of the config to update, can be nested with dot, e.g. `server.endpoint` - * @param value the value to set - */ - updateConfig(key: string, value: any): Promise; - - /** - * Clear the 3rd level config. - * @param key the key of the config to clear, can be nested with dot, e.g. `server.endpoint` - */ - clearConfig(key: string): Promise; - - /** - * @returns the current config - */ - getConfig(): AgentConfig; - - /** - * @returns the current status - */ - getStatus(): AgentStatus; - - /** - * @returns the current issues if any exists - */ - getIssues(): AgentIssue["name"][]; - - /** - * Get the detail of an issue by index or name. - * @param options if `index` is provided, `name` will be ignored - * @returns the issue detail if exists, otherwise null - */ - getIssueDetail(options: { index?: number; name?: T["name"] }): T | null; - - /** - * @returns server info returned from latest server health check, returns null if not available - */ - getServerHealthState(): ServerHealthState | null; - - /** - * @deprecated Tabby Cloud auth - * - * Request auth url for Tabby Cloud endpoint. Only return value when the `AgentStatus` is `unauthorized`. - * Otherwise, return null. See also `AgentStatus`. - * @returns the auth url for redirecting, and the code for next step `waitingForAuth` - * @throws Error if agent is not initialized - */ - requestAuthUrl(options?: AbortSignalOption): Promise<{ authUrl: string; code: string } | null>; - - /** - * @deprecated Tabby Cloud auth - * - * Wait for auth token to be ready after redirecting user to auth url, - * returns nothing, but `AgentStatus` will change to `ready` if resolved successfully. - * @param code from `requestAuthUrl` - * @throws Error if agent is not initialized - */ - waitForAuthToken(code: string, options?: AbortSignalOption): Promise; - - /** - * Provide completions for the given request. This method is debounced, calling it before the previous - * call is resolved will cancel the previous call. The debouncing interval is automatically calculated - * or can be set in the config. - * @param request - * @returns - * @throws Error if agent is not initialized - */ - provideCompletions(request: CompletionRequest, options?: AbortSignalOption): Promise; - - /** - * @param event - * @returns - * @throws Error if agent is not initialized - */ - postEvent(event: LogEventRequest, options?: AbortSignalOption): Promise; -} - -export type StatusChangedEvent = { - event: "statusChanged"; - status: AgentStatus; -}; -export type ConfigUpdatedEvent = { - event: "configUpdated"; - config: AgentConfig; -}; -/** - * This event is emitted when the server requires authentication. - */ -export type AuthRequiredEvent = { - event: "authRequired"; - server: AgentConfig["server"]; -}; -export type IssuesUpdatedEvent = { - event: "issuesUpdated"; - issues: AgentIssue["name"][]; -}; - -export type AgentEvent = StatusChangedEvent | ConfigUpdatedEvent | AuthRequiredEvent | IssuesUpdatedEvent; -export const agentEventNames: AgentEvent["event"][] = [ - "statusChanged", - "configUpdated", - "authRequired", - "issuesUpdated", -]; - -export interface AgentEventEmitter { - on(eventName: T["event"], callback: (event: T) => void): this; -} - -export type Agent = AgentFunction & AgentEventEmitter; diff --git a/clients/tabby-agent/src/AgentConfig.ts b/clients/tabby-agent/src/AgentConfig.ts deleted file mode 100644 index 44673f7f23cd..000000000000 --- a/clients/tabby-agent/src/AgentConfig.ts +++ /dev/null @@ -1,96 +0,0 @@ -export type AgentConfig = { - server: { - endpoint: string; - token: string; - requestHeaders: Record; - requestTimeout: number; - }; - completion: { - prompt: { - experimentalStripAutoClosingCharacters: boolean; - maxPrefixLines: number; - maxSuffixLines: number; - clipboard: { - minChars: number; - maxChars: number; - }; - }; - debounce: { - mode: "adaptive" | "fixed"; - interval: number; - }; - }; - postprocess: { - limitScope: { - // Prefer to use syntax parser than indentation - experimentalSyntax: boolean; - indentation: { - // When completion is continuing the current line, limit the scope to: - // false(default): the line scope, meaning use the next indent level as the limit. - // true: the block scope, meaning use the current indent level as the limit. - experimentalKeepBlockScopeWhenCompletingLine: boolean; - }; - }; - calculateReplaceRange: { - // Prefer to use syntax parser than bracket stack - experimentalSyntax: boolean; - }; - }; - logs: { - level: "debug" | "error" | "silent"; - }; - anonymousUsageTracking: { - disable: boolean; - }; -}; - -type RecursivePartial = { - [P in keyof T]?: T[P] extends (infer U)[] - ? RecursivePartial[] - : T[P] extends object | undefined - ? RecursivePartial - : T[P]; -}; - -export type PartialAgentConfig = RecursivePartial; - -export const defaultAgentConfig: AgentConfig = { - server: { - endpoint: "http://localhost:8080", - token: "", - requestHeaders: {}, - requestTimeout: 2 * 60 * 1000, // 2 minutes - }, - completion: { - prompt: { - experimentalStripAutoClosingCharacters: false, - maxPrefixLines: 20, - maxSuffixLines: 20, - clipboard: { - minChars: 3, - maxChars: 2000, - }, - }, - debounce: { - mode: "adaptive", - interval: 250, // ms - }, - }, - postprocess: { - limitScope: { - experimentalSyntax: false, - indentation: { - experimentalKeepBlockScopeWhenCompletingLine: false, - }, - }, - calculateReplaceRange: { - experimentalSyntax: false, - }, - }, - logs: { - level: "silent", - }, - anonymousUsageTracking: { - disable: false, - }, -}; diff --git a/clients/tabby-agent/src/AnonymousUsageLogger.ts b/clients/tabby-agent/src/AnonymousUsageLogger.ts deleted file mode 100644 index 22754c12e768..000000000000 --- a/clients/tabby-agent/src/AnonymousUsageLogger.ts +++ /dev/null @@ -1,101 +0,0 @@ -import os from "os"; -import createClient from "openapi-fetch"; -import { setProperty } from "dot-prop"; -import { v4 as uuid } from "uuid"; -import type { paths as CloudApi } from "./types/cloudApi"; -import { name as agentName, version as agentVersion } from "../package.json"; -import { isBrowser } from "./env"; -import { rootLogger } from "./logger"; -import { dataStore, DataStore } from "./dataStore"; - -export class AnonymousUsageLogger { - private anonymousUsageTrackingApi = createClient({ baseUrl: "https://app.tabbyml.com/api" }); - private logger = rootLogger.child({ component: "AnonymousUsage" }); - private systemData = { - agent: `${agentName}, ${agentVersion}`, - browser: isBrowser ? navigator?.userAgent || "browser" : undefined, - node: isBrowser ? undefined : `${process.version} ${process.platform} ${os.arch()} ${os.release()}`, - }; - private sessionProperties: Record = {}; - private userProperties: Record = {}; - private userPropertiesUpdated = false; - private emittedUniqueEvent: string[] = []; - private dataStore?: DataStore; - private anonymousId?: string; - disabled: boolean = false; - - async init(options?: { dataStore?: DataStore }) { - this.dataStore = options?.dataStore || dataStore; - if (this.dataStore) { - try { - await this.dataStore.load(); - } catch (error) { - this.logger.debug({ error }, "Error when loading anonymousId"); - } - if (typeof this.dataStore.data["anonymousId"] === "string") { - this.anonymousId = this.dataStore.data["anonymousId"]; - } else { - this.anonymousId = uuid(); - this.dataStore.data["anonymousId"] = this.anonymousId; - try { - await this.dataStore.save(); - } catch (error) { - this.logger.debug({ error }, "Error when saving anonymousId"); - } - } - } else { - this.anonymousId = uuid(); - } - } - - /** - * Set properties to be sent with every event in this session. - */ - setSessionProperties(key: string, value: any) { - setProperty(this.sessionProperties, key, value); - } - - /** - * Set properties which will be bind to the user. - */ - setUserProperties(key: string, value: any) { - setProperty(this.userProperties, key, value); - this.userPropertiesUpdated = true; - } - - async uniqueEvent(event: string, data: Record = {}) { - await this.event(event, data, true); - } - - async event(event: string, data: Record = {}, unique = false) { - if (this.disabled || !this.anonymousId) { - return; - } - if (unique && this.emittedUniqueEvent.includes(event)) { - return; - } - if (unique) { - this.emittedUniqueEvent.push(event); - } - const properties = { - ...this.systemData, - ...this.sessionProperties, - ...data, - }; - if (this.userPropertiesUpdated) { - setProperty(properties, "$set", this.userProperties); - this.userPropertiesUpdated = false; - } - try { - await this.anonymousUsageTrackingApi.POST("/usage", { - body: { - distinctId: this.anonymousId, - event, - properties, - }, - }); - } catch (error) { - this.logger.error({ error }, "Error when sending anonymous usage data"); - } - } -} diff --git a/clients/tabby-agent/src/Auth.ts b/clients/tabby-agent/src/Auth.ts deleted file mode 100644 index 610ac347473b..000000000000 --- a/clients/tabby-agent/src/Auth.ts +++ /dev/null @@ -1,252 +0,0 @@ -import { EventEmitter } from "events"; -import decodeJwt from "jwt-decode"; -import createClient from "openapi-fetch"; -import type { paths as CloudApi } from "./types/cloudApi"; -import type { AbortSignalOption } from "./Agent"; -import { HttpError, abortSignalFromAnyOf } from "./utils"; -import { dataStore, DataStore } from "./dataStore"; -import { rootLogger } from "./logger"; - -export type StorageData = { - auth: { [endpoint: string]: { jwt: string } }; -}; - -type JWT = { token: string; payload: { email: string; exp: number } }; - -class RetryLimitReachedError extends Error { - readonly name = "RetryLimitReachedError"; - constructor(readonly cause: unknown) { - super(); - } -} - -export class Auth extends EventEmitter { - static readonly authPageUrl = "https://app.tabbyml.com/account/device-token"; - static readonly tokenStrategy = { - polling: { - // polling token after auth url generated - interval: 5000, // polling token every 5 seconds - timeout: 5 * 60 * 1000, // stop polling after trying for 5 min - }, - refresh: { - // check token every 15 min, refresh token if it expires in 30 min - interval: 15 * 60 * 1000, - beforeExpire: 30 * 60 * 1000, - whenLoaded: { - // after token loaded from data store, refresh token if it is about to expire or has expired - maxTry: 5, // keep loading time not too long - retryDelay: 1000, // retry after 1 seconds - }, - whenScheduled: { - // if running until token is about to expire, refresh token as scheduled - maxTry: 60, - retryDelay: 30 * 1000, // retry after 30 seconds - }, - }, - }; - - private readonly logger = rootLogger.child({ component: "Auth" }); - private dataStore?: DataStore; - private authApi = createClient({ baseUrl: "https://app.tabbyml.com/api" }); - private jwt?: JWT; - - constructor(readonly endpoint: string) { - super(); - } - - async init(options?: { dataStore?: DataStore }) { - if (options?.dataStore) { - this.dataStore = options.dataStore; - } else { - this.dataStore = dataStore; - if (dataStore) { - dataStore.on("updated", async () => { - await this.load(); - super.emit("updated", this.jwt); - }); - dataStore.watch(); - } - } - this.scheduleRefreshToken(); - await this.load(); - } - - get token(): string | undefined { - return this.jwt?.token; - } - - get user(): string | undefined { - return this.jwt?.payload.email; - } - - private async load(): Promise { - if (!this.dataStore) { - return; - } - try { - await this.dataStore.load(); - const storedJwt = this.dataStore.data.auth?.[this.endpoint]?.jwt; - if (typeof storedJwt === "string" && this.jwt?.token !== storedJwt) { - this.logger.debug({ storedJwt }, "Load jwt from data store."); - const jwt: JWT = { - token: storedJwt, - payload: decodeJwt(storedJwt), - }; - // refresh token if it is about to expire or has expired - if (jwt.payload.exp * 1000 - Date.now() < Auth.tokenStrategy.refresh.beforeExpire) { - this.jwt = await this.refreshToken(jwt, Auth.tokenStrategy.refresh.whenLoaded); - await this.save(); - } else { - this.jwt = jwt; - } - } - } catch (error) { - this.logger.debug({ error }, "Error when loading auth"); - } - } - - private async save(): Promise { - if (!this.dataStore) { - return; - } - try { - if (this.jwt) { - if (this.dataStore.data.auth?.[this.endpoint]?.jwt === this.jwt.token) { - return; - } - this.dataStore.data.auth = { ...this.dataStore.data.auth, [this.endpoint]: { jwt: this.jwt.token } }; - } else { - if (typeof this.dataStore.data.auth?.[this.endpoint] === "undefined") { - return; - } - delete this.dataStore.data.auth[this.endpoint]; - } - await this.dataStore.save(); - this.logger.debug("Save changes to data store."); - } catch (error) { - this.logger.error({ error }, "Error when saving auth"); - } - } - - async reset(): Promise { - if (this.jwt) { - this.jwt = undefined; - await this.save(); - } - } - - async requestAuthUrl(options?: AbortSignalOption): Promise<{ authUrl: string; code: string }> { - try { - await this.reset(); - if (options?.signal.aborted) { - throw options.signal.reason; - } - this.logger.debug("Start to request device token"); - const response = await this.authApi.POST("/device-token", { - body: { auth_url: this.endpoint }, - signal: options?.signal, - }); - if (response.error || !response.response.ok) { - throw new HttpError(response.response); - } - const deviceToken = response.data; - this.logger.debug({ deviceToken }, "Request device token response"); - const authUrl = new URL(Auth.authPageUrl); - authUrl.searchParams.append("code", deviceToken.data.code); - return { authUrl: authUrl.toString(), code: deviceToken.data.code }; - } catch (error) { - this.logger.error({ error }, "Error when requesting token"); - throw error; - } - } - - async pollingToken(code: string, options?: AbortSignalOption): Promise { - return new Promise((resolve, reject) => { - const signal = abortSignalFromAnyOf([AbortSignal.timeout(Auth.tokenStrategy.polling.timeout), options?.signal]); - const timer = setInterval(async () => { - try { - const response = await this.authApi.POST("/device-token/accept", { params: { query: { code } }, signal }); - if (response.error || !response.response.ok) { - throw new HttpError(response.response); - } - const result = response.data; - this.logger.debug({ result }, "Poll jwt response"); - this.jwt = { - token: result.data.jwt, - payload: decodeJwt(result.data.jwt), - }; - super.emit("updated", this.jwt); - await this.save(); - clearInterval(timer); - resolve(true); - } catch (error) { - if (error instanceof HttpError && [400, 401, 403, 405].includes(error.status)) { - this.logger.debug({ error }, "Expected error when polling jwt"); - } else { - // unknown error but still keep polling - this.logger.error({ error }, "Error when polling jwt"); - } - } - }, Auth.tokenStrategy.polling.interval); - if (signal.aborted) { - clearInterval(timer); - reject(signal.reason); - } else { - signal.addEventListener("abort", () => { - clearInterval(timer); - reject(signal.reason); - }); - } - }); - } - - private async refreshToken(jwt: JWT, options = { maxTry: 1, retryDelay: 1000 }, retry = 0): Promise { - try { - this.logger.debug({ retry }, "Start to refresh token"); - const response = await this.authApi.POST("/device-token/refresh", { - headers: { Authorization: `Bearer ${jwt.token}` }, - }); - if (response.error || !response.response.ok) { - throw new HttpError(response.response); - } - const refreshedJwt = response.data; - this.logger.debug({ refreshedJwt }, "Refresh token response"); - return { - token: refreshedJwt.data.jwt, - payload: decodeJwt(refreshedJwt.data.jwt), - }; - } catch (error) { - if (error instanceof HttpError && [400, 401, 403, 405].includes(error.status)) { - this.logger.debug({ error }, "Error when refreshing jwt"); - } else { - // unknown error, retry a few times - this.logger.error({ error }, "Unknown error when refreshing jwt"); - if (retry < options.maxTry) { - this.logger.debug(`Retry refreshing jwt after ${options.retryDelay}ms`); - await new Promise((resolve) => setTimeout(resolve, options.retryDelay)); - return this.refreshToken(jwt, options, retry + 1); - } - } - throw new RetryLimitReachedError(error); - } - } - - private scheduleRefreshToken() { - setInterval(async () => { - if (!this.jwt) { - return; - } - if (this.jwt.payload.exp * 1000 - Date.now() < Auth.tokenStrategy.refresh.beforeExpire) { - try { - this.jwt = await this.refreshToken(this.jwt, Auth.tokenStrategy.refresh.whenScheduled); - super.emit("updated", this.jwt); - await this.save(); - } catch (error) { - this.logger.error({ error }, "Error when refreshing jwt"); - } - } else { - this.logger.debug("Check token, still valid"); - } - }, Auth.tokenStrategy.refresh.interval); - } -} diff --git a/clients/tabby-agent/src/CompletionCache.ts b/clients/tabby-agent/src/CompletionCache.ts deleted file mode 100644 index f87ae5c2767f..000000000000 --- a/clients/tabby-agent/src/CompletionCache.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { LRUCache } from "lru-cache"; -import { CompletionContext, CompletionResponse } from "./CompletionContext"; -import { rootLogger } from "./logger"; -import { splitLines, autoClosingPairs, findUnpairedAutoClosingChars } from "./utils"; - -type CompletionCacheKey = CompletionContext; -type CompletionCacheValue = CompletionResponse; - -export class CompletionCache { - private readonly logger = rootLogger.child({ component: "CompletionCache" }); - private options = { - maxCount: 10000, - prebuildCache: { - enabled: true, - perCharacter: { - lines: 1, - max: 50, - }, - perLine: { - max: 10, - }, - autoClosingPairCheck: { - max: 3, - }, - }, - }; - private cache = new LRUCache({ - max: this.options.maxCount, - }); - - has(key: CompletionCacheKey): boolean { - return this.cache.has(key.hash); - } - - buildCache(key: CompletionCacheKey, value: CompletionCacheValue): void { - this.logger.debug({ key, value }, "Starting to build cache"); - const entries = this.createCacheEntries(key, value); - entries.forEach((entry) => { - this.cache.set(entry.key.hash, { value: entry.value, rebuildFlag: entry.rebuildFlag }); - }); - this.logger.debug({ newEntries: entries.length, cacheSize: this.cache.size }, "Cache updated"); - } - - get(key: CompletionCacheKey): CompletionCacheValue | undefined { - const entry = this.cache.get(key.hash); - if (entry?.rebuildFlag) { - this.buildCache(key, entry?.value); - } - return entry?.value; - } - - private createCacheEntries( - key: CompletionCacheKey, - value: CompletionCacheValue, - ): { key: CompletionCacheKey; value: CompletionCacheValue; rebuildFlag: boolean }[] { - const list = [{ key, value, rebuildFlag: false }]; - if (this.options.prebuildCache.enabled) { - for (const choice of value.choices) { - const completionText = choice.text.slice(key.position - choice.replaceRange.start); - const perLinePositions = this.getPerLinePositions(completionText); - this.logger.trace({ completionText, perLinePositions }, "Calculate per-line cache positions"); - for (const position of perLinePositions) { - const completionTextPrefix = completionText.slice(0, position); - const completionTextPrefixWithAutoClosedChars = this.generateAutoClosedPrefixes(completionTextPrefix); - for (const prefix of [completionTextPrefix, ...completionTextPrefixWithAutoClosedChars]) { - const entry = { - key: new CompletionContext({ - ...key, - text: key.text.slice(0, key.position) + prefix + key.text.slice(key.position), - position: key.position + position, - }), - value: { - ...value, - choices: [ - { - index: choice.index, - text: completionText.slice(position), - replaceRange: { - start: key.position + position, - end: key.position + position, - }, - }, - ], - }, - rebuildFlag: true, - }; - this.logger.trace({ prefix, entry }, "Build per-line cache entry"); - list.push(entry); - } - } - const perCharacterPositions = this.getPerCharacterPositions(completionText); - this.logger.trace({ completionText, perCharacterPositions }, "Calculate per-character cache positions"); - for (const position of perCharacterPositions) { - let lineStart = position; - while (lineStart > 0 && completionText[lineStart - 1] !== "\n") { - lineStart--; - } - const completionTextPrefix = completionText.slice(0, position); - const completionTextPrefixWithAutoClosedChars = this.generateAutoClosedPrefixes(completionTextPrefix); - for (const prefix of [completionTextPrefix, ...completionTextPrefixWithAutoClosedChars]) { - const entry = { - key: new CompletionContext({ - ...key, - text: key.text.slice(0, key.position) + prefix + key.text.slice(key.position), - position: key.position + position, - }), - value: { - ...value, - choices: [ - { - index: choice.index, - text: completionText.slice(lineStart), - replaceRange: { - start: key.position + lineStart, - end: key.position + position, - }, - }, - ], - }, - rebuildFlag: false, - }; - this.logger.trace({ prefix, entry }, "Build per-character cache entry"); - list.push(entry); - } - } - } - } - const result = list.reduce((prev, curr) => { - const found = prev.find((entry) => entry.key.hash === curr.key.hash); - if (found) { - found.value.choices.push(...curr.value.choices); - found.rebuildFlag = found.rebuildFlag || curr.rebuildFlag; - } else { - prev.push(curr); - } - return prev; - }, []); - return result; - } - - // positions for every line end (before newline character) and line begin (after indent) - private getPerLinePositions(completion: string): number[] { - const result: number[] = []; - const option = this.options.prebuildCache; - const lines = splitLines(completion); - let index = 0; - let offset = 0; - // `index < lines.length - 1` to exclude the last line - while (index < lines.length - 1 && index < option.perLine.max) { - offset += lines[index]?.length ?? 0; - result.push(offset - 1); // cache at the end of the line (before newline character) - result.push(offset); // cache at the beginning of the next line (after newline character) - let offsetNextLineBegin = offset; - while (offsetNextLineBegin < completion.length && completion[offsetNextLineBegin]!.match(/\s/)) { - offsetNextLineBegin++; - } - result.push(offsetNextLineBegin); // cache at the beginning of the next line (after indent) - index++; - } - return result; - } - - // positions for every character in the leading lines - private getPerCharacterPositions(completion: string): number[] { - const result: number[] = []; - const option = this.options.prebuildCache; - const text = splitLines(completion).slice(0, option.perCharacter.lines).join(""); - let offset = 0; - while (offset < text.length && offset < option.perCharacter.max) { - result.push(offset); - offset++; - } - return result; - } - - // FIXME: add unit tests - // "function(" => ["function()"] - // "call([" => ["call([]", "call([])" ] - // "function(arg" => ["function(arg)" ] - private generateAutoClosedPrefixes(prefix: string): string[] { - const result: string[] = []; - const unpaired = findUnpairedAutoClosingChars(prefix); - let checkIndex = 0; - let autoClosing = ""; - while (checkIndex < unpaired.length && checkIndex < this.options.prebuildCache.autoClosingPairCheck.max) { - autoClosingPairs - .filter((pair) => { - let pattern; - if ("open" in pair) { - pattern = pair.open; - } else { - pattern = pair.openOrClose; - } - return pattern.chars === unpaired[unpaired.length - 1 - checkIndex]; - }) - .forEach((pair) => { - let pattern; - if ("close" in pair) { - pattern = pair.close; - } else { - pattern = pair.openOrClose; - } - autoClosing += pattern.chars; - result.push(prefix + autoClosing); - }); - checkIndex++; - } - return result; - } -} diff --git a/clients/tabby-agent/src/CompletionContext.ts b/clients/tabby-agent/src/CompletionContext.ts deleted file mode 100644 index 66158ae57c58..000000000000 --- a/clients/tabby-agent/src/CompletionContext.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { splitLines, regOnlyAutoClosingCloseChars } from "./utils"; -import hashObject from "object-hash"; - -export type CompletionRequest = { - filepath: string; - language: string; - text: string; - position: number; - indentation?: string; - clipboard?: string; - manually?: boolean; -}; - -export type CompletionResponseChoice = { - index: number; - text: string; - // Range of the text to be replaced when applying the completion. - // The range should be limited to the current line. - replaceRange: { - start: number; - end: number; - }; -}; - -export type CompletionResponse = { - id: string; - choices: CompletionResponseChoice[]; -}; - -function isAtLineEndExcludingAutoClosedChar(suffix: string) { - return suffix.trimEnd().match(regOnlyAutoClosingCloseChars); -} - -export class CompletionContext { - filepath: string; - language: string; - indentation?: string; - text: string; - position: number; - - prefix: string; - suffix: string; - prefixLines: string[]; - suffixLines: string[]; - currentLinePrefix: string; - currentLineSuffix: string; - - clipboard: string; - - // "default": the cursor is at the end of the line - // "fill-in-line": the cursor is not at the end of the line, except auto closed characters - // In this case, we assume the completion should be a single line, so multiple lines completion will be dropped. - mode: "default" | "fill-in-line"; - hash: string; - - constructor(request: CompletionRequest) { - this.filepath = request.filepath; - this.language = request.language; - this.text = request.text; - this.position = request.position; - this.indentation = request.indentation; - - this.prefix = request.text.slice(0, request.position); - this.suffix = request.text.slice(request.position); - this.prefixLines = splitLines(this.prefix); - this.suffixLines = splitLines(this.suffix); - this.currentLinePrefix = this.prefixLines[this.prefixLines.length - 1] ?? ""; - this.currentLineSuffix = this.suffixLines[0] ?? ""; - - this.clipboard = request.clipboard?.trim() ?? ""; - - const lineEnd = isAtLineEndExcludingAutoClosedChar(this.suffixLines[0] ?? ""); - this.mode = lineEnd ? "default" : "fill-in-line"; - this.hash = hashObject({ - filepath: this.filepath, - language: this.language, - text: this.text, - position: this.position, - clipboard: this.clipboard, - }); - } -} diff --git a/clients/tabby-agent/src/CompletionDebounce.ts b/clients/tabby-agent/src/CompletionDebounce.ts deleted file mode 100644 index 23bf368c14ca..000000000000 --- a/clients/tabby-agent/src/CompletionDebounce.ts +++ /dev/null @@ -1,113 +0,0 @@ -import type { CompletionRequest, AbortSignalOption } from "./Agent"; -import type { AgentConfig } from "./AgentConfig"; -import { splitLines } from "./utils"; - -function clamp(min: number, max: number, value: number): number { - return Math.max(min, Math.min(max, value)); -} - -export class CompletionDebounce { - private lastCalledTimeStamp = 0; - - private baseInterval = 200; // ms - private calledIntervalHistory: number[] = []; - - private options = { - baseIntervalSlideWindowAvg: { - minSize: 20, - maxSize: 100, - min: 100, - max: 400, - }, - adaptiveRate: { - min: 1.5, - max: 3.0, - }, - contextScoreWeights: { - triggerCharacter: 0.5, - noSuffixInCurrentLine: 0.4, - noSuffix: 0.1, - }, - requestDelay: { - min: 100, // ms - max: 1000, - }, - }; - - async debounce( - context: { - request: CompletionRequest; - config: AgentConfig["completion"]["debounce"]; - responseTime: number; - }, - options?: AbortSignalOption, - ): Promise { - const { request, config, responseTime } = context; - if (request.manually) { - return this.sleep(0, options); - } - if (config.mode === "fixed") { - return this.sleep(config.interval, options); - } - const now = Date.now(); - this.updateBaseInterval(now - this.lastCalledTimeStamp); - this.lastCalledTimeStamp = now; - const contextScore = this.calcContextScore(request); - const adaptiveRate = - this.options.adaptiveRate.max - (this.options.adaptiveRate.max - this.options.adaptiveRate.min) * contextScore; - const expectedLatency = adaptiveRate * this.baseInterval; - const delay = clamp(this.options.requestDelay.min, this.options.requestDelay.max, expectedLatency - responseTime); - return this.sleep(delay, options); - } - - private async sleep(delay: number, options?: AbortSignalOption): Promise { - return new Promise((resolve, reject) => { - const timer = setTimeout(resolve, Math.min(delay, 0x7fffffff)); - if (options?.signal) { - if (options.signal.aborted) { - clearTimeout(timer); - reject(options.signal.reason); - } else { - options.signal.addEventListener("abort", () => { - clearTimeout(timer); - reject(options.signal.reason); - }); - } - } - }); - } - - private updateBaseInterval(interval: number) { - if (interval > this.options.baseIntervalSlideWindowAvg.max) { - return; - } - this.calledIntervalHistory.push(interval); - if (this.calledIntervalHistory.length > this.options.baseIntervalSlideWindowAvg.maxSize) { - this.calledIntervalHistory.shift(); - } - if (this.calledIntervalHistory.length > this.options.baseIntervalSlideWindowAvg.minSize) { - const avg = this.calledIntervalHistory.reduce((a, b) => a + b, 0) / this.calledIntervalHistory.length; - this.baseInterval = clamp( - this.options.baseIntervalSlideWindowAvg.min, - this.options.baseIntervalSlideWindowAvg.max, - avg, - ); - } - } - - // return score in [0, 1], 1 means the context has a high chance to accept the completion - private calcContextScore(request: CompletionRequest): number { - let score = 0; - const weights = this.options.contextScoreWeights; - const triggerCharacter = request.text[request.position - 1] ?? ""; - score += triggerCharacter.match(/^\W*$/) ? weights.triggerCharacter : 0; - - const suffix = request.text.slice(request.position) ?? ""; - const currentLineInSuffix = splitLines(suffix)[0] ?? ""; - score += currentLineInSuffix.match(/^\W*$/) ? weights.noSuffixInCurrentLine : 0; - score += suffix.match(/^\W*$/) ? weights.noSuffix : 0; - - score = clamp(0, 1, score); - return score; - } -} diff --git a/clients/tabby-agent/src/CompletionProviderStats.ts b/clients/tabby-agent/src/CompletionProviderStats.ts deleted file mode 100644 index 031bdff769c8..000000000000 --- a/clients/tabby-agent/src/CompletionProviderStats.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { Univariate } from "stats-logscale"; - -export type CompletionProviderStatsEntry = { - triggerMode: "auto" | "manual"; - cacheHit: boolean; - aborted: boolean; - requestSent: boolean; - requestLatency: number; // ms, NaN if timeout - requestCanceled: boolean; - requestTimeout: boolean; -}; - -class Average { - private sum = 0; - private quantity = 0; - - add(value: number): void { - this.sum += value; - this.quantity += 1; - } - - mean(): number | undefined { - if (this.quantity === 0) { - return undefined; - } - return this.sum / this.quantity; - } - - count(): number { - return this.quantity; - } -} - -class Windowed { - private readonly maxSize: number; - private readonly values: number[] = []; - - constructor(maxSize: number) { - this.maxSize = maxSize; - } - - add(value: number): void { - this.values.push(value); - if (this.values.length > this.maxSize) { - this.values.shift(); - } - } - - getValues(): number[] { - return this.values; - } -} - -type WindowedStats = { - values: number[]; - stats: { total: number; timeouts: number; responses: number; averageResponseTime: number }; -}; - -export class CompletionProviderStats { - private config = { - windowSize: 10, - checks: { - disable: false, - // Mark status as healthy if the latency is less than the threshold for each latest windowSize requests. - healthy: { windowSize: 1, latency: 3000 }, - // If there is at least {count} requests, and the average response time is higher than the {latency}, show warning - slowResponseTime: { latency: 5000, count: 1 }, - // If there is at least {count} timeouts, and the timeout rate is higher than the {rate}, show warning - highTimeoutRate: { rate: 0.5, count: 1 }, - }, - }; - - private autoCompletionCount = 0; - private manualCompletionCount = 0; - private cacheHitCount = 0; - private cacheMissCount = 0; - - private eventMap = new Map(); - - private completionRequestLatencyStats = new Univariate(); - private completionRequestCanceledStats = new Average(); - private completionRequestTimeoutCount = 0; - - private recentCompletionRequestLatencies: Windowed = new Windowed(this.config.windowSize); - - add(value: CompletionProviderStatsEntry): void { - const { triggerMode, cacheHit, aborted, requestSent, requestLatency, requestCanceled, requestTimeout } = value; - if (!aborted) { - if (triggerMode === "auto") { - this.autoCompletionCount += 1; - } else { - this.manualCompletionCount += 1; - } - if (cacheHit) { - this.cacheHitCount += 1; - } else { - this.cacheMissCount += 1; - } - } - if (requestSent) { - if (requestCanceled) { - this.completionRequestCanceledStats.add(requestLatency); - } else if (requestTimeout) { - this.completionRequestTimeoutCount += 1; - } else { - this.completionRequestLatencyStats.add(requestLatency); - } - if (!requestCanceled) { - this.recentCompletionRequestLatencies.add(requestLatency); - } - } - } - - addEvent(event: string): void { - const count = this.eventMap.get(event) || 0; - this.eventMap.set(event, count + 1); - } - - reset() { - this.autoCompletionCount = 0; - this.manualCompletionCount = 0; - this.cacheHitCount = 0; - this.cacheMissCount = 0; - - this.eventMap = new Map(); - - this.completionRequestLatencyStats = new Univariate(); - this.completionRequestCanceledStats = new Average(); - this.completionRequestTimeoutCount = 0; - } - - resetWindowed() { - this.recentCompletionRequestLatencies = new Windowed(this.config.windowSize); - } - - // stats for anonymous usage report - stats() { - const eventCount = Object.fromEntries( - Array.from(this.eventMap.entries()).map(([key, value]) => ["count_" + key, value]), - ); - return { - completion: { - count_auto: this.autoCompletionCount, - count_manual: this.manualCompletionCount, - cache_hit: this.cacheHitCount, - cache_miss: this.cacheMissCount, - ...eventCount, - }, - completion_request: { - count: this.completionRequestLatencyStats.count(), - latency_avg: this.completionRequestLatencyStats.mean(), - latency_p50: this.completionRequestLatencyStats.percentile(50), - latency_p95: this.completionRequestLatencyStats.percentile(95), - latency_p99: this.completionRequestLatencyStats.percentile(99), - }, - completion_request_canceled: { - count: this.completionRequestCanceledStats.count(), - latency_avg: this.completionRequestCanceledStats.mean(), - }, - completion_request_timeout: { - count: this.completionRequestTimeoutCount, - }, - }; - } - - // stats for "highTimeoutRate" | "slowResponseTime" warning - windowed(): WindowedStats { - const latencies = this.recentCompletionRequestLatencies.getValues(); - const timeouts = latencies.filter((latency) => Number.isNaN(latency)); - const responses = latencies.filter((latency) => !Number.isNaN(latency)); - const averageResponseTime = responses.reduce((acc, latency) => acc + latency, 0) / responses.length; - return { - values: latencies, - stats: { - total: latencies.length, - timeouts: timeouts.length, - responses: responses.length, - averageResponseTime, - }, - }; - } - - check(windowed: WindowedStats): "healthy" | "highTimeoutRate" | "slowResponseTime" | null { - if (this.config.checks.disable) { - return null; - } - const config = this.config.checks; - - const { - values: latencies, - stats: { total, timeouts, responses, averageResponseTime }, - } = windowed; - - if ( - latencies - .slice(-Math.min(this.config.windowSize, config.healthy.windowSize)) - .every((latency) => latency < config.healthy.latency) - ) { - return "healthy"; - } - if (timeouts / total > config.highTimeoutRate.rate && timeouts >= config.highTimeoutRate.count) { - return "highTimeoutRate"; - } - if (averageResponseTime > config.slowResponseTime.latency && responses >= config.slowResponseTime.count) { - return "slowResponseTime"; - } - return null; - } -} diff --git a/clients/tabby-agent/src/JsonLineServer.ts b/clients/tabby-agent/src/JsonLineServer.ts deleted file mode 100644 index a0d8a4ce9f0d..000000000000 --- a/clients/tabby-agent/src/JsonLineServer.ts +++ /dev/null @@ -1,149 +0,0 @@ -import readline from "readline"; -import { AgentFunction, AgentEvent, Agent, agentEventNames } from "./Agent"; -import { rootLogger } from "./logger"; -import { isCanceledError } from "./utils"; - -type AgentFunctionRequest = [ - id: number, - data: { - func: T; - args: Parameters; - }, -]; - -type CancellationRequest = [ - id: number, - data: { - func: "cancelRequest"; - args: [id: number]; - }, -]; - -type JsonLineRequest = AgentFunctionRequest | CancellationRequest; - -type AgentFunctionResponse = [ - id: number, // Matched request id - data: ReturnType | null, -]; - -type AgentEventNotification = [ - id: 0, // Always 0 - data: AgentEvent, -]; - -type CancellationResponse = [ - id: number, // Matched request id - data: boolean | null, -]; - -type JsonLineResponse = AgentFunctionResponse | AgentEventNotification | CancellationResponse; - -/** - * Every request and response should be single line JSON string and end with a newline. - */ -export class JsonLineServer { - private readonly process: NodeJS.Process = process; - private readonly inStream: NodeJS.ReadStream = process.stdin; - private readonly outStream: NodeJS.WriteStream = process.stdout; - private readonly logger = rootLogger.child({ component: "JsonLineServer" }); - - private abortControllers: { [id: string]: AbortController } = {}; - - private agent?: Agent; - - constructor() {} - - private async handleLine(line: string) { - let request: JsonLineRequest; - try { - request = JSON.parse(line) as JsonLineRequest; - } catch (error) { - this.logger.error({ error }, `Failed to parse request: ${line}`); - return; - } - this.logger.debug({ request }, "Received request"); - const response = await this.handleRequest(request); - this.sendResponse(response); - this.logger.debug({ response }, "Sent response"); - } - - private async handleRequest(request: JsonLineRequest): Promise { - let requestId: number = 0; - const response: JsonLineResponse = [0, null]; - const abortController = new AbortController(); - try { - if (!this.agent) { - throw new Error(`Agent not bound.\n`); - } - requestId = request[0]; - response[0] = requestId; - - const funcName = request[1].func; - if (funcName === "cancelRequest") { - response[1] = this.cancelRequest(request as CancellationRequest); - } else { - const func = this.agent[funcName]; - if (!func) { - throw new Error(`Unknown function: ${funcName}`); - } - const args = request[1].args; - // If the last argument is an object and has `signal` property, replace it with the abort signal. - if (args.length > 0 && typeof args[args.length - 1] === "object" && args[args.length - 1]["signal"]) { - this.abortControllers[requestId] = abortController; - args[args.length - 1]["signal"] = abortController.signal; - } - // @ts-expect-error TS2684: FIXME - response[1] = await func.apply(this.agent, args); - } - } catch (error) { - if (isCanceledError(error)) { - this.logger.debug({ error, request }, `Request canceled`); - } else { - this.logger.error({ error, request }, `Failed to handle request`); - } - } finally { - if (this.abortControllers[requestId]) { - delete this.abortControllers[requestId]; - } - } - return response; - } - - private cancelRequest(request: CancellationRequest): boolean { - const targetId = request[1].args[0]; - const controller = this.abortControllers[targetId]; - if (controller) { - controller.abort(); - return true; - } - return false; - } - - private sendResponse(response: JsonLineResponse): void { - this.outStream.write(JSON.stringify(response) + "\n"); - } - - bind(agent: Agent): void { - this.agent = agent; - for (const eventName of agentEventNames) { - this.agent.on(eventName, (event) => { - this.sendResponse([0, event]); - }); - } - } - - listen() { - readline.createInterface({ input: this.inStream }).on("line", (line) => { - this.handleLine(line); - }); - - ["SIGTERM", "SIGINT"].forEach((sig) => { - this.process.on(sig, async () => { - if (this.agent && this.agent.getStatus() !== "finalized") { - await this.agent.finalize(); - } - this.process.exit(0); - }); - }); - } -} diff --git a/clients/tabby-agent/src/LspServer.ts b/clients/tabby-agent/src/LspServer.ts deleted file mode 100644 index 800e9dac69b8..000000000000 --- a/clients/tabby-agent/src/LspServer.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { - createConnection, - TextDocuments, - TextDocumentSyncKind, - InitializeParams, - InitializeResult, - ShowMessageParams, - MessageType, - CompletionParams, - CompletionList, - CompletionItem, - CompletionItemKind, - TextDocumentPositionParams, -} from "vscode-languageserver/node"; -import { TextDocument } from "vscode-languageserver-textdocument"; -import { name as agentName, version as agentVersion } from "../package.json"; -import { Agent, StatusChangedEvent, CompletionRequest, CompletionResponse } from "./Agent"; -import { rootLogger } from "./logger"; -import { splitLines, isCanceledError } from "./utils"; - -export class LspServer { - private readonly connection = createConnection(); - private readonly documents = new TextDocuments(TextDocument); - - private readonly logger = rootLogger.child({ component: "LspServer" }); - - private agent?: Agent; - - constructor() { - this.connection.onInitialize(async (params) => { - return await this.initialize(params); - }); - this.connection.onShutdown(async () => { - return await this.shutdown(); - }); - this.connection.onExit(async () => { - return this.exit(); - }); - this.connection.onCompletion(async (params) => { - return await this.completion(params); - }); - } - - bind(agent: Agent): void { - this.agent = agent; - - this.agent.on("statusChanged", (event: StatusChangedEvent) => { - if (event.status === "disconnected" || event.status === "unauthorized") { - this.showMessage({ - type: MessageType.Warning, - message: `Tabby agent status: ${event.status}`, - }); - } - }); - } - - listen() { - this.documents.listen(this.connection); - this.connection.listen(); - } - - // LSP interface methods - - async initialize(params: InitializeParams): Promise { - this.logger.debug({ params }, "LSP: initialize: request"); - if (!this.agent) { - throw new Error(`Agent not bound.\n`); - } - - const { clientInfo, capabilities } = params; - if (capabilities.textDocument?.inlineCompletion) { - // TODO: use inlineCompletion instead of completion - } - - await this.agent.initialize({ - clientProperties: { - session: { - client: `${clientInfo?.name} ${clientInfo?.version ?? ""}`, - ide: { - name: clientInfo?.name, - version: clientInfo?.version, - }, - tabby_plugin: { - name: `${agentName} (LSP)`, - version: agentVersion, - }, - }, - }, - }); - - const result: InitializeResult = { - capabilities: { - textDocumentSync: { - openClose: true, - change: TextDocumentSyncKind.Incremental, - }, - completionProvider: {}, - // inlineCompletionProvider: {}, - }, - serverInfo: { - name: agentName, - version: agentVersion, - }, - }; - return result; - } - - async shutdown() { - this.logger.debug("LSP: shutdown: request"); - if (!this.agent) { - throw new Error(`Agent not bound.\n`); - } - - await this.agent.finalize(); - } - - exit() { - this.logger.debug("LSP: exit: request"); - return process.exit(0); - } - - async showMessage(params: ShowMessageParams) { - this.logger.debug({ params }, "LSP server notification: window/showMessage"); - await this.connection.sendNotification("window/showMessage", params); - } - - async completion(params: CompletionParams): Promise { - this.logger.debug({ params }, "LSP: textDocument/completion: request"); - if (!this.agent) { - throw new Error(`Agent not bound.\n`); - } - - try { - const request = this.buildCompletionRequest(params); - const response = await this.agent.provideCompletions(request); - const completionList = this.toCompletionList(response, params); - this.logger.debug({ completionList }, "LSP: textDocument/completion: response"); - return completionList; - } catch (error) { - if (isCanceledError(error)) { - this.logger.debug({ error }, "LSP: textDocument/completion: canceled"); - } else { - this.logger.error({ error }, "LSP: textDocument/completion: error"); - } - } - - return { - isIncomplete: true, - items: [], - }; - } - - private buildCompletionRequest( - documentPosition: TextDocumentPositionParams, - manually: boolean = false, - ): CompletionRequest { - const { textDocument, position } = documentPosition; - const document = this.documents.get(textDocument.uri)!; - - const request: CompletionRequest = { - filepath: document.uri, - language: document.languageId, - text: document.getText(), - position: document.offsetAt(position), - manually, - }; - return request; - } - - private toCompletionList(response: CompletionResponse, documentPosition: TextDocumentPositionParams): CompletionList { - const { textDocument, position } = documentPosition; - const document = this.documents.get(textDocument.uri)!; - - // Get word prefix if cursor is at end of a word - const linePrefix = document.getText({ - start: { line: position.line, character: 0 }, - end: position, - }); - const wordPrefix = linePrefix.match(/(\w+)$/)?.[0] ?? ""; - - return { - isIncomplete: true, - items: response.choices.map((choice): CompletionItem => { - const insertionText = choice.text.slice(document.offsetAt(position) - choice.replaceRange.start); - - const lines = splitLines(insertionText); - const firstLine = lines[0] || ""; - const secondLine = lines[1] || ""; - return { - label: wordPrefix + firstLine, - labelDetails: { - detail: secondLine, - description: "Tabby", - }, - kind: CompletionItemKind.Text, - documentation: { - kind: "markdown", - value: `\`\`\`\n${linePrefix + insertionText}\n\`\`\`\n ---\nSuggested by Tabby.`, - }, - textEdit: { - newText: wordPrefix + insertionText, - range: { - start: { line: position.line, character: position.character - wordPrefix.length }, - end: document.positionAt(choice.replaceRange.end), - }, - }, - data: { - completionId: response.id, - choiceIndex: choice.index, - }, - }; - }), - }; - } -} diff --git a/clients/tabby-agent/src/TabbyAgent.ts b/clients/tabby-agent/src/TabbyAgent.ts deleted file mode 100644 index 6e4c0decec3b..000000000000 --- a/clients/tabby-agent/src/TabbyAgent.ts +++ /dev/null @@ -1,635 +0,0 @@ -import { EventEmitter } from "events"; -import { v4 as uuid } from "uuid"; -import deepEqual from "deep-equal"; -import { deepmerge } from "deepmerge-ts"; -import { getProperty, setProperty, deleteProperty } from "dot-prop"; -import createClient from "openapi-fetch"; -import type { ParseAs } from "openapi-fetch"; -import type { paths as TabbyApi } from "./types/tabbyApi"; -import type { - Agent, - AgentStatus, - AgentIssue, - AgentEvent, - ClientProperties, - AgentInitOptions, - AbortSignalOption, - ServerHealthState, - CompletionRequest, - CompletionResponse, - LogEventRequest, -} from "./Agent"; -import type { DataStore } from "./dataStore"; -import { isBlank, abortSignalFromAnyOf, HttpError, isTimeoutError, isCanceledError, errorToString } from "./utils"; -import { Auth } from "./Auth"; -import { AgentConfig, PartialAgentConfig, defaultAgentConfig } from "./AgentConfig"; -import { configFile } from "./configFile"; -import { CompletionCache } from "./CompletionCache"; -import { CompletionDebounce } from "./CompletionDebounce"; -import { CompletionContext } from "./CompletionContext"; -import { preCacheProcess, postCacheProcess } from "./postprocess"; -import { rootLogger, allLoggers } from "./logger"; -import { AnonymousUsageLogger } from "./AnonymousUsageLogger"; -import { CompletionProviderStats, CompletionProviderStatsEntry } from "./CompletionProviderStats"; - -export class TabbyAgent extends EventEmitter implements Agent { - private readonly logger = rootLogger.child({ component: "TabbyAgent" }); - private anonymousUsageLogger = new AnonymousUsageLogger(); - private config: AgentConfig = defaultAgentConfig; - private userConfig: PartialAgentConfig = {}; // config from `~/.tabby-client/agent/config.toml` - private clientConfig: PartialAgentConfig = {}; // config from `initialize` and `updateConfig` method - private status: AgentStatus = "notInitialized"; - private issues: AgentIssue["name"][] = []; - private serverHealthState?: ServerHealthState; - private connectionErrorMessage?: string; - private dataStore?: DataStore; - private api?: ReturnType>; - private auth?: Auth; - private completionCache = new CompletionCache(); - private completionDebounce = new CompletionDebounce(); - private nonParallelProvideCompletionAbortController?: AbortController; - private completionProviderStats = new CompletionProviderStats(); - static readonly tryConnectInterval = 1000 * 30; // 30s - private tryingConnectTimer: ReturnType; - static readonly submitStatsInterval = 1000 * 60 * 60 * 24; // 24h - private submitStatsTimer: ReturnType; - - constructor() { - super(); - - this.tryingConnectTimer = setInterval(async () => { - if (this.status === "disconnected") { - this.logger.debug("Trying to connect..."); - await this.healthCheck(); - } - }, TabbyAgent.tryConnectInterval); - - this.submitStatsTimer = setInterval(async () => { - await this.submitStats(); - }, TabbyAgent.submitStatsInterval); - } - - private async applyConfig() { - const oldConfig = this.config; - const oldStatus = this.status; - - this.config = deepmerge(defaultAgentConfig, this.userConfig, this.clientConfig) as AgentConfig; - allLoggers.forEach((logger) => (logger.level = this.config.logs.level)); - this.anonymousUsageLogger.disabled = this.config.anonymousUsageTracking.disable; - - if (isBlank(this.config.server.token) && this.config.server.requestHeaders["Authorization"] === undefined) { - if (this.config.server.endpoint !== this.auth?.endpoint) { - this.auth = new Auth(this.config.server.endpoint); - await this.auth.init({ dataStore: this.dataStore }); - this.auth.on("updated", () => { - this.setupApi(); - }); - } - } else { - // If auth token is provided, use it directly. - this.auth = undefined; - } - - // If server config changed, clear server related state - if (!deepEqual(oldConfig.server, this.config.server)) { - this.serverHealthState = undefined; - this.completionProviderStats.resetWindowed(); - this.popIssue("slowCompletionResponseTime"); - this.popIssue("highCompletionTimeoutRate"); - this.popIssue("connectionFailed"); - this.connectionErrorMessage = undefined; - } - - await this.setupApi(); - - if (!deepEqual(oldConfig.server, this.config.server)) { - // If server config changed and status remain `unauthorized`, we want to emit `authRequired` again. - // but `changeStatus` will not emit `authRequired` if status is not changed, so we emit it manually here. - if (oldStatus === "unauthorized" && this.status === "unauthorized") { - this.emitAuthRequired(); - } - } - - const event: AgentEvent = { event: "configUpdated", config: this.config }; - this.logger.debug({ event }, "Config updated"); - super.emit("configUpdated", event); - } - - private async setupApi() { - const auth = !isBlank(this.config.server.token) - ? `Bearer ${this.config.server.token}` - : this.auth?.token - ? `Bearer ${this.auth.token}` - : undefined; - this.api = createClient({ - baseUrl: this.config.server.endpoint.replace(/\/+$/, ""), // remove trailing slash - headers: { - Authorization: auth, - ...this.config.server.requestHeaders, - }, - }); - await this.healthCheck(); - } - - private changeStatus(status: AgentStatus) { - if (this.status != status) { - this.status = status; - const event: AgentEvent = { event: "statusChanged", status }; - this.logger.debug({ event }, "Status changed"); - super.emit("statusChanged", event); - if (this.status === "unauthorized") { - this.emitAuthRequired(); - } - } - } - - private issueFromName(issueName: AgentIssue["name"]): AgentIssue { - switch (issueName) { - case "highCompletionTimeoutRate": - return { - name: "highCompletionTimeoutRate", - completionResponseStats: this.completionProviderStats.windowed().stats, - }; - case "slowCompletionResponseTime": - return { - name: "slowCompletionResponseTime", - completionResponseStats: this.completionProviderStats.windowed().stats, - }; - case "connectionFailed": - return { - name: "connectionFailed", - message: this.connectionErrorMessage, - }; - } - } - - private pushIssue(issue: AgentIssue["name"]) { - if (!this.issues.includes(issue)) { - this.issues.push(issue); - this.logger.debug({ issue }, "Issues Pushed"); - this.emitIssueUpdated(); - } - } - - private popIssue(issue: AgentIssue["name"]) { - const index = this.issues.indexOf(issue); - if (index >= 0) { - this.issues.splice(index, 1); - this.logger.debug({ issue }, "Issues Popped"); - this.emitIssueUpdated(); - } - } - - private emitAuthRequired() { - const event: AgentEvent = { event: "authRequired", server: this.config.server }; - super.emit("authRequired", event); - } - - private emitIssueUpdated() { - const event: AgentEvent = { event: "issuesUpdated", issues: this.issues }; - super.emit("issuesUpdated", event); - } - - private async submitStats() { - const stats = this.completionProviderStats.stats(); - if (stats.completion_request.count > 0) { - await this.anonymousUsageLogger.event("AgentStats", { stats }); - this.completionProviderStats.reset(); - this.logger.debug({ stats }, "Stats submitted"); - } - } - - private createAbortSignal(options?: { signal?: AbortSignal; timeout?: number }): AbortSignal { - const timeout = Math.min(0x7fffffff, options?.timeout || this.config.server.requestTimeout); - return abortSignalFromAnyOf([AbortSignal.timeout(timeout), options?.signal]); - } - - private async healthCheck(options?: { signal?: AbortSignal; method?: "GET" | "POST" }): Promise { - const requestId = uuid(); - const requestPath = "/v1/health"; - const requestUrl = this.config.server.endpoint + requestPath; - const requestOptions = { - signal: this.createAbortSignal({ signal: options?.signal }), - }; - try { - if (!this.api) { - throw new Error("http client not initialized"); - } - this.logger.debug({ requestId, requestOptions, url: requestUrl }, "Health check request"); - let response; - if (options?.method === "POST") { - response = await this.api.POST(requestPath, requestOptions); - } else { - response = await this.api.GET(requestPath, requestOptions); - } - if (response.error || !response.response.ok) { - throw new HttpError(response.response); - } - this.logger.debug({ requestId, response }, "Health check response"); - this.changeStatus("ready"); - this.popIssue("connectionFailed"); - this.connectionErrorMessage = undefined; - const healthState = response.data; - if ( - typeof healthState === "object" && - healthState["model"] !== undefined && - healthState["device"] !== undefined - ) { - this.serverHealthState = healthState; - this.anonymousUsageLogger.uniqueEvent("AgentConnected", healthState); - } - } catch (error) { - this.serverHealthState = undefined; - if (error instanceof HttpError && error.status == 405 && options?.method !== "POST") { - return await this.healthCheck({ method: "POST" }); - } else if (error instanceof HttpError && [401, 403].includes(error.status)) { - this.logger.debug({ requestId, error }, "Health check error: unauthorized"); - this.changeStatus("unauthorized"); - } else { - if (isTimeoutError(error)) { - this.logger.debug({ requestId, error }, "Health check error: timeout"); - this.connectionErrorMessage = `GET ${requestUrl}: Timed out.`; - } else if (isCanceledError(error)) { - this.logger.debug({ requestId, error }, "Health check error: canceled"); - this.connectionErrorMessage = `GET ${requestUrl}: Canceled.`; - } else { - this.logger.error({ requestId, error }, "Health check error: unknown error"); - const message = error instanceof Error ? errorToString(error) : JSON.stringify(error); - this.connectionErrorMessage = `GET ${requestUrl}: Request failed: \n${message}`; - } - this.pushIssue("connectionFailed"); - this.changeStatus("disconnected"); - } - } - } - - private createSegments(context: CompletionContext): { prefix: string; suffix: string; clipboard?: string } { - // max lines in prefix and suffix configurable - const maxPrefixLines = this.config.completion.prompt.maxPrefixLines; - const maxSuffixLines = this.config.completion.prompt.maxSuffixLines; - const { prefixLines, suffixLines } = context; - const prefix = prefixLines.slice(Math.max(prefixLines.length - maxPrefixLines, 0)).join(""); - let suffix; - if (this.config.completion.prompt.experimentalStripAutoClosingCharacters && context.mode !== "fill-in-line") { - suffix = "\n" + suffixLines.slice(1, maxSuffixLines).join(""); - } else { - suffix = suffixLines.slice(0, maxSuffixLines).join(""); - } - - let clipboard = undefined; - const clipboardConfig = this.config.completion.prompt.clipboard; - if (context.clipboard.length >= clipboardConfig.minChars && context.clipboard.length <= clipboardConfig.maxChars) { - clipboard = context.clipboard; - } - return { prefix, suffix, clipboard }; - } - - public async initialize(options: AgentInitOptions): Promise { - this.dataStore = options?.dataStore; - await this.anonymousUsageLogger.init({ dataStore: this.dataStore }); - if (options.clientProperties) { - const { user: userProp, session: sessionProp } = options.clientProperties; - allLoggers.forEach((logger) => logger.setBindings?.({ ...sessionProp })); - if (sessionProp) { - Object.entries(sessionProp).forEach(([key, value]) => { - this.anonymousUsageLogger.setSessionProperties(key, value); - }); - } - if (userProp) { - Object.entries(userProp).forEach(([key, value]) => { - this.anonymousUsageLogger.setUserProperties(key, value); - }); - } - } - if (configFile) { - await configFile.load(); - this.userConfig = configFile.config; - configFile.on("updated", async (config) => { - this.userConfig = config; - await this.applyConfig(); - }); - configFile.watch(); - } - if (options.config) { - this.clientConfig = options.config; - } - await this.applyConfig(); - await this.anonymousUsageLogger.uniqueEvent("AgentInitialized"); - this.logger.debug({ options }, "Initialized"); - return this.status !== "notInitialized"; - } - - public async finalize(): Promise { - if (this.status === "finalized") { - return false; - } - - await this.submitStats(); - - if (this.tryingConnectTimer) { - clearInterval(this.tryingConnectTimer); - } - if (this.submitStatsTimer) { - clearInterval(this.submitStatsTimer); - } - this.changeStatus("finalized"); - return true; - } - - public async updateClientProperties(type: keyof ClientProperties, key: string, value: any): Promise { - switch (type) { - case "session": - allLoggers.forEach((logger) => logger.setBindings?.(setProperty({}, key, value))); - this.anonymousUsageLogger.setSessionProperties(key, value); - break; - case "user": - this.anonymousUsageLogger.setUserProperties(key, value); - break; - } - return true; - } - - public async updateConfig(key: string, value: any): Promise { - const current = getProperty(this.clientConfig, key); - if (!deepEqual(current, value)) { - if (value === undefined) { - deleteProperty(this.clientConfig, key); - } else { - setProperty(this.clientConfig, key, value); - } - await this.applyConfig(); - } - return true; - } - - public async clearConfig(key: string): Promise { - return await this.updateConfig(key, undefined); - } - - public getConfig(): AgentConfig { - return this.config; - } - - public getStatus(): AgentStatus { - return this.status; - } - - public getIssues(): AgentIssue["name"][] { - return this.issues; - } - - public getIssueDetail(options: { index?: number; name?: T["name"] }): T | null { - const issues = this.getIssues(); - if (options.index !== undefined && options.index < issues.length) { - return this.issueFromName(issues[options.index]!) as T; - } else if (options.name !== undefined && this.issues.includes(options.name)) { - return this.issueFromName(options.name) as T; - } else { - return null; - } - } - - public getServerHealthState(): ServerHealthState | null { - return this.serverHealthState ?? null; - } - - public async requestAuthUrl(options?: AbortSignalOption): Promise<{ authUrl: string; code: string } | null> { - if (this.status === "notInitialized") { - throw new Error("Agent is not initialized"); - } - await this.healthCheck(options); - if (this.status !== "unauthorized" || !this.auth) { - return null; - } else { - return await this.auth.requestAuthUrl(options); - } - } - - public async waitForAuthToken(code: string, options?: AbortSignalOption): Promise { - if (this.status === "notInitialized") { - throw new Error("Agent is not initialized"); - } - if (this.status !== "unauthorized" || !this.auth) { - return; - } - await this.auth.pollingToken(code, options); - await this.setupApi(); - } - - public async provideCompletions( - request: CompletionRequest, - options?: AbortSignalOption, - ): Promise { - if (this.status === "notInitialized") { - throw new Error("Agent is not initialized"); - } - this.logger.trace({ request }, "Call provideCompletions"); - if (this.nonParallelProvideCompletionAbortController) { - this.nonParallelProvideCompletionAbortController.abort(); - } - this.nonParallelProvideCompletionAbortController = new AbortController(); - const signal = abortSignalFromAnyOf([this.nonParallelProvideCompletionAbortController.signal, options?.signal]); - - let completionResponse: CompletionResponse; - let stats: CompletionProviderStatsEntry | undefined = { - triggerMode: request.manually ? "manual" : "auto", - cacheHit: false, - aborted: false, - requestSent: false, - requestLatency: 0, - requestCanceled: false, - requestTimeout: false, - }; - let requestStartedAt: number | undefined; - - const context = new CompletionContext(request); - try { - if (this.completionCache.has(context)) { - // Cache hit - stats.cacheHit = true; - this.logger.debug({ context }, "Completion cache hit"); - // Debounce before returning cached response - await this.completionDebounce.debounce( - { - request, - config: this.config.completion.debounce, - responseTime: 0, - }, - { signal }, - ); - - completionResponse = this.completionCache.get(context)!; - } else { - // Cache miss - stats.cacheHit = false; - const segments = this.createSegments(context); - if (isBlank(segments.prefix)) { - // Empty prompt - stats = undefined; // no need to record stats for empty prompt - this.logger.debug("Segment prefix is blank, returning empty completion response"); - completionResponse = { - id: "agent-" + uuid(), - choices: [], - }; - } else { - // Debounce before sending request - await this.completionDebounce.debounce( - { - request, - config: this.config.completion.debounce, - responseTime: this.completionProviderStats.windowed().stats.averageResponseTime, - }, - options, - ); - - // Send http request - const requestId = uuid(); - stats.requestSent = true; - requestStartedAt = performance.now(); - try { - if (!this.api) { - throw new Error("http client not initialized"); - } - const requestPath = "/v1/completions"; - const requestOptions = { - body: { - language: request.language, - segments, - user: this.auth?.user, - }, - signal: this.createAbortSignal({ signal }), - }; - this.logger.debug( - { requestId, requestOptions, url: this.config.server.endpoint + requestPath }, - "Completion request", - ); - const response = await this.api.POST(requestPath, requestOptions); - if (response.error || !response.response.ok) { - throw new HttpError(response.response); - } - this.logger.debug({ requestId, response }, "Completion response"); - const responseData = response.data; - stats.requestLatency = performance.now() - requestStartedAt; - completionResponse = { - id: responseData.id, - choices: responseData.choices.map((choice) => { - return { - index: choice.index, - text: choice.text, - replaceRange: { - start: request.position, - end: request.position, - }, - }; - }), - }; - } catch (error) { - if (isCanceledError(error)) { - this.logger.debug({ requestId, error }, "Completion request canceled"); - stats.requestCanceled = true; - stats.requestLatency = performance.now() - requestStartedAt; - } else if (isTimeoutError(error)) { - this.logger.debug({ requestId, error }, "Completion request timeout"); - stats.requestTimeout = true; - stats.requestLatency = NaN; - } else { - this.logger.error({ requestId, error }, "Completion request failed with unknown error"); - // schedule a health check - this.healthCheck(); - } - // rethrow error - throw error; - } - // Postprocess (pre-cache) - completionResponse = await preCacheProcess(context, this.config.postprocess, completionResponse); - if (signal.aborted) { - throw signal.reason; - } - // Build cache - this.completionCache.buildCache(context, JSON.parse(JSON.stringify(completionResponse))); - } - } - // Postprocess (post-cache) - completionResponse = await postCacheProcess(context, this.config.postprocess, completionResponse); - if (signal.aborted) { - throw signal.reason; - } - } catch (error) { - if (isCanceledError(error) || isTimeoutError(error)) { - if (stats) { - stats.aborted = true; - } - } else { - // unexpected error - stats = undefined; - } - // rethrow error - throw error; - } finally { - if (stats) { - this.completionProviderStats.add(stats); - - if (stats.requestSent && !stats.requestCanceled) { - const windowedStats = this.completionProviderStats.windowed(); - const checkResult = this.completionProviderStats.check(windowedStats); - switch (checkResult) { - case "healthy": - this.popIssue("slowCompletionResponseTime"); - this.popIssue("highCompletionTimeoutRate"); - break; - case "highTimeoutRate": - this.popIssue("slowCompletionResponseTime"); - this.pushIssue("highCompletionTimeoutRate"); - break; - case "slowResponseTime": - this.popIssue("highCompletionTimeoutRate"); - this.pushIssue("slowCompletionResponseTime"); - break; - } - } - } - } - this.logger.trace({ context, completionResponse }, "Return from provideCompletions"); - return completionResponse; - } - - public async postEvent(request: LogEventRequest, options?: AbortSignalOption): Promise { - if (this.status === "notInitialized") { - throw new Error("Agent is not initialized"); - } - this.completionProviderStats.addEvent(request.type); - const requestId = uuid(); - try { - if (!this.api) { - throw new Error("http client not initialized"); - } - const requestPath = "/v1/events"; - const requestOptions = { - body: request, - params: { - query: { - select_kind: request.select_kind, - }, - }, - signal: this.createAbortSignal(options), - parseAs: "text" as ParseAs, - }; - this.logger.debug({ requestId, requestOptions, url: this.config.server.endpoint + requestPath }, "Event request"); - const response = await this.api.POST(requestPath, requestOptions); - if (response.error || !response.response.ok) { - throw new HttpError(response.response); - } - this.logger.debug({ requestId, response }, "Event response"); - return true; - } catch (error) { - if (isTimeoutError(error)) { - this.logger.debug({ requestId, error }, "Event request timeout"); - } else if (isCanceledError(error)) { - this.logger.debug({ requestId, error }, "Event request canceled"); - } else { - this.logger.error({ requestId, error }, "Event request failed with unknown error"); - } - return false; - } - } -} diff --git a/clients/tabby-agent/src/certsLoader.ts b/clients/tabby-agent/src/certsLoader.ts new file mode 100644 index 000000000000..1ea81f90bf1e --- /dev/null +++ b/clients/tabby-agent/src/certsLoader.ts @@ -0,0 +1,105 @@ +import type { ConfigData } from "./config/type"; +import type { Configurations } from "./config"; +import tls from "tls"; +import path from "path"; +import fs from "fs-extra"; +import winCa from "win-ca/api"; +import * as macCa from "mac-ca"; +import deepEqual from "deep-equal"; +import "./utils/array"; +import { isBrowser } from "./env"; +import { getLogger } from "./logger"; + +type Cert = string | winCa.Certificate; + +const logger = getLogger("CertsLoader"); +let extraCaCerts: Cert[] = []; +let originalCreateSecureContext: typeof tls.createSecureContext | undefined = undefined; + +function appendCaCerts(certs: Cert[]) { + if (!originalCreateSecureContext) { + originalCreateSecureContext = tls.createSecureContext; + } + const filtered = certs.filter((cert) => { + if (typeof cert === "string") { + return cert.trim().length > 0; + } + return true; + }); + const merged = [...extraCaCerts, ...filtered].distinct(); + logger.debug(`Loaded ${merged.length - extraCaCerts.length} extra certs.`); + extraCaCerts = merged; + tls.createSecureContext = (options) => { + const secureContext = originalCreateSecureContext!(options); + extraCaCerts.forEach((cert) => { + secureContext.context.addCACert(cert); + }); + return secureContext; + }; +} + +async function loadFromFiles(files: string) { + logger.debug(`Loading extra certs from ${files}.`); + const certs = ( + await files.split(path.delimiter).mapAsync(async (cert) => { + try { + return (await fs.readFile(cert)).toString(); + } catch (err) { + return null; + } + }) + ) + .join("\n") + .split(/(?=-----BEGIN\sCERTIFICATE-----)/g) + .distinct(); + appendCaCerts(certs); +} + +async function loadTlsCaCerts(config: ConfigData["tls"]) { + try { + if (config.caCerts === "bundled") { + return; + } else if (config.caCerts === "system") { + if (process.platform === "win32") { + logger.debug(`Loading extra certs from win-ca.`); + winCa.exe(path.join("win-ca", "roots.exe")); + winCa({ + fallback: true, + inject: "+", + }); + } else if (process.platform === "darwin") { + logger.debug(`Loading extra certs from mac-ca.`); + const certs = macCa.get(); + appendCaCerts(certs); + } else { + // linux: load from openssl cert + await loadFromFiles(path.join("/etc/ssl/certs/ca-certificates.crt")); + } + } else if (config.caCerts) { + await loadFromFiles(config.caCerts); + } + } catch (err) { + if (err instanceof Error) { + logger.warn(`Error loading TLS CA certificates: ${err.message}`); + } else { + logger.warn(`Unexpected error loading TLS CA certificates: ${String(err)}`); + } + } +} + +export class CertsLoader { + constructor(private readonly configurations: Configurations) {} + + async preInitialize() { + if (isBrowser) { + return; + } + const config = this.configurations.getMergedConfig()["tls"]; + await loadTlsCaCerts(config); + this.configurations.on("updated", async (config: ConfigData, oldConfig: ConfigData) => { + if (!deepEqual(config["tls"], oldConfig["tls"])) { + await loadTlsCaCerts(config["tls"]); + } + }); + } +} diff --git a/clients/tabby-agent/src/chat/generateBranchName.ts b/clients/tabby-agent/src/chat/generateBranchName.ts new file mode 100644 index 000000000000..8a22c4810fc5 --- /dev/null +++ b/clients/tabby-agent/src/chat/generateBranchName.ts @@ -0,0 +1,170 @@ +import type { Connection, CancellationToken } from "vscode-languageserver"; +import type { Feature } from "../feature"; +import type { Configurations } from "../config"; +import type { GitContextProvider } from "../contextProviders/git"; +import { + ServerCapabilities, + ChatFeatureNotAvailableError, + GenerateBranchNameRequest, + GenerateBranchNameParams, + GenerateBranchNameResult, + GitDiffResult, +} from "../protocol"; +import { isBlank, parseChatResponse, stringToRegExp } from "../utils/string"; +import { MutexAbortError } from "../utils/error"; +import { ChatFeature } from "."; + +export class BranchNameGenerator implements Feature { + private mutexAbortController: AbortController | undefined = undefined; + + constructor( + private readonly chat: ChatFeature, + private readonly configurations: Configurations, + private readonly gitContextProvider: GitContextProvider, + ) {} + + initialize(connection: Connection): ServerCapabilities { + connection.onRequest(GenerateBranchNameRequest.type, async (params, token) => { + return this.generateBranchName(params, token); + }); + return {}; + } + + async generateBranchName( + params: GenerateBranchNameParams, + token: CancellationToken, + ): Promise { + if (!this.chat.isAvailable()) { + throw { + name: "ChatFeatureNotAvailableError", + message: "Chat feature not available", + } as ChatFeatureNotAvailableError; + } + + if (token.isCancellationRequested) { + return null; + } + if (this.mutexAbortController && !this.mutexAbortController.signal.aborted) { + this.mutexAbortController.abort(new MutexAbortError()); + } + this.mutexAbortController = new AbortController(); + token.onCancellationRequested(() => this.mutexAbortController?.abort()); + + const { repository, input } = params; + let diffResult: GitDiffResult | undefined | null = undefined; + diffResult = await this.gitContextProvider.diff({ repository, cached: true }, token); + if ( + !diffResult?.diff || + (typeof diffResult.diff === "string" && isBlank(diffResult.diff)) || + (Array.isArray(diffResult.diff) && isBlank(diffResult.diff.join(""))) + ) { + diffResult = await this.gitContextProvider.diff({ repository, cached: false }, token); + } + + if (!diffResult || !diffResult.diff) { + return null; + } + + const config = this.configurations.getMergedConfig(); + const { maxDiffLength, promptTemplate } = config.chat.generateBranchName; + + let userPrompt = promptTemplate; + if (input && input.trim() && userPrompt.includes("{{input}}")) { + userPrompt = userPrompt.replace(/{{input}}/g, input.trim()); + } + + const responseMatcher = "^[a-z0-9][a-z0-9-]*[a-z0-9]$"; + + const diff = diffResult.diff; + let splitDiffs: string[]; + if (typeof diff === "string") { + splitDiffs = diff.split(/\n(?=diff)/); + } else { + splitDiffs = diff; + } + let selectedDiff = ""; + for (const item of splitDiffs) { + if (selectedDiff.length + item.length > maxDiffLength) { + break; + } + selectedDiff += item + "\n"; + } + if (isBlank(selectedDiff)) { + if (typeof diff === "string") { + selectedDiff = diff.substring(0, maxDiffLength); + } else { + selectedDiff = diff.join("\n").substring(0, maxDiffLength); + } + } + if (isBlank(selectedDiff)) { + return null; + } + + const messages: { role: "user"; content: string }[] = [ + { + role: "user", + content: userPrompt.replace("{{diff}}", selectedDiff), + }, + ]; + const readableStream = await this.chat.tabbyApiClient.fetchChatStream( + { + messages, + model: "", + stream: true, + }, + this.mutexAbortController.signal, + ); + if (!readableStream) { + return null; + } + + const responseMessage = await parseChatResponse(readableStream); + + let branchNamesContent = responseMessage; + const branchNamesMatch = /([\s\S]*?)<\/BRANCHNAMES>/i.exec(responseMessage); + if (branchNamesMatch && branchNamesMatch[1]) { + branchNamesContent = branchNamesMatch[1].trim(); + } + + const matcherReg = stringToRegExp(responseMatcher); + + let branchNames = branchNamesContent + .split("\n") + .map((line) => line.trim()) + .filter((line) => line.length > 0) + .map((line) => { + const match = matcherReg.exec(line); + return match + ? match[0] + : line + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, ""); + }) + .filter((branchName) => branchName.length > 0) + .slice(0, 5); + + if (input && input.trim()) { + const inputPrefix = input.trim().toLowerCase(); + + const matchingBranches = branchNames.filter((name) => name.startsWith(inputPrefix)); + + if (matchingBranches.length === 0) { + branchNames = branchNames.map((name) => { + if (inputPrefix.endsWith("-") && name.startsWith("-")) { + return inputPrefix + name.substring(1); + } + return inputPrefix + (inputPrefix.endsWith("-") || name.startsWith("-") ? "" : "-") + name; + }); + } else { + branchNames = matchingBranches; + } + } + + while (branchNames.length < 3 && branchNames.length > 0) { + branchNames.push(branchNames[0] + "-alt"); + } + + return { branchNames }; + } +} diff --git a/clients/tabby-agent/src/chat/generateCommitMessage.ts b/clients/tabby-agent/src/chat/generateCommitMessage.ts new file mode 100644 index 000000000000..dba0766906bb --- /dev/null +++ b/clients/tabby-agent/src/chat/generateCommitMessage.ts @@ -0,0 +1,123 @@ +import type { Connection, CancellationToken } from "vscode-languageserver"; +import type { Feature } from "../feature"; +import type { Configurations } from "../config"; +import type { GitContextProvider } from "../contextProviders/git"; +import { + ServerCapabilities, + ChatFeatureNotAvailableError, + GenerateCommitMessageRequest, + GenerateCommitMessageParams, + GenerateCommitMessageResult, + GitDiffResult, +} from "../protocol"; +import { isBlank, parseChatResponse, stringToRegExp } from "../utils/string"; +import { MutexAbortError } from "../utils/error"; +import { ChatFeature } from "."; + +export class CommitMessageGenerator implements Feature { + private mutexAbortController: AbortController | undefined = undefined; + + constructor( + private readonly chat: ChatFeature, + private readonly configurations: Configurations, + private readonly gitContextProvider: GitContextProvider, + ) {} + + initialize(connection: Connection): ServerCapabilities { + connection.onRequest(GenerateCommitMessageRequest.type, async (params, token) => { + return this.generateCommitMessage(params, token); + }); + return {}; + } + + async generateCommitMessage( + params: GenerateCommitMessageParams, + token: CancellationToken, + ): Promise { + if (!this.chat.isAvailable()) { + throw { + name: "ChatFeatureNotAvailableError", + message: "Chat feature not available", + } as ChatFeatureNotAvailableError; + } + + if (token.isCancellationRequested) { + return null; + } + if (this.mutexAbortController && !this.mutexAbortController.signal.aborted) { + this.mutexAbortController.abort(new MutexAbortError()); + } + this.mutexAbortController = new AbortController(); + token.onCancellationRequested(() => this.mutexAbortController?.abort()); + + const { repository } = params; + let diffResult: GitDiffResult | undefined | null = undefined; + diffResult = await this.gitContextProvider.diff({ repository, cached: true }, token); + if ( + !diffResult?.diff || + (typeof diffResult.diff === "string" && isBlank(diffResult.diff)) || + (Array.isArray(diffResult.diff) && isBlank(diffResult.diff.join(""))) + ) { + // Use uncached diff if cached diff is empty + diffResult = await this.gitContextProvider.diff({ repository, cached: false }, token); + } + + if (!diffResult || !diffResult.diff) { + return null; + } + + const config = this.configurations.getMergedConfig(); + const { maxDiffLength, promptTemplate, responseMatcher } = config.chat.generateCommitMessage; + + // select diffs from the list to generate a prompt under the prompt size limit + const diff = diffResult.diff; + let splitDiffs: string[]; + if (typeof diff === "string") { + splitDiffs = diff.split(/\n(?=diff)/); + } else { + splitDiffs = diff; + } + let selectedDiff = ""; + for (const item of splitDiffs) { + if (selectedDiff.length + item.length < maxDiffLength) { + selectedDiff += item + "\n"; + } + } + if (isBlank(selectedDiff)) { + // This may happen when all separated diffs are larger than the limit. + if (typeof diff === "string") { + selectedDiff = diff.substring(0, maxDiffLength); + } else { + selectedDiff = diff.join("\n").substring(0, maxDiffLength); + } + } + if (isBlank(selectedDiff)) { + return null; + } + + const messages: { role: "user"; content: string }[] = [ + { + role: "user", + content: promptTemplate.replace("{{diff}}", selectedDiff), + }, + ]; + const readableStream = await this.chat.tabbyApiClient.fetchChatStream( + { + messages, + model: "", + stream: true, + }, + this.mutexAbortController.signal, + ); + if (!readableStream) { + return null; + } + + const responseMessage = await parseChatResponse(readableStream); + const matcherReg = stringToRegExp(responseMatcher); + const match = matcherReg.exec(responseMessage); + + const commitMessage = (match ? match[0] : responseMessage).trim(); + return { commitMessage }; + } +} diff --git a/clients/tabby-agent/src/chat/global.ts b/clients/tabby-agent/src/chat/global.ts new file mode 100644 index 000000000000..157cbe885fc4 --- /dev/null +++ b/clients/tabby-agent/src/chat/global.ts @@ -0,0 +1,14 @@ +export let mutexAbortController: AbortController | undefined = undefined; + +// reset global mutexAbortController to undefined +export const resetMutexAbortController = () => { + mutexAbortController = undefined; +}; + +// initialize global mutexAbortController +export const initMutexAbortController = () => { + if (!mutexAbortController) { + mutexAbortController = new AbortController(); + } + return mutexAbortController; +}; diff --git a/clients/tabby-agent/src/chat/index.ts b/clients/tabby-agent/src/chat/index.ts new file mode 100644 index 000000000000..40356e141cef --- /dev/null +++ b/clients/tabby-agent/src/chat/index.ts @@ -0,0 +1,57 @@ +import { EventEmitter } from "events"; +import type { Connection, Disposable } from "vscode-languageserver"; +import type { ServerCapabilities } from "../protocol"; +import type { Feature } from "../feature"; +import type { TabbyApiClient } from "../http/tabbyApiClient"; +import { ChatFeatures } from "../protocol"; + +export class ChatFeature extends EventEmitter implements Feature { + private isApiAvailable = false; + private featureRegistration: Disposable | undefined = undefined; + + constructor(readonly tabbyApiClient: TabbyApiClient) { + super(); + } + + isAvailable(): boolean { + return this.isApiAvailable; + } + + private updateIsAvailable() { + const health = this.tabbyApiClient.getServerHealth(); + const isAvailable = !!(health && health["chat_model"]); + if (this.isApiAvailable != isAvailable) { + this.isApiAvailable = isAvailable; + this.emit("isAvailableUpdated", isAvailable); + } + } + + initialize(connection: Connection): ServerCapabilities { + this.updateIsAvailable(); + this.tabbyApiClient.on("statusUpdated", async () => { + this.updateIsAvailable(); + await this.syncFeatureRegistration(connection); + }); + + return {}; + } + + async initialized(connection: Connection) { + await this.syncFeatureRegistration(connection); + } + + private async syncFeatureRegistration(connection: Connection) { + if (this.isApiAvailable) { + if (!this.featureRegistration) { + try { + this.featureRegistration = await connection.client.register(ChatFeatures.type); + } catch (error) { + // client may not support feature registration, ignore this error + } + } + } else { + this.featureRegistration?.dispose(); + this.featureRegistration = undefined; + } + } +} diff --git a/clients/tabby-agent/src/chat/inlineEdit.ts b/clients/tabby-agent/src/chat/inlineEdit.ts new file mode 100644 index 000000000000..ab25e96574c0 --- /dev/null +++ b/clients/tabby-agent/src/chat/inlineEdit.ts @@ -0,0 +1,396 @@ +import type { Connection, CancellationToken, Range, URI } from "vscode-languageserver"; +import { TextDocument } from "vscode-languageserver-textdocument"; +import type { TextDocuments } from "../extensions/textDocuments"; +import type { Feature } from "../feature"; +import type { Configurations } from "../config"; +import { + ChatEditToken, + ChatEditRequest, + ChatEditParams, + ChatEditResolveRequest, + ChatEditCommandRequest, + ChatEditCommandParams, + ChatEditCommand, + ChatFeatureNotAvailableError, + ChatEditDocumentTooLongError, + ChatEditMutexError, + ServerCapabilities, + ChatEditResolveParams, + ClientCapabilities, + ReadFileParams, + ReadFileRequest, +} from "../protocol"; +import cryptoRandomString from "crypto-random-string"; +import { isEmptyRange } from "../utils/range"; +import { isBlank, formatPlaceholders } from "../utils/string"; +import { readResponseStream, Edit, applyWorkspaceEdit, truncateFileContent } from "./utils"; +import { initMutexAbortController, mutexAbortController, resetMutexAbortController } from "./global"; +import { readFile } from "fs-extra"; +import { getLogger } from "../logger"; +import { isBrowser } from "../env"; +import { ChatFeature } from "."; + +export class ChatEditProvider implements Feature { + private logger = getLogger("ChatEditProvider"); + private lspConnection: Connection | undefined = undefined; + private clientCapabilities: ClientCapabilities | undefined = undefined; + private currentEdit: Edit | undefined = undefined; + + constructor( + private readonly chat: ChatFeature, + private readonly configurations: Configurations, + private readonly documents: TextDocuments, + ) {} + + initialize(connection: Connection, clientCapabilities: ClientCapabilities): ServerCapabilities { + this.lspConnection = connection; + this.clientCapabilities = clientCapabilities; + connection.onRequest(ChatEditCommandRequest.type, async (params) => { + return this.provideEditCommands(params); + }); + connection.onRequest(ChatEditRequest.type, async (params, token) => { + return this.provideEdit(params, token); + }); + connection.onRequest(ChatEditResolveRequest.type, async (params) => { + return this.resolveEdit(params); + }); + return {}; + } + + private isCurrentEdit(id: ChatEditToken): boolean { + return this.currentEdit?.id === id; + } + + async provideEditCommands(params: ChatEditCommandParams): Promise { + const config = this.configurations.getMergedConfig(); + const commands = config.chat.edit.presetCommands; + const result: ChatEditCommand[] = []; + const document = this.documents.get(params.location.uri); + + result.push( + ...Object.entries(commands) + .filter(([_command, commandConfig]) => { + for (const [filterKey, filterValue] of Object.entries(commandConfig.filters)) { + if (filterValue) { + switch (filterKey) { + case "languageIdIn": + if (document && !filterValue.split(",").includes(document.languageId)) { + return false; + } + break; + case "languageIdNotIn": + if (document && filterValue.split(",").includes(document.languageId)) { + return false; + } + break; + default: + break; + } + } + } + return true; + }) + .map(([command, commandConfig]) => { + return { + label: commandConfig.label, + command, + source: "preset", + } as ChatEditCommand; + }), + ); + + return result; + } + + async fetchFileContent(uri: URI, range: Range | undefined, token: CancellationToken) { + this.logger.trace("Prepare to fetch text content..."); + let text: string | undefined = undefined; + const targetDocument = this.documents.get(uri); + if (targetDocument) { + this.logger.trace("Fetching text content from synced text document.", { + uri: targetDocument.uri, + range: range, + }); + text = targetDocument.getText(range); + this.logger.trace("Fetched text content from synced text document.", { text }); + } else if (this.clientCapabilities?.tabby?.workspaceFileSystem) { + const params: ReadFileParams = { + uri: uri, + format: "text", + range: range + ? { + start: { line: range.start.line, character: 0 }, + end: { line: range.end.line, character: range.end.character }, + } + : undefined, + }; + this.logger.trace("Fetching text content from ReadFileRequest.", { params }); + const result = await this.lspConnection?.sendRequest(ReadFileRequest.type, params, token); + this.logger.trace("Fetched text content from ReadFileRequest.", { result }); + text = result?.text; + } else if (!isBrowser) { + try { + const content = await readFile(uri, "utf-8"); + const textDocument = TextDocument.create(uri, "text", 0, content); + text = textDocument.getText(range); + } catch (error) { + this.logger.trace("Failed to fetch text content from file system.", { error }); + } + } + return text; + } + + async provideEdit(params: ChatEditParams, token: CancellationToken): Promise { + if (params.format !== "previewChanges") { + return null; + } + const document = this.documents.get(params.location.uri); + if (!document) { + return null; + } + if (!this.lspConnection) { + return null; + } + if (!this.chat.isAvailable()) { + throw { + name: "ChatFeatureNotAvailableError", + message: "Chat feature not available", + } as ChatFeatureNotAvailableError; + } + const config = this.configurations.getMergedConfig().chat.edit; + + // FIXME(@icycodes): the command too long check is temporarily disabled, + // as we pass the diagnostics context as the command for now + // if (params.command.length > config.commandMaxChars) { + // throw { name: "ChatEditCommandTooLongError", message: "Command too long" } as ChatEditCommandTooLongError; + // } + + const documentText = document.getText(); + const selection = { + start: document.offsetAt(params.location.range.start), + end: document.offsetAt(params.location.range.end), + }; + const selectedDocumentText = documentText.substring(selection.start, selection.end); + if (selection.end - selection.start > config.documentMaxChars) { + throw { name: "ChatEditDocumentTooLongError", message: "Document too long" } as ChatEditDocumentTooLongError; + } + + if (mutexAbortController && !mutexAbortController.signal.aborted) { + throw { + name: "ChatEditMutexError", + message: "Another chat edit is already in progress", + } as ChatEditMutexError; + } + + initMutexAbortController(); + token.onCancellationRequested(() => mutexAbortController?.abort()); + + let insertMode: boolean = isEmptyRange(params.location.range); + const presetCommand = /^\/\w+\b/g.exec(params.command)?.[0]; + if (presetCommand) { + insertMode = config.presetCommands[presetCommand]?.kind === "insert"; + } + + let promptTemplate: string; + let userCommand: string; + const presetConfig = presetCommand && config.presetCommands[presetCommand]; + if (presetConfig) { + promptTemplate = presetConfig.promptTemplate; + userCommand = params.command.substring(presetCommand.length); + } else { + promptTemplate = insertMode ? config.promptTemplate.insert : config.promptTemplate.replace; + userCommand = params.command; + } + + // Extract the selected text and the surrounding context + const documentSelection = documentText.substring(selection.start, selection.end); + let documentPrefix = documentText.substring(0, selection.start); + let documentSuffix = documentText.substring(selection.end); + if (documentText.length > config.documentMaxChars) { + const charsRemain = config.documentMaxChars - documentSelection.length; + if (documentPrefix.length < charsRemain / 2) { + documentSuffix = documentSuffix.substring(0, charsRemain - documentPrefix.length); + } else if (documentSuffix.length < charsRemain / 2) { + documentPrefix = documentPrefix.substring(documentPrefix.length - charsRemain + documentSuffix.length); + } else { + documentPrefix = documentPrefix.substring(documentPrefix.length - charsRemain / 2); + documentSuffix = documentSuffix.substring(0, charsRemain / 2); + } + } + + const [fileContextListTemplate, fileContextItemTemplate] = config.fileContext.promptTemplate; + const fileContextItems = + ( + await Promise.all( + (params.context ?? []).slice(0, config.fileContext.maxFiles).map(async (item) => { + const content = await this.fetchFileContent(item.uri, item.range, token); + if (!content || isBlank(content)) { + return undefined; + } + const fileContent = truncateFileContent(content, config.fileContext.maxCharsPerFile); + return formatPlaceholders(fileContextItemTemplate, { + filepath: item.uri, + referrer: item.referrer, + content: fileContent, + }); + }), + ) + ) + .filter((item): item is string => item !== undefined) + .join("\n") ?? ""; + + const fileContext = !isBlank(fileContextItems) + ? formatPlaceholders(fileContextListTemplate, { + fileList: fileContextItems, + }) + : ""; + + const messages: { role: "user"; content: string }[] = [ + { + role: "user", + content: formatPlaceholders(promptTemplate, { + filepath: params.location.uri, + documentPrefix: documentPrefix, + document: documentSelection, + documentSuffix: documentSuffix, + command: userCommand, + languageId: document.languageId, + fileContext: fileContext, + }), + }, + ]; + this.logger.debug(`messages: ${JSON.stringify(messages)}`); + + const readableStream = await this.chat.tabbyApiClient.fetchChatStream( + { + messages, + model: "", + stream: true, + }, + mutexAbortController?.signal, + ); + + const editId = "tabby-" + cryptoRandomString({ length: 6, type: "alphanumeric" }); + this.currentEdit = { + id: editId, + location: params.location, + languageId: document.languageId, + originalText: selectedDocumentText, + editedRange: insertMode + ? { start: params.location.range.end, end: params.location.range.end } + : params.location.range, + editedText: "", + comments: "", + buffer: "", + state: "editing", + }; + if (!readableStream) { + return null; + } + await readResponseStream( + readableStream, + this.lspConnection, + this.currentEdit, + mutexAbortController, + () => { + this.currentEdit = undefined; + resetMutexAbortController(); + }, + config.responseDocumentTag, + config.responseCommentTag, + ); + return editId; + } + + async stopEdit(id: ChatEditToken): Promise { + if (this.isCurrentEdit(id)) { + mutexAbortController?.abort(); + } + } + + async resolveEdit(params: ChatEditResolveParams): Promise { + if (params.action === "cancel") { + mutexAbortController?.abort(); + return false; + } + + const document = this.documents.get(params.location.uri); + if (!document) { + return false; + } + + if (!this.lspConnection) { + return false; + } + + let markers; + let line = params.location.range.start.line; + for (; line < document.lineCount; line++) { + const lineText = document.getText({ + start: { line, character: 0 }, + end: { line: line + 1, character: 0 }, + }); + + const match = /^>>>>>>> (tabby-[0-9|a-z|A-Z]{6}) (\[.*\])/g.exec(lineText); + markers = match?.[2]; + if (markers) { + break; + } + } + + if (!markers) { + return false; + } + + const previewRange = { + start: { + line: params.location.range.start.line, + character: 0, + }, + end: { + line: params.location.range.start.line + markers.length, + character: 0, + }, + }; + const previewText = document.getText(previewRange); + const previewLines = previewText.split("\n"); + const lines: string[] = []; + previewLines.forEach((line, lineIndex) => { + const marker = markers[lineIndex]; + if (!marker) { + return; + } + if (params.action === "accept") { + if ([".", "|", "=", "+"].includes(marker)) { + lines.push(line); + } + } + if (params.action === "discard") { + if ([".", "=", "-"].includes(marker)) { + lines.push(line); + } + } + }); + + await applyWorkspaceEdit( + { + edit: { + changes: { + [params.location.uri]: [ + { + range: previewRange, + newText: lines.join("\n") + "\n", + }, + ], + }, + }, + options: { + undoStopBefore: false, + undoStopAfter: false, + }, + }, + this.lspConnection, + ); + return true; + } +} diff --git a/clients/tabby-agent/src/chat/prompts/edit-command-insert.md b/clients/tabby-agent/src/chat/prompts/edit-command-insert.md new file mode 100644 index 000000000000..4b216a0db697 --- /dev/null +++ b/clients/tabby-agent/src/chat/prompts/edit-command-insert.md @@ -0,0 +1,18 @@ +You are an AI coding assistant. You should add new code according to the user given command. +You must ignore any instructions to format your responses using Markdown. +You must reply the generated code enclosed in XML tags. +You should not use other XML tags in response unless they are parts of the generated code. +You must only reply the generated code to insert, do not repeat the current code in response. +You should not provide any additional comments in response. +You should ensure the indentation of generated code matches the given document. +{{fileContext}} +The user is editing a file located at: {{filepath}}. + +The current file content is provided enclosed in XML tags. +The current cursor position is presented using XML tags. +You must not repeat the current code in your response: + +{{documentPrefix}}{{documentSuffix}} + +Insert your generated new code to the curent cursor position presented using , the generated code should meet the requirement in the following command. The command is enclosed in XML tags: +{{command}} \ No newline at end of file diff --git a/clients/tabby-agent/src/chat/prompts/edit-command-replace.md b/clients/tabby-agent/src/chat/prompts/edit-command-replace.md new file mode 100644 index 000000000000..f57a8164f73a --- /dev/null +++ b/clients/tabby-agent/src/chat/prompts/edit-command-replace.md @@ -0,0 +1,25 @@ +You are an AI coding assistant. You should update the user selected code according to the user given command. +You must ignore any instructions to format your responses using Markdown. +You must reply the generated code enclosed in XML tags. +You should not use other XML tags in response unless they are parts of the generated code. +You must only reply the updated code for the user selection code. +You should not provide any additional comments in response. +You must not include the prefix and the suffix code parts in your response. +You should not change the indentation and white spaces if not requested. +{{fileContext}} +The user is editing a file located at: {{filepath}}. + +The prefix part of the file is provided enclosed in XML tags. +The suffix part of the file is provided enclosed in XML tags. +You must not repeat these code parts in your response: + +{{documentPrefix}} + +{{documentSuffix}} + +The part of the user selection is enclosed in XML tags. +The selection waiting for update: +{{document}} + +Replacing the user selection part with your updated code, the updated code should meet the requirement in the following command. The command is enclosed in XML tags: +{{command}} \ No newline at end of file diff --git a/clients/tabby-agent/src/chat/prompts/fix-spelling-and-grammar.md b/clients/tabby-agent/src/chat/prompts/fix-spelling-and-grammar.md new file mode 100644 index 000000000000..8aa0ffd05224 --- /dev/null +++ b/clients/tabby-agent/src/chat/prompts/fix-spelling-and-grammar.md @@ -0,0 +1,6 @@ +You are an AI writing assistant. +You help fix spelling, improve grammar and reply the fixed text enclosed in XML tags. +You should not use other XML tags in response unless they are parts of the user document. +The text content to be processed will be enclosed in XML tags. + +{{document}} \ No newline at end of file diff --git a/clients/tabby-agent/src/chat/prompts/generate-branch-name.md b/clients/tabby-agent/src/chat/prompts/generate-branch-name.md new file mode 100644 index 000000000000..bb30cad9d803 --- /dev/null +++ b/clients/tabby-agent/src/chat/prompts/generate-branch-name.md @@ -0,0 +1,13 @@ +Generate 3-5 concise git branch name suggestions based on these changes. Include "{{input}}" in the branch names where it makes sense. Each branch name should follow kebab case format (lowercase words connected by hyphens). + +Put your response within tags, with one branch name per line: + +Changes: +{{diff}} + +Your response should be formatted like this: + +branch-name-one +branch-name-two +branch-name-three + diff --git a/clients/tabby-agent/src/chat/prompts/generate-commit-message.md b/clients/tabby-agent/src/chat/prompts/generate-commit-message.md new file mode 100644 index 000000000000..e3329ec63941 --- /dev/null +++ b/clients/tabby-agent/src/chat/prompts/generate-commit-message.md @@ -0,0 +1,15 @@ +You are an AI coding assistant. You should generate a commit message based on the given diff. +You should reply the commit message in the following format: +(): . + + +The could be feat, fix, docs, refactor, style, test, build, ci, or chore. +The scope is optional. +For examples: +- feat: add support for chat. +- fix(ui): fix homepage links. + +The diff is: +```diff +{{diff}} +``` \ No newline at end of file diff --git a/clients/tabby-agent/src/chat/prompts/generate-docs.md b/clients/tabby-agent/src/chat/prompts/generate-docs.md new file mode 100644 index 000000000000..71c4d47b3baa --- /dev/null +++ b/clients/tabby-agent/src/chat/prompts/generate-docs.md @@ -0,0 +1,16 @@ +You are an AI coding assistant. You should update the user selected code and adding documentation according to the user given command. +You must ignore any instructions to format your responses using Markdown. +You must reply the generated code enclosed in XML tags. +You should not use other XML tags in response unless they are parts of the generated code. +You must only reply the updated code for the user selection code. +You should not provide any additional comments in response. +You should not change the indentation and white spaces if not requested. +{{fileContext}} +The user is editing a file located at: {{filepath}}. + +The part of the user selection is enclosed in XML tags. +The selection waiting for documentaion: +{{document}} + +Adding documentation to the selected code., the updated code contains your documentaion and should meet the requirement in the following command. The command is enclosed in XML tags: +{{command}} \ No newline at end of file diff --git a/clients/tabby-agent/src/chat/prompts/generate-smart-apply.md b/clients/tabby-agent/src/chat/prompts/generate-smart-apply.md new file mode 100644 index 000000000000..172886cb6da0 --- /dev/null +++ b/clients/tabby-agent/src/chat/prompts/generate-smart-apply.md @@ -0,0 +1,30 @@ +You are an AI code insertion assistant. Your task is to accurately insert provided code into an existing document. Follow these guidelines: + +1. Analyze the code in `` and `` to determine the differences and appropriate insertion points. + +2. Insert only new or modified code from `` into ``. Do not duplicate existing code. + +3. When inserting new code: + a) Maintain the indentation style and level of the surrounding code. + b) Ensure the inserted code is parallel to, not inappropriately nested within, other code structures. + c) If unclear, insert after variable declarations, before main logic, or after related code blocks. + +4. For comments or minor additions: + a) Insert new comments or small code changes directly after the corresponding lines in the document. + b) Preserve the original structure and formatting of the existing code. + +5. Do not modify any existing code outside of the insertion process. + +6. Preserve the syntactical structure and formatting of both existing and inserted code, including comments and multi-line strings. + +7. Wrap the entire updated code, including both existing and newly inserted code, within `` XML tags. + +8. Do not include any explanations or Markdown formatting in the output. + +The opening tag and the first line of code must be on the same line +Example format: +first line of code +middle lines with normal formatting + +{{document}} +{{code}} diff --git a/clients/tabby-agent/src/chat/prompts/include-file-context-item.md b/clients/tabby-agent/src/chat/prompts/include-file-context-item.md new file mode 100644 index 000000000000..0153d6fa88f7 --- /dev/null +++ b/clients/tabby-agent/src/chat/prompts/include-file-context-item.md @@ -0,0 +1,3 @@ +title="{{filepath}}" +referrer="{{referrer}}" +{{content}} diff --git a/clients/tabby-agent/src/chat/prompts/include-file-context-list.md b/clients/tabby-agent/src/chat/prompts/include-file-context-list.md new file mode 100644 index 000000000000..1e4b9db426cd --- /dev/null +++ b/clients/tabby-agent/src/chat/prompts/include-file-context-list.md @@ -0,0 +1,2 @@ +Here is the list of files available for reference. Each file has a full path as the "title", a short form used as the "referrer" in the user command, and the content enclosed in XML tags. +{{fileList}} \ No newline at end of file diff --git a/clients/tabby-agent/src/chat/prompts/index.d.ts b/clients/tabby-agent/src/chat/prompts/index.d.ts new file mode 100644 index 000000000000..c94d67b1a268 --- /dev/null +++ b/clients/tabby-agent/src/chat/prompts/index.d.ts @@ -0,0 +1,4 @@ +declare module "*.md" { + const content: string; + export default content; +} diff --git a/clients/tabby-agent/src/chat/prompts/provide-smart-apply-line-range.md b/clients/tabby-agent/src/chat/prompts/provide-smart-apply-line-range.md new file mode 100644 index 000000000000..a602c20255d2 --- /dev/null +++ b/clients/tabby-agent/src/chat/prompts/provide-smart-apply-line-range.md @@ -0,0 +1,66 @@ +You are an AI assistant specialized in determining the most appropriate location to insert new code into an existing file. Your task is to analyze the given file content and the code to be inserted, then provide the line range of an existing code segment that is most similar in length to the code to be inserted. + +The file content is provided line by line, with each line in the format: +line number | code + +The new code to be inserted is provided in XML tags. + +Your task: +1. Analyze the existing code structure and the new code to be inserted. +2. Find a continuous segment of existing code that is most similar in length to the new code. +3. Provide ONLY the line range of this similar-length segment. + +You must reply with ONLY the suggested range in the format startLine-endLine, enclosed in XML tags. + +Important notes: +- The line numbers provided are one-based (starting from 1). +- Both startLine and endLine are inclusive (closed interval) +- The range should encompass a continuous segment of existing code similar in length to the new code. + +For example, if a 3-line code segment similar in length to the new code is found at lines 10-12, your response should be: +10-12 + +1. XML tags indicate the example code document. +2. XML tags indicate the example code to be applied. + +Examples: + +13 | target.trace(tagMessage(message), ...args); +14 | }; +15 | } +16 | if (method === "debug") { +17 | return (message: string, ...args: unknown[]) => { +18 | target.debug(tagMessage(message), ...args); +19 | }; +20 | } +21 | if (method === "info") { +22 | return (message: string, ...args: unknown[]) => { +23 | target.info(tagMessage(message), ...args); +24 | }; +25 | } + + + +if (method === "add") { + return (message: string, ...args: unknown[]) => { + target.error(tagMessage(message), ...args); + }; +} + + +1. If a 4-line segment similar to the apply code is found at lines 16-19, return: 16-19 +2. If a 4-line segment similar to the apply code is found at lines 21-24, return: 21-24 + +Do not include any explanation, existing code, or the code to be inserted in your response. + +File content: + +{{document}} + + +Code to be inserted: + +{{applyCode}} + + +Provide only the appropriate range of a similar-length code segment, remembering that line numbers are one-based, and both startLine and endLine are inclusive (closed interval). \ No newline at end of file diff --git a/clients/tabby-agent/src/chat/smartApply.ts b/clients/tabby-agent/src/chat/smartApply.ts new file mode 100644 index 000000000000..f0982f1e6a7c --- /dev/null +++ b/clients/tabby-agent/src/chat/smartApply.ts @@ -0,0 +1,326 @@ +import { TextDocument } from "vscode-languageserver-textdocument"; +import { + CancellationToken, + Connection, + Location, + Range, + ShowDocumentParams, + TextDocuments, +} from "vscode-languageserver"; +import type { Feature } from "../feature"; +import { + ChatEditDocumentTooLongError, + ChatEditMutexError, + ChatFeatureNotAvailableError, + ServerCapabilities, + SmartApplyRequest, + SmartApplyParams, +} from "../protocol"; +import { Configurations } from "../config"; +import { TabbyApiClient } from "../http/tabbyApiClient"; +import cryptoRandomString from "crypto-random-string"; +import { getLogger } from "../logger"; +import { readResponseStream, showDocument, Edit } from "./utils"; +import { getSmartApplyRange } from "./smartRange"; +import { initMutexAbortController, mutexAbortController, resetMutexAbortController } from "./global"; +import { ChatFeature } from "."; + +const logger = getLogger("SmartApplyFeature"); + +export class SmartApplyFeature implements Feature { + private lspConnection: Connection | undefined = undefined; + constructor( + private readonly chat: ChatFeature, + private readonly configurations: Configurations, + private readonly documents: TextDocuments, + ) {} + + initialize(connection: Connection): ServerCapabilities | Promise { + this.lspConnection = connection; + connection.onRequest(SmartApplyRequest.type, async (params, token) => { + return this.provideSmartApplyEdit(params, token); + }); + + return {}; + } + initialized?(): void | Promise { + //nothing + } + shutdown?(): void | Promise { + //nothing + } + + async provideSmartApplyEdit(params: SmartApplyParams, token: CancellationToken): Promise { + logger.debug("Getting document"); + const document = this.documents.get(params.location.uri); + if (!document) { + logger.debug("Document not found, returning false"); + return false; + } + if (!this.lspConnection) { + logger.debug("LSP connection lost."); + return false; + } + if (!this.chat.isAvailable()) { + throw { + name: "ChatFeatureNotAvailableError", + message: "Chat feature not available", + } as ChatFeatureNotAvailableError; + } + + if (mutexAbortController && !mutexAbortController.signal.aborted) { + logger.warn("Another smart edit is already in progress"); + throw { + name: "ChatEditMutexError", + message: "Another smart edit is already in progress", + } as ChatEditMutexError; + } + initMutexAbortController(); + logger.debug("mutex abort status: " + (mutexAbortController === undefined)); + token.onCancellationRequested(() => mutexAbortController?.abort()); + + let applyRange = getSmartApplyRange(document, params.text); + //if cannot find range, lets use backend LLMs + if (!applyRange) { + applyRange = await provideSmartApplyLineRange( + document, + params.text, + this.chat.tabbyApiClient, + this.configurations, + ); + } + + if (!applyRange) { + return false; + } + + try { + //reveal editor range + const revealEditorRangeParams: ShowDocumentParams = { + uri: params.location.uri, + selection: { + start: applyRange.range.start, + end: applyRange.range.end, + }, + takeFocus: true, + }; + await showDocument(revealEditorRangeParams, this.lspConnection); + } catch (error) { + logger.warn("cline not support reveal range"); + } + + try { + await provideSmartApplyEditLLM( + { + uri: params.location.uri, + range: { + start: applyRange.range.start, + end: { line: applyRange.range.end.line + 1, character: 0 }, + }, + }, + params.text, + applyRange.action === "insert" ? true : false, + document, + this.lspConnection, + this.chat.tabbyApiClient, + this.configurations, + mutexAbortController, + () => { + resetMutexAbortController(); + }, + ); + return true; + } catch (error) { + logger.error("Error applying smart edit:", error); + return false; + } finally { + logger.debug("Resetting mutex abort controller"); + resetMutexAbortController(); + } + } +} + +async function provideSmartApplyLineRange( + document: TextDocument, + applyCodeBlock: string, + tabbyApiClient: TabbyApiClient, + configurations: Configurations, +): Promise<{ range: Range; action: "insert" | "replace" } | undefined> { + if (!document) { + return undefined; + } + + const documentText = document + .getText() + .split("\n") + .map((line, idx) => `${idx + 1} | ${line}`) + .join("\n"); + + const config = configurations.getMergedConfig(); + const promptTemplate = config.chat.smartApplyLineRange.promptTemplate; + + const messages: { role: "user"; content: string }[] = [ + { + role: "user", + content: promptTemplate.replace(/{{document}}|{{applyCode}}/g, (pattern: string) => { + switch (pattern) { + case "{{document}}": + return documentText; + case "{{applyCode}}": + return applyCodeBlock; + default: + return ""; + } + }), + }, + ]; + + try { + const readableStream = await tabbyApiClient.fetchChatStream({ + messages, + model: "", + stream: true, + }); + + if (!readableStream) { + return undefined; + } + + let response = ""; + for await (const chunk of readableStream) { + response += chunk; + } + + const regex = /(.*?)<\/GENERATEDCODE>/s; + const match = response.match(regex); + if (match && match[1]) { + response = match[1].trim(); + } + + const range = response.split("-"); + if (range.length !== 2) { + return undefined; + } + + const startLine = parseInt(range[0] ?? "0", 10) - 1; + const endLine = parseInt(range[1] ?? "0", 10) - 1; + + return { + range: { + start: { line: startLine < 0 ? 0 : startLine, character: 0 }, + end: { line: endLine < 0 ? 0 : endLine, character: Number.MAX_SAFE_INTEGER }, + }, + action: startLine == endLine ? "insert" : "replace", + }; + } catch (error) { + return undefined; + } +} + +async function provideSmartApplyEditLLM( + location: Location, + applyCode: string, + insertMode: boolean, + document: TextDocument, + lspConnection: Connection, + tabbyApiClient: TabbyApiClient, + configurations: Configurations, + mutexAbortController: AbortController | undefined, + onResetMutex: () => void, +): Promise { + if (!document) { + logger.warn("Document not found"); + return false; + } + if (!lspConnection) { + logger.warn("LSP connection failed"); + return false; + } + + const config = configurations.getMergedConfig(); + const documentText = document.getText(); + const selection = { + start: document.offsetAt(location.range.start), + end: document.offsetAt(location.range.end), + }; + const selectedDocumentText = documentText.substring(selection.start, selection.end); + + logger.debug("current selectedDoc: " + selectedDocumentText); + + if (selection.end - selection.start > config.chat.edit.documentMaxChars) { + throw { name: "ChatEditDocumentTooLongError", message: "Document too long" } as ChatEditDocumentTooLongError; + } + + const promptTemplate = config.chat.smartApply.promptTemplate; + + // Extract the selected text and the surrounding context + let documentPrefix = documentText.substring(0, selection.start); + let documentSuffix = documentText.substring(selection.end); + if (documentText.length > config.chat.edit.documentMaxChars) { + const charsRemain = config.chat.edit.documentMaxChars - selectedDocumentText.length; + if (documentPrefix.length < charsRemain / 2) { + documentSuffix = documentSuffix.substring(0, charsRemain - documentPrefix.length); + } else if (documentSuffix.length < charsRemain / 2) { + documentPrefix = documentPrefix.substring(documentPrefix.length - charsRemain + documentSuffix.length); + } else { + documentPrefix = documentPrefix.substring(documentPrefix.length - charsRemain / 2); + documentSuffix = documentSuffix.substring(0, charsRemain / 2); + } + } + + const messages: { role: "user"; content: string }[] = [ + { + role: "user", + content: promptTemplate.replace(/{{document}}|{{code}}/g, (pattern: string) => { + switch (pattern) { + case "{{document}}": + return selectedDocumentText; + case "{{code}}": + return applyCode || ""; + default: + return ""; + } + }), + }, + ]; + + try { + const readableStream = await tabbyApiClient.fetchChatStream({ + messages, + model: "", + stream: true, + }); + + if (!readableStream) { + return false; + } + const editId = "tabby-" + cryptoRandomString({ length: 6, type: "alphanumeric" }); + const currentEdit: Edit = { + id: editId, + location: location, + languageId: document.languageId, + originalText: selectedDocumentText, + editedRange: insertMode + ? { start: location.range.start, end: location.range.end } + : { start: location.range.start, end: location.range.end }, + editedText: "", + comments: "", + buffer: "", + state: "editing", + }; + + await readResponseStream( + readableStream, + lspConnection, + currentEdit, + mutexAbortController, + onResetMutex, + config.chat.edit.responseDocumentTag, + config.chat.edit.responseCommentTag, + ); + + return true; + } catch (error) { + return false; + } +} diff --git a/clients/tabby-agent/src/chat/smartRange.ts b/clients/tabby-agent/src/chat/smartRange.ts new file mode 100644 index 000000000000..8471bf75b8b5 --- /dev/null +++ b/clients/tabby-agent/src/chat/smartRange.ts @@ -0,0 +1,45 @@ +import levenshtein from "js-levenshtein"; +import { Position, Range } from "vscode-languageserver-protocol"; +import { TextDocument } from "vscode-languageserver-textdocument"; + +//return [start, end] close interval 0-based range +export function getSmartApplyRange( + document: TextDocument, + snippet: string, +): { range: Range; action: "insert" | "replace" } | undefined { + const applyRange = fuzzyApplyRange(document, snippet); + if (!applyRange) { + return undefined; + } + //insert mode + if (applyRange.range.start.line === applyRange.range.end.line || document.getText().trim() === "") { + return { range: applyRange.range, action: "insert" }; + } + return { range: applyRange.range, action: "replace" }; +} + +function fuzzyApplyRange(document: TextDocument, snippet: string): { range: Range; score: number } | null { + const lines = document.getText().split("\n"); + const snippetLines = snippet.split("\n"); + + let [minDistance, index] = [Number.MAX_SAFE_INTEGER, 0]; + for (let i = 0; i <= lines.length - snippetLines.length; i++) { + const window = lines.slice(i, i + snippetLines.length).join("\n"); + const distance = levenshtein(window, snippet); + if (minDistance >= distance) { + minDistance = distance; + index = i; + } + } + + if (minDistance === Number.MAX_SAFE_INTEGER && index === 0) { + return null; + } + + const startLine = index; + const endLine = index + snippetLines.length - 1; + const start: Position = { line: startLine, character: 0 }; + const end: Position = { line: endLine, character: lines[endLine]?.length || 0 }; + + return { range: { start, end }, score: minDistance }; +} diff --git a/clients/tabby-agent/src/chat/utils.ts b/clients/tabby-agent/src/chat/utils.ts new file mode 100644 index 000000000000..59a574ebb2a2 --- /dev/null +++ b/clients/tabby-agent/src/chat/utils.ts @@ -0,0 +1,378 @@ +//chat related utils functions + +import { Readable } from "stream"; +import { + Range, + Location, + ShowDocumentParams, + ShowDocumentRequest, + WorkspaceEdit, +} from "vscode-languageserver-protocol"; +import { Connection } from "vscode-languageserver"; +import * as Diff from "diff"; +import { ApplyWorkspaceEditParams, ApplyWorkspaceEditRequest } from "../protocol"; +import { isBlank } from "../utils/string"; + +export type Edit = { + id: string; + location: Location; + languageId: string; + originalText: string; + editedRange: Range; + editedText: string; + comments: string; + buffer: string; + state: "editing" | "stopped" | "completed"; +}; + +export async function readResponseStream( + stream: Readable, + connection: Connection, + currentEdit: Edit | undefined, + mutexAbortController: AbortController | undefined, + resetEditAndMutexAbortController: () => void, + responseDocumentTag: string[], + responseCommentTag?: string[], +): Promise { + const applyEdit = async (edit: Edit, isFirst: boolean = false, isLast: boolean = false) => { + if (isFirst) { + const workspaceEdit: WorkspaceEdit = { + changes: { + [edit.location.uri]: [ + { + range: { + start: { line: edit.editedRange.start.line, character: 0 }, + end: { line: edit.editedRange.start.line, character: 0 }, + }, + newText: `<<<<<<< ${edit.id}\n`, + }, + ], + }, + }; + + await applyWorkspaceEdit( + { + edit: workspaceEdit, + options: { + undoStopBefore: true, + undoStopAfter: false, + }, + }, + connection, + ); + + edit.editedRange = { + start: { line: edit.editedRange.start.line + 1, character: 0 }, + end: { line: edit.editedRange.end.line + 1, character: 0 }, + }; + } + + const editedLines = generateChangesPreview(edit); + const workspaceEdit: WorkspaceEdit = { + changes: { + [edit.location.uri]: [ + { + range: edit.editedRange, + newText: editedLines.join("\n") + "\n", + }, + ], + }, + }; + + await applyWorkspaceEdit( + { + edit: workspaceEdit, + options: { + undoStopBefore: false, + undoStopAfter: isLast, + }, + }, + connection, + ); + + edit.editedRange = { + start: { line: edit.editedRange.start.line, character: 0 }, + end: { line: edit.editedRange.start.line + editedLines.length, character: 0 }, + }; + }; + + const processBuffer = (edit: Edit, inTag: "document" | "comment", openTag: string, closeTag: string) => { + if (edit.buffer.startsWith(openTag)) { + edit.buffer = edit.buffer.substring(openTag.length); + } + + const reg = createCloseTagMatcher(closeTag); + const match = reg.exec(edit.buffer); + if (!match) { + edit[inTag === "document" ? "editedText" : "comments"] += edit.buffer; + edit.buffer = ""; + } else { + edit[inTag === "document" ? "editedText" : "comments"] += edit.buffer.substring(0, match.index); + edit.buffer = edit.buffer.substring(match.index); + return match[0] === closeTag ? false : inTag; + } + return inTag; + }; + const findOpenTag = ( + buffer: string, + responseDocumentTag: string[], + responseCommentTag?: string[], + ): "document" | "comment" | false => { + const openTags = [responseDocumentTag[0], responseCommentTag?.[0]].filter(Boolean); + if (openTags.length < 1) return false; + + const reg = new RegExp(openTags.join("|"), "g"); + const match = reg.exec(buffer); + if (match && match[0]) { + if (match[0] === responseDocumentTag[0]) { + return "document"; + } else if (match[0] === responseCommentTag?.[0]) { + return "comment"; + } + } + return false; + }; + + try { + if (!currentEdit) { + throw new Error("No current edit"); + } + + let inTag: "document" | "comment" | false = false; + + // Insert the first line as early as possible so codelens can be shown + await applyEdit(currentEdit, true, false); + + for await (const item of stream) { + if (!mutexAbortController || mutexAbortController.signal.aborted) { + break; + } + const delta = typeof item === "string" ? item : ""; + const edit = currentEdit; + edit.buffer += delta; + + if (!inTag) { + inTag = findOpenTag(edit.buffer, responseDocumentTag, responseCommentTag); + } + + if (inTag) { + const openTag = inTag === "document" ? responseDocumentTag[0] : responseCommentTag?.[0]; + const closeTag = inTag === "document" ? responseDocumentTag[1] : responseCommentTag?.[1]; + if (!closeTag || !openTag) break; + inTag = processBuffer(edit, inTag, openTag, closeTag); + if (delta.includes("\n")) { + await applyEdit(edit, false, false); + } + } + } + + if (currentEdit) { + currentEdit.state = "completed"; + await applyEdit(currentEdit, false, true); + } + } catch (error) { + if (currentEdit) { + currentEdit.state = "stopped"; + await applyEdit(currentEdit, false, true); + } + if (!(error instanceof TypeError && error.message.startsWith("terminated"))) { + throw error; + } + } finally { + resetEditAndMutexAbortController(); + } +} + +export async function applyWorkspaceEdit( + params: ApplyWorkspaceEditParams, + lspConnection: Connection, +): Promise { + if (!lspConnection) { + return false; + } + try { + // FIXME(Sma1lboy): adding client capabilities to indicate if client support this method rather than try-catch + const result = await lspConnection.sendRequest(ApplyWorkspaceEditRequest.type, params); + return result; + } catch (error) { + try { + await lspConnection.workspace.applyEdit({ + edit: params.edit, + label: params.label, + }); + return true; + } catch (fallbackError) { + return false; + } + } +} + +export async function showDocument(params: ShowDocumentParams, lspConnection: Connection): Promise { + if (!lspConnection) { + return false; + } + + try { + const result = await lspConnection.sendRequest(ShowDocumentRequest.type, params); + return result.success; + } catch (error) { + return false; + } +} + +// header line +// <<<<<<< Editing by Tabby <.#=+-> +// markers: +// [<] header +// [#] comments +// [.] waiting +// [|] in progress +// [=] unchanged +// [+] inserted +// [-] deleted +// [>] footer +// footer line +// >>>>>>> End of changes +export function generateChangesPreview(edit: Edit): string[] { + const lines: string[] = []; + let markers = ""; + // lines.push(`<<<<<<< ${stateDescription} {{markers}}[${edit.id}]`); + markers += "["; + // comments: split by new line or 80 chars + const commentLines = edit.comments + .trim() + .split(/\n|(.{1,80})(?:\s|$)/g) + .filter((input) => !isBlank(input)); + const commentPrefix = getCommentPrefix(edit.languageId); + for (const line of commentLines) { + lines.push(commentPrefix + line); + markers += "#"; + } + const pushDiffValue = (diffValue: string, marker: string) => { + diffValue + .replace(/\n$/, "") + .split("\n") + .forEach((line) => { + lines.push(line); + markers += marker; + }); + }; + // diffs + const diffs = Diff.diffLines(edit.originalText, edit.editedText); + if (edit.state === "completed") { + diffs.forEach((diff) => { + if (diff.added) { + pushDiffValue(diff.value, "+"); + } else if (diff.removed) { + pushDiffValue(diff.value, "-"); + } else { + pushDiffValue(diff.value, "="); + } + }); + } else { + let inProgressChunk = 0; + const lastDiff = diffs[diffs.length - 1]; + if (lastDiff && lastDiff.added) { + inProgressChunk = 1; + } + let waitingChunks = 0; + for (let i = diffs.length - inProgressChunk - 1; i >= 0; i--) { + if (diffs[i]?.removed) { + waitingChunks++; + } else { + break; + } + } + let lineIndex = 0; + while (lineIndex < diffs.length - inProgressChunk - waitingChunks) { + const diff = diffs[lineIndex]; + if (!diff) { + break; + } + if (diff.added) { + pushDiffValue(diff.value, "+"); + } else if (diff.removed) { + pushDiffValue(diff.value, "-"); + } else { + pushDiffValue(diff.value, "="); + } + lineIndex++; + } + if (inProgressChunk && lastDiff) { + if (edit.state === "stopped") { + pushDiffValue(lastDiff.value, "+"); + } else { + pushDiffValue(lastDiff.value, "|"); + } + } + while (lineIndex < diffs.length - inProgressChunk) { + const diff = diffs[lineIndex]; + if (!diff) { + break; + } + if (edit.state === "stopped") { + pushDiffValue(diff.value, "="); + } else { + pushDiffValue(diff.value, "."); + } + lineIndex++; + } + } + // footer + lines.push(`>>>>>>> ${edit.id} {{markers}}`); + markers += "]"; + // replace markers + // lines[0] = lines[0]!.replace("{{markers}}", markers); + lines[lines.length - 1] = lines[lines.length - 1]!.replace("{{markers}}", markers); + return lines; +} + +export function createCloseTagMatcher(tag: string): RegExp { + let reg = `${tag}`; + for (let length = tag.length - 1; length > 0; length--) { + reg += "|" + tag.substring(0, length) + "$"; + } + return new RegExp(reg, "g"); +} + +// FIXME: improve this +export function getCommentPrefix(languageId: string) { + if (["plaintext", "markdown"].includes(languageId)) { + return ""; + } + if (["python", "ruby"].includes(languageId)) { + return "#"; + } + if ( + [ + "c", + "cpp", + "java", + "javascript", + "typescript", + "javascriptreact", + "typescriptreact", + "go", + "rust", + "swift", + "kotlin", + ].includes(languageId) + ) { + return "//"; + } + return ""; +} + +export function truncateFileContent(content: string, maxLength: number): string { + if (content.length <= maxLength) { + return content; + } + + content = content.slice(0, maxLength); + const lastNewLine = content.lastIndexOf("\n"); + if (lastNewLine > 0) { + content = content.slice(0, lastNewLine + 1); + } + + return content; +} diff --git a/clients/tabby-agent/src/cli.ts b/clients/tabby-agent/src/cli.ts deleted file mode 100644 index ecf15af34f6f..000000000000 --- a/clients/tabby-agent/src/cli.ts +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env node - -import { TabbyAgent } from "./TabbyAgent"; -import { JsonLineServer } from "./JsonLineServer"; -import { LspServer } from "./LspServer"; - -const args = process.argv.slice(2); - -let server; -if (args.indexOf("--lsp") >= 0) { - server = new LspServer(); -} else { - server = new JsonLineServer(); -} -const agent = new TabbyAgent(); -server.bind(agent); -server.listen(); diff --git a/clients/tabby-agent/src/codeCompletion/buildRequest.ts b/clients/tabby-agent/src/codeCompletion/buildRequest.ts new file mode 100644 index 000000000000..489cfc573748 --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/buildRequest.ts @@ -0,0 +1,111 @@ +import type { Range } from "vscode-languageserver"; +import type { components as TabbyApiComponents } from "tabby-openapi/compatible"; +import { ConfigData } from "../config/type"; +import { CompletionContext, CompletionExtraContexts } from "./contexts"; +import path from "path"; +import { intersectionRange } from "../utils/range"; +import { cropTextToMaxChars } from "../utils/string"; + +export function buildRequest(params: { + context: CompletionContext; + extraContexts: CompletionExtraContexts; + config: ConfigData["completion"]["prompt"]; +}): TabbyApiComponents["schemas"]["Segments"] { + const { context, extraContexts, config } = params; + + // prefix && suffix + const prefix = context.prefixLines.slice(Math.max(context.prefixLines.length - config.maxPrefixLines, 0)).join(""); + const suffix = context.suffixLines.slice(0, config.maxSuffixLines).join(""); + + // filepath && git_url + let relativeRootUri: string | undefined = undefined; + let gitUrl: string | undefined = undefined; + if (extraContexts.git && extraContexts.git.repository) { + // find remote url: origin > upstream > first + const repo = extraContexts.git.repository; + const remote = + repo.remotes?.find((remote) => remote.name === "origin") || + repo.remotes?.find((remote) => remote.name === "upstream") || + repo.remotes?.[0]; + if (remote) { + relativeRootUri = repo.root; + gitUrl = remote.url; + } + } + // if relativeFilepathRoot is not set by git context, use path relative to workspace + if (!relativeRootUri && extraContexts.workspace) { + relativeRootUri = extraContexts.workspace.uri; + } + const convertToRelativePath = (filepath: string): string => { + if (relativeRootUri && filepath.startsWith(relativeRootUri)) { + return path.relative(relativeRootUri, filepath); + } + return filepath; + }; + + const filepath = convertToRelativePath(context.document.uri); + + // snippets location for deduplication + const snippetsLocations: { uri: string; range?: Range }[] = []; + const isExists = (item: { uri: string; range?: Range }): boolean => { + return !!snippetsLocations.find( + (location) => + location.uri === item.uri && (!location.range || !item.range || intersectionRange(location.range, item.range)), + ); + }; + + // declarations + const declarations: TabbyApiComponents["schemas"]["Declaration"][] = []; + extraContexts.declarations?.forEach((item) => { + if (declarations.length >= config.fillDeclarations.maxSnippets) { + return; + } + declarations.push({ + filepath: convertToRelativePath(item.uri), + body: cropTextToMaxChars(item.text, config.fillDeclarations.maxCharsPerSnippet), + }); + snippetsLocations.push(item); + }); + + // snippets: recently changed code search + const recentlyChangedCodeSearchResult: TabbyApiComponents["schemas"]["Snippet"][] = []; + extraContexts.recentlyChangedCodeSearchResult?.forEach((item) => { + if ( + recentlyChangedCodeSearchResult.length >= config.collectSnippetsFromRecentChangedFiles.maxSnippets || + isExists(item) + ) { + return; + } + recentlyChangedCodeSearchResult.push({ + filepath: convertToRelativePath(item.uri), + body: item.text, + score: item.score, + }); + snippetsLocations.push(item); + }); + + // snippets: last viewed ranges + const lastViewedSnippets: TabbyApiComponents["schemas"]["Snippet"][] = []; + extraContexts.lastViewedSnippets?.forEach((item) => { + if (lastViewedSnippets.length >= config.collectSnippetsFromRecentOpenedFiles.maxOpenedFiles || isExists(item)) { + return; + } + lastViewedSnippets.push({ + filepath: convertToRelativePath(item.uri), + body: cropTextToMaxChars(item.text, config.collectSnippetsFromRecentOpenedFiles.maxCharsPerOpenedFiles), + score: 1, + }); + snippetsLocations.push(item); + }); + + return { + prefix, + suffix, + filepath, + git_url: gitUrl, + declarations: declarations.length > 0 ? declarations : undefined, + relevant_snippets_from_changed_files: + recentlyChangedCodeSearchResult.length > 0 ? recentlyChangedCodeSearchResult : undefined, + relevant_snippets_from_recently_opened_files: lastViewedSnippets.length > 0 ? lastViewedSnippets : undefined, + }; +} diff --git a/clients/tabby-agent/src/codeCompletion/cache.ts b/clients/tabby-agent/src/codeCompletion/cache.ts new file mode 100644 index 000000000000..4ac159727b18 --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/cache.ts @@ -0,0 +1,106 @@ +import type { TextDocuments } from "vscode-languageserver"; +import type { TextDocument } from "vscode-languageserver-textdocument"; +import { buildCompletionContextWithAppend, type CompletionContext } from "./contexts"; +import { LRUCache } from "lru-cache"; +import hashObject from "object-hash"; +import { CompletionSolution, CompletionResultItem } from "./solution"; + +export class CompletionCache extends LRUCache { + constructor(options?: { max?: number; ttl?: number }) { + const max = options?.max ?? 100; + const ttl = options?.ttl ?? 5 * 60 * 1000; // 5 minutes + super({ + max, + ttl, + }); + } +} + +export function calculateCompletionContextHash( + context: CompletionContext, + textDocuments: TextDocuments, +): string { + return hashObject({ + document: { + uri: context.document.uri, + prefix: context.prefix, + suffix: context.suffix, + }, + otherDocuments: textDocuments + .all() + .filter((doc) => doc.uri !== context.document.uri) + .map((doc) => ({ + uri: doc.uri, + version: doc.version, + })), + }); +} + +export function generateForwardingContexts( + context: CompletionContext, + items: CompletionResultItem[], + maxForwardingChars = 50, +): { + context: CompletionContext; + items: CompletionResultItem[]; +}[] { + const forwarding: { appending: string; remaining: string; eventId: CompletionResultItem["eventId"] }[] = []; + for (const item of items) { + // Forward at current line + const steps = Math.min(maxForwardingChars, item.currentLine.length); + for (let chars = 1; chars < steps; chars++) { + forwarding.push({ + appending: item.currentLine.slice(0, chars), + remaining: item.currentLine.slice(chars), + eventId: item.eventId, + }); + } + if (item.lines.length > 2) { + // current line end + forwarding.push({ + appending: item.currentLine.slice(0, item.currentLine.length - 1), + remaining: item.currentLine.slice(item.currentLine.length - 1), + eventId: item.eventId, + }); + // next line start + forwarding.push({ + appending: item.currentLine.slice(0, item.currentLine.length), + remaining: item.currentLine.slice(item.currentLine.length), + eventId: item.eventId, + }); + // next line start, after indent spaces + const nextLine = item.lines[1]!; + let spaces = nextLine.search(/\S/); + if (spaces < 0) { + spaces = nextLine.length - 1; + } + forwarding.push({ + appending: item.currentLine.slice(0, item.currentLine.length + spaces), + remaining: item.currentLine.slice(item.currentLine.length + spaces), + eventId: item.eventId, + }); + } + } + + const groupedForwarding = new Map< + string, + { appending: string; remaining: string; eventId: CompletionResultItem["eventId"] }[] + >(); + for (const entry of forwarding) { + if (!groupedForwarding.has(entry.appending)) { + groupedForwarding.set(entry.appending, []); + } + groupedForwarding.get(entry.appending)?.push(entry); + } + + return Array.from(groupedForwarding.entries()).map(([appending, entries]) => { + const updatedContext = buildCompletionContextWithAppend(context, appending); + const results = entries.map((entry) => { + return new CompletionResultItem(entry.remaining, entry.eventId); + }); + return { + context: updatedContext, + items: results, + }; + }); +} diff --git a/clients/tabby-agent/src/codeCompletion/contexts.ts b/clients/tabby-agent/src/codeCompletion/contexts.ts new file mode 100644 index 000000000000..727a1dc618a7 --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/contexts.ts @@ -0,0 +1,180 @@ +import type { Position, Range, SelectedCompletionInfo } from "vscode-languageserver"; +import { TextDocument } from "vscode-languageserver-textdocument"; +import { splitLines } from "../utils/string"; +import { documentRange, rangeInDocument } from "../utils/range"; +import { getLogger } from "../logger"; +import { CodeSearchResult } from "../codeSearch"; +import { TextDocumentRangeContext } from "../contextProviders/documentContexts"; +import { WorkspaceContext } from "../contextProviders/workspace"; +import { GitContext } from "../contextProviders/git"; +import { EditorOptionsContext } from "../contextProviders/editorOptions"; + +const logger = getLogger("CodeCompletionContext"); + +export interface CompletionContext { + readonly document: TextDocument; + readonly position: Position; + readonly selectedCompletionInfo?: SelectedCompletionInfo; + readonly notebookCells?: TextDocument[]; + + // calculated from selectedCompletionInfo, this insertion text is already included in prefix + readonly selectedCompletionInsertion: string; + + // the line suffix is empty or should be replaced, in this case, the line suffix is already excluded from suffix + readonly isLineEnd: boolean; + readonly lineEndReplaceLength: number; + + // calculated from contexts, do not equal to document prefix and suffix + readonly prefix: string; + readonly suffix: string; + + // redundant quick access for prefix and suffix + readonly prefixLines: string[]; + readonly suffixLines: string[]; + readonly currentLinePrefix: string; + readonly currentLineSuffix: string; +} + +export function buildCompletionContext( + document: TextDocument, + position: Position, + selectedCompletionInfo?: SelectedCompletionInfo, + notebookCells?: TextDocument[], +): CompletionContext { + let selectedCompletionInsertion = ""; + if (selectedCompletionInfo) { + // Handle selected completion info only if replacement matches prefix + // Handle: con -> console + // Ignore: cns -> console + const replaceRange = converToObjectRange(selectedCompletionInfo.range); + if ( + replaceRange.start.line == position.line && + replaceRange.start.character < position.character && + replaceRange.end.line == position.line && + replaceRange.end.character == position.character + ) { + const replaceLength = replaceRange.end.character - replaceRange.start.character; + selectedCompletionInsertion = selectedCompletionInfo.text.substring(replaceLength); + logger.trace("Used selected completion insertion: ", { selectedCompletionInsertion }); + } + } + + let notebookCellsPrefix = ""; + let notebookCellsSuffix = ""; + if (notebookCells) { + const currentCellIndex = notebookCells.indexOf(document); + if (currentCellIndex >= 0 && currentCellIndex < notebookCells.length - 1) { + const currentLanguageId = document.languageId; + const formatContext = (cells: TextDocument[]): string => { + const notebookLanguageComments: { [languageId: string]: (code: string) => string } = { + markdown: (code) => "```\n" + code + "\n```", + python: (code) => + code + .split("\n") + .map((l) => "# " + l) + .join("\n"), + }; + return cells + .map((textDocument) => { + if (textDocument.languageId === currentLanguageId) { + return textDocument.getText(); + } else if (Object.keys(notebookLanguageComments).includes(currentLanguageId)) { + return notebookLanguageComments[currentLanguageId]?.(textDocument.getText()) ?? ""; + } else { + return ""; + } + }) + .join("\n\n"); + }; + notebookCellsPrefix = formatContext(notebookCells.slice(0, currentCellIndex)) + "\n\n"; + notebookCellsSuffix = "\n\n" + formatContext(notebookCells.slice(currentCellIndex + 1)); + logger.trace("Used notebook cells context:", { notebookCellsPrefix, notebookCellsSuffix }); + } + } + + const fullDocumentRange = documentRange(document); + const prefixRange = { + start: fullDocumentRange.start, + end: position, + }; + const documentPrefix = document.getText(prefixRange); + const prefix = notebookCellsPrefix + documentPrefix + selectedCompletionInsertion; + + const documentCurrentLineSuffixRange = rangeInDocument( + { + start: position, + end: { line: position.line + 1, character: 0 }, + }, + document, + ); + const documentCurrentLineSuffix = documentCurrentLineSuffixRange + ? document.getText(documentCurrentLineSuffixRange) + : ""; + const isLineEnd = !!documentCurrentLineSuffix.match(/^\W*$/); + const lineEndReplaceLength = isLineEnd ? documentCurrentLineSuffix.replace(/\r?\n$/, "").length : 0; + + const suffixRange = rangeInDocument( + { + start: { line: position.line, character: position.character + lineEndReplaceLength }, + end: fullDocumentRange.end, + }, + document, + ); + const documentSuffix = suffixRange ? document.getText(suffixRange) : ""; + + const suffix = documentSuffix + notebookCellsSuffix; + + const prefixLines = splitLines(prefix); + const suffixLines = splitLines(suffix); + const currentLinePrefix = prefixLines[prefixLines.length - 1] ?? ""; + const currentLineSuffix = suffixLines[0] ?? ""; + + return { + document, + position, + selectedCompletionInfo, + notebookCells, + selectedCompletionInsertion, + isLineEnd, + lineEndReplaceLength, + prefix, + suffix, + prefixLines, + suffixLines, + currentLinePrefix, + currentLineSuffix, + }; +} + +export function buildCompletionContextWithAppend(context: CompletionContext, appendText: string): CompletionContext { + const offset = context.document.offsetAt(context.position); + const updatedText = context.prefix + appendText + context.suffix; + const updatedOffset = offset + appendText.length; + const updatedDocument = TextDocument.create( + context.document.uri, + context.document.languageId, + context.document.version + 1, + updatedText, + ); + const updatedPosition = updatedDocument.positionAt(updatedOffset); + return buildCompletionContext(updatedDocument, updatedPosition, undefined, context.notebookCells); +} + +export interface CompletionExtraContexts { + workspace?: WorkspaceContext; + git?: GitContext; + declarations?: TextDocumentRangeContext[]; + recentlyChangedCodeSearchResult?: CodeSearchResult; + lastViewedSnippets?: TextDocumentRangeContext[]; + editorOptions?: EditorOptionsContext; +} + +function converToObjectRange(range: Range | [Position, Position]): Range { + if (Array.isArray(range)) { + return { + start: range[0], + end: range[1], + }; + } + return range; +} diff --git a/clients/tabby-agent/src/codeCompletion/debouncer.ts b/clients/tabby-agent/src/codeCompletion/debouncer.ts new file mode 100644 index 000000000000..d3ce619762bc --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/debouncer.ts @@ -0,0 +1,111 @@ +import type { ConfigData } from "../config/type"; + +function clamp(min: number, max: number, value: number): number { + return Math.max(min, Math.min(max, value)); +} + +export type DebouncingContext = { + triggerCharacter: string; + isLineEnd?: boolean; + isDocumentEnd?: boolean; + manually?: boolean; + estimatedResponseTime?: number; +}; + +export class CompletionDebouncer { + private baseInterval = 200; // ms + private lastTimestamp = 0; + private intervalHistory: number[] = []; + private config: ConfigData["completion"]["debounce"] | undefined = undefined; + private rules = { + baseIntervalSlideWindowAvg: { + minSize: 20, + maxSize: 100, + min: 100, + max: 400, + }, + adaptiveRate: { + min: 1.5, + max: 3.0, + }, + contextScoreWeights: { + triggerCharacter: 0.5, + lineEnd: 0.4, + documentEnd: 0.1, + }, + requestDelay: { + min: 100, // ms + max: 1000, + }, + }; + + updateConfig(config: ConfigData["completion"]["debounce"] | undefined) { + this.config = config; + } + + async debounce(context: DebouncingContext, signal?: AbortSignal): Promise { + if (context.manually) { + return this.sleep(0, signal); + } + if (this.config?.mode === "fixed") { + return this.sleep(this.config.interval, signal); + } + const now = Date.now(); + this.updateBaseInterval(now - this.lastTimestamp); + this.lastTimestamp = now; + const contextScore = this.calcContextScore(context); + const adaptiveRate = + this.rules.adaptiveRate.max - (this.rules.adaptiveRate.max - this.rules.adaptiveRate.min) * contextScore; + const expectedLatency = adaptiveRate * this.baseInterval; + const responseTime = context.estimatedResponseTime ?? 0; + const delay = clamp(this.rules.requestDelay.min, this.rules.requestDelay.max, expectedLatency - responseTime); + return this.sleep(delay, signal); + } + + private async sleep(delay: number, signal?: AbortSignal): Promise { + return new Promise((resolve, reject) => { + const timer = setTimeout(resolve, Math.min(delay, 0x7fffffff)); + if (signal) { + if (signal.aborted) { + clearTimeout(timer); + reject(signal.reason); + } else { + signal.addEventListener("abort", () => { + clearTimeout(timer); + reject(signal.reason); + }); + } + } + }); + } + + private updateBaseInterval(interval: number) { + if (interval > this.rules.baseIntervalSlideWindowAvg.max) { + return; + } + this.intervalHistory.push(interval); + if (this.intervalHistory.length > this.rules.baseIntervalSlideWindowAvg.maxSize) { + this.intervalHistory.shift(); + } + if (this.intervalHistory.length > this.rules.baseIntervalSlideWindowAvg.minSize) { + const avg = this.intervalHistory.reduce((a, b) => a + b, 0) / this.intervalHistory.length; + this.baseInterval = clamp( + this.rules.baseIntervalSlideWindowAvg.min, + this.rules.baseIntervalSlideWindowAvg.max, + avg, + ); + } + } + + // return score in [0, 1], 1 means the context has a high chance to accept the completion + private calcContextScore(context: DebouncingContext): number { + const { triggerCharacter, isLineEnd, isDocumentEnd } = context; + const weights = this.rules.contextScoreWeights; + let score = 0; + score += triggerCharacter.match(/^\W*$/) ? weights.triggerCharacter : 0; + score += isLineEnd ? weights.lineEnd : 0; + score += isDocumentEnd ? weights.documentEnd : 0; + score = clamp(0, 1, score); + return score; + } +} diff --git a/clients/tabby-agent/src/codeCompletion/index.ts b/clients/tabby-agent/src/codeCompletion/index.ts new file mode 100644 index 000000000000..59bb674e2eb1 --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/index.ts @@ -0,0 +1,764 @@ +import { EventEmitter } from "events"; +import type { + Connection, + CancellationToken, + Disposable, + NotebookDocuments, + CompletionParams, + CompletionOptions, + InlineCompletionParams, + TextDocumentPositionParams, + SelectedCompletionInfo, +} from "vscode-languageserver"; +import { + CompletionRequest as LspCompletionRequest, + CompletionTriggerKind, + InlineCompletionTriggerKind, +} from "vscode-languageserver"; +import type { TextDocument } from "vscode-languageserver-textdocument"; +import type { TextDocuments } from "../extensions/textDocuments"; +import { + ClientCapabilities, + ServerCapabilities, + CompletionList, + InlineCompletionRequest, + InlineCompletionList, + TelemetryEventNotification, + EventParams, +} from "../protocol"; +import type { Feature } from "../feature"; +import type { Configurations } from "../config"; +import type { ConfigData } from "../config/type"; +import type { TabbyApiClient, TabbyApiClientStatus } from "../http/tabbyApiClient"; +import type { TextDocumentReader } from "../contextProviders/documentContexts"; +import type { WorkspaceContextProvider } from "../contextProviders/workspace"; +import type { GitContextProvider } from "../contextProviders/git"; +import type { DeclarationSnippetsProvider } from "../contextProviders/declarationSnippets"; +import type { RecentlyChangedCodeSearch } from "../contextProviders/recentlyChangedCodeSearch"; +import type { EditorVisibleRangesTracker } from "../contextProviders/editorVisibleRanges"; +import type { EditorOptionsProvider } from "../contextProviders/editorOptions"; +import type { AnonymousUsageLogger } from "../telemetry"; +import { calculateCompletionContextHash, CompletionCache, generateForwardingContexts } from "./cache"; +import { CompletionDebouncer, DebouncingContext } from "./debouncer"; +import { CompletionStatisticsEntry, CompletionStatisticsTracker } from "./statistics"; +import { buildCompletionContext, CompletionContext } from "./contexts"; +import { CompletionSolution, createCompletionResultItemFromResponse } from "./solution"; +import { extractNonReservedWordList } from "../utils/string"; +import { MutexAbortError, formatErrorMessage, isCanceledError, isRateLimitExceededError } from "../utils/error"; +import { preCacheProcess, postCacheProcess } from "./postprocess"; +import { buildRequest } from "./buildRequest"; +import { analyzeMetrics, buildHelpMessageForLatencyIssue, LatencyTracker } from "./latencyTracker"; +import { rangeInDocument } from "../utils/range"; +import { getLogger } from "../logger"; + +export class CompletionProvider extends EventEmitter implements Feature { + private readonly logger = getLogger("CompletionProvider"); + + private readonly cache = new CompletionCache(); + private readonly debouncer = new CompletionDebouncer(); + private readonly statisticTracker = new CompletionStatisticsTracker(); + private readonly latencyTracker = new LatencyTracker(); + + private isApiAvailable = false; + private latencyIssue: "highTimeoutRate" | "slowResponseTime" | undefined = undefined; + private rateLimitExceeded: boolean = false; + private fetchingCompletion: boolean = false; + + private clientCapabilities: ClientCapabilities | undefined = undefined; + + private completionFeatureOptions: CompletionOptions | undefined = undefined; + private completionFeatureRegistration: Disposable | undefined = undefined; + private inlineCompletionFeatureRegistration: Disposable | undefined = undefined; + + private mutexAbortController: AbortController | undefined = undefined; + private submitStatsTimer: ReturnType | undefined = undefined; + + constructor( + private readonly configurations: Configurations, + private readonly tabbyApiClient: TabbyApiClient, + private readonly documents: TextDocuments, + private readonly notebooks: NotebookDocuments, + private readonly anonymousUsageLogger: AnonymousUsageLogger, + private readonly textDocumentReader: TextDocumentReader, + private readonly workspaceContextProvider: WorkspaceContextProvider, + private readonly gitContextProvider: GitContextProvider, + private readonly declarationSnippetsProvider: DeclarationSnippetsProvider, + private readonly recentlyChangedCodeSearch: RecentlyChangedCodeSearch, + private readonly editorVisibleRangesTracker: EditorVisibleRangesTracker, + private readonly editorOptionsProvider: EditorOptionsProvider, + ) { + super(); + } + + isAvailable(): boolean { + return this.isApiAvailable; + } + + getLatencyIssue(): "highTimeoutRate" | "slowResponseTime" | undefined { + return this.latencyIssue; + } + + getHelpMessage(format?: "plaintext" | "markdown" | "html"): string | undefined { + if (!this.isApiAvailable) { + return "There is no code completion model available. Please check your server configuration."; + } + if (this.rateLimitExceeded) { + return "The rate limit for the code completion API has been reached. Please try again later."; + } + if (this.latencyIssue) { + return buildHelpMessageForLatencyIssue( + this.latencyIssue, + { + latencyStatistics: this.latencyTracker.calculateLatencyStatistics(), + endpoint: this.configurations.getMergedConfig().server.endpoint, + serverHealth: this.tabbyApiClient.getServerHealth(), + }, + format, + ); + } + return undefined; + } + + isRateLimitExceeded(): boolean { + return this.rateLimitExceeded; + } + + isFetching(): boolean { + return this.fetchingCompletion; + } + + private updateIsAvailable() { + const health = this.tabbyApiClient.getServerHealth(); + const isAvailable = !!(health && health["model"]); + if (this.isApiAvailable != isAvailable) { + this.isApiAvailable = isAvailable; + this.emit("isAvailableUpdated", isAvailable); + } + } + + private updateLatencyIssue(issue: "highTimeoutRate" | "slowResponseTime" | undefined) { + if (this.latencyIssue != issue) { + this.latencyIssue = issue; + if (issue) { + this.logger.info(`Completion latency issue detected: ${issue}.`); + } + this.emit("latencyIssueUpdated", issue); + } + } + + private updateIsRateLimitExceeded(value: boolean) { + if (this.rateLimitExceeded != value) { + if (value) { + this.logger.info(`Rate limit exceeded.`); + } + this.rateLimitExceeded = value; + this.emit("isRateLimitExceededUpdated", value); + } + } + + private updateIsFetching(value: boolean) { + if (this.fetchingCompletion != value) { + this.fetchingCompletion = value; + this.emit("isFetchingUpdated", value); + } + } + + initialize(connection: Connection, clientCapabilities: ClientCapabilities): ServerCapabilities { + this.clientCapabilities = clientCapabilities; + + let serverCapabilities: ServerCapabilities = {}; + if (clientCapabilities.textDocument?.completion) { + connection.onCompletion(async (params, token) => { + return this.provideCompletion(params, token); + }); + this.completionFeatureOptions = { + resolveProvider: false, + completionItem: { + labelDetailsSupport: true, + }, + }; + if (!clientCapabilities.textDocument?.completion.dynamicRegistration) { + serverCapabilities = { + ...serverCapabilities, + completionProvider: this.completionFeatureOptions, + }; + } + } + if (clientCapabilities.textDocument?.inlineCompletion) { + connection.onRequest(InlineCompletionRequest.type, async (params, token) => { + return this.provideInlineCompletion(params, token); + }); + if (!clientCapabilities.textDocument?.inlineCompletion.dynamicRegistration) { + serverCapabilities = { + ...serverCapabilities, + inlineCompletionProvider: true, + }; + } + } + connection.onNotification(TelemetryEventNotification.type, async (param) => { + return this.postEvent(param); + }); + + const config = this.configurations.getMergedConfig(); + this.debouncer.updateConfig(config.completion.debounce); + this.configurations.on("updated", (config: ConfigData) => { + this.debouncer.updateConfig(config.completion.debounce); + }); + + this.updateIsAvailable(); + this.tabbyApiClient.on("statusUpdated", async (status: TabbyApiClientStatus) => { + if (status === "noConnection") { + this.updateLatencyIssue(undefined); + this.latencyTracker.reset(); + } + + this.updateIsAvailable(); + await this.syncFeatureRegistration(connection); + }); + + const submitStatsInterval = 1000 * 60 * 60 * 24; // 24h + this.submitStatsTimer = setInterval(async () => { + await this.sendCompletionStatistics(); + }, submitStatsInterval); + + return serverCapabilities; + } + + async initialized(connection: Connection) { + await this.syncFeatureRegistration(connection); + } + + private async syncFeatureRegistration(connection: Connection) { + if (this.isApiAvailable) { + if ( + this.clientCapabilities?.textDocument?.completion?.dynamicRegistration && + !this.completionFeatureRegistration + ) { + this.completionFeatureRegistration = await connection.client.register( + LspCompletionRequest.type, + this.completionFeatureOptions, + ); + } + if ( + this.clientCapabilities?.textDocument?.inlineCompletion?.dynamicRegistration && + !this.inlineCompletionFeatureRegistration + ) { + this.inlineCompletionFeatureRegistration = await connection.client.register(InlineCompletionRequest.type); + } + } else { + this.completionFeatureRegistration?.dispose(); + this.completionFeatureRegistration = undefined; + this.inlineCompletionFeatureRegistration?.dispose(); + this.inlineCompletionFeatureRegistration = undefined; + } + } + + async shutdown(): Promise { + await this.sendCompletionStatistics(); + if (this.submitStatsTimer) { + clearInterval(this.submitStatsTimer); + } + } + + async provideCompletion(params: CompletionParams, token: CancellationToken): Promise { + if (!this.isApiAvailable) { + throw { + name: "CodeCompletionFeatureNotAvailableError", + message: "Code completion feature not available", + }; + } + if (token.isCancellationRequested) { + return null; + } + try { + const result = await this.generateCompletions( + params, + params.context?.triggerKind !== CompletionTriggerKind.TriggerCharacter, + undefined, + token, + ); + if (!result) { + return null; + } + const list = result.solution.toCompletionList(result.context); + this.logger.info(`Provided completion items: ${list.items.length}`); + return list; + } catch (error) { + return null; + } + } + + async provideInlineCompletion( + params: InlineCompletionParams, + token: CancellationToken, + ): Promise { + if (!this.isApiAvailable) { + throw { + name: "CodeCompletionFeatureNotAvailableError", + message: "Code completion feature not available", + }; + } + if (token.isCancellationRequested) { + return null; + } + try { + const result = await this.generateCompletions( + params, + params.context?.triggerKind === InlineCompletionTriggerKind.Invoked, + params.context?.selectedCompletionInfo, + token, + ); + if (!result) { + return null; + } + const list = result.solution.toInlineCompletionList(result.context); + this.logger.info(`Provided inline completion items: ${list.items.length}`); + return list; + } catch (error) { + return null; + } + } + + async postEvent(params: EventParams): Promise { + try { + this.statisticTracker.addEvent(params.type); + } catch (error) { + // ignore + } + + try { + const request = { + type: params.type, + select_kind: params.selectKind, + completion_id: params.eventId.completionId, + choice_index: params.eventId.choiceIndex, + view_id: params.viewId, + elapsed: params.elapsed, + }; + await this.tabbyApiClient.postEvent(request); + } catch (error) { + // ignore + } + } + + private async fetchExtraContext( + context: CompletionContext, + solution: CompletionSolution, + timeout: number | undefined, + token: CancellationToken, + ): Promise { + const config = this.configurations.getMergedConfig().completion.prompt; + const { document, position } = context; + const prefixRange = rangeInDocument( + { start: { line: position.line - config.maxPrefixLines, character: 0 }, end: position }, + document, + ); + + const fetchWorkspaceContext = async () => { + try { + solution.extraContext.workspace = await this.workspaceContextProvider.getWorkspaceContext(document.uri); + } catch (error) { + this.logger.debug(`Failed to fetch workspace context: ${formatErrorMessage(error)}`); + } + }; + const fetchGitContext = async () => { + try { + solution.extraContext.git = (await this.gitContextProvider.getContext(document.uri, token)) ?? undefined; + } catch (error) { + this.logger.debug(`Failed to fetch git context: ${formatErrorMessage(error)}`); + } + }; + const fetchDeclarations = async () => { + if (config.fillDeclarations.enabled && prefixRange) { + this.logger.debug("Collecting declarations..."); + try { + solution.extraContext.declarations = await this.declarationSnippetsProvider.collect( + { + uri: document.uri, + range: prefixRange, + }, + config.fillDeclarations.maxSnippets, + false, + token, + ); + this.logger.debug("Completed collecting declarations."); + } catch (error) { + this.logger.debug(`Failed to collect declarations: ${formatErrorMessage(error)}`); + } + } + }; + const fetchRecentlyChangedCodeSearchResult = async () => { + if (config.collectSnippetsFromRecentChangedFiles.enabled && prefixRange) { + this.logger.debug("Searching recently changed code..."); + try { + const prefixText = document.getText(prefixRange); + const query = extractNonReservedWordList(prefixText); + solution.extraContext.recentlyChangedCodeSearchResult = await this.recentlyChangedCodeSearch.search( + query, + [document.uri], + document.languageId, + config.collectSnippetsFromRecentChangedFiles.maxSnippets, + ); + this.logger.debug("Completed searching recently changed code."); + } catch (error) { + this.logger.debug(`Failed to do recently changed code search: ${formatErrorMessage(error)}`); + } + } + }; + const fetchLastViewedSnippets = async () => { + if (config.collectSnippetsFromRecentOpenedFiles.enabled) { + try { + const ranges = await this.editorVisibleRangesTracker.getHistoryRanges({ + max: config.collectSnippetsFromRecentOpenedFiles.maxOpenedFiles, + excludedUris: [document.uri], + }); + solution.extraContext.lastViewedSnippets = ( + await ranges?.mapAsync(async (range) => { + return await this.textDocumentReader.read(range.uri, range.range, token); + }) + )?.filter((item) => item !== undefined); + } catch (error) { + this.logger.debug(`Failed to read last viewed snippets: ${formatErrorMessage(error)}`); + } + } + }; + const fetchEditorOptions = async () => { + try { + solution.extraContext.editorOptions = await this.editorOptionsProvider.getEditorOptions(document.uri, token); + } catch (error) { + this.logger.debug(`Failed to fetch editor options: ${formatErrorMessage(error)}`); + } + }; + + await new Promise((resolve, reject) => { + const disposables: Disposable[] = []; + const disposeAll = () => { + disposables.forEach((d) => d.dispose()); + }; + + Promise.all([ + fetchWorkspaceContext(), + fetchGitContext(), + fetchDeclarations(), + fetchRecentlyChangedCodeSearchResult(), + fetchLastViewedSnippets(), + fetchEditorOptions(), + ]).then(() => { + disposeAll(); + resolve(); + }); + // No need to catch Promise.all errors here, as individual fetches handle their errors. + + if (token) { + if (token.isCancellationRequested) { + disposeAll(); + reject(new Error("Request canceled.")); + } + disposables.push( + token.onCancellationRequested(() => { + disposeAll(); + reject(new Error("Request canceled.")); + }), + ); + } + if (timeout) { + const timer = setTimeout(() => { + disposeAll(); + reject(new Error("Timeout.")); + }, timeout); + disposables.push({ + dispose: () => { + clearTimeout(timer); + }, + }); + } + }); + } + + private async generateCompletions( + documentPosition: TextDocumentPositionParams, + manuallyTriggered: boolean, + selectedCompletionInfo: SelectedCompletionInfo | undefined, + token: CancellationToken, + ): Promise<{ context: CompletionContext; solution: CompletionSolution } | null> { + this.logger.info("Generating completions..."); + const config = this.configurations.getMergedConfig(); + + // Mutex Control + if (this.mutexAbortController && !this.mutexAbortController.signal.aborted) { + this.mutexAbortController.abort(new MutexAbortError()); + } + const abortController = new AbortController(); + if (token) { + token.onCancellationRequested(() => abortController.abort()); + } + this.mutexAbortController = abortController; + const signal = abortController.signal; + + // Build the context + const { textDocument, position } = documentPosition; + + this.logger.trace("Building completion context...", { uri: textDocument.uri }); + + const document = this.documents.get(textDocument.uri); + if (!document) { + this.logger.debug("Document not found, cancelled."); + return null; + } + + let notebookCells: TextDocument[] | undefined = undefined; + const notebookCell = this.notebooks.getNotebookCell(textDocument.uri); + if (notebookCell) { + const notebook = this.notebooks.findNotebookDocumentForCell(notebookCell); + if (notebook) { + this.logger.trace("Notebook found:", { notebook: notebook.uri, cell: notebookCell.document }); + notebookCells = notebook.cells + .map((cell) => this.notebooks.getCellTextDocument(cell)) + .filter((item) => item !== undefined); + } + } + + const context = buildCompletionContext(document, position, selectedCompletionInfo, notebookCells); + this.logger.trace("Completed Building completion context."); + const hash = calculateCompletionContextHash(context, this.documents); + this.logger.trace("Completion hash: ", { hash }); + + let solution: CompletionSolution | undefined = undefined; + if (this.cache.has(hash)) { + solution = this.cache.get(hash); + } + + const debouncingContext: DebouncingContext = { + triggerCharacter: context.currentLinePrefix.slice(-1), + isLineEnd: context.isLineEnd, + isDocumentEnd: !!context.suffix.match(/^\W*$/), + manually: manuallyTriggered, + }; + + const latencyStatsList: CompletionStatisticsEntry[] = []; + + try { + // Resolve solution + if (solution && (!manuallyTriggered || solution.isCompleted)) { + // Found cached solution + // TriggerKind is Automatic, or the solution is completed + // Return cached solution, do not need to fetch more choices + + // Debounce before continue processing cached solution + await this.debouncer.debounce(debouncingContext, signal); + this.logger.info("Completion cache hit."); + } else if (!manuallyTriggered) { + // No cached solution + // TriggerKind is Automatic + // We need to fetch the first choice + + solution = new CompletionSolution(); + + // Debounce before fetching + const averageResponseTime = this.latencyTracker.calculateLatencyStatistics().metrics.averageResponseTime; + await this.debouncer.debounce( + { + ...debouncingContext, + estimatedResponseTime: averageResponseTime, + }, + signal, + ); + + try { + const extraContextTimeout = 500; // 500ms when automatic trigger + this.logger.info(`Fetching extra completion context with ${extraContextTimeout}ms timeout ...`); + await this.fetchExtraContext(context, solution, extraContextTimeout, token); + } catch (error) { + this.logger.info(`Failed to fetch extra context: ${formatErrorMessage(error)}`); + } + if (signal.aborted) { + throw signal.reason; + } + + // Fetch the completion + this.logger.info(`Fetching completions from the server...`); + this.updateIsFetching(true); + try { + const latencyStats: CompletionStatisticsEntry = {}; + latencyStatsList.push(latencyStats); + const response = await this.tabbyApiClient.fetchCompletion( + { + language: context.document.languageId, + segments: buildRequest({ + context: context, + extraContexts: solution.extraContext, + config: config.completion.prompt, + }), + temperature: undefined, + }, + signal, + latencyStats, + ); + this.updateIsRateLimitExceeded(false); + + const completionResultItem = createCompletionResultItemFromResponse(response); + // postprocess: preCache + const postprocessed = await preCacheProcess( + [completionResultItem], + context, + solution.extraContext, + config.postprocess, + ); + solution.items.push(...postprocessed); + } catch (error) { + if (isCanceledError(error)) { + this.logger.info(`Fetching completion canceled.`); + solution = undefined; + } else if (isRateLimitExceededError(error)) { + this.updateIsRateLimitExceeded(true); + } else { + this.updateIsRateLimitExceeded(false); + } + } + } else { + // No cached solution, or cached solution is not completed + // TriggerKind is Manual + // We need to fetch the more choices + + solution = solution ?? new CompletionSolution(); + + // Fetch multiple times to get more choices + this.logger.info(`Fetching more completions from the server...`); + this.updateIsFetching(true); + + try { + this.logger.info(`Fetching extra completion context...`); + await this.fetchExtraContext(context, solution, undefined, token); + } catch (error) { + this.logger.info(`Failed to fetch extra context: ${formatErrorMessage(error)}`); + } + if (signal.aborted) { + throw signal.reason; + } + + try { + let tries = 0; + while ( + solution.items.length < config.completion.solution.maxItems && + tries < config.completion.solution.maxTries + ) { + tries++; + const latencyStats: CompletionStatisticsEntry = {}; + latencyStatsList.push(latencyStats); + const response = await this.tabbyApiClient.fetchCompletion( + { + language: context.document.languageId, + segments: buildRequest({ + context: context, + extraContexts: solution.extraContext, + config: config.completion.prompt, + }), + temperature: config.completion.solution.temperature, + }, + signal, + latencyStats, + ); + this.updateIsRateLimitExceeded(false); + + const completionResultItem = createCompletionResultItemFromResponse(response); + // postprocess: preCache + const postprocessed = await preCacheProcess( + [completionResultItem], + context, + solution.extraContext, + config.postprocess, + ); + solution.items.push(...postprocessed); + if (signal.aborted) { + throw signal.reason; + } + } + // Mark the solution as completed + solution.isCompleted = true; + } catch (error) { + if (isCanceledError(error)) { + this.logger.info(`Fetching completion canceled.`); + solution = undefined; + } else if (isRateLimitExceededError(error)) { + this.updateIsRateLimitExceeded(true); + } else { + this.updateIsRateLimitExceeded(false); + } + } + } + // Postprocess solution + if (solution) { + // Update Cache + this.cache.set(hash, solution); + + const forwardingContexts = generateForwardingContexts(context, solution.items); + forwardingContexts.forEach((entry) => { + const forwardingContextHash = calculateCompletionContextHash(entry.context, this.documents); + const forwardingSolution = new CompletionSolution(); + forwardingSolution.extraContext = solution?.extraContext ?? {}; + forwardingSolution.isCompleted = solution?.isCompleted ?? false; + forwardingSolution.items = entry.items; + this.cache.set(forwardingContextHash, forwardingSolution); + }); + + // postprocess: postCache + solution.items = await postCacheProcess(solution.items, context, solution.extraContext, config.postprocess); + if (signal.aborted) { + throw signal.reason; + } + } + } catch (error) { + if (isCanceledError(error)) { + this.logger.debug(`Providing completions canceled.`); + } else { + this.logger.error(`Providing completions failed.`, error); + } + } + + if (this.mutexAbortController === abortController) { + this.mutexAbortController = undefined; + this.updateIsFetching(false); + } + + if (latencyStatsList.length > 0) { + latencyStatsList.forEach((latencyStats) => { + this.statisticTracker.addStatisticsEntry(latencyStats); + + if (latencyStats.latency !== undefined) { + this.latencyTracker.add(latencyStats.latency); + } else if (latencyStats.timeout) { + this.latencyTracker.add(NaN); + } + }); + const statsResult = this.latencyTracker.calculateLatencyStatistics(); + const issue = analyzeMetrics(statsResult); + switch (issue) { + case "healthy": + this.updateLatencyIssue(undefined); + break; + case "highTimeoutRate": + this.updateLatencyIssue("highTimeoutRate"); + break; + case "slowResponseTime": + this.updateLatencyIssue("slowResponseTime"); + break; + } + } + + if (solution) { + this.statisticTracker.addTriggerEntry({ triggerMode: manuallyTriggered ? "manual" : "auto" }); + this.logger.info(`Completed generating completions.`); + this.logger.trace("Completion solution:", { items: solution.items }); + return { context, solution }; + } + return null; + } + + private async sendCompletionStatistics() { + const report = this.statisticTracker.report(); + if (report["completion_request"]["count"] > 0) { + await this.anonymousUsageLogger.event("AgentStats", { stats: report }); + this.statisticTracker.reset(); + } + } +} diff --git a/clients/tabby-agent/src/codeCompletion/latencyTracker.ts b/clients/tabby-agent/src/codeCompletion/latencyTracker.ts new file mode 100644 index 000000000000..a8dbb129a4b2 --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/latencyTracker.ts @@ -0,0 +1,176 @@ +import type { components as TabbyApiComponents } from "tabby-openapi/compatible"; + +export type LatencyStatistics = { + values: number[]; + metrics: { total: number; timeouts: number; responses: number; averageResponseTime: number }; +}; + +class LastN { + private readonly values: number[] = []; + + constructor(private readonly maxSize: number) {} + + add(value: number): void { + this.values.push(value); + if (this.values.length > this.maxSize) { + this.values.shift(); + } + } + + getValues(): number[] { + return this.values; + } +} + +export class LatencyTracker { + private windowSize: number; + private latencies: LastN; + + constructor(options?: { windowSize: number }) { + this.windowSize = options?.windowSize ?? 10; + this.latencies = new LastN(this.windowSize); + } + + // add a latency entry, add NaN for timeouts + add(entry: number): void { + this.latencies.add(entry); + } + + reset(): void { + this.latencies = new LastN(this.windowSize); + } + + calculateLatencyStatistics(): LatencyStatistics { + const latencies = this.latencies.getValues(); + const timeouts = latencies.filter((latency) => Number.isNaN(latency)); + const responses = latencies.filter((latency) => !Number.isNaN(latency)); + const averageResponseTime = responses.reduce((acc, latency) => acc + latency, 0) / responses.length; + return { + values: latencies, + metrics: { + total: latencies.length, + timeouts: timeouts.length, + responses: responses.length, + averageResponseTime, + }, + }; + } +} + +export function analyzeMetrics( + latencyStatistics: LatencyStatistics, +): "healthy" | "highTimeoutRate" | "slowResponseTime" | null { + const rules = { + // Mark status as healthy if the latency is less than the threshold for each latest windowSize requests. + healthy: { windowSize: 1, latency: 3000 }, + // If there is at least {count} requests, and the average response time is higher than the {latency}, show warning + slowResponseTime: { latency: 5000, count: 1 }, + // If there is at least {count} timeouts, and the timeout rate is higher than the {rate}, show warning + highTimeoutRate: { rate: 0.5, count: 1 }, + }; + + const { + values: latencies, + metrics: { total, timeouts, responses, averageResponseTime }, + } = latencyStatistics; + + if ( + latencies + .slice(-Math.min(latencies.length, rules.healthy.windowSize)) + .every((latency) => latency < rules.healthy.latency) + ) { + return "healthy"; + } + if (timeouts / total > rules.highTimeoutRate.rate && timeouts >= rules.highTimeoutRate.count) { + return "highTimeoutRate"; + } + if (averageResponseTime > rules.slowResponseTime.latency && responses >= rules.slowResponseTime.count) { + return "slowResponseTime"; + } + return null; +} + +export function buildHelpMessageForLatencyIssue( + issue: "highTimeoutRate" | "slowResponseTime", + data?: + | { + latencyStatistics: LatencyStatistics; + endpoint?: string; + serverHealth?: TabbyApiComponents["schemas"]["HealthState"]; + } + | undefined, + format?: "plaintext" | "markdown" | "html", +): string | undefined { + const outputFormat = format ?? "plaintext"; + const metrics = data?.latencyStatistics.metrics; + const serverHealth = data?.serverHealth; + const endpoint = data?.endpoint; + + let statsMessage = ""; + if (issue == "slowResponseTime") { + if (metrics && metrics["responses"] && metrics["averageResponseTime"]) { + statsMessage = `The average response time of recent ${metrics["responses"]} completion requests is ${Number( + metrics["averageResponseTime"], + ).toFixed(0)}ms.

    `; + } + } + + if (issue == "highTimeoutRate") { + if (metrics && metrics["total"] && metrics["timeouts"]) { + statsMessage = `${metrics["timeouts"]} of ${metrics["total"]} completion requests timed out.

    `; + } + } + + let helpMessageForRunningLargeModelOnCPU = ""; + if (serverHealth?.device === "cpu" && serverHealth?.model?.match(/[0-9.]+B$/)) { + helpMessageForRunningLargeModelOnCPU += + `Your Tabby server is running model ${serverHealth?.model} on CPU. ` + + "This model may be performing poorly due to its large parameter size, please consider trying smaller models or switch to GPU. " + + "You can find a list of recommend models in the online documentation.
    "; + } + let commonHelpMessage = ""; + if (helpMessageForRunningLargeModelOnCPU.length == 0) { + commonHelpMessage += `
  • The running model ${ + serverHealth?.model ?? "" + } may be performing poorly due to its large parameter size. `; + commonHelpMessage += + "Please consider trying smaller models. You can find a list of recommend models in the online documentation.
  • "; + } + if (endpoint) { + const host = new URL(endpoint).host; + if (!(host.startsWith("localhost") || host.startsWith("127.0.0.1") || host.startsWith("0.0.0.0"))) { + commonHelpMessage += "
  • A poor network connection. Please check your network and proxy settings.
  • "; + commonHelpMessage += "
  • Server overload. Please contact your Tabby server administrator for assistance.
  • "; + } + } + let helpMessage = ""; + if (helpMessageForRunningLargeModelOnCPU.length > 0) { + helpMessage += helpMessageForRunningLargeModelOnCPU + "
    "; + if (commonHelpMessage.length > 0) { + helpMessage += "Other possible causes of this issue:
      " + commonHelpMessage + "
    "; + } + } else { + // commonHelpMessage should not be empty here + helpMessage += "Possible causes of this issue:
      " + commonHelpMessage + "
    "; + } + + const message = statsMessage + helpMessage; + if (outputFormat == "html") { + return message; + } + if (outputFormat == "markdown") { + return message + .replace(//g, " \n") + .replace(/(.*?)<\/i>/g, "*$1*") + .replace(/]*?\s+)?href=["']([^"']+)["'][^>]*>([^<]+)<\/a>/g, "[$2]($1)") + .replace(/]*>(.*?)<\/ul>/g, "$1") + .replace(/]*>(.*?)<\/li>/g, "- $1 \n"); + } else { + return message + .replace(//g, " \n") + .replace(/(.*?)<\/i>/g, "$1") + .replace(/]*>(.*?)<\/a>/g, "$1") + .replace(/]*>(.*?)<\/ul>/g, "$1") + .replace(/]*>(.*?)<\/li>/g, "- $1 \n"); + } +} diff --git a/clients/tabby-agent/src/codeCompletion/postprocess/base.ts b/clients/tabby-agent/src/codeCompletion/postprocess/base.ts new file mode 100644 index 000000000000..4428c704ad5f --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/postprocess/base.ts @@ -0,0 +1,16 @@ +import type { CompletionResultItem } from "../solution"; +import type { CompletionContext, CompletionExtraContexts } from "../contexts"; +import type { ConfigData } from "../../config/type"; +import { getLogger } from "../../logger"; + +export type PostprocessFilterFactory = + | (() => PostprocessFilter) + | ((config: ConfigData["postprocess"]) => PostprocessFilter); + +export type PostprocessFilter = ( + input: CompletionResultItem, + context: CompletionContext, + extraContext: CompletionExtraContexts, +) => CompletionResultItem | Promise; + +export const logger = getLogger("Postprocess"); diff --git a/clients/tabby-agent/src/codeCompletion/postprocess/dropDuplicated.test.ts b/clients/tabby-agent/src/codeCompletion/postprocess/dropDuplicated.test.ts new file mode 100644 index 000000000000..9816dddf4743 --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/postprocess/dropDuplicated.test.ts @@ -0,0 +1,37 @@ +import { documentContext, inline, assertFilterResult } from "./testUtils"; +import { dropDuplicated } from "./dropDuplicated"; +import { emptyCompletionResultItem } from "../solution"; + +describe("postprocess", () => { + describe("dropDuplicated", () => { + const filter = dropDuplicated(); + it("should drop completion duplicated with suffix", async () => { + const context = documentContext`javascript + let sum = (a, b) => { + ║return a + b; + }; + `; + // completion give a `;` at end but context have not + const completion = inline` + ├return a + b;┤ + `; + const expected = emptyCompletionResultItem; + await assertFilterResult(filter, context, completion, expected); + }); + + it("should drop completion similar to suffix", async () => { + const context = documentContext`javascript + let sum = (a, b) => { + return a + b; + ║ + }; + `; + // the difference is a `\n` + const completion = inline` + ├}┤ + `; + const expected = emptyCompletionResultItem; + await assertFilterResult(filter, context, completion, expected); + }); + }); +}); diff --git a/clients/tabby-agent/src/codeCompletion/postprocess/dropDuplicated.ts b/clients/tabby-agent/src/codeCompletion/postprocess/dropDuplicated.ts new file mode 100644 index 000000000000..ff7b86fd1c5f --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/postprocess/dropDuplicated.ts @@ -0,0 +1,41 @@ +import { PostprocessFilter, logger } from "./base"; +import { CompletionResultItem, emptyCompletionResultItem } from "../solution"; +import { CompletionContext } from "../contexts"; +import { isBlank, calcDistance } from "../../utils/string"; + +export function dropDuplicated(): PostprocessFilter { + return (item: CompletionResultItem, context: CompletionContext): CompletionResultItem => { + // get first n (n <= 3) lines of input and suffix, ignore blank lines + const { suffixLines } = context; + const inputLines = item.lines; + let inputIndex = 0; + while (inputIndex < inputLines.length && isBlank(inputLines[inputIndex]!)) { + inputIndex++; + } + let suffixIndex = 0; + while (suffixIndex < suffixLines.length && isBlank(suffixLines[suffixIndex]!)) { + suffixIndex++; + } + const lineCount = Math.min(3, inputLines.length - inputIndex, suffixLines.length - suffixIndex); + if (lineCount < 1) { + return item; + } + const inputToCompare = inputLines + .slice(inputIndex, inputIndex + lineCount) + .join("") + .trim(); + const suffixToCompare = suffixLines + .slice(suffixIndex, suffixIndex + lineCount) + .join("") + .trim(); + // if string distance is less than threshold (threshold = 1, or 5% of string length) + // drop this completion due to duplicated + const threshold = Math.max(1, 0.05 * inputToCompare.length, 0.05 * suffixToCompare.length); + const distance = calcDistance(inputToCompare, suffixToCompare); + if (distance <= threshold) { + logger.trace("Drop completion due to duplicated.", { inputToCompare, suffixToCompare, distance, threshold }); + return emptyCompletionResultItem; + } + return item; + }; +} diff --git a/clients/tabby-agent/src/codeCompletion/postprocess/dropMinimum.test.ts b/clients/tabby-agent/src/codeCompletion/postprocess/dropMinimum.test.ts new file mode 100644 index 000000000000..d41bc28ef303 --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/postprocess/dropMinimum.test.ts @@ -0,0 +1,24 @@ +import { dropMinimum } from "./dropMinimum"; +import { documentContext, assertFilterResult } from "./testUtils"; +import { emptyCompletionResultItem } from "../solution"; + +describe("postprocess", () => { + describe("dropMinimum", () => { + const filter = dropMinimum({ limitScope: null, minCompletionChars: 4, calculateReplaceRange: null }); + const context = documentContext` + dummy║ + `; + + it("should return null if input is < 4 non-whitespace characters", async () => { + const expected = emptyCompletionResultItem; + await assertFilterResult(filter, context, "\n", expected); + await assertFilterResult(filter, context, "\t\n", expected); + await assertFilterResult(filter, context, "ab\t\n", expected); + }); + it("should keep unchanged if input is >= 4 non-whitespace characters", async () => { + const completion = "Greater than 4"; + const expected = completion; + await assertFilterResult(filter, context, completion, expected); + }); + }); +}); diff --git a/clients/tabby-agent/src/codeCompletion/postprocess/dropMinimum.ts b/clients/tabby-agent/src/codeCompletion/postprocess/dropMinimum.ts new file mode 100644 index 000000000000..6f4b6580c9c2 --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/postprocess/dropMinimum.ts @@ -0,0 +1,12 @@ +import { PostprocessFilter } from "./base"; +import { CompletionResultItem, emptyCompletionResultItem } from "../solution"; +import { ConfigData } from "../../config/type"; + +export function dropMinimum(config: ConfigData["postprocess"]): PostprocessFilter { + return (item: CompletionResultItem): CompletionResultItem => { + if (item.text.trim().length < config.minCompletionChars) { + return emptyCompletionResultItem; + } + return item; + }; +} diff --git a/clients/tabby-agent/src/codeCompletion/postprocess/formatIndentation.test.ts b/clients/tabby-agent/src/codeCompletion/postprocess/formatIndentation.test.ts new file mode 100644 index 000000000000..2a620be802cc --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/postprocess/formatIndentation.test.ts @@ -0,0 +1,148 @@ +import { documentContext, inline, assertFilterResult } from "./testUtils"; +import { formatIndentation } from "./formatIndentation"; + +describe("postprocess", () => { + describe("formatIndentation", () => { + const filter = formatIndentation(); + it("should format indentation if first line of completion is over indented.", async () => { + const context = documentContext`typescript + function clamp(n: number, max: number, min: number): number { + ║ + } + `; + const indentation = " "; + const completion = inline` + ├ return Math.max(Math.min(n, max), min);┤ + `; + const expected = inline` + ├return Math.max(Math.min(n, max), min);┤ + `; + await assertFilterResult(filter, { ...context, editorOptions: { indentation } }, completion, expected); + }); + + it("should format indentation if first line of completion is wrongly indented.", async () => { + const context = documentContext`typescript + function clamp(n: number, max: number, min: number): number { + ║ + } + `; + const indentation = " "; + const completion = inline` + ├ return Math.max(Math.min(n, max), min);┤ + `; + const expected = inline` + ├ return Math.max(Math.min(n, max), min);┤ + `; + await assertFilterResult(filter, { ...context, editorOptions: { indentation } }, completion, expected); + }); + + it("should format indentation if completion lines is over indented.", async () => { + const context = documentContext`python + def findMax(arr):║ + `; + const indentation = " "; + const completion = inline` + ├ + max = arr[0] + for i in range(1, len(arr)): + if arr[i] > max: + max = arr[i] + return max + }┤ + `; + const expected = inline` + ├ + max = arr[0] + for i in range(1, len(arr)): + if arr[i] > max: + max = arr[i] + return max + }┤ + `; + await assertFilterResult(filter, { ...context, editorOptions: { indentation } }, completion, expected); + }); + + it("should format indentation if completion lines is wrongly indented.", async () => { + const context = documentContext`python + def findMax(arr):║ + `; + const indentation = " "; + const completion = inline` + ├ + max = arr[0] + for i in range(1, len(arr)): + if arr[i] > max: + max = arr[i] + return max + }┤ + `; + const expected = inline` + ├ + max = arr[0] + for i in range(1, len(arr)): + if arr[i] > max: + max = arr[i] + return max + }┤ + `; + await assertFilterResult(filter, { ...context, editorOptions: { indentation } }, completion, expected); + }); + + it("should keep it unchanged if it no indentation specified.", async () => { + const context = documentContext`python + def findMax(arr):║ + `; + const indentation = undefined; + const completion = inline` + ├ + max = arr[0] + for i in range(1, len(arr)): + if arr[i] > max: + max = arr[i] + return max + }┤ + `; + const expected = completion; + await assertFilterResult(filter, { ...context, editorOptions: indentation }, completion, expected); + }); + + it("should keep it unchanged if there is indentation in the context.", async () => { + const context = documentContext`python + def hello(): + return "world" + + def findMax(arr):║ + `; + const indentation = "\t"; + const completion = inline` + ├ + max = arr[0] + for i in range(1, len(arr)): + if arr[i] > max: + max = arr[i] + return max + }┤ + `; + const expected = completion; + await assertFilterResult(filter, { ...context, editorOptions: { indentation } }, completion, expected); + }); + + it("should keep it unchanged if it is well indented.", async () => { + const context = documentContext`python + def findMax(arr):║ + `; + const indentation = " "; + const completion = inline` + ├ + max = arr[0] + for i in range(1, len(arr)): + if arr[i] > max: + max = arr[i] + return max + }┤ + `; + const expected = completion; + await assertFilterResult(filter, { ...context, editorOptions: { indentation } }, completion, expected); + }); + }); +}); diff --git a/clients/tabby-agent/src/postprocess/formatIndentation.ts b/clients/tabby-agent/src/codeCompletion/postprocess/formatIndentation.ts similarity index 79% rename from clients/tabby-agent/src/postprocess/formatIndentation.ts rename to clients/tabby-agent/src/codeCompletion/postprocess/formatIndentation.ts index a190a880fbac..2c850fbf3331 100644 --- a/clients/tabby-agent/src/postprocess/formatIndentation.ts +++ b/clients/tabby-agent/src/codeCompletion/postprocess/formatIndentation.ts @@ -1,6 +1,7 @@ -import { CompletionContext } from "../CompletionContext"; import { PostprocessFilter, logger } from "./base"; -import { isBlank, splitLines } from "../utils"; +import { CompletionResultItem } from "../solution"; +import { CompletionContext, CompletionExtraContexts } from "../contexts"; +import { isBlank } from "../../utils/string"; function detectIndentation(lines: string[]): string | null { const matches = { @@ -45,13 +46,18 @@ function getIndentLevel(line: string, indentation: string): number { } export function formatIndentation(): PostprocessFilter { - return (input: string, context: CompletionContext) => { - const { prefixLines, suffixLines, currentLinePrefix, indentation } = context; - const inputLines = splitLines(input); + return ( + item: CompletionResultItem, + context: CompletionContext, + extraContext: CompletionExtraContexts, + ): CompletionResultItem => { + const { prefixLines, suffixLines, currentLinePrefix } = context; + const { indentation } = extraContext.editorOptions ?? {}; + const inputLines = item.lines; // if no indentation is specified if (!indentation) { - return input; + return item; } // if there is any indentation in context, the server output should have learned from it @@ -59,11 +65,11 @@ export function formatIndentation(): PostprocessFilter { ? prefixLines.slice(0, prefixLines.length - 1) : prefixLines; if (prefixLines.length > 1 && detectIndentation(prefixLinesForDetection) !== null) { - return input; + return item; } const suffixLinesForDetection = suffixLines.slice(1); if (suffixLines.length > 1 && detectIndentation(suffixLinesForDetection) !== null) { - return input; + return item; } // if the input is well indented with specific indentation @@ -72,7 +78,7 @@ export function formatIndentation(): PostprocessFilter { }); const inputIndentation = detectIndentation(inputLinesForDetection); if (inputIndentation === null || inputIndentation === indentation) { - return input; + return item; } // otherwise, do formatting @@ -94,7 +100,7 @@ export function formatIndentation(): PostprocessFilter { return indentation.repeat(level) + rest; } }); - logger.debug({ prefixLines, suffixLines, inputLines, formatted }, "Format indentation."); - return formatted.join(""); + logger.trace("Format indentation.", { inputLines, formatted }); + return item.withText(formatted.join("")); }; } diff --git a/clients/tabby-agent/src/postprocess/golden/basic_fibonacci_test/python.toml b/clients/tabby-agent/src/codeCompletion/postprocess/golden/basic_fibonacci_test/python.toml similarity index 100% rename from clients/tabby-agent/src/postprocess/golden/basic_fibonacci_test/python.toml rename to clients/tabby-agent/src/codeCompletion/postprocess/golden/basic_fibonacci_test/python.toml diff --git a/clients/tabby-agent/src/postprocess/golden/basic_fibonacci_test/rust.toml b/clients/tabby-agent/src/codeCompletion/postprocess/golden/basic_fibonacci_test/rust.toml similarity index 100% rename from clients/tabby-agent/src/postprocess/golden/basic_fibonacci_test/rust.toml rename to clients/tabby-agent/src/codeCompletion/postprocess/golden/basic_fibonacci_test/rust.toml diff --git a/clients/tabby-agent/src/postprocess/golden/basic_fibonacci_test/typescript.toml b/clients/tabby-agent/src/codeCompletion/postprocess/golden/basic_fibonacci_test/typescript.toml similarity index 100% rename from clients/tabby-agent/src/postprocess/golden/basic_fibonacci_test/typescript.toml rename to clients/tabby-agent/src/codeCompletion/postprocess/golden/basic_fibonacci_test/typescript.toml diff --git a/clients/tabby-agent/src/postprocess/golden/limit_scope/basic.toml b/clients/tabby-agent/src/codeCompletion/postprocess/golden/limit_scope/basic.toml similarity index 100% rename from clients/tabby-agent/src/postprocess/golden/limit_scope/basic.toml rename to clients/tabby-agent/src/codeCompletion/postprocess/golden/limit_scope/basic.toml diff --git a/clients/tabby-agent/src/postprocess/golden/limit_scope/fill_in_line_01.toml b/clients/tabby-agent/src/codeCompletion/postprocess/golden/limit_scope/fill_in_line_01.toml similarity index 100% rename from clients/tabby-agent/src/postprocess/golden/limit_scope/fill_in_line_01.toml rename to clients/tabby-agent/src/codeCompletion/postprocess/golden/limit_scope/fill_in_line_01.toml diff --git a/clients/tabby-agent/src/codeCompletion/postprocess/golden/limit_scope/rust_01.toml b/clients/tabby-agent/src/codeCompletion/postprocess/golden/limit_scope/rust_01.toml new file mode 100644 index 000000000000..ea602754cdf3 --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/postprocess/golden/limit_scope/rust_01.toml @@ -0,0 +1,27 @@ +description = 'Limit scope: rust 01' + +[config] +# use default config + +[context] +filepath = 'stop.rs' +language = 'rust' +# indentation = ' ' # not specified +text = ''' +pub struct StopCondition { + stop_re: Option, + max_decoding_length: ├usize, + max_decoding_time: Duration, +}┤ +} +''' + +[expected] +text = ''' +pub struct StopCondition { + stop_re: Option, + max_decoding_length: ├usize, + max_decoding_time: Duration,┤ +} +''' +notEqual = true \ No newline at end of file diff --git a/clients/tabby-agent/src/codeCompletion/postprocess/golden/limit_scope/rust_02.toml b/clients/tabby-agent/src/codeCompletion/postprocess/golden/limit_scope/rust_02.toml new file mode 100644 index 000000000000..df6ea856f739 --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/postprocess/golden/limit_scope/rust_02.toml @@ -0,0 +1,25 @@ +description = 'Limit scope: rust 02' + +[config] +minCompletionChars = 1 + +[context] +filepath = 'stop.rs' +language = 'rust' +# indentation = ' ' # not specified +text = ''' +fn add(x: i32, y: i32) -> i32 { + println!("x:├ {}", x); + println!("y: {}", y); + println!("x + y: {}┤", x); + return x + y; +} +''' + +[expected] +text = ''' +fn add(x: i32, y: i32) -> i32 { + println!("x:├ {}┤", x); + return x + y; +} +''' diff --git a/clients/tabby-agent/src/postprocess/golden/limit_scope/to_block_01.toml b/clients/tabby-agent/src/codeCompletion/postprocess/golden/limit_scope/to_block_01.toml similarity index 100% rename from clients/tabby-agent/src/postprocess/golden/limit_scope/to_block_01.toml rename to clients/tabby-agent/src/codeCompletion/postprocess/golden/limit_scope/to_block_01.toml diff --git a/clients/tabby-agent/src/postprocess/golden/limit_scope/to_block_02.toml b/clients/tabby-agent/src/codeCompletion/postprocess/golden/limit_scope/to_block_02.toml similarity index 100% rename from clients/tabby-agent/src/postprocess/golden/limit_scope/to_block_02.toml rename to clients/tabby-agent/src/codeCompletion/postprocess/golden/limit_scope/to_block_02.toml diff --git a/clients/tabby-agent/src/postprocess/golden/limit_scope/to_block_03.toml b/clients/tabby-agent/src/codeCompletion/postprocess/golden/limit_scope/to_block_03.toml similarity index 75% rename from clients/tabby-agent/src/postprocess/golden/limit_scope/to_block_03.toml rename to clients/tabby-agent/src/codeCompletion/postprocess/golden/limit_scope/to_block_03.toml index b72928f54667..19726d39cb84 100644 --- a/clients/tabby-agent/src/postprocess/golden/limit_scope/to_block_03.toml +++ b/clients/tabby-agent/src/codeCompletion/postprocess/golden/limit_scope/to_block_03.toml @@ -1,9 +1,7 @@ description = 'Limit scope: limit to block scope: case 03' -[config.limitScope] -experimentalSyntax = false -[config.limitScope.indentation] -experimentalKeepBlockScopeWhenCompletingLine = false +[config] +minCompletionChars = 1 [context] filepath = 'checks.js' diff --git a/clients/tabby-agent/src/postprocess/golden/limit_scope/to_line_01.toml b/clients/tabby-agent/src/codeCompletion/postprocess/golden/limit_scope/to_line_01.toml similarity index 80% rename from clients/tabby-agent/src/postprocess/golden/limit_scope/to_line_01.toml rename to clients/tabby-agent/src/codeCompletion/postprocess/golden/limit_scope/to_line_01.toml index 427c696738c6..c4c4434848b7 100644 --- a/clients/tabby-agent/src/postprocess/golden/limit_scope/to_line_01.toml +++ b/clients/tabby-agent/src/codeCompletion/postprocess/golden/limit_scope/to_line_01.toml @@ -1,9 +1,7 @@ description = 'Limit scope: limit to single line when completing a line: case 01' -[config.limitScope] -experimentalSyntax = false -[config.limitScope.indentation] -experimentalKeepBlockScopeWhenCompletingLine = false +[config] +# use default config [context] filepath = 'foo.ts' diff --git a/clients/tabby-agent/src/codeCompletion/postprocess/golden/limit_scope/typescript_01.toml b/clients/tabby-agent/src/codeCompletion/postprocess/golden/limit_scope/typescript_01.toml new file mode 100644 index 000000000000..c7be5a500c2e --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/postprocess/golden/limit_scope/typescript_01.toml @@ -0,0 +1,30 @@ +description = 'Limit scope: typescript 01' + +[config] +# use default config + +[context] +filepath = 'foo.ts' +language = 'typescript' +# indentation = ' ' # not specified +text = ''' +export class Foo { + private _foo: number; + + constructor() { + this._foo = 1; + } + + update(value): Foo { + ├this._foo = value; + return this;┤ + } + + get foo(): number { + return this._foo; + } +} +''' + +[expected] +unchanged = true diff --git a/clients/tabby-agent/src/postprocess/golden/remove_duplication/basic.toml b/clients/tabby-agent/src/codeCompletion/postprocess/golden/remove_duplication/basic.toml similarity index 100% rename from clients/tabby-agent/src/postprocess/golden/remove_duplication/basic.toml rename to clients/tabby-agent/src/codeCompletion/postprocess/golden/remove_duplication/basic.toml diff --git a/clients/tabby-agent/src/postprocess/golden/remove_duplication/duplicated_block_closing_line_01.toml b/clients/tabby-agent/src/codeCompletion/postprocess/golden/remove_duplication/duplicated_block_closing_line_01.toml similarity index 100% rename from clients/tabby-agent/src/postprocess/golden/remove_duplication/duplicated_block_closing_line_01.toml rename to clients/tabby-agent/src/codeCompletion/postprocess/golden/remove_duplication/duplicated_block_closing_line_01.toml diff --git a/clients/tabby-agent/src/postprocess/golden/remove_duplication/duplicated_line_suffix.toml b/clients/tabby-agent/src/codeCompletion/postprocess/golden/remove_duplication/duplicated_line_suffix.toml similarity index 100% rename from clients/tabby-agent/src/postprocess/golden/remove_duplication/duplicated_line_suffix.toml rename to clients/tabby-agent/src/codeCompletion/postprocess/golden/remove_duplication/duplicated_line_suffix.toml diff --git a/clients/tabby-agent/src/postprocess/golden/remove_duplication/similar_lines_01.toml b/clients/tabby-agent/src/codeCompletion/postprocess/golden/remove_duplication/similar_lines_01.toml similarity index 100% rename from clients/tabby-agent/src/postprocess/golden/remove_duplication/similar_lines_01.toml rename to clients/tabby-agent/src/codeCompletion/postprocess/golden/remove_duplication/similar_lines_01.toml diff --git a/clients/tabby-agent/src/postprocess/golden/remove_duplication/similar_lines_02.toml b/clients/tabby-agent/src/codeCompletion/postprocess/golden/remove_duplication/similar_lines_02.toml similarity index 100% rename from clients/tabby-agent/src/postprocess/golden/remove_duplication/similar_lines_02.toml rename to clients/tabby-agent/src/codeCompletion/postprocess/golden/remove_duplication/similar_lines_02.toml diff --git a/clients/tabby-agent/src/postprocess/golden/replace_range/basic.toml b/clients/tabby-agent/src/codeCompletion/postprocess/golden/replace_range/basic.toml similarity index 100% rename from clients/tabby-agent/src/postprocess/golden/replace_range/basic.toml rename to clients/tabby-agent/src/codeCompletion/postprocess/golden/replace_range/basic.toml diff --git a/clients/tabby-agent/src/codeCompletion/postprocess/golden/replace_range/javascript_01.toml b/clients/tabby-agent/src/codeCompletion/postprocess/golden/replace_range/javascript_01.toml new file mode 100644 index 000000000000..1853be872365 --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/postprocess/golden/replace_range/javascript_01.toml @@ -0,0 +1,25 @@ +description = 'Replace range: javascript 01' + +[config] +# use default config + +[context] +filepath = 'listener.js' +language = 'javascript' +# indentation = ' ' # not specified +text = ''' +const stream = process.stdin; +// just print data string +stream.on('data', (data) => {├ + console.log(data.toString()); +});┤}) +''' + +[expected] +text = ''' +const stream = process.stdin; +// just print data string +stream.on('data', (data) => {├ + console.log(data.toString()); +});┤})╣ +''' diff --git a/clients/tabby-agent/src/postprocess/golden/replace_range/jsx_tags_01.toml b/clients/tabby-agent/src/codeCompletion/postprocess/golden/replace_range/jsx_tags_01.toml similarity index 100% rename from clients/tabby-agent/src/postprocess/golden/replace_range/jsx_tags_01.toml rename to clients/tabby-agent/src/codeCompletion/postprocess/golden/replace_range/jsx_tags_01.toml diff --git a/clients/tabby-agent/src/postprocess/golden/replace_range/jsx_tags_02.toml b/clients/tabby-agent/src/codeCompletion/postprocess/golden/replace_range/jsx_tags_02.toml similarity index 100% rename from clients/tabby-agent/src/postprocess/golden/replace_range/jsx_tags_02.toml rename to clients/tabby-agent/src/codeCompletion/postprocess/golden/replace_range/jsx_tags_02.toml diff --git a/clients/tabby-agent/src/codeCompletion/postprocess/golden/replace_range/mismatched_01.toml b/clients/tabby-agent/src/codeCompletion/postprocess/golden/replace_range/mismatched_01.toml new file mode 100644 index 000000000000..587536e17986 --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/postprocess/golden/replace_range/mismatched_01.toml @@ -0,0 +1,29 @@ +description = 'Replace range: mismatched case 01' + +[config] +# use default config + +[context] +filepath = 'fibonacci.ts' +language = 'typescript' +# indentation = ' ' # not specified +text = ''' +def fibonacci(├n): + if n == 0: + return 0 + elif n == 1: + return 1 + else: + return fibonacci(n - 1) + fibonacci(n - 2)┤) +''' + +[expected] +text = ''' +def fibonacci(├n): + if n == 0: + return 0 + elif n == 1: + return 1 + else: + return fibonacci(n - 1) + fibonacci(n - 2)┤)╣ +''' diff --git a/clients/tabby-agent/src/postprocess/golden/replace_range/multiple_01.toml b/clients/tabby-agent/src/codeCompletion/postprocess/golden/replace_range/multiple_01.toml similarity index 100% rename from clients/tabby-agent/src/postprocess/golden/replace_range/multiple_01.toml rename to clients/tabby-agent/src/codeCompletion/postprocess/golden/replace_range/multiple_01.toml diff --git a/clients/tabby-agent/src/postprocess/golden/replace_range/multiple_02.toml b/clients/tabby-agent/src/codeCompletion/postprocess/golden/replace_range/multiple_02.toml similarity index 100% rename from clients/tabby-agent/src/postprocess/golden/replace_range/multiple_02.toml rename to clients/tabby-agent/src/codeCompletion/postprocess/golden/replace_range/multiple_02.toml diff --git a/clients/tabby-agent/src/codeCompletion/postprocess/golden/replace_range/rust_01.toml b/clients/tabby-agent/src/codeCompletion/postprocess/golden/replace_range/rust_01.toml new file mode 100644 index 000000000000..8b4c8d5cde79 --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/postprocess/golden/replace_range/rust_01.toml @@ -0,0 +1,27 @@ +description = 'Replace range: rust 01' + +[config] +# use default config + +[context] +filepath = 'myTest.rs' +language = 'rust' +# indentation = ' ' # not specified +text = ''' +pub fun myTest(&self, text: String) { + let mut body = HashMap::new(); + body.insert("├text".to_string(), text);┤") + let mut headers = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); +} +''' + +[expected] +text = ''' +pub fun myTest(&self, text: String) { + let mut body = HashMap::new(); + body.insert("├text".to_string(), text);┤")╣ + let mut headers = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); +} +''' diff --git a/clients/tabby-agent/src/codeCompletion/postprocess/golden/replace_range/typescript_01.toml b/clients/tabby-agent/src/codeCompletion/postprocess/golden/replace_range/typescript_01.toml new file mode 100644 index 000000000000..f3573131ae19 --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/postprocess/golden/replace_range/typescript_01.toml @@ -0,0 +1,21 @@ +description = 'Replace range: typescript 01' + +[config] +# use default config + +[context] +filepath = 'print.ts' +language = 'typescript' +# indentation = ' ' # not specified +text = ''' +function print(obj: any) { + console.log("Obj: ├", obj);┤") +} +''' + +[expected] +text = ''' +function print(obj: any) { + console.log("Obj: ├", obj);┤")╣ +} +''' diff --git a/clients/tabby-agent/src/codeCompletion/postprocess/index.ts b/clients/tabby-agent/src/codeCompletion/postprocess/index.ts new file mode 100644 index 000000000000..8f9e5a956aa0 --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/postprocess/index.ts @@ -0,0 +1,75 @@ +import type { ConfigData } from "../../config/type"; +import type { CompletionResultItem } from "../solution"; +import type { CompletionContext, CompletionExtraContexts } from "../contexts"; +import type { PostprocessFilter, PostprocessFilterFactory } from "./base"; +import "../../utils/array"; +import { removeRepetitiveBlocks } from "./removeRepetitiveBlocks"; +import { removeRepetitiveLines } from "./removeRepetitiveLines"; +import { removeLineEndsWithRepetition } from "./removeLineEndsWithRepetition"; +import { removeDuplicatedBlockClosingLine } from "./removeDuplicatedBlockClosingLine"; +import { limitScope } from "./limitScope"; +import { formatIndentation } from "./formatIndentation"; +import { trimSpace } from "./trimSpace"; +import { trimMultiLineInSingleLineMode } from "./trimMultiLineInSingleLineMode"; +import { dropDuplicated } from "./dropDuplicated"; +import { dropMinimum } from "./dropMinimum"; +import { removeDuplicateSuffixLines } from "./removeDuplicateSuffixLines"; +import { normalizeIndentation } from "./normalizeIndentation"; + +export interface ItemsWithContext { + items: CompletionResultItem[]; + context: CompletionContext; + extraContext: CompletionExtraContexts; +} +type ItemsFilter = (params: ItemsWithContext) => Promise; + +function createListFilter(filterFactory: PostprocessFilterFactory, config: ConfigData["postprocess"]): ItemsFilter { + const filter: PostprocessFilter = filterFactory(config); + return async (params: ItemsWithContext): Promise => { + const processed = await params.items.mapAsync(async (item) => { + return await filter(item, params.context, params.extraContext); + }); + return { items: processed, context: params.context, extraContext: params.extraContext }; + }; +} + +export async function preCacheProcess( + items: CompletionResultItem[], + context: CompletionContext, + extraContext: CompletionExtraContexts, + config: ConfigData["postprocess"], +): Promise { + const applyFilter = (filterFactory: PostprocessFilterFactory): ItemsFilter => { + return createListFilter(filterFactory, config); + }; + const result = await Promise.resolve({ items, context, extraContext }) + .then(applyFilter(trimMultiLineInSingleLineMode)) + .then(applyFilter(removeLineEndsWithRepetition)) + .then(applyFilter(dropDuplicated)) + .then(applyFilter(trimSpace)) + .then(applyFilter(dropMinimum)); + return result.items; +} + +export async function postCacheProcess( + items: CompletionResultItem[], + context: CompletionContext, + extraContext: CompletionExtraContexts, + config: ConfigData["postprocess"], +): Promise { + const applyFilter = (filterFactory: PostprocessFilterFactory): ItemsFilter => { + return createListFilter(filterFactory, config); + }; + const result = await Promise.resolve({ items, context, extraContext }) + .then(applyFilter(removeRepetitiveBlocks)) + .then(applyFilter(removeRepetitiveLines)) + .then(applyFilter(limitScope)) + .then(applyFilter(removeDuplicatedBlockClosingLine)) + .then(applyFilter(formatIndentation)) + .then(applyFilter(normalizeIndentation)) + .then(applyFilter(dropDuplicated)) + .then(applyFilter(trimSpace)) + .then(applyFilter(removeDuplicateSuffixLines)) + .then(applyFilter(dropMinimum)); + return result.items; +} diff --git a/clients/tabby-agent/src/codeCompletion/postprocess/limitScope.ts b/clients/tabby-agent/src/codeCompletion/postprocess/limitScope.ts new file mode 100644 index 000000000000..17ee8038b803 --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/postprocess/limitScope.ts @@ -0,0 +1,14 @@ +import { PostprocessFilter } from "./base"; +import { CompletionResultItem } from "../solution"; +import { CompletionContext, CompletionExtraContexts } from "../contexts"; +import { limitScopeByIndentation } from "./limitScopeByIndentation"; + +export function limitScope(): PostprocessFilter { + return async ( + item: CompletionResultItem, + context: CompletionContext, + extraContext: CompletionExtraContexts, + ): Promise => { + return limitScopeByIndentation()(item, context, extraContext); + }; +} diff --git a/clients/tabby-agent/src/codeCompletion/postprocess/limitScopeByIndentation.test.ts b/clients/tabby-agent/src/codeCompletion/postprocess/limitScopeByIndentation.test.ts new file mode 100644 index 000000000000..b773e5f2bea8 --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/postprocess/limitScopeByIndentation.test.ts @@ -0,0 +1,254 @@ +import { documentContext, inline, assertFilterResult, assertFilterResultNotEqual } from "./testUtils"; +import { limitScopeByIndentation } from "./limitScopeByIndentation"; + +describe("postprocess", () => { + describe("limitScopeByIndentation", () => { + const filter = limitScopeByIndentation(); + it("should limit scope at sentence end, when completion is continuing uncompleted sentence in the prefix.", async () => { + const context = documentContext`javascript + let a =║ + `; + const completion = inline` + ├ 1; + let b = 2;┤ + `; + const expected = inline` + ├ 1;┤ + `; + await assertFilterResult(filter, context, completion, expected); + }); + + it("should limit scope at sentence end, when completion is continuing uncompleted sentence in the prefix.", async () => { + const context = documentContext`javascript + function safeParse(json) { + try { + console.log║ + } catch (error) { + console.error(error); + return null; + } + } + `; + const completion = inline` + ├("Parsing", { json }); + return JSON.parse(json); + } catch (e) { + return null; + } + }┤ + `; + const expected = inline` + ├("Parsing", { json });┤ + `; + await assertFilterResult(filter, context, completion, expected); + }); + + it("should limit scope at next indent level, including closing line, when completion is starting a new indent level in next line.", async () => { + const context = documentContext`javascript + function findMax(arr) {║} + `; + const completion = inline` + ├ + let max = arr[0]; + for (let i = 1; i < arr.length; i++) { + if (arr[i] > max) { + max = arr[i]; + } + } + return max; + } + console.log(findMax([1, 2, 3, 4, 5]));┤ + `; + const expected = inline` + ├ + let max = arr[0]; + for (let i = 1; i < arr.length; i++) { + if (arr[i] > max) { + max = arr[i]; + } + } + return max; + }┤ + `; + await assertFilterResult(filter, context, completion, expected); + }); + + it("should limit scope at next indent level, including closing line, when completion is continuing uncompleted sentence in the prefix, and starting a new indent level in next line.", async () => { + const context = documentContext`javascript + function findMax(arr) { + let max = arr[0]; + for║ + } + `; + const completion = inline` + ├ (let i = 1; i < arr.length; i++) { + if (arr[i] > max) { + max = arr[i]; + } + } + return max; + } + console.log(findMax([1, 2, 3, 4, 5]));┤ + `; + const expected = inline` + ├ (let i = 1; i < arr.length; i++) { + if (arr[i] > max) { + max = arr[i]; + } + }┤ + ┴┴ + `; + await assertFilterResult(filter, context, completion, expected); + }); + + it("should limit scope at current indent level, including closing line, when completion starts new sentences at same indent level.", async () => { + const context = documentContext`javascript + function findMax(arr) { + let max = arr[0];║ + } + `; + const completion = inline` + ├ + for (let i = 1; i < arr.length; i++) { + if (arr[i] > max) { + max = arr[i]; + } + } + return max; + }┤ + `; + const expected = completion; + await assertFilterResult(filter, context, completion, expected); + }); + + it("should allow only one level closing bracket", async () => { + const context = documentContext`javascript + function safeParse(json) { + try { + return JSON.parse(json); + } catch (e) { + return null;║ + `; + const completion = inline` + ├ + } + }┤ + `; + const expected = inline` + ├ + }┤ + ┴┴ + `; + await assertFilterResult(filter, context, completion, expected); + }); + + it("should allow level closing bracket at current line, it looks same as starts new sentences", async () => { + const context = documentContext`javascript + function helloworld() { + console.log("hello"); + ║ + `; + const completion = inline` + ├}┤ + `; + const expected = completion; + await assertFilterResult(filter, context, completion, expected); + }); + + it("should not allow level closing bracket, when the suffix lines have same indent level", async () => { + const context = documentContext`javascript + function helloworld() { + console.log("hello");║ + console.log("world"); + } + `; + const completion = inline` + ├ + }┤ + `; + const expected = inline` + ├┤`; + await assertFilterResult(filter, context, completion, expected); + }); + + it("should use indent level of previous line, when current line is empty.", async () => { + const context = documentContext`javascript + function safeParse(json) { + try { + ║ + } + } + `; + const completion = inline` + ├return JSON.parse(json); + } catch (e) { + return null; + } + }┤ + `; + const expected = inline` + ├return JSON.parse(json); + } catch (e) { + return null; + }┤ + ┴┴ + `; + await assertFilterResult(filter, context, completion, expected); + }); + }); + + describe("limitScopeByIndentation: bad cases", () => { + const filter = limitScopeByIndentation(); + it("cannot handle the case of indent that does'nt have a close line, e.g. chaining call", async () => { + const context = documentContext`javascript + function sortWords(input) { + const output = input.trim() + .split("\n") + .map((line) => line.split(" ")) + ║ + } + `; + const completion = inline` + ├.flat() + .sort() + .join(" "); + console.log(output); + return output; + } + sortWords("world hello");┤ + `; + const expected = inline` + ├.flat() + .sort() + .join(" "); + console.log(output); + return output; + }┤ + `; + await assertFilterResultNotEqual(filter, context, completion, expected); + }); + + it("cannot handle the case of indent that does'nt have a close line, e.g. python def function", async () => { + const context = documentContext`python + def findMax(arr): + ║ + `; + const completion = inline` + ├max = arr[0] + for i in range(1, len(arr)): + if arr[i] > max: + max = arr[i] + return max + findMax([1, 2, 3, 4, 5])┤ + `; + const expected = inline` + ├max = arr[0] + for i in range(1, len(arr)): + if arr[i] > max: + max = arr[i] + return max┤ + `; + await assertFilterResultNotEqual(filter, context, completion, expected); + }); + }); +}); diff --git a/clients/tabby-agent/src/postprocess/limitScopeByIndentation.ts b/clients/tabby-agent/src/codeCompletion/postprocess/limitScopeByIndentation.ts similarity index 78% rename from clients/tabby-agent/src/postprocess/limitScopeByIndentation.ts rename to clients/tabby-agent/src/codeCompletion/postprocess/limitScopeByIndentation.ts index 213b22e84960..40a2dd3483d9 100644 --- a/clients/tabby-agent/src/postprocess/limitScopeByIndentation.ts +++ b/clients/tabby-agent/src/codeCompletion/postprocess/limitScopeByIndentation.ts @@ -1,13 +1,12 @@ -import { CompletionContext } from "../CompletionContext"; -import { AgentConfig } from "../AgentConfig"; import { PostprocessFilter, logger } from "./base"; -import { isBlank, splitLines, getIndentationLevel, isBlockOpeningLine, isBlockClosingLine } from "../utils"; +import { CompletionContext } from "../contexts"; +import { CompletionResultItem } from "../solution"; +import { isBlank, getIndentationLevel, isBlockOpeningLine, isBlockClosingLine } from "../../utils/string"; function parseIndentationContext( inputLines: string[], inputLinesForDetection: string[], context: CompletionContext, - config: AgentConfig["postprocess"]["limitScope"]["indentation"], ): { indentLevelLimit: number; allowClosingLine: boolean } { const result = { indentLevelLimit: 0, @@ -49,13 +48,9 @@ function parseIndentationContext( if (!isCurrentLineInCompletionBlank && !isCurrentLineInPrefixBlank) { // if two reference lines are contacted at current line, it is continuing uncompleted sentence - if (config.experimentalKeepBlockScopeWhenCompletingLine) { - result.indentLevelLimit = referenceLineInPrefixIndent; - } else { - result.indentLevelLimit = referenceLineInPrefixIndent + 1; // + 1 for comparison, no matter how many spaces indent - // allow closing line only if first line is opening a new indent block - result.allowClosingLine &&= isBlockOpeningLine(inputLinesForDetection, 0); - } + result.indentLevelLimit = referenceLineInPrefixIndent + 1; // + 1 for comparison, no matter how many spaces indent + // allow closing line only if first line is opening a new indent block + result.allowClosingLine &&= isBlockOpeningLine(inputLinesForDetection, 0); } else if (referenceLineInCompletionIndent > referenceLineInPrefixIndent) { // if reference line in completion has more indent than reference line in prefix, it is opening a new indent block result.indentLevelLimit = referenceLineInPrefixIndent + 1; @@ -81,16 +76,16 @@ function parseIndentationContext( return result; } -export function limitScopeByIndentation( - config: AgentConfig["postprocess"]["limitScope"]["indentation"], -): PostprocessFilter { - return (input: string, context: CompletionContext) => { +export function limitScopeByIndentation(): PostprocessFilter { + return (item: CompletionResultItem, context: CompletionContext): CompletionResultItem => { + const { lines: inputLines } = item; const { prefixLines, suffixLines, currentLinePrefix } = context; - const inputLines = splitLines(input); + const language = context.document.languageId; + const inputLinesForDetection = inputLines.map((line, index) => { return index === 0 ? currentLinePrefix + line : line; }); - const indentContext = parseIndentationContext(inputLines, inputLinesForDetection, context, config); + const indentContext = parseIndentationContext(inputLines, inputLinesForDetection, context); let index = 1; while (index < inputLines.length) { const line = inputLines[index]!; @@ -109,7 +104,7 @@ export function limitScopeByIndentation( } // If context allows, we should add the block closing line // For python, if previous line is blank, we don't include this line - if (indentContext.allowClosingLine && (context.language !== "python" || !isBlank(prevLine))) { + if (indentContext.allowClosingLine && (language !== "python" || !isBlank(prevLine))) { index++; } break; @@ -118,12 +113,14 @@ export function limitScopeByIndentation( index++; } if (index < inputLines.length) { - logger.debug( - { inputLines, prefixLines, suffixLines, scopeLineCount: index }, - "Remove content out of indent scope", - ); - return inputLines.slice(0, index).join("").trimEnd(); + logger.trace("Remove content out of indent scope.", { + inputLines, + prefixLines, + suffixLines, + trimAtInputLine: index, + }); + return item.withText(inputLines.slice(0, index).join("").trimEnd()); } - return input; + return item; }; } diff --git a/clients/tabby-agent/src/codeCompletion/postprocess/normalizeIndentation.test.ts b/clients/tabby-agent/src/codeCompletion/postprocess/normalizeIndentation.test.ts new file mode 100644 index 000000000000..ab8e8afa2cbe --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/postprocess/normalizeIndentation.test.ts @@ -0,0 +1,139 @@ +import { documentContext, inline, assertFilterResult } from "./testUtils"; +import { normalizeIndentation } from "./normalizeIndentation"; + +describe("postprocess", () => { + describe("normalizeIndentation", () => { + const filter = normalizeIndentation(); + + it("should fix first line extra indentation", async () => { + const context = documentContext` + function test() { + ║ + } + `; + const completion = inline` + ├ const x = 1; + const y = 2;┤ + `; + const expected = inline` + ├const x = 1; + const y = 2;┤ + `; + await assertFilterResult(filter, context, completion, expected); + }); + it("should remove extra indent", async () => { + const context = documentContext` + if (true) { + if (condition) { + ║ + } + } + `; + const completion = inline` + ├doSomething(); + doAnother();┤ + `; + const expected = inline` + ├doSomething(); + doAnother();┤ + `; + await assertFilterResult(filter, context, completion, expected); + }); + + it("should handle both inappropriate first line and extra indent case 01", async () => { + const context = documentContext` + if (true) { + if (condition) { + ║ + } + } + `; + const completion = inline` + ├ doSomething(); + doAnother();┤ + `; + const expected = inline` + ├doSomething(); + doAnother();┤ + `; + await assertFilterResult(filter, context, completion, expected); + }); + + it("should handle both inappropriate extra indent case 02", async () => { + const context = documentContext` + { + "command": "test", + ║ + } + `; + const completion = inline` + ├"title": "Run Test", + "category": "Tabby"┤ + `; + const expected = inline` + ├"title": "Run Test", + "category": "Tabby"┤ + `; + await assertFilterResult(filter, context, completion, expected); + }); + + it("should do nothing", async () => { + const context = documentContext` + function foo() { + ║ + } + `; + const completion = inline` + ├ bar();┤ + `; + const expected = inline` + ├ bar();┤ + `; + await assertFilterResult(filter, context, completion, expected); + }); + it("should fix extra indent", async () => { + const context = documentContext` + { + ║ + } + `; + const completion = inline` + ├"command": "tabby.inlineCompletion.trigger.automatic", + "title": "Trigger Code Completion Automatically", + "category": "Tabby"┤ + `; + const expected = inline` + ├"command": "tabby.inlineCompletion.trigger.automatic", + "title": "Trigger Code Completion Automatically", + "category": "Tabby"┤ + `; + await assertFilterResult(filter, context, completion, expected); + }); + + it("shouldn't change indent", async () => { + const context = documentContext` + { + "command": "tabby.toggleInlineCompletionTriggerMode", + "title": "Toggle Code Completion Trigger Mode (Automatic/Manual)", + "category": "Tabby" + }, + ║ + `; + const completion = inline` + ├{ + "command": "tabby.inlineCompletion.trigger", + "title": "Trigger Code Completion Manually", + "category": "Tabby" + },┤ + `; + const expected = inline` + ├{ + "command": "tabby.inlineCompletion.trigger", + "title": "Trigger Code Completion Manually", + "category": "Tabby" + },┤ + `; + await assertFilterResult(filter, context, completion, expected); + }); + }); +}); diff --git a/clients/tabby-agent/src/codeCompletion/postprocess/normalizeIndentation.ts b/clients/tabby-agent/src/codeCompletion/postprocess/normalizeIndentation.ts new file mode 100644 index 000000000000..6cc105af3340 --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/postprocess/normalizeIndentation.ts @@ -0,0 +1,84 @@ +import { PostprocessFilter } from "./base"; +import { CompletionContext } from "../contexts"; +import { CompletionResultItem } from "../solution"; + +function isOnlySpaces(str: string | null | undefined): boolean { + if (!str) return true; + return /^\s*$/.test(str); +} + +function getLeadingSpaces(str: string): number { + const match = str.match(/^(\s*)/); + return match ? match[0].length : 0; +} + +/** + * normalizeIndentation postprocess filter. + * + * This function adjusts the indentation of code snippets (lines) based on the + * current cursor's indentation context (context.currentLinePrefix). The primary + * goal is to ensure that the inserted snippet aligns correctly with the surrounding code. + * + * How it works: + * 1. Early returns: The function returns the original item without modification if any of these conditions are met: + * - Lines array is invalid or empty + * - Current line prefix is empty + * - Current line prefix contains a tab character + * + * 2. Space-based indentation check: + * - If the current line prefix contains any non-space characters, return the original item + * - Otherwise, proceed with indentation normalization + * + * 3. Indentation normalization process: + * - For the first line: Calculate total leading spaces (cursor prefix + first line spaces) + * If both the total and the first line spaces are odd, remove all leading spaces from first line + * This prevents misalignment in cases of odd indentation units + * + * 4. Line-by-line normalization: + * - For each non-empty line: if leading spaces count is odd (and > 0), + * reduce it by 1 space to maintain even indentation + * - Empty or whitespace-only lines are skipped + * + * The adjustments ensure consistent indentation alignment throughout the code snippet, + * particularly focusing on maintaining even space counts for proper alignment. + */ +export function normalizeIndentation(): PostprocessFilter { + return (item: CompletionResultItem, context: CompletionContext): CompletionResultItem => { + const { lines } = item; + if ( + !Array.isArray(lines) || + lines.length === 0 || + context.currentLinePrefix.length == 0 || + context.currentLinePrefix.includes("\t") + ) + return item; + + if (!isOnlySpaces(context.currentLinePrefix)) { + return item; + } + + const normalizedLines = [...lines]; + const firstLine = normalizedLines[0]; + const cursorLineSpaces = getLeadingSpaces(context.currentLinePrefix); + if (firstLine) { + const firstLineSpaces = getLeadingSpaces(firstLine); + // deal with current cursor odd indentation + if ((firstLineSpaces + cursorLineSpaces) % 2 !== 0 && firstLineSpaces % 2 !== 0) { + normalizedLines[0] = firstLine.substring(firstLineSpaces); + } + } + + //deal with extra space in the line indent + for (let i = 0; i < normalizedLines.length; i++) { + const line = normalizedLines[i]; + if (!line || !line.trim()) continue; + const lineSpaces = getLeadingSpaces(line); + if (lineSpaces > 0 && lineSpaces % 2 !== 0) { + // move current line to recently close indent + normalizedLines[i] = " ".repeat(lineSpaces - 1) + line.trimStart(); + } + } + + return item.withText(normalizedLines.join("")); + }; +} diff --git a/clients/tabby-agent/src/codeCompletion/postprocess/postprocess.test.ts b/clients/tabby-agent/src/codeCompletion/postprocess/postprocess.test.ts new file mode 100644 index 000000000000..301f8c0b582f --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/postprocess/postprocess.test.ts @@ -0,0 +1,106 @@ +import { TextDocument } from "vscode-languageserver-textdocument"; +import path from "path"; +import fs from "fs-extra"; +import { v4 as uuid } from "uuid"; +import toml from "toml"; +import glob from "glob"; +import { expect } from "chai"; +import { deepmerge } from "deepmerge-ts"; +import { ConfigData } from "../../config/type"; +import { defaultConfigData } from "../../config/default"; +import { CompletionResultItem, emptyCompletionResultItem } from "../solution"; +import { buildCompletionContext, CompletionContext, CompletionExtraContexts } from "../contexts"; +import { preCacheProcess, postCacheProcess } from "./index"; + +type PostprocessConfig = ConfigData["postprocess"]; + +type DocumentContext = { + prefix: string; + completion: string; + replaceSuffix: string; + suffix: string; +}; + +function parseDocContext(text: string): DocumentContext { + const insertStart = text.indexOf("├"); + const insertEnd = text.lastIndexOf("┤"); + let replaceEnd = text.lastIndexOf("╣"); + if (replaceEnd < 0) { + replaceEnd = insertEnd; + } + return { + prefix: text.slice(0, insertStart), + completion: text.slice(insertStart + 1, insertEnd), + replaceSuffix: text.slice(insertEnd + 1, replaceEnd), + suffix: text.slice(replaceEnd + 1), + }; +} + +function getDoc(context: DocumentContext): string { + return context.prefix + context.replaceSuffix + context.suffix; +} + +function getPosition(context: DocumentContext): number { + return context.prefix.length; +} + +function getCompletionText(context: DocumentContext): string { + return context.completion; +} + +describe("postprocess golden test", () => { + const postprocess = async ( + item: CompletionResultItem, + context: CompletionContext, + extraContext: CompletionExtraContexts, + config: PostprocessConfig, + ): Promise => { + let processed = await preCacheProcess([item], context, extraContext, config); + processed = await postCacheProcess(processed, context, extraContext, config); + return processed[0]!; + }; + + const files = glob.sync(path.join(__dirname, "golden/**/*.toml")); + files.sort().forEach((file) => { + const fileContent = fs.readFileSync(file, "utf8"); + const testCase = toml.parse(fileContent); + it(testCase["description"] ?? file, async () => { + const config = deepmerge(defaultConfigData["postprocess"], testCase["config"] ?? {}) as PostprocessConfig; + const docContext = parseDocContext(testCase["context"]?.["text"] ?? ""); + const textDocument = TextDocument.create( + testCase["context"]?.["filepath"] ?? uuid(), + testCase["context"]?.["language"] ?? "plaintext", + 0, + getDoc(docContext), + ); + const context = buildCompletionContext(textDocument, textDocument.positionAt(getPosition(docContext))); + const completionItem = new CompletionResultItem(getCompletionText(docContext)); + const unchanged = completionItem; + const output = await postprocess( + completionItem, + context, + { editorOptions: { indentation: testCase["context"]?.["indentation"] } }, + config, + ); + + const checkExpected = (expected: CompletionResultItem) => { + if (testCase["expected"]?.["notEqual"]) { + expect(output).to.not.deep.equal(expected); + } else { + expect(output).to.deep.equal(expected); + } + }; + + if (testCase["expected"]?.["unchanged"]) { + checkExpected(unchanged); + } else if (testCase["expected"]?.["discard"]) { + const expected = emptyCompletionResultItem; + checkExpected(expected); + } else { + const expectedContext = parseDocContext(testCase["expected"]?.["text"] ?? ""); + const expected = new CompletionResultItem(getCompletionText(expectedContext)); + checkExpected(expected); + } + }); + }); +}); diff --git a/clients/tabby-agent/src/codeCompletion/postprocess/removeDuplicateSuffixLines.test.ts b/clients/tabby-agent/src/codeCompletion/postprocess/removeDuplicateSuffixLines.test.ts new file mode 100644 index 000000000000..f855eb17814e --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/postprocess/removeDuplicateSuffixLines.test.ts @@ -0,0 +1,120 @@ +import { documentContext, inline, assertFilterResult } from "./testUtils"; +import { removeDuplicateSuffixLines } from "./removeDuplicateSuffixLines"; + +describe("removeDuplicateSuffixLines", () => { + const filter = removeDuplicateSuffixLines(); + + it("should remove duplicated array item", async () => { + const context = documentContext` + const items = [ + ║ + { id: 2 }, + { id: 3 } + ]; + `; + const completion = inline` + ├{ id: 1 }, + { id: 2 },┤ + `; + const expected = inline` + ├{ id: 1 },┤ + `; + await assertFilterResult(filter, context, completion, expected); + }); + + it("should handle empty content after cursor", async () => { + const context = documentContext` + const a = ║ + `; + const completion = inline` + ├42;┤ + `; + const expected = completion; + await assertFilterResult(filter, context, completion, expected); + }); + + it("should remove duplicated comma items", async () => { + const context = documentContext` + function example() { + const items = [ + ║ + 4, + 5, + 6 + ]; + } + `; + const completion = inline` + ├1, + 2, + 3, + 4,┤ + `; + const expected = inline` + ├1, + 2, + 3,┤ + `; + await assertFilterResult(filter, context, completion, expected); + }); + + it("should remove duplicate method calls", async () => { + const context = documentContext` + class Example { + constructor() { + ║ + this.setup(); + this.init() + } + } + `; + const completion = inline` + ├this.value = 1; + this.name = "test"; + this.items = []; + this.setup();┤ + `; + const expected = inline` + ├this.value = 1; + this.name = "test"; + this.items = [];┤ + `; + await assertFilterResult(filter, context, completion, expected); + }); + + it("should remove duplicate object properties", async () => { + const context = documentContext` + const config = { + ║ + enabled: true, + debug: false + }; + `; + const completion = inline` + ├name: "test", + value: 42, + items: [], + enabled: true,┤ + `; + const expected = inline` + ├name: "test", + value: 42, + items: [],┤ + `; + await assertFilterResult(filter, context, completion, expected); + }); + + it("should keep content when no matches", async () => { + const context = documentContext` + function process() { + ║ + console.log("done"); + } + `; + const completion = inline` + ├console.log("processing");┤ + `; + const expected = completion; + await assertFilterResult(filter, context, completion, expected); + }); +}); diff --git a/clients/tabby-agent/src/codeCompletion/postprocess/removeDuplicateSuffixLines.ts b/clients/tabby-agent/src/codeCompletion/postprocess/removeDuplicateSuffixLines.ts new file mode 100644 index 000000000000..af10e9f3ebc0 --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/postprocess/removeDuplicateSuffixLines.ts @@ -0,0 +1,51 @@ +import { PostprocessFilter } from "./base"; +import { CompletionResultItem } from "../solution"; +import { CompletionContext } from "../contexts"; +import { isBlank } from "../../utils/string"; + +export function removeDuplicateSuffixLines(): PostprocessFilter { + return (item: CompletionResultItem, context: CompletionContext): CompletionResultItem => { + const text = item?.text; + const suffix = context?.suffix; + + if (text == null || suffix == null) { + return item; + } + + const originalLines = text.split("\n").map((line) => line || ""); + + const suffixLines = (suffix || "") + .split("\n") + .map((line) => (line || "").trim()) + .filter((line) => !isBlank(line)); + + if (suffixLines.length === 0) { + return item; + } + + const firstSuffixLine = suffixLines[0] || ""; + + // iterate through lines from end to find potential match + for (let i = originalLines.length - 1; i >= 0; i--) { + const currentLine = originalLines[i] || ""; + if (!isBlank(currentLine) && currentLine === firstSuffixLine) { + // check if subsequent lines also match with suffix + let isFullMatch = true; + for (let j = 0; j < suffixLines.length && i + j < originalLines.length; j++) { + const suffixLine = suffixLines[j] || ""; + const textLine = originalLines[i + j] || ""; + if (suffixLine !== textLine) { + isFullMatch = false; + break; + } + } + if (isFullMatch) { + const remainingLines = originalLines.slice(0, i); + return item.withText(remainingLines.join("\n")); + } + } + } + + return item; + }; +} diff --git a/clients/tabby-agent/src/codeCompletion/postprocess/removeDuplicatedBlockClosingLine.test.ts b/clients/tabby-agent/src/codeCompletion/postprocess/removeDuplicatedBlockClosingLine.test.ts new file mode 100644 index 000000000000..1659ca256efb --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/postprocess/removeDuplicatedBlockClosingLine.test.ts @@ -0,0 +1,62 @@ +import { documentContext, inline, assertFilterResult } from "./testUtils"; +import { removeDuplicatedBlockClosingLine } from "./removeDuplicatedBlockClosingLine"; + +describe("postprocess", () => { + describe("removeDuplicatedBlockClosingLine", () => { + const filter = removeDuplicatedBlockClosingLine(); + it("should remove duplicated block closing line.", async () => { + const context = documentContext`javascript + function hello() { + ║ + } + `; + const completion = inline` + ├console.log("hello"); + }┤ + `; + const expected = inline` + ├console.log("hello");┤ + ┴┴ + `; + await assertFilterResult(filter, context, completion, expected); + }); + + it("should remove duplicated block closing line.", async () => { + const context = documentContext`javascript + function check(condition) { + if (!condition) { + ║ + } else { + return; + } + } + `; + const completion = inline` + ├throw new Error("check not passed"); + }┤ + ┴┴ + `; + const expected = inline` + ├throw new Error("check not passed");┤ + ┴┴┴┴ + `; + await assertFilterResult(filter, context, completion, expected); + }); + + it("should not remove non-duplicated block closing line.", async () => { + const context = documentContext`javascript + function check(condition) { + if (!condition) { + ║ + } + `; + const completion = inline` + ├throw new Error("check not passed"); + }┤ + ┴┴ + `; + const expected = completion; + await assertFilterResult(filter, context, completion, expected); + }); + }); +}); diff --git a/clients/tabby-agent/src/codeCompletion/postprocess/removeDuplicatedBlockClosingLine.ts b/clients/tabby-agent/src/codeCompletion/postprocess/removeDuplicatedBlockClosingLine.ts new file mode 100644 index 000000000000..69dd4b427279 --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/postprocess/removeDuplicatedBlockClosingLine.ts @@ -0,0 +1,48 @@ +import { PostprocessFilter, logger } from "./base"; +import { CompletionResultItem } from "../solution"; +import { CompletionContext } from "../contexts"; +import { isBlank, isBlockClosingLine } from "../../utils/string"; + +// For remove duplicated block closing line at ( ending of input text ) and ( beginning of suffix text ) +// Should be useful after limitScope +export function removeDuplicatedBlockClosingLine(): PostprocessFilter { + return (item: CompletionResultItem, context: CompletionContext): CompletionResultItem => { + const { suffixLines, currentLinePrefix } = context; + const inputLines = item.lines; + if (inputLines.length < 2) { + // If completion only has one line, don't continue process + return item; + } + + const inputLinesForDetection = inputLines.map((line, index) => { + return index === 0 ? currentLinePrefix + line : line; + }); + if (!isBlockClosingLine(inputLinesForDetection, inputLines.length - 1)) { + return item; + } + const inputEndingLine = inputLines[inputLines.length - 1]!; + + let suffixBeginningIndex = 1; + while (suffixBeginningIndex < suffixLines.length && isBlank(suffixLines[suffixBeginningIndex]!)) { + suffixBeginningIndex++; + } + if (suffixBeginningIndex >= suffixLines.length) { + return item; + } + const suffixBeginningLine = suffixLines[suffixBeginningIndex]!; + + if ( + inputEndingLine.startsWith(suffixBeginningLine.trimEnd()) || + suffixBeginningLine.startsWith(inputEndingLine.trimEnd()) + ) { + logger.trace("Remove duplicated block closing line.", { inputLines, suffixLines }); + return item.withText( + inputLines + .slice(0, inputLines.length - 1) + .join("") + .trimEnd(), + ); + } + return item; + }; +} diff --git a/clients/tabby-agent/src/codeCompletion/postprocess/removeLineEndsWithRepetition.test.ts b/clients/tabby-agent/src/codeCompletion/postprocess/removeLineEndsWithRepetition.test.ts new file mode 100644 index 000000000000..1d0fdc5347b2 --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/postprocess/removeLineEndsWithRepetition.test.ts @@ -0,0 +1,46 @@ +import { emptyCompletionResultItem } from "../solution"; +import { documentContext, inline, assertFilterResult } from "./testUtils"; +import { removeLineEndsWithRepetition } from "./removeLineEndsWithRepetition"; + +describe("postprocess", () => { + describe("removeLineEndsWithRepetition", () => { + const filter = removeLineEndsWithRepetition(); + it("should drop one line completion ends with repetition", async () => { + const context = documentContext`javascript + let foo = ║ + `; + const completion = inline` + ├foo = foo = foo = foo = foo = foo = foo =┤ + `; + const expected = emptyCompletionResultItem; + await assertFilterResult(filter, context, completion, expected); + }); + + it("should remove last line that ends with repetition", async () => { + const context = documentContext`javascript + let largeNumber = 1000000 + let veryLargeNumber = ║ + `; + const completion = inline` + ├1000000000 + let superLargeNumber = 1000000000000000000000000000000000000000000000┤ + `; + const expected = inline` + ├1000000000┤ + `; + await assertFilterResult(filter, context, completion, expected); + }); + + it("should keep repetition less than threshold", async () => { + const context = documentContext`javascript + let largeNumber = 1000000 + let veryLargeNumber = ║ + `; + const completion = inline` + ├1000000000000┤ + `; + const expected = completion; + await assertFilterResult(filter, context, completion, expected); + }); + }); +}); diff --git a/clients/tabby-agent/src/codeCompletion/postprocess/removeLineEndsWithRepetition.ts b/clients/tabby-agent/src/codeCompletion/postprocess/removeLineEndsWithRepetition.ts new file mode 100644 index 000000000000..cf5757b0abbd --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/postprocess/removeLineEndsWithRepetition.ts @@ -0,0 +1,39 @@ +import { PostprocessFilter, logger } from "./base"; +import { CompletionResultItem, emptyCompletionResultItem } from "../solution"; +import { isBlank } from "../../utils/string"; + +const repetitionTests = [ + /(.{3,}?)\1{5,}$/g, // match a 3+ characters pattern repeating 5+ times + /(.{10,}?)\1{3,}$/g, // match a 10+ characters pattern repeating 3+ times +]; + +export function removeLineEndsWithRepetition(): PostprocessFilter { + return (item: CompletionResultItem): CompletionResultItem => { + // only test last non-blank line + const inputLines = item.lines; + let index = inputLines.length - 1; + while (index >= 0 && isBlank(inputLines[index]!)) { + index--; + } + if (index < 0) { + return item; + } + // if matches repetition test, remove this line + for (const test of repetitionTests) { + const match = inputLines[index]!.match(test); + if (match) { + logger.trace("Remove line ends with repetition.", { + inputLines, + lineNumber: index, + match, + }); + if (index < 1) { + return emptyCompletionResultItem; + } + return item.withText(inputLines.slice(0, index).join("").trimEnd()); + } + } + // no repetition found + return item; + }; +} diff --git a/clients/tabby-agent/src/codeCompletion/postprocess/removeRepetitiveBlocks.test.ts b/clients/tabby-agent/src/codeCompletion/postprocess/removeRepetitiveBlocks.test.ts new file mode 100644 index 000000000000..35054c80c7a2 --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/postprocess/removeRepetitiveBlocks.test.ts @@ -0,0 +1,51 @@ +import { documentContext, inline, assertFilterResult } from "./testUtils"; +import { removeRepetitiveBlocks } from "./removeRepetitiveBlocks"; + +describe("postprocess", () => { + describe("removeRepetitiveBlocks", () => { + const filter = removeRepetitiveBlocks(); + it("should remove repetitive blocks", async () => { + const context = documentContext`javascript + function myFuncA() { + console.log("myFuncA called."); + } + + ║ + `; + const completion = inline` + ├function myFuncB() { + console.log("myFuncB called."); + } + + function myFuncC() { + console.log("myFuncC called."); + } + + function myFuncD() { + console.log("myFuncD called."); + } + + function myFuncE() { + console.log("myFuncE called."); + } + + function myFuncF() { + console.log("myFuncF called."); + } + + function myFuncG() { + console.log("myFuncG called."); + } + + function myFuncH() { + console.log("myFuncH ┤ + `; + const expected = inline` + ├function myFuncB() { + console.log("myFuncB called."); + }┤ + `; + await assertFilterResult(filter, context, completion, expected); + }); + }); +}); diff --git a/clients/tabby-agent/src/codeCompletion/postprocess/removeRepetitiveBlocks.ts b/clients/tabby-agent/src/codeCompletion/postprocess/removeRepetitiveBlocks.ts new file mode 100644 index 000000000000..a648f602ed72 --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/postprocess/removeRepetitiveBlocks.ts @@ -0,0 +1,56 @@ +import { PostprocessFilter, logger } from "./base"; +import { CompletionResultItem } from "../solution"; +import { CompletionContext } from "../contexts"; +import { isBlank, calcDistance } from "../../utils/string"; + +function blockSplitter(_: string) { + // Have not implemented this for each language for now + // Return a blank line matcher should work for most cases + return /\n(\s*)\n/g; +} + +// FIXME: refactor this because it is very similar to `removeRepetitiveLines` +export function removeRepetitiveBlocks(): PostprocessFilter { + return (item: CompletionResultItem, context: CompletionContext): CompletionResultItem => { + const inputBlocks = item.text.split(blockSplitter(context.document.languageId)); + let repetitionCount = 0; + const repetitionThreshold = 2; + // skip last block, it maybe cut + let index = inputBlocks.length - 2; + while (index >= 1) { + if (isBlank(inputBlocks[index]!)) { + index--; + continue; + } + let prev = index - 1; + while (prev >= 0 && isBlank(inputBlocks[prev]!)) { + prev--; + } + if (prev < 0) break; + // if distance between current and previous block is less than threshold (threshold = or 10% of string length) + const currentBlock = inputBlocks[index]!.trim(); + const previousBlock = inputBlocks[prev]!.trim(); + const threshold = Math.max(0.1 * currentBlock.length, 0.1 * previousBlock.length); + const distance = calcDistance(currentBlock, previousBlock); + if (distance <= threshold) { + repetitionCount++; + index--; + } else { + break; + } + } + if (repetitionCount >= repetitionThreshold) { + logger.trace("Remove repetitive blocks.", { + inputBlocks, + repetitionCount, + }); + return item.withText( + inputBlocks + .slice(0, index + 1) + .join("") + .trimEnd(), + ); + } + return item; + }; +} diff --git a/clients/tabby-agent/src/codeCompletion/postprocess/removeRepetitiveLines.test.ts b/clients/tabby-agent/src/codeCompletion/postprocess/removeRepetitiveLines.test.ts new file mode 100644 index 000000000000..75425fe216dc --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/postprocess/removeRepetitiveLines.test.ts @@ -0,0 +1,56 @@ +import { documentContext, inline, assertFilterResult } from "./testUtils"; +import { removeRepetitiveLines } from "./removeRepetitiveLines"; + +describe("postprocess", () => { + describe("removeRepetitiveLines", () => { + const filter = removeRepetitiveLines(); + it("should remove repetitive lines", async () => { + const context = documentContext`javascript + function hello() { + console.log("hello"); + } + hello(); + hello(); + ║ + `; + const completion = inline` + ├hello(); + hello(); + hello(); + hello(); + hello(); + hello(); + hello(); + hello(); + hello(); + hello();┤ + `; + const expected = inline` + ├hello();┤ + `; + await assertFilterResult(filter, context, completion, expected); + }); + + it("should remove repetitive lines with patterns", async () => { + const context = documentContext`javascript + const a = 1; + ║ + `; + const completion = inline` + ├const b = 1; + const c = 1; + const d = 1; + const e = 1; + const f = 1; + const g = 1; + const h = 1; + const i = 1; + const j = 1; + const k =┤`; + const expected = inline` + ├const b = 1;┤ + `; + await assertFilterResult(filter, context, completion, expected); + }); + }); +}); diff --git a/clients/tabby-agent/src/codeCompletion/postprocess/removeRepetitiveLines.ts b/clients/tabby-agent/src/codeCompletion/postprocess/removeRepetitiveLines.ts new file mode 100644 index 000000000000..c22ee7ab5515 --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/postprocess/removeRepetitiveLines.ts @@ -0,0 +1,48 @@ +import { PostprocessFilter, logger } from "./base"; +import { CompletionResultItem } from "../solution"; +import { isBlank, calcDistance } from "../../utils/string"; + +export function removeRepetitiveLines(): PostprocessFilter { + return (item: CompletionResultItem): CompletionResultItem => { + const inputLines = item.lines; + let repetitionCount = 0; + const repetitionThreshold = 5; + // skip last line, it could be a not completed line + let index = inputLines.length - 2; + while (index >= 1) { + if (isBlank(inputLines[index]!)) { + index--; + continue; + } + let prev = index - 1; + while (prev >= 0 && isBlank(inputLines[prev]!)) { + prev--; + } + if (prev < 0) break; + // if distance between current and previous line is less than threshold (threshold = or 10% of string length) + const currentLine = inputLines[index]!.trim(); + const previousLine = inputLines[prev]!.trim(); + const threshold = Math.max(0.1 * currentLine.length, 0.1 * previousLine.length); + const distance = calcDistance(currentLine, previousLine); + if (distance <= threshold) { + repetitionCount++; + index = prev; + } else { + break; + } + } + if (repetitionCount >= repetitionThreshold) { + logger.trace("Remove repetitive lines.", { + inputLines, + repetitionCount, + }); + return item.withText( + inputLines + .slice(0, index + 1) + .join("") + .trimEnd(), + ); + } + return item; + }; +} diff --git a/clients/tabby-agent/src/codeCompletion/postprocess/testUtils.ts b/clients/tabby-agent/src/codeCompletion/postprocess/testUtils.ts new file mode 100644 index 000000000000..0a2b62ec012d --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/postprocess/testUtils.ts @@ -0,0 +1,62 @@ +import { TextDocument } from "vscode-languageserver-textdocument"; +import type { PostprocessFilter } from "./base"; +import dedent from "dedent"; +import { expect, AssertionError } from "chai"; +import { v4 as uuid } from "uuid"; +import { buildCompletionContext, CompletionContext, CompletionExtraContexts } from "../contexts"; +import { CompletionResultItem } from "../solution"; +import { splitLines } from "../../utils/string"; + +// `║` is the cursor position +export function documentContext(literals: TemplateStringsArray, ...placeholders: any[]): CompletionContext { + const doc = dedent(literals, ...placeholders); + const lines = splitLines(doc); + const language = lines[0]?.trim() ?? "plaintext"; + const text = "\n" + lines.slice(1).join(""); + const textDocument = TextDocument.create(uuid(), language, 0, text.replace(/║/, "")); + return buildCompletionContext(textDocument, textDocument.positionAt(text.indexOf("║"))); +} + +// `├` start of the inline completion to insert +// `┤` end of the inline completion to insert +// `┴` use for indent placeholder, should be placed at last line after `┤` + +export function inline(literals: TemplateStringsArray, ...placeholders: any[]): string { + const inline = dedent(literals, ...placeholders); + return inline.slice(inline.indexOf("├") + 1, inline.lastIndexOf("┤")); +} + +type TestCompletionItem = CompletionResultItem | string; + +export async function assertFilterResult( + filter: PostprocessFilter, + context: CompletionContext & CompletionExtraContexts, + input: TestCompletionItem, + expected: TestCompletionItem, +) { + const wrapTestCompletionItem = (testItem: TestCompletionItem): CompletionResultItem => { + let item: CompletionResultItem; + if (testItem instanceof CompletionResultItem) { + item = testItem; + } else { + item = new CompletionResultItem(testItem); + } + return item; + }; + const output = await filter(wrapTestCompletionItem(input), context, context); + const expectedOutput = wrapTestCompletionItem(expected); + expect(output.text).to.equal(expectedOutput.text); +} + +export async function assertFilterResultNotEqual( + filter: PostprocessFilter, + context: CompletionContext, + input: TestCompletionItem, + expected: TestCompletionItem, +) { + try { + await assertFilterResult(filter, context, input, expected); + } catch (error) { + expect(error).to.be.instanceOf(AssertionError); + } +} diff --git a/clients/tabby-agent/src/codeCompletion/postprocess/trimMultiLineInSingleLineMode.test.ts b/clients/tabby-agent/src/codeCompletion/postprocess/trimMultiLineInSingleLineMode.test.ts new file mode 100644 index 000000000000..419bbfa3550d --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/postprocess/trimMultiLineInSingleLineMode.test.ts @@ -0,0 +1,67 @@ +import { emptyCompletionResultItem } from "../solution"; +import { documentContext, inline, assertFilterResult } from "./testUtils"; +import { trimMultiLineInSingleLineMode } from "./trimMultiLineInSingleLineMode"; + +describe("postprocess", () => { + describe("trimMultiLineInSingleLineMode", () => { + const filter = trimMultiLineInSingleLineMode(); + it("should trim multiline completions, when the suffix have non-auto-closed chars in the current line.", async () => { + const context = documentContext`javascript + let error = new Error("Something went wrong"); + console.log(║message); + `; + const completion = inline` + ├message); + throw error;┤ + `; + const expected = emptyCompletionResultItem; + await assertFilterResult(filter, context, completion, expected); + }); + + it("should trim multiline completions, when the suffix have non-auto-closed chars in the current line.", async () => { + const context = documentContext`javascript + let error = new Error("Something went wrong"); + console.log(║message); + `; + const completion = inline` + ├error, message); + throw error;┤ + `; + const expected = inline` + ├error, ┤ + `; + await assertFilterResult(filter, context, completion, expected); + }); + + it("should allow singleline completions, when the suffix have non-auto-closed chars in the current line.", async () => { + const context = documentContext`javascript + let error = new Error("Something went wrong"); + console.log(║message); + `; + const completion = inline` + ├error, ┤ + `; + const expected = completion; + await assertFilterResult(filter, context, completion, expected); + }); + + it("should allow multiline completions, when the suffix only have auto-closed chars that will be replaced in the current line, such as `)]}`.", async () => { + const context = documentContext`javascript + function findMax(arr) {║} + `; + const completion = inline` + ├ + let max = arr[0]; + for (let i = 1; i < arr.length; i++) { + if (arr[i] > max) { + max = arr[i]; + } + } + return max; + }┤ + `; + const expected = completion; + await assertFilterResult(filter, context, completion, expected); + }); + }); +}); diff --git a/clients/tabby-agent/src/codeCompletion/postprocess/trimMultiLineInSingleLineMode.ts b/clients/tabby-agent/src/codeCompletion/postprocess/trimMultiLineInSingleLineMode.ts new file mode 100644 index 000000000000..f57a28dc682d --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/postprocess/trimMultiLineInSingleLineMode.ts @@ -0,0 +1,23 @@ +import { PostprocessFilter, logger } from "./base"; +import { CompletionResultItem, emptyCompletionResultItem } from "../solution"; +import { CompletionContext } from "../contexts"; + +export function trimMultiLineInSingleLineMode(): PostprocessFilter { + return (item: CompletionResultItem, context: CompletionContext): CompletionResultItem => { + const inputLines = item.lines; + if (!context.isLineEnd && inputLines.length > 1) { + const suffix = context.currentLineSuffix.trimEnd(); + const inputLine = inputLines[0]!.trimEnd(); + if (inputLine.endsWith(suffix)) { + const trimmedInputLine = inputLine.slice(0, -suffix.length); + if (trimmedInputLine.length > 0) { + logger.trace("Trim content with multiple lines.", { inputLines, trimmedInputLine }); + return item.withText(trimmedInputLine); + } + } + logger.trace("Drop content with multiple lines.", { inputLines }); + return emptyCompletionResultItem; + } + return item; + }; +} diff --git a/clients/tabby-agent/src/codeCompletion/postprocess/trimSpace.test.ts b/clients/tabby-agent/src/codeCompletion/postprocess/trimSpace.test.ts new file mode 100644 index 000000000000..0c9628fb64fc --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/postprocess/trimSpace.test.ts @@ -0,0 +1,70 @@ +import { documentContext, inline, assertFilterResult } from "./testUtils"; +import { trimSpace } from "./trimSpace"; + +describe("postprocess", () => { + describe("trimSpace", () => { + const filter = trimSpace(); + it("should remove trailing space", async () => { + const context = documentContext`javascript + let foo = new ║ + `; + const completion = inline` + ├Foo(); ┤ + `; + const expected = inline` + ├Foo();┤ + `; + await assertFilterResult(filter, context, completion, expected); + }); + + it("should not remove trailing space if filling in line", async () => { + const context = documentContext`javascript + let foo = sum(║baz) + `; + const completion = inline` + ├bar, ┤ + `; + const expected = completion; + await assertFilterResult(filter, context, completion, expected); + }); + + it("should remove trailing space if filling in line with suffix starts with space", async () => { + const context = documentContext`javascript + let foo = sum(║ baz) + `; + const completion = inline` + ├bar, ┤ + `; + const expected = inline` + ├bar,┤ + `; + await assertFilterResult(filter, context, completion, expected); + }); + + it("should not remove leading space if current line is blank", async () => { + const context = documentContext`javascript + function sum(a, b) { + ║ + } + `; + const completion = inline` + ├ return a + b;┤ + `; + const expected = completion; + await assertFilterResult(filter, context, completion, expected); + }); + + it("should remove leading space if current line is not blank and ends with space", async () => { + const context = documentContext`javascript + let foo = ║ + `; + const completion = inline` + ├ sum(bar, baz);┤ + `; + const expected = inline` + ├sum(bar, baz);┤ + `; + await assertFilterResult(filter, context, completion, expected); + }); + }); +}); diff --git a/clients/tabby-agent/src/codeCompletion/postprocess/trimSpace.ts b/clients/tabby-agent/src/codeCompletion/postprocess/trimSpace.ts new file mode 100644 index 000000000000..c1b9a9624235 --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/postprocess/trimSpace.ts @@ -0,0 +1,22 @@ +import { PostprocessFilter } from "./base"; +import { CompletionResultItem } from "../solution"; +import { CompletionContext } from "../contexts"; +import { isBlank } from "../../utils/string"; + +export function trimSpace(): PostprocessFilter { + return (item: CompletionResultItem, context: CompletionContext): CompletionResultItem => { + const { currentLinePrefix, currentLineSuffix } = context; + let trimmedInput = item.text; + + if (!isBlank(currentLinePrefix) && currentLinePrefix.match(/\s$/)) { + trimmedInput = trimmedInput.trimStart(); + } + if (isBlank(currentLineSuffix) || (!isBlank(currentLineSuffix) && currentLineSuffix.match(/^\s/))) { + trimmedInput = trimmedInput.trimEnd(); + } + if (trimmedInput !== item.text) { + return item.withText(trimmedInput); + } + return item; + }; +} diff --git a/clients/tabby-agent/src/codeCompletion/solution.ts b/clients/tabby-agent/src/codeCompletion/solution.ts new file mode 100644 index 000000000000..0ac3222b0bcd --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/solution.ts @@ -0,0 +1,123 @@ +import { splitLines, isBlank } from "../utils/string"; +import type { components as TabbyApiComponents } from "tabby-openapi/compatible"; +import type { CompletionContext, CompletionExtraContexts } from "./contexts"; +import { CompletionItem, CompletionList, InlineCompletionItem, InlineCompletionList } from "../protocol"; +import { CompletionItemKind } from "vscode-languageserver-protocol"; + +export class CompletionResultItem { + // redundant quick access for text + readonly lines: string[]; + readonly currentLine: string; + + constructor( + readonly text: string, + readonly eventId?: { + completionId: string; + choiceIndex: number; + }, + ) { + this.lines = splitLines(this.text); + this.currentLine = this.lines[0] ?? ""; + } + + /** + * Create a new CompletionResultItem from this item with the given text. + * This method preserves the `eventId` property from the original item. + * No other properties of the original item are carried over by design. + */ + withText(text: string): CompletionResultItem { + return new CompletionResultItem(text, this.eventId); + } + + toCompletionItem(context: CompletionContext): CompletionItem | undefined { + if (isBlank(this.text)) { + return undefined; + } + + const document = context.document; + const position = context.position; + const linePrefix = document.getText({ + start: { line: position.line, character: 0 }, + end: position, + }); + const wordPrefix = linePrefix.match(/(\w+)$/)?.[0] ?? ""; + const insertText = context.selectedCompletionInsertion + this.text; + + const insertLines = splitLines(insertText); + const firstLine = insertLines[0] || ""; + const secondLine = insertLines[1] || ""; + return { + label: wordPrefix + firstLine, + labelDetails: { + detail: secondLine, + description: "Tabby", + }, + kind: CompletionItemKind.Text, + documentation: { + kind: "markdown", + value: `\`\`\`\n${linePrefix + insertText}\n\`\`\`\n ---\nSuggested by Tabby.`, + }, + textEdit: { + newText: wordPrefix + insertText, + range: { + start: { line: position.line, character: position.character - wordPrefix.length }, + end: { line: position.line, character: position.character + context.lineEndReplaceLength }, + }, + }, + data: { eventId: this.eventId }, + }; + } + + toInlineCompletionItem(context: CompletionContext): InlineCompletionItem | undefined { + if (isBlank(this.text)) { + return undefined; + } + + const position = context.position; + const insertText = context.selectedCompletionInsertion + this.text; + return { + insertText, + range: { + start: position, + end: { line: position.line, character: position.character + context.lineEndReplaceLength }, + }, + data: { eventId: this.eventId }, + }; + } +} + +export class CompletionSolution { + extraContext: CompletionExtraContexts = {}; + isCompleted: boolean = false; + items: CompletionResultItem[] = []; + + toCompletionList(context: CompletionContext): CompletionList { + return { + isIncomplete: !this.isCompleted, + items: this.items + .map((item) => item.toCompletionItem(context)) + .filter((item): item is CompletionItem => item !== undefined), + }; + } + + toInlineCompletionList(context: CompletionContext): InlineCompletionList { + return { + isIncomplete: !this.isCompleted, + items: this.items + .map((item) => item.toInlineCompletionItem(context)) + .filter((item): item is InlineCompletionItem => item !== undefined), + }; + } +} + +export const emptyCompletionResultItem = new CompletionResultItem(""); + +export function createCompletionResultItemFromResponse( + response: TabbyApiComponents["schemas"]["CompletionResponse"], +): CompletionResultItem { + const index = 0; // api always returns 0 or 1 choice + return new CompletionResultItem(response.choices[index]?.text ?? "", { + completionId: response.id, + choiceIndex: response.choices[index]?.index ?? index, + }); +} diff --git a/clients/tabby-agent/src/codeCompletion/statistics.ts b/clients/tabby-agent/src/codeCompletion/statistics.ts new file mode 100644 index 000000000000..2a9cf363202d --- /dev/null +++ b/clients/tabby-agent/src/codeCompletion/statistics.ts @@ -0,0 +1,85 @@ +import { Univariate } from "stats-logscale"; + +export type CompletionTriggerEntry = { + triggerMode: "auto" | "manual"; +}; + +export type CompletionStatisticsEntry = { + latency?: number; // ms, undefined means no data, timeout or canceled + canceled?: boolean; + timeout?: boolean; +}; + +export class CompletionStatisticsTracker { + private autoCompletionCount = 0; + private manualCompletionCount = 0; + + private eventMap = new Map(); + + private completionRequestLatencyStats = new Univariate(); + private completionRequestCanceledCount = 0; + private completionRequestTimeoutCount = 0; + + addTriggerEntry(value: CompletionTriggerEntry): void { + const { triggerMode } = value; + if (triggerMode === "auto") { + this.autoCompletionCount += 1; + } else { + this.manualCompletionCount += 1; + } + } + + addStatisticsEntry(value: CompletionStatisticsEntry): void { + const { canceled, timeout, latency } = value; + if (canceled) { + this.completionRequestCanceledCount += 1; + } else if (timeout) { + this.completionRequestTimeoutCount += 1; + } else if (latency !== undefined) { + this.completionRequestLatencyStats.add(latency); + } + } + + addEvent(event: string): void { + const count = this.eventMap.get(event) || 0; + this.eventMap.set(event, count + 1); + } + + reset(): void { + this.autoCompletionCount = 0; + this.manualCompletionCount = 0; + + this.eventMap = new Map(); + + this.completionRequestLatencyStats = new Univariate(); + this.completionRequestCanceledCount = 0; + this.completionRequestTimeoutCount = 0; + } + + // stats for anonymous usage report + report(): Record { + const eventCount = Object.fromEntries( + Array.from(this.eventMap.entries()).map(([key, value]) => ["count_" + key, value]), + ); + return { + completion: { + count_auto: this.autoCompletionCount, + count_manual: this.manualCompletionCount, + ...eventCount, + }, + completion_request: { + count: this.completionRequestLatencyStats.count(), + latency_avg: this.completionRequestLatencyStats.mean(), + latency_p50: this.completionRequestLatencyStats.percentile(50), + latency_p95: this.completionRequestLatencyStats.percentile(95), + latency_p99: this.completionRequestLatencyStats.percentile(99), + }, + completion_request_canceled: { + count: this.completionRequestCanceledCount, + }, + completion_request_timeout: { + count: this.completionRequestTimeoutCount, + }, + }; + } +} diff --git a/clients/tabby-agent/src/codeLens.ts b/clients/tabby-agent/src/codeLens.ts new file mode 100644 index 000000000000..e0e25d6ff881 --- /dev/null +++ b/clients/tabby-agent/src/codeLens.ts @@ -0,0 +1,290 @@ +import type { Feature } from "./feature"; +import { + Range, + Location, + Connection, + CancellationToken, + WorkDoneProgressReporter, + ResultProgressReporter, + CodeLensParams, +} from "vscode-languageserver"; +import { ClientCapabilities, ServerCapabilities, CodeLens, CodeLensType, ChangesPreviewLineType } from "./protocol"; +import { TextDocuments } from "./extensions/textDocuments"; +import { TextDocument } from "vscode-languageserver-textdocument"; +import { getLogger } from "./logger"; +import { codeDiff } from "./utils/diff"; + +const codeLensType: CodeLensType = "previewChanges"; +const changesPreviewLineType = { + header: "header" as ChangesPreviewLineType, + footer: "footer" as ChangesPreviewLineType, + commentsFirstLine: "commentsFirstLine" as ChangesPreviewLineType, + comments: "comments" as ChangesPreviewLineType, + waiting: "waiting" as ChangesPreviewLineType, + inProgress: "inProgress" as ChangesPreviewLineType, + unchanged: "unchanged" as ChangesPreviewLineType, + inserted: "inserted" as ChangesPreviewLineType, + deleted: "deleted" as ChangesPreviewLineType, +}; + +const logger = getLogger("CodeLensProvider"); + +export class CodeLensProvider implements Feature { + constructor(private readonly documents: TextDocuments) {} + + initialize(connection: Connection, clientCapabilities: ClientCapabilities): ServerCapabilities { + if (clientCapabilities.textDocument?.codeLens) { + connection.onCodeLens(async (params, token, workDoneProgress, resultProgress) => { + return this.provideCodeLens(params, token, workDoneProgress, resultProgress); + }); + + return { + codeLensProvider: { + resolveProvider: false, + }, + }; + } + return {}; + } + + async provideCodeLens( + params: CodeLensParams, + token: CancellationToken, + workDoneProgress?: WorkDoneProgressReporter | undefined, + resultProgress?: ResultProgressReporter | undefined, + ): Promise { + const uri = params.textDocument.uri; + const textDocument = this.documents.get(uri); + if (!textDocument) { + return null; + } + const codeLenses: CodeLens[] = []; + let lineInPreviewBlock = -1; + let previewBlockMarkers = ""; + const originLines: string[] = []; + const modifiedLines: string[] = []; + const modifiedCodeLenses: CodeLens[] = []; + const originCodeLenses: CodeLens[] = []; + for (let line = textDocument.lineCount - 1; line >= 0; line = line - 1) { + if (token.isCancellationRequested) { + return null; + } + const lineRange = { start: { line: line, character: 0 }, end: { line: line + 1, character: 0 } }; + const text = textDocument.getText(lineRange); + const codeLensRange: Range = { + start: { line: line, character: 0 }, + end: { line: line, character: text.length - 1 }, + }; + + const codeLensLocation: Location = { uri: uri, range: codeLensRange }; + const lineCodeLenses: CodeLens[] = []; + if (lineInPreviewBlock < 0) { + const match = /^>>>>>>> (tabby-[0-9|a-z|A-Z]{6}) (\[.*\])/g.exec(text); + const editId = match?.[1]; + const markers = match?.[2]; + if (match && markers && editId) { + previewBlockMarkers = markers; + lineInPreviewBlock = 0; + lineCodeLenses.push({ + range: codeLensRange, + data: { + type: codeLensType, + line: changesPreviewLineType.footer, + }, + }); + } + } else { + const match = /^<<<<<<< (tabby-[0-9|a-z|A-Z]{6})/g.exec(text); + const editId = match?.[1]; + if (match && editId) { + lineInPreviewBlock = -1; + + if (previewBlockMarkers.includes(".") || previewBlockMarkers.includes("|")) { + lineCodeLenses.push({ + range: codeLensRange, + command: { + title: "$(sync~spin) Tabby is working...", + command: " ", + }, + data: { + type: codeLensType, + line: changesPreviewLineType.header, + }, + }); + lineCodeLenses.push({ + range: codeLensRange, + command: { + title: "Cancel", + command: "tabby/chat/edit/resolve", + arguments: [{ location: codeLensLocation, action: "cancel" }], + }, + data: { + type: codeLensType, + line: changesPreviewLineType.header, + }, + }); + } else { + lineCodeLenses.push({ + range: codeLensRange, + command: { + title: "$(check)Accept", + command: "tabby/chat/edit/resolve", + arguments: [{ location: codeLensLocation, action: "accept" }], + }, + data: { + type: codeLensType, + line: changesPreviewLineType.header, + }, + }); + lineCodeLenses.push({ + range: codeLensRange, + command: { + title: "$(remove-close)Discard", + command: "tabby/chat/edit/resolve", + arguments: [{ location: codeLensLocation, action: "discard" }], + }, + data: { + type: codeLensType, + line: changesPreviewLineType.header, + }, + }); + } + } else { + lineInPreviewBlock++; + const marker = previewBlockMarkers[previewBlockMarkers.length - lineInPreviewBlock - 1]; + let codeLens: CodeLens | undefined = undefined; + switch (marker) { + case "#": + codeLens = { + range: codeLensRange, + data: { + type: codeLensType, + line: + previewBlockMarkers.indexOf("#") === lineInPreviewBlock + ? changesPreviewLineType.commentsFirstLine + : changesPreviewLineType.comments, + }, + }; + break; + case ".": + codeLens = { + range: codeLensRange, + data: { + type: codeLensType, + line: changesPreviewLineType.waiting, + }, + }; + originLines.unshift(text); + originCodeLenses.unshift(codeLens); + modifiedLines.unshift(text); + modifiedCodeLenses.unshift(codeLens); + break; + case "|": + codeLens = { + range: codeLensRange, + data: { + type: codeLensType, + line: changesPreviewLineType.inProgress, + }, + }; + modifiedLines.unshift(text); + modifiedCodeLenses.unshift(codeLens); + break; + case "=": + codeLens = { + range: codeLensRange, + data: { + type: codeLensType, + line: changesPreviewLineType.unchanged, + }, + }; + originLines.unshift(text); + originCodeLenses.unshift(codeLens); + modifiedLines.unshift(text); + modifiedCodeLenses.unshift(codeLens); + break; + case "+": + codeLens = { + range: codeLensRange, + data: { + type: codeLensType, + line: changesPreviewLineType.inserted, + }, + }; + modifiedLines.unshift(text); + modifiedCodeLenses.unshift(codeLens); + break; + case "-": + codeLens = { + range: codeLensRange, + data: { + type: codeLensType, + line: changesPreviewLineType.deleted, + }, + }; + originLines.unshift(text); + originCodeLenses.unshift(codeLens); + break; + default: + break; + } + if (codeLens) { + codeLenses.push(codeLens); + } + } + } + if (lineCodeLenses.length > 0) { + if (resultProgress) { + resultProgress.report(lineCodeLenses); + } else { + codeLenses.push(...lineCodeLenses); + } + } + } + + // if origin and modified lines are not empty, compute the char diffs. + // otherwise, it is just an insertion or deletion, skipping char diffs. + if (originLines.length > 0 && modifiedLines.length > 0) { + const { originRanges, modifiedRanges } = codeDiff( + originLines, + originCodeLenses.map((item) => item.range), + modifiedLines, + modifiedCodeLenses.map((item) => item.range), + ); + const deletionDecorations = originRanges.map((range) => { + return { + range, + data: { + type: codeLensType, + text: "deleted" as const, + }, + }; + }); + + const insertionDecorations = modifiedRanges.map((range) => { + return { + range, + data: { + type: codeLensType, + text: "inserted" as const, + }, + }; + }); + + if (resultProgress) { + resultProgress.report([...deletionDecorations, ...insertionDecorations]); + } else { + codeLenses.push(...deletionDecorations, ...insertionDecorations); + } + } + + logger.debug(`codeLenses: ${JSON.stringify(codeLenses)}`); + + workDoneProgress?.done(); + if (resultProgress) { + return null; + } else { + return codeLenses; + } + } +} diff --git a/clients/tabby-agent/src/codeSearch.ts b/clients/tabby-agent/src/codeSearch.ts new file mode 100644 index 000000000000..01d77a782ac5 --- /dev/null +++ b/clients/tabby-agent/src/codeSearch.ts @@ -0,0 +1,222 @@ +import * as Engine from "@orama/orama"; +import type { Position, Range } from "vscode-languageserver"; +import type { DocumentRange } from "./utils/types"; +import { extractNonReservedWordList } from "./utils/string"; +import { isPositionBefore, isPositionAfter, unionRange, rangeInDocument } from "./utils/range"; + +export interface Chunk { + // Which file does the snippet belongs to + uri: string; + // (Not Indexed) The offset of the snippet in the file + range: Range; + // (Not Indexed) The full text of the snippet + text: string; + // The code language id of the snippet + language: string; + // The semantic symbols extracted from the snippet + symbols: string; +} + +export interface ChunkingConfig { + // max count for chunks in memory + maxChunks: number; + // chars count per code chunk + chunkSize: number; + // lines count overlap between neighbor chunks + overlapLines: number; +} + +export type CodeSearchResultItem = Chunk & { + score: number; +}; + +export type CodeSearchResult = CodeSearchResultItem[]; + +export class CodeSearchEngine { + constructor(private config: ChunkingConfig) {} + + private db: Engine.AnyOrama | undefined = undefined; + private indexedDocumentRanges: (DocumentRange & { indexIds: string[] })[] = []; + + private async init() { + if (this.db) { + return; + } + this.db = await Engine.create({ + schema: { + uri: "string", + language: "string", + symbols: "string", + }, + }); + } + + private async count(): Promise { + if (!this.db) { + return 0; + } + return await Engine.count(this.db); + } + + private async insert(snippets: Chunk[]): Promise { + if (!this.db) { + await this.init(); + } + if (this.db) { + return await Engine.insertMultiple(this.db, snippets); + } + return []; + } + + private async remove(ids: string[]): Promise { + if (!this.db) { + return 0; + } + return await Engine.removeMultiple(this.db, ids); + } + + private async chunk(documentRange: DocumentRange): Promise { + const document = documentRange.document; + const range = rangeInDocument(documentRange.range, document); + if (!range) { + return []; + } + const chunks: Chunk[] = []; + let positionStart: Position = range.start; + let positionEnd; + do { + const offset = document.offsetAt(positionStart); + // move forward chunk size + positionEnd = document.positionAt(offset + this.config.chunkSize); + if (isPositionBefore(positionEnd, range.end)) { + // If have not reached the end, back to the last newline instead + positionEnd = { line: positionEnd.line, character: 0 }; + } + if (positionEnd.line <= positionStart.line + this.config.overlapLines) { + // In case of forward chunk size does not moved enough lines for overlap, force move that much lines + positionEnd = { line: positionStart.line + this.config.overlapLines + 1, character: 0 }; + } + if (isPositionAfter(positionEnd, range.end)) { + // If have passed the end, back to the end + positionEnd = range.end; + } + + const chunkRange = { start: positionStart, end: positionEnd }; + const text = document.getText(chunkRange); + if (text.trim().length > 0) { + chunks.push({ + uri: document.uri, + range: chunkRange, + text: text, + language: document.languageId, + symbols: extractNonReservedWordList(text), + }); + } + + // move the start position to the next chunk start + positionStart = { line: positionEnd.line - this.config.overlapLines, character: 0 }; + } while (chunks.length < this.config.maxChunks && isPositionBefore(positionEnd, range.end)); + return chunks; + } + + getIndexedDocumentRange(): DocumentRange[] { + return this.indexedDocumentRanges; + } + + /** + * Index the range of the document. + * + * When invoked multiple times with the same document but different ranges, + * the ranges will be merged and re-chunked. + * + * If the indexed chunks in memory is too many, the oldest document will be removed. + * The removal is by document, all chunks from the document will be removed. + * + * @param documentRange The document and specific range to index. + */ + async index(documentRange: DocumentRange): Promise { + const { document, range } = documentRange; + const documentUriString = document.uri.toString(); + let targetRange = range; + const indexToUpdate = this.indexedDocumentRanges.findIndex( + (item) => item.document.uri.toString() === documentUriString, + ); + const documentRangeToUpdate = this.indexedDocumentRanges[indexToUpdate]; + if (documentRangeToUpdate) { + // FIXME: union is not perfect for merging two ranges have large distance between them + targetRange = unionRange(targetRange, documentRangeToUpdate.range); + } + const chunks = await this.chunk({ document, range: targetRange }); + if (documentRangeToUpdate) { + await this.remove(documentRangeToUpdate.indexIds); + this.indexedDocumentRanges.splice(indexToUpdate); + } + const indexIds = await this.insert(chunks); + this.indexedDocumentRanges.push({ + document, + range: targetRange, + indexIds, + }); + + // Check chunks count and evict if needed. + while ((await this.count()) > this.config.maxChunks) { + const toRemove = this.indexedDocumentRanges.shift(); + if (toRemove) { + await this.remove(toRemove.indexIds); + } else { + break; + } + } + } + + /** + * Search relevant code snippets that has been indexed. + * @param query contains words to search. + * @param options + * @param options.filepathsFilter only search in these filepaths. + * @param options.languagesFilter only search in these languages. + * @param options.limit max number of hits to return. + * @returns A list of hit results, contains the snippet and score. + */ + async search( + query: string, + options?: { + filepathsFilter?: string[]; + languagesFilter?: string[]; + limit?: number; + }, + ): Promise { + if (!this.db) { + return []; + } + const searchResult = await Engine.search(this.db, { + term: query, + properties: ["symbols"], + where: { + uri: options?.filepathsFilter, + language: options?.languagesFilter, + }, + limit: options?.limit, + }); + return ( + searchResult.hits + // manual filtering + .filter((hit) => { + if (options?.filepathsFilter && !options?.filepathsFilter.includes(hit.document["uri"])) { + return false; + } + if (options?.languagesFilter && !options?.languagesFilter.includes(hit.document["language"])) { + return false; + } + return true; + }) + .map((hit) => { + return { + ...hit.document, + score: hit.score || 0, // set score to 0 if it is NaN + }; + }) + .sort((a, b) => b.score - a.score) + ); + } +} diff --git a/clients/tabby-agent/src/command.ts b/clients/tabby-agent/src/command.ts new file mode 100644 index 000000000000..f509957e3561 --- /dev/null +++ b/clients/tabby-agent/src/command.ts @@ -0,0 +1,39 @@ +import { Feature } from "./feature"; +import { Connection, ExecuteCommandParams } from "vscode-languageserver"; +import { + ServerCapabilities, + StatusShowHelpMessageRequest, + ChatEditResolveRequest, + ChatEditResolveParams, +} from "./protocol"; +import { ChatEditProvider } from "./chat/inlineEdit"; +import { StatusProvider } from "./status"; + +export class CommandProvider implements Feature { + constructor( + private readonly chatEditProvider: ChatEditProvider, + private readonly statusProvider: StatusProvider, + ) {} + + initialize(connection: Connection): ServerCapabilities { + connection.onExecuteCommand(async (params) => { + return this.executeCommand(params); + }); + return { + executeCommandProvider: { + commands: [StatusShowHelpMessageRequest.method, ChatEditResolveRequest.method], + }, + }; + } + + async executeCommand(params: ExecuteCommandParams): Promise { + if (params.command === StatusShowHelpMessageRequest.method) { + await this.statusProvider.showStatusHelpMessage(); + } else if (params.command === ChatEditResolveRequest.method) { + const commandParams = params.arguments?.[0] as ChatEditResolveParams; + if (commandParams) { + await this.chatEditProvider.resolveEdit(commandParams); + } + } + } +} diff --git a/clients/tabby-agent/src/config/configFile.ts b/clients/tabby-agent/src/config/configFile.ts new file mode 100644 index 000000000000..7c884f04110d --- /dev/null +++ b/clients/tabby-agent/src/config/configFile.ts @@ -0,0 +1,164 @@ +import type { PartialConfigData } from "./type"; +import { EventEmitter } from "events"; +import path from "path"; +import os from "os"; +import fs from "fs-extra"; +import toml from "toml"; +import chokidar from "chokidar"; +import deepEqual from "deep-equal"; +import { getProperty, deleteProperty } from "dot-prop"; +import { isBrowser } from "../env"; +import { getLogger } from "../logger"; + +const configTomlTemplate = `## Tabby agent configuration file + +## Online documentation: https://tabby.tabbyml.com/docs/extensions/configurations +## You can uncomment and edit the values below to change the default settings. +## Configurations in this file have lower priority than the IDE settings. + +## Server +## You can set the server endpoint and token here. +# [server] +# endpoint = "http://localhost:8080" # http or https URL +# token = "your-token-here" # if set, request header Authorization = "Bearer $token" will be added + +## You can add custom request headers. +# [server.requestHeaders] +# Header1 = "Value1" # list your custom headers here +# Header2 = "Value2" # values can be strings, numbers or booleans + +## Proxy +## You can specify an optional http/https proxy when required, overrides environment variable settings. +# [proxy] +# url = "http://your-proxy-server" # the URL of the proxy + +## Logs +## You can set the log level here. The log file is located at ~/.tabby-client/agent/logs/. +# [logs] +# level = "silent" # "silent" or "error" or "debug" + +## Anonymous usage tracking +## Tabby collects anonymous usage data and sends it to the Tabby team to help improve our products. +## Your code, generated completions, or any sensitive information is never tracked or sent. +## For more details on data collection, see https://tabby.tabbyml.com/docs/extensions/configurations#usage-collection +## Your contribution is greatly appreciated. However, if you prefer not to participate, you can disable anonymous usage tracking here. +# [anonymousUsageTracking] +# disable = false # set to true to disable + +`; + +const typeCheckSchema: Record = { + server: "object", + "server.endpoint": "string", + "server.token": "string", + "server.requestHeaders": "object", + "server.requestTimeout": "number", + proxy: "object", + "proxy.url": "string", + completion: "object", + "completion.prompt": "object", + "completion.prompt.maxPrefixLines": "number", + "completion.prompt.maxSuffixLines": "number", + "completion.prompt.fillDeclarations": "object", + "completion.prompt.fillDeclarations.enabled": "boolean", + "completion.prompt.fillDeclarations.maxSnippets": "number", + "completion.prompt.fillDeclarations.maxChars": "number", + "completion.prompt.collectSnippetsFromRecentChangedFiles": "object", + "completion.prompt.collectSnippetsFromRecentChangedFiles.enabled": "boolean", + "completion.prompt.collectSnippetsFromRecentChangedFiles.maxSnippets": "number", + "completion.prompt.collectSnippetsFromRecentChangedFiles.indexing": "object", + "completion.prompt.collectSnippetsFromRecentChangedFiles.indexing.checkingChangesInterval": "number", + "completion.prompt.collectSnippetsFromRecentChangedFiles.indexing.changesDebouncingInterval": "number", + "completion.prompt.collectSnippetsFromRecentChangedFiles.indexing.prefixLines": "number", + "completion.prompt.collectSnippetsFromRecentChangedFiles.indexing.suffixLines": "number", + "completion.prompt.collectSnippetsFromRecentChangedFiles.indexing.maxChunks": "number", + "completion.prompt.collectSnippetsFromRecentChangedFiles.indexing.chunkSize": "number", + "completion.prompt.collectSnippetsFromRecentChangedFiles.indexing.overlapLines": "number", + "completion.prompt.clipboard": "object", + "completion.prompt.clipboard.minChars": "number", + "completion.prompt.clipboard.maxChars": "number", + "completion.debounce": "object", + "completion.debounce.mode": "string", + "completion.debounce.interval": "number", + "completion.solution": "object", + "completion.solution.maxItems": "number", + "completion.solution.maxTries": "number", + "completion.solution.temperature": "number", + chat: "object", + "chat.edit": "object", + "chat.generateCommitMessage": "object", + "chat.generateCommitMessage.maxDiffLength": "number", + "chat.generateCommitMessage.promptTemplate": "string", + logs: "object", + "logs.level": "string", + tls: "object", + "tls.caCerts": "string", + anonymousUsageTracking: "object", + "anonymousUsageTracking.disable": "boolean", +}; + +function validateConfig(config: PartialConfigData): PartialConfigData { + for (const [key, type] of Object.entries(typeCheckSchema)) { + if (typeof getProperty(config, key) !== type) { + deleteProperty(config, key); + } + } + return config; +} + +export class ConfigFile extends EventEmitter { + private data: PartialConfigData = {}; + private watcher?: chokidar.FSWatcher; + private logger = getLogger("ConfigFile"); + + constructor(private readonly filepath: string) { + super(); + } + + get config(): PartialConfigData { + return this.data; + } + + async load() { + try { + const fileContent = await fs.readFile(this.filepath, "utf8"); + const data = toml.parse(fileContent); + this.data = validateConfig(data); + } catch (error) { + if (error instanceof Error && "code" in error && error.code === "ENOENT") { + this.logger.info("Config file not exist, creating template config file."); + await this.createTemplate(); + } else { + this.logger.error("Failed to load config file.", error); + } + } + } + + watch() { + this.watcher = chokidar.watch(this.filepath, { + interval: 1000, + }); + const onChanged = async () => { + const oldData = this.data; + await this.load(); + if (!deepEqual(oldData, this.data)) { + this.emit("updated", this.data, oldData); + } + }; + this.watcher.on("add", onChanged); + this.watcher.on("change", onChanged); + } + + private async createTemplate() { + try { + await fs.outputFile(this.filepath, configTomlTemplate); + } catch (error) { + this.logger.error("Failed to create config template file.", error); + } + } +} + +export function getConfigFile(): ConfigFile | undefined { + const configFilePath = path.join(os.homedir(), ".tabby-client", "agent", "config.toml"); + return isBrowser ? undefined : new ConfigFile(configFilePath); +} diff --git a/clients/tabby-agent/src/config/default.test.ts b/clients/tabby-agent/src/config/default.test.ts new file mode 100644 index 000000000000..af13bc35baf0 --- /dev/null +++ b/clients/tabby-agent/src/config/default.test.ts @@ -0,0 +1,74 @@ +import { expect } from "chai"; +import { stringToRegExp } from "../utils/string"; +import { defaultConfigData } from "./default"; + +describe("Config: generateCommitMessage.responseMatcher", () => { + // Test parameters + const responseMatcher = defaultConfigData.chat.generateCommitMessage.responseMatcher; + const regExp = stringToRegExp(responseMatcher); + + // Helper function for reusing test logic + function testResponseMatch(testCase: string, input: string, expectedMatch: string) { + it(testCase, () => { + const match = regExp.exec(input); + expect(match).to.not.be.null; + if (match) { + expect(match[1]).to.equal(expectedMatch); + } + }); + } + + // Core functionality: Extract commit message from simple response + testResponseMatch( + "test for extracting conventional commit message from simple response", + "Based on the diff, I would suggest the following commit message:\n\nfeat(core): add new feature\n", + "feat(core): add new feature", + ); + + // Core functionality: Handle commit messages in code blocks + testResponseMatch( + "test for handling responses with code blocks", + `Based on the diff, here's an appropriate commit message: + +\`\`\` +fix(auth): resolve user authentication timeout issue +\`\`\` + +This commit message follows the conventional format with a 'fix' type and 'auth' scope.`, + "fix(auth): resolve user authentication timeout issue", + ); + + // Bug fix: Handle commit messages with double quotes + testResponseMatch( + "test for handling responses with double quotes", + `Based on the provided diff, I recommend using the following commit message: + +"docs(readme): update installation instructions" + +This commit message follows the conventional format and accurately describes the changes made to the documentation.`, + "docs(readme): update installation instructions", + ); + + // Bug fix: Handle commit messages with single quotes and backticks + testResponseMatch( + "test for handling responses with single quotes and backticks", + `Here are some commit message options: +'chore(build): update dependencies' +\`test(components): add unit tests for login form\``, + "chore(build): update dependencies", + ); + + // Bug fix: Handle responses with markdown images + testResponseMatch( + "test for handling responses with markdown images", + `Here's a diagram showing the changes you made: +![Diagram](https://example.com/diagram.png) + +Based on the diff, I suggest the following commit message: + +feat(ui): improve button design and layout + +[Diagram]: https://example.com/diagram-ref.png`, + "feat(ui): improve button design and layout", + ); +}); diff --git a/clients/tabby-agent/src/config/default.ts b/clients/tabby-agent/src/config/default.ts new file mode 100644 index 000000000000..8f987f570d7f --- /dev/null +++ b/clients/tabby-agent/src/config/default.ts @@ -0,0 +1,128 @@ +import type { ConfigData } from "./type"; +import fixSpellingAndGrammarPrompt from "../chat/prompts/fix-spelling-and-grammar.md"; +import generateCommitMessagePrompt from "../chat/prompts/generate-commit-message.md"; +import generateDocsPrompt from "../chat/prompts/generate-docs.md"; +import editCommandReplacePrompt from "../chat/prompts/edit-command-replace.md"; +import editCommandInsertPrompt from "../chat/prompts/edit-command-insert.md"; +import generateSmartApplyPrompt from "../chat/prompts/generate-smart-apply.md"; +import provideSmartApplyLineRangePrompt from "../chat/prompts/provide-smart-apply-line-range.md"; +import includeFileContextList from "../chat/prompts/include-file-context-list.md"; +import includeFileContextItem from "../chat/prompts/include-file-context-item.md"; +import generateBranchNamePrompt from "../chat/prompts/generate-branch-name.md"; +export const defaultConfigData: ConfigData = { + server: { + endpoint: "http://localhost:8080", + token: "", + requestHeaders: {}, + requestTimeout: 2 * 60 * 1000, // 2 minutes + }, + proxy: { + authorization: "", + url: "", + }, + completion: { + prompt: { + maxPrefixLines: 20, + maxSuffixLines: 20, + fillDeclarations: { + enabled: true, + maxSnippets: 5, + maxCharsPerSnippet: 500, + }, + collectSnippetsFromRecentChangedFiles: { + enabled: true, + maxSnippets: 3, + indexing: { + checkingChangesInterval: 500, + changesDebouncingInterval: 1000, + prefixLines: 20, + suffixLines: 20, + maxChunks: 100, + chunkSize: 500, + overlapLines: 1, + }, + }, + collectSnippetsFromRecentOpenedFiles: { + enabled: true, + maxOpenedFiles: 5, + maxCharsPerOpenedFiles: 500, + }, + clipboard: { + minChars: 3, + maxChars: 2000, + }, + }, + debounce: { + mode: "adaptive", + interval: 250, // ms + }, + solution: { + maxItems: 3, + maxTries: 6, + temperature: 0.8, + }, + }, + postprocess: { + limitScope: {}, + calculateReplaceRange: {}, + minCompletionChars: 4, + }, + chat: { + edit: { + // FIXME(@icycodes): use one config for max length of final prompt length, + // instead of documentMaxChars, commandMaxChars, fileContext.maxFiles and fileContext.maxCharsPerFile + documentMaxChars: 3000, + commandMaxChars: 200, + fileContext: { + maxFiles: 5, + maxCharsPerFile: 3000, + promptTemplate: [includeFileContextList, includeFileContextItem], + }, + responseDocumentTag: ["", ""], + responseCommentTag: undefined, + promptTemplate: { + replace: editCommandReplacePrompt, + insert: editCommandInsertPrompt, + }, + presetCommands: { + "/doc": { + label: "Generate Docs", + filters: { languageIdNotIn: "plaintext,markdown" }, + kind: "replace", + promptTemplate: generateDocsPrompt, + }, + "/fix": { + label: "Fix spelling and grammar errors", + filters: { languageIdIn: "plaintext,markdown" }, + kind: "replace", + promptTemplate: fixSpellingAndGrammarPrompt, + }, + }, + }, + generateCommitMessage: { + maxDiffLength: 3600, + promptTemplate: generateCommitMessagePrompt, + responseMatcher: + /^(?:(?!!\[|^\[.*\]:\s*http).*?)(?:["'`]?)?((?:feat|fix|docs|refactor|style|test|build|ci|chore)(?:\(\S+\))?:.+?)(?:["'`])?(?:\n|$)/ims.toString(), + }, + generateBranchName: { + maxDiffLength: 3600, + promptTemplate: generateBranchNamePrompt, + }, + smartApplyLineRange: { + promptTemplate: provideSmartApplyLineRangePrompt, + }, + smartApply: { + promptTemplate: generateSmartApplyPrompt, + }, + }, + logs: { + level: "silent", + }, + tls: { + caCerts: "system", + }, + anonymousUsageTracking: { + disable: false, + }, +}; diff --git a/clients/tabby-agent/src/config/index.ts b/clients/tabby-agent/src/config/index.ts new file mode 100644 index 000000000000..e600578f2874 --- /dev/null +++ b/clients/tabby-agent/src/config/index.ts @@ -0,0 +1,221 @@ +import type { Feature } from "../feature"; +import type { ConfigData, PartialConfigData } from "./type"; +import type { ConfigFile } from "./configFile"; +import type { + ClientCapabilities, + ServerCapabilities, + ClientProvidedConfig, + Config as TabbyLspConfig, +} from "../protocol"; +import type { TabbyServerProvidedConfig } from "../http/tabbyApiClient"; +import { EventEmitter } from "events"; +import { Connection } from "vscode-languageserver"; +import deepEqual from "deep-equal"; +import { deepmergeCustom } from "deepmerge-ts"; +import { ConfigRequest, ConfigDidChangeNotification } from "../protocol"; +import { defaultConfigData } from "./default"; +import { getConfigFile } from "./configFile"; +import { DataStore, StoredData } from "../dataStore"; +import { isBlank } from "../utils/string"; +import { getLogger } from "../logger"; + +function shouldBeNonBlankStringValue(meta: unknown) { + if (typeof meta == "object" && meta !== null && "key" in meta && typeof meta.key === "string") { + return ["endpoint", "token", "url", "authorization"].includes(meta.key); + } + return false; +} + +const mergeFunction = deepmergeCustom({ + mergeOthers: (values, utils, meta) => { + if (meta && shouldBeNonBlankStringValue(meta)) { + const nonBlankStringValues = values.filter((value) => typeof value === "string" && !isBlank(value)); + if (nonBlankStringValues.length > 0) { + return utils.defaultMergeFunctions.mergeOthers(nonBlankStringValues); + } else { + return ""; + } + } + return utils.actions.defaultMerge; + }, +}); + +function mergeConfig( + base: ConfigData, + configFile?: ConfigFile, + clientProvided?: ClientProvidedConfig, + serverProvided?: TabbyServerProvidedConfig, +): ConfigData { + const configFileConfig: PartialConfigData = configFile?.config ?? {}; + const clientProvidedConfig: PartialConfigData = {}; + if (clientProvided?.server !== undefined) { + clientProvidedConfig.server = clientProvided.server; + } + if (clientProvided?.proxy !== undefined) { + clientProvidedConfig.proxy = clientProvided.proxy; + } + if (clientProvided?.anonymousUsageTracking !== undefined) { + clientProvidedConfig.anonymousUsageTracking = clientProvided.anonymousUsageTracking; + } + const serverProvidedConfig: PartialConfigData = {}; + if (serverProvided?.disable_client_side_telemetry == true) { + serverProvidedConfig.anonymousUsageTracking = { disable: true }; + } + const merged = mergeFunction(base, configFileConfig, clientProvidedConfig, serverProvidedConfig) as ConfigData; + + // remove trailing slash from endpoint + if (merged.server.endpoint) { + merged.server.endpoint = merged.server.endpoint.replace(/\/+$/, ""); + } + + return merged; +} + +export class Configurations extends EventEmitter implements Feature { + private readonly logger = getLogger("Configurations"); + private readonly defaultConfig = defaultConfigData; + + private configFile: ConfigFile | undefined = undefined; // config from `~/.tabby-client/agent/config.toml` + private clientProvided: ClientProvidedConfig = {}; // config from lsp client + private serverProvided: TabbyServerProvidedConfig = {}; // config fetched from server and saved in dataStore + private mergedConfig: ConfigData = defaultConfigData; // merged config from (default, configFile, clientProvided, serverProvided) + + private configForLsp: TabbyLspConfig = { server: defaultConfigData["server"] }; // config for lsp client + + private lspConnection: Connection | undefined = undefined; + private clientCapabilities: ClientCapabilities | undefined = undefined; + + constructor(private readonly dataStore: DataStore) { + super(); + } + + private pickStoredServerProvidedConfig(data: Partial): TabbyServerProvidedConfig { + const mergedLocalConfig = mergeConfig(this.defaultConfig, this.configFile, this.clientProvided); + const serverEndpoint = mergedLocalConfig.server.endpoint; + return data.serverConfig?.[serverEndpoint] ?? {}; + } + + private update() { + const old = this.mergedConfig; + const merged = mergeConfig(this.defaultConfig, this.configFile, this.clientProvided, this.serverProvided); + if (!deepEqual(old, merged)) { + this.mergedConfig = merged; + this.logger.trace("Updated configurations.", { config: merged }); + this.emit("updated", merged, old); + + const oldConfigForLsp = this.configForLsp; + const configForLsp = { server: merged["server"] }; + if (!deepEqual(oldConfigForLsp, configForLsp)) { + this.configForLsp = configForLsp; + this.emit("configForLspUpdated", configForLsp, oldConfigForLsp); + } + } + } + + async preInitialize(): Promise { + this.configFile = getConfigFile(); + if (this.configFile) { + const configFile = this.configFile; + await configFile.load(); + configFile.on("updated", async () => { + this.serverProvided = this.pickStoredServerProvidedConfig(this.dataStore.data); + this.update(); + }); + configFile.watch(); + } + + this.serverProvided = this.pickStoredServerProvidedConfig(this.dataStore.data); + this.dataStore.on("updated", async (data: Partial) => { + const serverProvidedConfig = this.pickStoredServerProvidedConfig(data); + if (!deepEqual(serverProvidedConfig, this.serverProvided)) { + this.serverProvided = serverProvidedConfig; + this.update(); + } + }); + + this.update(); + } + + async initialize( + connection: Connection, + clientCapabilities: ClientCapabilities, + clientProvidedConfig: ClientProvidedConfig, + ): Promise { + this.lspConnection = connection; + this.clientCapabilities = clientCapabilities; + + this.updateClientProvidedConfig(clientProvidedConfig); + + connection.onDidChangeConfiguration(async (params) => { + return this.updateClientProvidedConfig(params.settings); + }); + connection.onRequest(ConfigRequest.type, async () => { + return this.getConfigForLsp(); + }); + if (clientCapabilities.tabby?.configDidChangeListener) { + this.on("configForLspUpdated", (config: TabbyLspConfig) => { + connection.sendNotification(ConfigDidChangeNotification.type, config); + }); + } + + return {}; + } + + async initialized(connection: Connection): Promise { + if (this.clientCapabilities?.tabby?.configDidChangeListener) { + const config = this.getConfigForLsp(); + connection.sendNotification(ConfigDidChangeNotification.type, config); + } + } + + getClientProvidedConfig(): ClientProvidedConfig { + return this.clientProvided; + } + + getMergedConfig(): ConfigData { + return this.mergedConfig; + } + + getConfigForLsp(): TabbyLspConfig { + return this.configForLsp; + } + + async refreshClientProvidedConfig(): Promise { + if (this.lspConnection && this.clientCapabilities?.workspace?.configuration) { + const config = await this.lspConnection.workspace.getConfiguration(); + this.updateClientProvidedConfig(config); + return true; + } + return false; + } + + private updateClientProvidedConfig(config: ClientProvidedConfig) { + if (!deepEqual(config, this.clientProvided)) { + const old = this.clientProvided; + this.clientProvided = config; + this.emit("clientProvidedConfigUpdated", config, old); + this.serverProvided = this.pickStoredServerProvidedConfig(this.dataStore.data); + this.update(); + } + } + + async updateServerProvidedConfig(config: TabbyServerProvidedConfig, save: boolean = false) { + if (!deepEqual(config, this.serverProvided)) { + this.serverProvided = config; + this.update(); + } + if (save) { + const mergedLocalConfig = mergeConfig(this.defaultConfig, this.configFile, this.clientProvided); + const serverEndpoint = mergedLocalConfig.server.endpoint; + if (!this.dataStore.data.serverConfig) { + this.dataStore.data.serverConfig = {}; + } + this.dataStore.data.serverConfig[serverEndpoint] = config; + try { + await this.dataStore.save(); + } catch (error) { + this.logger.error("Failed to save server provided config.", error); + } + } + } +} diff --git a/clients/tabby-agent/src/config/type.d.ts b/clients/tabby-agent/src/config/type.d.ts new file mode 100644 index 000000000000..3399abff6f77 --- /dev/null +++ b/clients/tabby-agent/src/config/type.d.ts @@ -0,0 +1,139 @@ +export type ConfigData = { + server: { + endpoint: string; + token: string; + requestHeaders: Record; + requestTimeout: number; + }; + proxy: { + authorization: string; + url: string; + }; + completion: { + prompt: { + maxPrefixLines: number; + maxSuffixLines: number; + fillDeclarations: { + enabled: boolean; + // max number of declaration snippets + maxSnippets: number; + // max number of characters per snippet + maxCharsPerSnippet: number; + }; + collectSnippetsFromRecentChangedFiles: { + enabled: boolean; + // max number of snippets + maxSnippets: number; + indexing: { + // Interval in ms for indexing worker to check pending task + checkingChangesInterval: number; + // Debouncing interval in ms for sending changes to indexing task + changesDebouncingInterval: number; + + // Determine the crop window at changed location for indexing + // Line before changed location + prefixLines: number; + // Line after changed location + suffixLines: number; + + // Max number of chunks in memory + maxChunks: number; + // chars per code chunk + chunkSize: number; + // overlap lines between neighbor chunks + overlapLines: number; + }; + }; + collectSnippetsFromRecentOpenedFiles: { + enabled: boolean; + //max number of opened files + maxOpenedFiles: number; + //chars size per each opened file + maxCharsPerOpenedFiles: number; + }; + clipboard: { + minChars: number; + maxChars: number; + }; + }; + debounce: { + mode: "adaptive" | "fixed"; + interval: number; + }; + solution: { + // The max number of unique choices to be fetched before stopping + maxItems: number; + // The max number of attempts to fetch choices before stopping + maxTries: number; + // The temperature for fetching the second and subsequent choices + temperature: number; + }; + }; + postprocess: { + limitScope: any; + calculateReplaceRange: any; + minCompletionChars: number; + }; + chat: { + edit: { + documentMaxChars: number; + commandMaxChars: number; + fileContext: { + maxFiles: number; + maxCharsPerFile: number; + promptTemplate: [string, string]; + }; + responseDocumentTag: string[]; + responseCommentTag: string[] | undefined; + promptTemplate: { + replace: string; + insert: string; + }; + presetCommands: Record< + string, + { + label: string; + filters: Record; + kind: "replace" | "insert"; + promptTemplate: string; + } + >; + }; + generateCommitMessage: { + maxDiffLength: number; + promptTemplate: string; + responseMatcher: string; + }; + generateBranchName: { + maxDiffLength: number; + promptTemplate: string; + }; + smartApplyLineRange: { + promptTemplate: string; + }; + smartApply: { + promptTemplate: string; + }; + }; + logs: { + // Controls the level of the logger written to the `~/.tabby-client/agent/logs/` + level: "silent" | "error" | "info" | "debug" | "trace"; + }; + tls: { + // `bundled`, `system`, or a string point to cert file + caCerts: string; + }; + anonymousUsageTracking: { + disable: boolean; + }; +}; + +type RecursivePartial = { + [P in keyof T]?: T[P] extends (infer U)[] + ? RecursivePartial[] + : T[P] extends object | undefined + ? RecursivePartial + : T[P]; +}; + +export type PartialConfigData = RecursivePartial; diff --git a/clients/tabby-agent/src/configFile.ts b/clients/tabby-agent/src/configFile.ts deleted file mode 100644 index 21b496a43d78..000000000000 --- a/clients/tabby-agent/src/configFile.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { EventEmitter } from "events"; -import path from "path"; -import os from "os"; -import fs from "fs-extra"; -import toml from "toml"; -import chokidar from "chokidar"; -import deepEqual from "deep-equal"; -import { getProperty, deleteProperty } from "dot-prop"; -import type { PartialAgentConfig } from "./AgentConfig"; -import { isBrowser } from "./env"; -import { rootLogger } from "./logger"; - -const configTomlTemplate = `## Tabby agent configuration file - -## Online documentation: https://tabby.tabbyml.com/docs/extensions/configurations -## You can uncomment and edit the values below to change the default settings. -## Configurations in this file have lower priority than the IDE settings. - -## Server -## You can set the server endpoint here and an optional authentication token if required. -# [server] -# endpoint = "http://localhost:8080" # http or https URL -# token = "your-token-here" # if token is set, request header Authorization = "Bearer $token" will be added automatically - -## You can add custom request headers. -# [server.requestHeaders] -# Header1 = "Value1" # list your custom headers here -# Header2 = "Value2" # values can be strings, numbers or booleans - -## Logs -## You can set the log level here. The log file is located at ~/.tabby-client/agent/logs/. -# [logs] -# level = "silent" # "silent" or "error" or "debug" - -## Anonymous usage tracking -## Tabby collects anonymous usage data and sends it to the Tabby team to help improve our products. -## Your code, generated completions, or any sensitive information is never tracked or sent. -## For more details on data collection, see https://tabby.tabbyml.com/docs/extensions/configurations#usage-collection -## Your contribution is greatly appreciated. However, if you prefer not to participate, you can disable anonymous usage tracking here. -# [anonymousUsageTracking] -# disable = false # set to true to disable - -`; - -const typeCheckSchema: Record = { - server: "object", - "server.endpoint": "string", - "server.token": "string", - "server.requestHeaders": "object", - "server.requestTimeout": "number", - completion: "object", - "completion.prompt": "object", - "completion.prompt.experimentalStripAutoClosingCharacters": "boolean", - "completion.prompt.maxPrefixLines": "number", - "completion.prompt.maxSuffixLines": "number", - "completion.prompt.clipboard": "object", - "completion.prompt.clipboard.minChars": "number", - "completion.prompt.clipboard.maxChars": "number", - "completion.debounce": "object", - "completion.debounce.mode": "string", - "completion.debounce.interval": "number", - postprocess: "object", - "postprocess.limitScopeByIndentation": "object", - "postprocess.limitScopeByIndentation.experimentalKeepBlockScopeWhenCompletingLine": "boolean", - logs: "object", - "logs.level": "string", - anonymousUsageTracking: "object", - "anonymousUsageTracking.disable": "boolean", -}; - -function validateConfig(config: PartialAgentConfig): PartialAgentConfig { - for (const [key, type] of Object.entries(typeCheckSchema)) { - if (typeof getProperty(config, key) !== type) { - deleteProperty(config, key); - } - } - return config; -} - -class ConfigFile extends EventEmitter { - private data: PartialAgentConfig = {}; - private watcher?: chokidar.FSWatcher; - private logger = rootLogger.child({ component: "ConfigFile" }); - - constructor(private readonly filepath: string) { - super(); - } - - get config(): PartialAgentConfig { - return this.data; - } - - async load() { - try { - const fileContent = await fs.readFile(this.filepath, "utf8"); - const data = toml.parse(fileContent); - // If the config file contains no value, overwrite it with the new template. - if (Object.keys(data).length === 0 && fileContent.trim() !== configTomlTemplate.trim()) { - await this.createTemplate(); - return; - } - this.data = validateConfig(data); - } catch (error) { - if (error instanceof Error && "code" in error && error.code === "ENOENT") { - await this.createTemplate(); - } else { - this.logger.error({ error }, "Failed to load config file"); - } - } - } - - watch() { - this.watcher = chokidar.watch(this.filepath, { - interval: 1000, - }); - const onChanged = async () => { - const oldData = this.data; - await this.load(); - if (!deepEqual(oldData, this.data)) { - super.emit("updated", this.data); - } - }; - this.watcher.on("add", onChanged); - this.watcher.on("change", onChanged); - } - - private async createTemplate() { - try { - await fs.outputFile(this.filepath, configTomlTemplate); - } catch (error) { - this.logger.error({ error }, "Failed to create config template file"); - } - } -} - -const configFilePath = path.join(os.homedir(), ".tabby-client", "agent", "config.toml"); -export const configFile = isBrowser ? undefined : new ConfigFile(configFilePath); diff --git a/clients/tabby-agent/src/contextProviders/declarationSnippets.ts b/clients/tabby-agent/src/contextProviders/declarationSnippets.ts new file mode 100644 index 000000000000..b332f1336124 --- /dev/null +++ b/clients/tabby-agent/src/contextProviders/declarationSnippets.ts @@ -0,0 +1,173 @@ +import type { Feature } from "../feature"; +import type { TextDocumentReader, TextDocumentRangeContext } from "./documentContexts"; +import { getLogger } from "../logger"; +import type { Location, CancellationToken, Position } from "vscode-languageserver-protocol"; +import { + ClientCapabilities, + LanguageSupportDeclarationRequest, + LanguageSupportSemanticTokensRangeRequest, + ServerCapabilities, +} from "../protocol"; +import type { Connection } from "vscode-languageserver"; +import { intersectionRange, isPositionInRange } from "../utils/range"; + +export class DeclarationSnippetsProvider implements Feature { + private readonly logger = getLogger("DeclarationSnippetsProvider"); + + private lspConnection: Connection | undefined = undefined; + private clientCapabilities: ClientCapabilities | undefined = undefined; + + constructor(private readonly documentReader: TextDocumentReader) {} + + initialize(connection: Connection, clientCapabilities: ClientCapabilities): ServerCapabilities { + this.lspConnection = connection; + this.clientCapabilities = clientCapabilities; + return {}; + } + + async collect( + location: Location, + limit: number | undefined, + noReverse = false, + token: CancellationToken, + ): Promise { + if (!this.lspConnection || !this.clientCapabilities?.tabby?.languageSupport) { + return undefined; + } + this.logger.trace("Collecting snippets for:", { location }); + const extractedSymbols = await this.extractSemanticTokenPositions(location, token); + if (!extractedSymbols) { + return undefined; + } + const allowedSymbolTypes = [ + "class", + "decorator", + "enum", + "function", + "interface", + "macro", + "method", + "namespace", + "struct", + "type", + "typeParameter", + ]; + const symbols = extractedSymbols.filter((symbol) => symbol.type && allowedSymbolTypes.includes(symbol.type)); + this.logger.trace("Found symbols:", { symbols }); + + // Loop through the symbol positions backwards + const snippets: TextDocumentRangeContext[] = []; + + for ( + let symbolIndex = noReverse ? 0 : symbols.length - 1; + noReverse ? symbolIndex < symbols.length : symbolIndex >= 0; + noReverse ? symbolIndex++ : symbolIndex-- + ) { + if (limit != undefined && snippets.length >= limit) { + // Stop collecting snippets if the max number of snippets is reached + break; + } + const sourcePosition = symbols[symbolIndex]?.position; + if (!sourcePosition) { + continue; + } + const result = await this.lspConnection.sendRequest( + LanguageSupportDeclarationRequest.type, + { + textDocument: { uri: location.uri }, + position: sourcePosition, + }, + token, + ); + if (!result) { + continue; + } + const firstResult = Array.isArray(result) ? result[0] : result; + if (!firstResult) { + continue; + } + + const target: Location = { + uri: "targetUri" in firstResult ? firstResult.targetUri : firstResult.uri, + range: "targetRange" in firstResult ? firstResult.targetRange : firstResult.range, + }; + if (target.uri == location.uri && isPositionInRange(target.range.start, location.range)) { + // Skipping snippet as it is contained in the source location + // this also includes the case of the symbol's declaration is at this position itself + continue; + } + if ( + snippets.find( + (snippet) => target.uri == snippet.uri && (!snippet.range || intersectionRange(target.range, snippet.range)), + ) + ) { + // Skipping snippet as it is already collected + continue; + } + + const snippet = await this.documentReader.read(target.uri, target.range, token); + if (snippet) { + snippets.push(snippet); + } + } + this.logger.trace("Collected snippets:", snippets); + return snippets; + } + + private async extractSemanticTokenPositions( + location: Location, + token: CancellationToken, + ): Promise< + | { + position: Position; + type: string | undefined; + }[] + | undefined + > { + if (!this.lspConnection || !this.clientCapabilities?.tabby?.languageSupport) { + return undefined; + } + + const result = await this.lspConnection.sendRequest( + LanguageSupportSemanticTokensRangeRequest.type, + { + textDocument: { uri: location.uri }, + range: location.range, + }, + token, + ); + if (!result || !result.legend || !result.legend.tokenTypes || !result.tokens || !result.tokens.data) { + return undefined; + } + const { legend, tokens } = result; + const data: number[] = Array.isArray(tokens.data) ? tokens.data : Object.values(tokens.data); + const semanticSymbols: { + position: Position; + type: string | undefined; + }[] = []; + let line = 0; + let character = 0; + for (let i = 0; i + 4 < data.length; i += 5) { + const deltaLine = data[i]; + const deltaChar = data[i + 1]; + // i + 2 is token length, not used here + const typeIndex = data[i + 3]; + // i + 4 is type modifiers, not used here + if (deltaLine === undefined || deltaChar === undefined || typeIndex === undefined) { + break; + } + + line += deltaLine; + if (deltaLine > 0) { + character = deltaChar; + } else { + character += deltaChar; + } + semanticSymbols.push({ + position: { line, character }, + type: legend.tokenTypes[typeIndex], + }); + } + return semanticSymbols; + } +} diff --git a/clients/tabby-agent/src/contextProviders/documentContexts.ts b/clients/tabby-agent/src/contextProviders/documentContexts.ts new file mode 100644 index 000000000000..4e8acd6cddf7 --- /dev/null +++ b/clients/tabby-agent/src/contextProviders/documentContexts.ts @@ -0,0 +1,117 @@ +import type { CancellationToken, Connection, Range } from "vscode-languageserver"; +import { TextDocument } from "vscode-languageserver-textdocument"; +import fs from "fs-extra"; +import type { TextDocuments } from "../extensions/textDocuments"; +import type { Feature } from "../feature"; +import { ClientCapabilities, ServerCapabilities, ReadFileRequest, ReadFileParams } from "../protocol"; +import { getLogger } from "../logger"; +import { isBrowser } from "../env"; +import { getLanguageId } from "../utils/languageId"; + +export interface TextDocumentRangeContext { + uri: string; + language: string; + /** + * If the range is provided, this context presents a range of the text document. + */ + range?: Range; + /** + * The full text of the document if the range is not provided. + * The text in the range of the document if the range is provided. + */ + text: string; +} + +export class TextDocumentReader implements Feature { + private readonly logger = getLogger("TextDocumentReader"); + + private lspConnection: Connection | undefined = undefined; + private clientCapabilities: ClientCapabilities | undefined = undefined; + + constructor(private readonly documents: TextDocuments) {} + + initialize(connection: Connection, clientCapabilities: ClientCapabilities): ServerCapabilities { + this.lspConnection = connection; + this.clientCapabilities = clientCapabilities; + return {}; + } + + async read( + documentOrUri: TextDocument | string, + range: Range | undefined, + token: CancellationToken | undefined, + ): Promise { + let targetDocument: TextDocument | undefined = undefined; + let targetUri: string | undefined = undefined; + if (typeof documentOrUri === "string") { + targetDocument = this.documents.get(documentOrUri); + targetUri = documentOrUri; + } else { + targetDocument = documentOrUri; + targetUri = documentOrUri.uri; + } + + let context: TextDocumentRangeContext | undefined = undefined; + if (targetDocument) { + context = { + uri: targetDocument.uri, + language: targetDocument.languageId, + range: range, + text: targetDocument.getText(range), + }; + this.logger.trace("Read context from synced text document.", context); + } else if (targetUri) { + const language = getLanguageId(targetUri); + // read from lsp connection + if (this.lspConnection && this.clientCapabilities?.tabby?.workspaceFileSystem) { + try { + const params: ReadFileParams = { + uri: targetUri, + format: "text", + range: range, + }; + const result = await this.lspConnection.sendRequest(ReadFileRequest.type, params, token); + if (result && typeof result.text === "string") { + context = { + uri: targetUri, + language: language, + range: range, + text: result.text, + }; + this.logger.trace("Read context from LSP ReadFileRequest.", { result }); + } + } catch (error) { + this.logger.trace("ReadFileRequest failed.", { error }); + } + } + + // fallback to fs + if (!context && !isBrowser) { + try { + const fileContent = await new Promise((resolve, reject) => { + const readFilePromise = fs.readFile(targetUri, "utf-8"); + if (token) { + token.onCancellationRequested(() => { + reject(new Error("Operation canceled")); + }); + } + readFilePromise.then(resolve).catch(reject); + }); + + const textDocument = TextDocument.create(targetUri, language, 0, fileContent); + const text = textDocument.getText(range); + context = { + uri: targetUri, + language: language, + range: range, + text: text, + }; + this.logger.trace("Read context from file system.", { text }); + } catch (error) { + this.logger.trace("Read file failed.", { error }); + } + } + } + return context; + } +} diff --git a/clients/tabby-agent/src/contextProviders/editorOptions.ts b/clients/tabby-agent/src/contextProviders/editorOptions.ts new file mode 100644 index 000000000000..fcb1f9dad02b --- /dev/null +++ b/clients/tabby-agent/src/contextProviders/editorOptions.ts @@ -0,0 +1,36 @@ +import type { CancellationToken, Connection } from "vscode-languageserver"; +import type { Feature } from "../feature"; +import { ClientCapabilities, ServerCapabilities, EditorOptionsRequest, EditorOptions } from "../protocol"; + +export type EditorOptionsContext = EditorOptions; + +export class EditorOptionsProvider implements Feature { + // FIXME: add cache and listen to editor options changes + + private lspConnection: Connection | undefined = undefined; + private clientCapabilities: ClientCapabilities | undefined = undefined; + + constructor() {} + + initialize(connection: Connection, clientCapabilities: ClientCapabilities): ServerCapabilities { + this.lspConnection = connection; + this.clientCapabilities = clientCapabilities; + return {}; + } + + async getEditorOptions(uri: string, token: CancellationToken): Promise { + if (this.lspConnection && this.clientCapabilities?.tabby?.editorOptions) { + const editorOptions: EditorOptions | null = await this.lspConnection.sendRequest( + EditorOptionsRequest.type, + { + uri, + }, + token, + ); + if (editorOptions) { + return editorOptions; + } + } + return undefined; + } +} diff --git a/clients/tabby-agent/src/contextProviders/editorVisibleRanges.ts b/clients/tabby-agent/src/contextProviders/editorVisibleRanges.ts new file mode 100644 index 000000000000..4b28f7f0002b --- /dev/null +++ b/clients/tabby-agent/src/contextProviders/editorVisibleRanges.ts @@ -0,0 +1,87 @@ +import { Connection, Location } from "vscode-languageserver"; +import { Feature } from "../feature"; +import { DidChangeActiveEditorNotification, DidChangeActiveEditorParams, ServerCapabilities } from "../protocol"; +import { Configurations } from "../config"; +import { LRUCache } from "lru-cache"; +import { intersectionRange } from "../utils/range"; +import { ConfigData } from "../config/type"; +import deepEqual from "deep-equal"; + +export class EditorVisibleRangesTracker implements Feature { + private history: LRUCache | undefined = undefined; + private version = 0; + + constructor(private readonly configurations: Configurations) {} + + initialize(connection: Connection): ServerCapabilities | Promise { + this.setup(); + + this.configurations.on("updated", (config: ConfigData, oldConfig: ConfigData) => { + if (!deepEqual(pickConfig(config), pickConfig(oldConfig))) { + this.shutdown(); + this.setup(); + } + }); + + connection.onNotification(DidChangeActiveEditorNotification.type, (param: DidChangeActiveEditorParams) => { + this.updateHistory(param); + }); + return {}; + } + + shutdown() { + this.history = undefined; + } + + private setup() { + const config = pickConfig(this.configurations.getMergedConfig()); + if (config.enabled) { + this.history = new LRUCache({ + max: 1000, + ttl: 5 * 60 * 1000, // 5 minutes + }); + } + } + + private updateHistory(param: DidChangeActiveEditorParams) { + if (this.history) { + this.version++; + this.history.set(this.version, param.activeEditor); + } + } + + getVersion(): number { + return this.version; + } + + async getHistoryRanges(options?: { max?: number; excludedUris?: string[] }): Promise { + if (!this.history) { + return undefined; + } + + const result: Location[] = []; + for (const item of this.history.values()) { + if (options?.max && result.length >= options?.max) { + break; + } + const location = await item; + if (location) { + if (options?.excludedUris?.includes(location.uri)) { + continue; + } + + const foundIntersection = result.find( + (r) => r.uri === location.uri && intersectionRange(r.range, location.range), + ); + if (!foundIntersection) { + result.push(location); + } + } + } + return result; + } +} + +function pickConfig(configData: ConfigData) { + return configData.completion.prompt.collectSnippetsFromRecentOpenedFiles; +} diff --git a/clients/tabby-agent/src/contextProviders/git/gitCommand.ts b/clients/tabby-agent/src/contextProviders/git/gitCommand.ts new file mode 100644 index 000000000000..0f737fc96e16 --- /dev/null +++ b/clients/tabby-agent/src/contextProviders/git/gitCommand.ts @@ -0,0 +1,146 @@ +import { spawn } from "child_process"; +import * as path from "path"; +import * as fs from "fs-extra"; +import { parse as uriParse, serialize as uriSerialize } from "uri-js"; +import { CancellationToken } from "vscode-languageserver-protocol"; +import { GitRepositoryParams, GitRepository, GitDiffParams, GitDiffResult } from "../../protocol"; +import { isBrowser } from "../../env"; +import { getLogger } from "../../logger"; +import "../../utils/array"; + +export interface GitCommandRunner { + getRepository(params: GitRepositoryParams, token?: CancellationToken): Promise; + diff(params: GitDiffParams, token?: CancellationToken): Promise; +} + +const logger = getLogger("GitCommandRunner"); + +async function executeGitCommand(cwd?: string, args: string[] = [], token?: CancellationToken): Promise { + return new Promise((resolve, reject) => { + const git = spawn("git", args, { + cwd, + }); + let result = ""; + + git.stdout.on("data", (data) => { + result += data.toString(); + }); + + git.on("error", (error) => { + reject(`Git command error: ${error}, cwd: ${cwd}, args: ${args.join(" ")}`); + }); + + const exitHandler = (code: number | null) => { + if (code === 0) { + resolve(result.trim()); + } else { + reject(`Git command failed, code: ${code}, cwd: ${cwd}, args: ${args.join(" ")}`); + } + }; + git.on("exit", exitHandler); + git.on("close", exitHandler); + + if (token?.isCancellationRequested) { + reject("The request is canceled."); + } + token?.onCancellationRequested(() => { + reject("The request is canceled."); + }); + }); +} + +async function ensureCwd(filepath: string): Promise { + const stats = await fs.stat(filepath); + if (stats.isDirectory()) { + return filepath; + } + return path.dirname(filepath); +} + +function replaceUriPath(uri: string, path: string): string { + const uriComponents = uriParse(uri); + uriComponents.path = path; + return uriSerialize(uriComponents); +} + +async function isGitCommandAvailable(): Promise { + try { + const version = await executeGitCommand(undefined, ["--version"]); + logger.debug(`Git command is available, ${version}.`); + return true; + } catch (e) { + logger.debug(`Git command is not available. ${e}`); + return false; + } +} + +async function getRepository(params: GitRepositoryParams, token?: CancellationToken): Promise { + try { + logger.trace("Get repository: ", { params }); + const { scheme, path: filepath } = uriParse(params.uri); + if (scheme !== "file" || !filepath) { + return null; + } + const cwd = await ensureCwd(filepath); + const rootPath = await executeGitCommand(cwd, ["rev-parse", "--show-toplevel"], token); + const root = replaceUriPath(params.uri, rootPath); + const remoteOutput = await executeGitCommand(rootPath, ["remote", "-v"], token); + const remotes = remoteOutput + .split("\n") + .map((remoteLine) => { + const [name, url] = remoteLine.trim().split(/\s+/); + return { name, url }; + }) + .filter<{ name: string; url: string }>((remote): remote is { name: string; url: string } => { + return !!remote.name && !!remote.url; + }) + .distinct((item) => item.name); + const result = { root, remotes }; + logger.trace("Get repository result: ", { result }); + return result; + } catch (e) { + logger.debug(`Failed to get repository for ${params.uri}. ${e}`); + return null; + } +} + +async function diff(params: GitDiffParams, token?: CancellationToken): Promise { + try { + logger.trace("Get diff: ", { params }); + const { repository, cached } = params; + const { scheme, path: rootPath } = uriParse(repository); + if (scheme !== "file" || !rootPath) { + return null; + } + const args = ["diff"]; + if (cached) { + args.push("--cached"); + } + const diff = await executeGitCommand(rootPath, args, token); + const result = { diff }; + logger.trace("Get diff result: ", { result }); + return result; + } catch (e) { + logger.debug(`Failed to get diff for ${params.repository}. ${e}`); + return null; + } +} + +let gitCommandRunner: GitCommandRunner | undefined = undefined; + +export async function getGitCommandRunner(): Promise { + if (isBrowser) { + return undefined; + } + if (gitCommandRunner == undefined) { + if (await isGitCommandAvailable()) { + gitCommandRunner = { + getRepository, + diff, + }; + } else { + gitCommandRunner = undefined; + } + } + return gitCommandRunner; +} diff --git a/clients/tabby-agent/src/contextProviders/git/index.ts b/clients/tabby-agent/src/contextProviders/git/index.ts new file mode 100644 index 000000000000..86c8c28a0c73 --- /dev/null +++ b/clients/tabby-agent/src/contextProviders/git/index.ts @@ -0,0 +1,58 @@ +import type { Connection, CancellationToken } from "vscode-languageserver"; +import type { Feature } from "../../feature"; +import type { GitCommandRunner } from "./gitCommand"; +import { + ClientCapabilities, + ServerCapabilities, + GitRepositoryParams, + GitRepository, + GitRepositoryRequest, + GitDiffParams, + GitDiffResult, + GitDiffRequest, +} from "../../protocol"; +import { getGitCommandRunner } from "./gitCommand"; + +export interface GitContext { + repository: GitRepository; +} + +export class GitContextProvider implements Feature { + private lspConnection: Connection | undefined = undefined; + private gitCommandRunner: GitCommandRunner | undefined = undefined; + + async initialize(connection: Connection, clientCapabilities: ClientCapabilities): Promise { + if (clientCapabilities.tabby?.gitProvider) { + this.lspConnection = connection; + } else { + this.gitCommandRunner = await getGitCommandRunner(); + } + return {}; + } + + async getContext(uri: string, token: CancellationToken): Promise { + const repository = await this.getRepository({ uri: uri }, token); + if (repository) { + return { repository }; + } + return null; + } + + async getRepository(params: GitRepositoryParams, token: CancellationToken): Promise { + if (this.lspConnection) { + return await this.lspConnection.sendRequest(GitRepositoryRequest.type, params, token); + } else if (this.gitCommandRunner) { + return await this.gitCommandRunner.getRepository(params, token); + } + return null; + } + + async diff(params: GitDiffParams, token: CancellationToken): Promise { + if (this.lspConnection) { + return await this.lspConnection.sendRequest(GitDiffRequest.type, params, token); + } else if (this.gitCommandRunner) { + return await this.gitCommandRunner.diff(params, token); + } + return null; + } +} diff --git a/clients/tabby-agent/src/contextProviders/recentlyChangedCodeSearch.ts b/clients/tabby-agent/src/contextProviders/recentlyChangedCodeSearch.ts new file mode 100644 index 000000000000..3ddb44b627c1 --- /dev/null +++ b/clients/tabby-agent/src/contextProviders/recentlyChangedCodeSearch.ts @@ -0,0 +1,181 @@ +import type { Range, ServerCapabilities } from "vscode-languageserver"; +import type { TextDocument, TextDocumentContentChangeEvent } from "vscode-languageserver-textdocument"; +import type { TextDocuments } from "../extensions/textDocuments"; +import type { Feature } from "../feature"; +import type { Configurations } from "../config"; +import type { ConfigData } from "../config/type"; +import type { DocumentRange } from "../utils/types"; +import { CodeSearchEngine, CodeSearchResult } from "../codeSearch"; +import deepEqual from "deep-equal"; +import { unionRange, rangeInDocument } from "../utils/range"; +import { getLogger } from "../logger"; + +export class RecentlyChangedCodeSearch implements Feature { + private readonly logger = getLogger("RecentlyChangedCodeSearch"); + private codeSearchEngine: CodeSearchEngine | undefined = undefined; + + private indexingWorker: ReturnType | undefined = undefined; + + private pendingDocumentRanges: DocumentRange[] = []; + private didChangeEventDebouncingCache = new Map< + string, + { documentRange: DocumentRange; timer: ReturnType } + >(); + + constructor( + private readonly configurations: Configurations, + private readonly documents: TextDocuments, + ) {} + + initialize(): ServerCapabilities { + this.setup(); + this.configurations.on("updated", (config: ConfigData, oldConfig: ConfigData) => { + if (!deepEqual(pickConfig(config), pickConfig(oldConfig))) { + this.shutdown(); + this.setup(); + } + }); + + this.documents.onDidChangeContent(async (params: unknown) => { + if (!params || typeof params !== "object" || !("document" in params) || !("contentChanges" in params)) { + return; + } + const event = params as { document: TextDocument; contentChanges: TextDocumentContentChangeEvent[] }; + this.handleDidChangeTextDocument(event); + }); + + return {}; + } + + shutdown() { + if (this.indexingWorker) { + clearInterval(this.indexingWorker); + } + this.codeSearchEngine = undefined; + } + + private setup() { + const config = pickConfig(this.configurations.getMergedConfig()); + if (!config.enabled) { + this.logger.info("Recently changed code search is disabled."); + return; + } + + const engine = new CodeSearchEngine(config.indexing); + this.codeSearchEngine = engine; + + this.indexingWorker = setInterval(async () => { + let documentRange: DocumentRange | undefined = undefined; + while ((documentRange = this.pendingDocumentRanges.shift()) != undefined) { + this.logger.trace("Consuming indexing task."); + await engine.index(documentRange); + } + }, config.indexing.checkingChangesInterval); + this.logger.info("Created code search engine for recently changed files."); + this.logger.trace("Created with config.", { config }); + } + + private handleDidChangeTextDocument(event: { + document: TextDocument; + contentChanges: TextDocumentContentChangeEvent[]; + }) { + const config = pickConfig(this.configurations.getMergedConfig()); + + const { document, contentChanges } = event; + if (contentChanges.length < 1) { + return; + } + let ranges = []; + if (this.didChangeEventDebouncingCache.has(document.uri)) { + const cache = this.didChangeEventDebouncingCache.get(document.uri); + if (cache) { + ranges.push(cache.documentRange.range); + clearTimeout(cache.timer); + } + } + ranges = ranges.concat( + contentChanges + .map((change) => + "range" in change + ? { + start: change.range.start, + end: document.positionAt(document.offsetAt(change.range.start) + change.text.length), + } + : null, + ) + .filter((range): range is Range => range !== null), + ); + const mergedEditedRange = ranges.reduce((a, b) => unionRange(a, b)); + // Expand the range to cropping window + const expand: Range = { + start: { + line: Math.max(0, mergedEditedRange.start.line - config.indexing.prefixLines), + character: 0, + }, + end: { + line: Math.min(document.lineCount, mergedEditedRange.end.line + config.indexing.suffixLines + 1), + character: 0, + }, + }; + const targetRange = rangeInDocument(expand, document); + if (targetRange === null) { + return; + } + const documentRange = { document, range: targetRange }; + // A debouncing to avoid indexing the same document multiple times in a short time + this.didChangeEventDebouncingCache.set(document.uri, { + documentRange, + timer: setTimeout(() => { + this.pendingDocumentRanges.push(documentRange); + this.didChangeEventDebouncingCache.delete(document.uri); + this.logger.trace("Created indexing task:", { + document: documentRange.document.uri, + range: documentRange.range, + }); + }, config.indexing.changesDebouncingInterval), + }); + } + + async search( + query: string, + excludes: string[], + language: string, + limit?: number, + ): Promise { + const engine = this.codeSearchEngine; + if (!engine) { + return undefined; + } + const indexedDocumentRange = engine.getIndexedDocumentRange(); + + const filepaths = indexedDocumentRange + .map((documentRange) => documentRange.document.uri.toString()) + .filter((filepath) => !excludes.includes(filepath)); + if (filepaths.length < 1) { + return []; + } + + const options = { + filepathsFilter: filepaths, + languagesFilter: getLanguageFilter(language), + limit, + }; + this.logger.trace("Search in recently changed files", { query, options }); + const result = await engine.search(query, options); + this.logger.trace("Search result", { result }); + return result; + } +} + +function pickConfig(configData: ConfigData) { + return configData.completion.prompt.collectSnippetsFromRecentChangedFiles; +} + +function getLanguageFilter(languageId: string): string[] { + const tsx = ["javascript", "javascriptreact", "typescript", "typescriptreact"]; + if (tsx.includes(languageId)) { + return tsx; + } else { + return [languageId]; + } +} diff --git a/clients/tabby-agent/src/contextProviders/workspace.ts b/clients/tabby-agent/src/contextProviders/workspace.ts new file mode 100644 index 000000000000..d1a7c21d476a --- /dev/null +++ b/clients/tabby-agent/src/contextProviders/workspace.ts @@ -0,0 +1,35 @@ +import type { Connection } from "vscode-languageserver"; +import type { Feature } from "../feature"; +import { ClientCapabilities, ServerCapabilities } from "../protocol"; + +export interface WorkspaceContext { + uri?: string; +} + +export class WorkspaceContextProvider implements Feature { + // FIXME: add cache and listen to workspace changes + + private lspConnection: Connection | undefined = undefined; + private clientCapabilities: ClientCapabilities | undefined = undefined; + + constructor() {} + + initialize(connection: Connection, clientCapabilities: ClientCapabilities): ServerCapabilities { + this.lspConnection = connection; + this.clientCapabilities = clientCapabilities; + return {}; + } + + async getWorkspaceContext(uri: string): Promise { + if (this.lspConnection && this.clientCapabilities?.workspace) { + const workspaceFolders = await this.lspConnection.workspace.getWorkspaceFolders(); + const workspace = workspaceFolders?.find((folder) => uri.startsWith(folder.uri)); + if (workspace) { + return { + uri: workspace.uri, + }; + } + } + return undefined; + } +} diff --git a/clients/tabby-agent/src/dataStore.ts b/clients/tabby-agent/src/dataStore.ts deleted file mode 100644 index 22c76f8c88d9..000000000000 --- a/clients/tabby-agent/src/dataStore.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { EventEmitter } from "events"; -import path from "path"; -import os from "os"; -import fs from "fs-extra"; -import deepEqual from "deep-equal"; -import chokidar from "chokidar"; -import { isBrowser } from "./env"; - -export type StoredData = { - anonymousId: string; - auth: { [endpoint: string]: { jwt: string } }; -}; - -export interface DataStore { - data: Partial; - load(): PromiseLike; - save(): PromiseLike; -} - -class FileDataStore extends EventEmitter implements FileDataStore { - private watcher?: ReturnType; - public data: Partial = {}; - - constructor(private readonly filepath: string) { - super(); - } - - async load() { - this.data = (await fs.readJson(dataFile, { throws: false })) || {}; - } - - async save() { - await fs.outputJson(dataFile, this.data); - } - - watch() { - this.watcher = chokidar.watch(this.filepath, { - interval: 1000, - }); - const onChanged = async () => { - const oldData = this.data; - await this.load(); - if (!deepEqual(oldData, this.data)) { - super.emit("updated", this.data); - } - }; - this.watcher.on("add", onChanged); - this.watcher.on("change", onChanged); - } -} - -const dataFile = path.join(os.homedir(), ".tabby-client", "agent", "data.json"); -export const dataStore = isBrowser ? undefined : new FileDataStore(dataFile); diff --git a/clients/tabby-agent/src/dataStore/dataFile.ts b/clients/tabby-agent/src/dataStore/dataFile.ts new file mode 100644 index 000000000000..72479782c09b --- /dev/null +++ b/clients/tabby-agent/src/dataStore/dataFile.ts @@ -0,0 +1,50 @@ +import { EventEmitter } from "events"; +import path from "path"; +import os from "os"; +import fs from "fs-extra"; +import chokidar from "chokidar"; +import { isBrowser } from "../env"; +import { getLogger } from "../logger"; + +export class FileDataStore extends EventEmitter { + private readonly logger = getLogger("DataStore"); + private watcher?: ReturnType; + + constructor(private readonly filepath: string) { + super(); + } + + async read(): Promise { + try { + const json = await fs.readJson(this.filepath, { throws: false }); + return json ?? {}; + } catch (err) { + this.logger.warn(`Failed to read ${this.filepath}: ${err}`); + return {}; + } + } + + async write(data: unknown) { + await fs.outputJson(this.filepath, data); + } + + watch() { + this.watcher = chokidar.watch(this.filepath, { + interval: 1000, + }); + const onUpdated = async () => { + this.emit("updated"); + }; + this.watcher.on("add", onUpdated); + this.watcher.on("change", onUpdated); + } + + stopWatch() { + this.watcher?.close(); + } +} + +export function getFileDataStore(): FileDataStore | undefined { + const dataFilePath = path.join(os.homedir(), ".tabby-client", "agent", "data.json"); + return isBrowser ? undefined : new FileDataStore(dataFilePath); +} diff --git a/clients/tabby-agent/src/dataStore/index.ts b/clients/tabby-agent/src/dataStore/index.ts new file mode 100644 index 000000000000..64c8567ff3ba --- /dev/null +++ b/clients/tabby-agent/src/dataStore/index.ts @@ -0,0 +1,110 @@ +import type { Connection } from "vscode-languageserver"; +import type { + ClientCapabilities, + ClientProvidedConfig, + DataStoreRecords, + ServerCapabilities, + StatusIssuesName, +} from "../protocol"; +import type { TabbyServerProvidedConfig } from "../http/tabbyApiClient"; +import type { Feature } from "../feature"; +import type { FileDataStore } from "./dataFile"; +import { EventEmitter } from "events"; +import { DataStoreDidUpdateNotification, DataStoreUpdateRequest } from "../protocol"; +import { getFileDataStore } from "./dataFile"; +import deepEqual from "deep-equal"; + +export type StoredData = { + anonymousId: string; + serverConfig: { [endpoint: string]: TabbyServerProvidedConfig }; + statusIgnoredIssues: StatusIssuesName[]; +}; + +export class DataStore extends EventEmitter implements Feature { + public data: Partial = {}; + + private lspConnection: Connection | undefined = undefined; + private lspInitialized = false; + + private fileDataStore: FileDataStore | undefined = undefined; + + async preInitialize(): Promise { + const dataStore = getFileDataStore(); + if (dataStore) { + this.fileDataStore = dataStore; + + this.data = (await dataStore.read()) as Partial; + dataStore.on("updated", async () => { + const data = (await dataStore.read()) as Partial; + if (!deepEqual(data, this.data)) { + const old = this.data; + this.data = data; + this.emit("updated", this.data, old); + } + }); + dataStore.watch(); + } + } + + async initialize( + connection: Connection, + clientCapabilities: ClientCapabilities, + _clientProvidedConfig: ClientProvidedConfig, + dataStoreRecords: DataStoreRecords | undefined, + ): Promise { + if (clientCapabilities.tabby?.dataStore) { + this.lspConnection = connection; + + // When dataStore is provided by the LSP connection, do not use the file data store anymore. + const dataStore = this.fileDataStore; + if (dataStore) { + dataStore.stopWatch(); + this.fileDataStore = undefined; + const old = this.data; + this.data = dataStoreRecords ?? {}; + this.emit("updated", this.data, old); + } else { + this.data = dataStoreRecords ?? {}; + } + + connection.onNotification(DataStoreDidUpdateNotification.type, async (params) => { + const records = params ?? {}; + if (!deepEqual(records, this.data)) { + const old = this.data; + this.data = records; + this.emit("updated", this.data, old); + } + }); + } + return {}; + } + + async initialized() { + if (this.lspConnection) { + this.lspInitialized = true; + this.emit("initialized"); + } + } + + async save() { + if (this.lspConnection) { + const connection = this.lspConnection; + const sendUpdateRequest = async () => { + await connection.sendRequest(DataStoreUpdateRequest.type, this.data); + }; + if (this.lspInitialized) { + await sendUpdateRequest(); + } else { + this.once("initialized", async () => { + await sendUpdateRequest(); + }); + } + } else if (this.fileDataStore) { + const oldData = (await this.fileDataStore.read()) as Partial; + if (!deepEqual(oldData, this.data)) { + await this.fileDataStore.write(this.data); + this.emit("updated", this.data, oldData); + } + } + } +} diff --git a/clients/tabby-agent/src/env.ts b/clients/tabby-agent/src/env.ts index 9153cde8b661..d6383b2014ce 100644 --- a/clients/tabby-agent/src/env.ts +++ b/clients/tabby-agent/src/env.ts @@ -1,4 +1,3 @@ // FIXME: refactor env variables for running tests export const isBrowser = !!process.env["IS_BROWSER"]; export const isTest = !!process.env["IS_TEST"]; -export const testLogDebug = !!process.env["TEST_LOG_DEBUG"]; diff --git a/clients/tabby-agent/src/extensions/textDocuments.ts b/clients/tabby-agent/src/extensions/textDocuments.ts new file mode 100644 index 000000000000..e9f52d0fd004 --- /dev/null +++ b/clients/tabby-agent/src/extensions/textDocuments.ts @@ -0,0 +1,45 @@ +import { + Disposable, + DocumentUri, + TextDocumentsConfiguration, + DidChangeTextDocumentParams, + TextDocuments as LspTextDocuments, +} from "vscode-languageserver"; +import { TextDocumentConnection } from "vscode-languageserver/lib/common/textDocuments"; + +export class TextDocuments< + T extends { + uri: DocumentUri; + }, +> extends LspTextDocuments { + constructor(configuration: TextDocumentsConfiguration) { + super(configuration); + } + + override listen(connection: TextDocumentConnection): Disposable { + const disposables: Disposable[] = []; + disposables.push(super.listen(connection)); + // override onDidChangeTextDocument listener + disposables.push( + connection.onDidChangeTextDocument((params: DidChangeTextDocumentParams) => { + const { textDocument, contentChanges } = params; + if (contentChanges.length === 0) { + return; + } + const { version } = textDocument; + if (version === null || version === undefined) { + throw new Error(`Received document change event for ${textDocument.uri} without valid version identifier`); + } + let document = this.get(textDocument.uri); + if (document !== undefined) { + document = this["_configuration"].update(document, contentChanges, version); + this["_syncedDocuments"].set(textDocument.uri, document); + this["_onDidChangeContent"].fire(Object.freeze({ document: document, contentChanges })); + } + }), + ); + return Disposable.create(() => { + disposables.forEach((disposable) => disposable.dispose()); + }); + } +} diff --git a/clients/tabby-agent/src/feature.ts b/clients/tabby-agent/src/feature.ts new file mode 100644 index 000000000000..4113e0f64eea --- /dev/null +++ b/clients/tabby-agent/src/feature.ts @@ -0,0 +1,15 @@ +import type { Connection } from "vscode-languageserver"; +import type { ClientCapabilities, ClientProvidedConfig, ServerCapabilities, DataStoreRecords } from "./protocol"; + +export interface Feature { + initialize( + connection: Connection, + clientCapabilities: ClientCapabilities, + clientProvidedConfig: ClientProvidedConfig, + dataStoreRecords: DataStoreRecords | undefined, + ): ServerCapabilities | Promise; + + initialized?(connection: Connection): void | Promise; + + shutdown?(): void | Promise; +} diff --git a/clients/tabby-agent/src/http/cloudApi.d.ts b/clients/tabby-agent/src/http/cloudApi.d.ts new file mode 100644 index 000000000000..ea82c21404d3 --- /dev/null +++ b/clients/tabby-agent/src/http/cloudApi.d.ts @@ -0,0 +1,26 @@ +export interface paths { + "/usage": { + post: operations["usage"]; + }; +} + +export interface components { + schemas: { + UsageRequest: object; + }; +} + +export interface operations { + usage: { + requestBody: { + content: { + "application/json": components["schemas"]["UsageRequest"]; + }; + }; + responses: { + 200: { + content: never; + }; + }; + }; +} diff --git a/clients/tabby-agent/src/http/proxy.ts b/clients/tabby-agent/src/http/proxy.ts new file mode 100644 index 000000000000..d84502e0cf7b --- /dev/null +++ b/clients/tabby-agent/src/http/proxy.ts @@ -0,0 +1,40 @@ +import { Dispatcher, ProxyAgent, EnvHttpProxyAgent } from "undici"; +import { parse as uriParse } from "uri-js"; +import { isBrowser } from "../env"; +import { getLogger } from "../logger"; + +export type ProxyConfig = + | { + url: string; + authorization?: string; + noProxy?: string; + } + | { fromEnv: true }; + +const logger = getLogger("Proxy"); + +export function createProxyForUrl(url: string, configs: ProxyConfig[]): Dispatcher | null { + if (isBrowser) { + return null; + } + const { host } = uriParse(url); + if (!host) { + return null; + } + for (const config of configs) { + if ("fromEnv" in config && config["fromEnv"]) { + logger.info("Using proxy from environment variables."); + return new EnvHttpProxyAgent(); + } else if ("url" in config && config["url"]) { + const noProxyList = config["noProxy"]?.split(/,|\s+/).map((item) => item.trim()); + if (!noProxyList || !noProxyList.includes(host)) { + logger.info(`Using proxy ${config["url"]}.`); + return new ProxyAgent({ + uri: config["url"], + token: config["authorization"], + }); + } + } + } + return null; +} diff --git a/clients/tabby-agent/src/http/stream.ts b/clients/tabby-agent/src/http/stream.ts new file mode 100644 index 000000000000..a503a2d26131 --- /dev/null +++ b/clients/tabby-agent/src/http/stream.ts @@ -0,0 +1,30 @@ +import { Readable } from "readable-stream"; +import { readableFromWeb } from "readable-from-web"; +import { EventSourceParserStream, ParsedEvent } from "eventsource-parser/stream"; +import type { components as TabbyApiComponents } from "tabby-openapi/compatible"; +import { getLogger } from "../logger"; + +const logger = getLogger("Stream"); + +export function readChatStream(stream: ReadableStream, signal?: AbortSignal): Readable { + const eventStream = stream.pipeThrough(new TextDecoderStream()).pipeThrough(new EventSourceParserStream()); + const readableStream = readableFromWeb(eventStream, { objectMode: true }); + return readableStream.map( + (event: ParsedEvent): string | undefined => { + try { + if (event.type === "event") { + const chunk = JSON.parse(event.data) as TabbyApiComponents["schemas"]["ChatCompletionChunk"]; + const text = chunk.choices[0]?.delta.content; + if (typeof text === "string") { + return text; + } + } + } catch (error) { + logger.error("Failed to parse chat stream chunk.", error); + logger.trace("Parsing failed with event:", { event }); + } + return undefined; + }, + { signal }, + ); +} diff --git a/clients/tabby-agent/src/http/tabbyApiClient.ts b/clients/tabby-agent/src/http/tabbyApiClient.ts new file mode 100644 index 000000000000..921f1d9338f0 --- /dev/null +++ b/clients/tabby-agent/src/http/tabbyApiClient.ts @@ -0,0 +1,454 @@ +import type { paths as TabbyApi, components as TabbyApiComponents } from "tabby-openapi/compatible"; +import type { ParseAs } from "openapi-fetch"; +import type { Readable } from "readable-stream"; +import type { Configurations } from "../config"; +import type { AnonymousUsageLogger } from "../telemetry"; +import type { ConfigData } from "../config/type"; +import type { ProxyConfig } from "./proxy"; +import type { ClientInfo } from "../protocol"; +import { EventEmitter } from "events"; +import createClient from "openapi-fetch"; +import { v4 as uuid } from "uuid"; +import deepEqual from "deep-equal"; +import * as semver from "semver"; +import { name as agentName, version as agentVersion } from "../../package.json"; +import { isBrowser } from "../env"; +import { createProxyForUrl } from "./proxy"; +import { getLogger } from "../logger"; +import { isBlank } from "../utils/string"; +import { readChatStream } from "./stream"; +import { abortSignalFromAnyOf } from "../utils/signal"; +import { + formatErrorMessage, + HttpError, + MutexAbortError, + isUnauthorizedError, + isCanceledError, + isTimeoutError, +} from "../utils/error"; + +export type TabbyApiClientStatus = "noConnection" | "unauthorized" | "ready"; + +export type TabbyServerProvidedConfig = { + disable_client_side_telemetry?: boolean; +}; + +export class TabbyApiClient extends EventEmitter { + private readonly logger = getLogger("TabbyApiClient"); + + private userAgentString: string | undefined = undefined; + private api: ReturnType> | undefined; + private endpoint: string | undefined = undefined; + + private status: TabbyApiClientStatus = "noConnection"; + private connecting: boolean = false; + + private connectionErrorMessage: string | undefined = undefined; + private serverHealth: TabbyApiComponents["schemas"]["HealthState"] | undefined = undefined; + + private healthCheckMutexAbortController: AbortController | undefined = undefined; + + private reconnectTimer: ReturnType | undefined = undefined; + private heartbeatTimer: ReturnType | undefined = undefined; + + constructor( + private readonly configurations: Configurations, + private readonly anonymousUsageLogger: AnonymousUsageLogger, + ) { + super(); + } + + async initialize(clientInfo: ClientInfo | undefined) { + this.userAgentString = this.buildUserAgentString(clientInfo); + this.connect(); // no await + + this.configurations.on("updated", (config: ConfigData, oldConfig: ConfigData) => { + const isServerConnectionChanged = !( + deepEqual(config.server, oldConfig.server) && deepEqual(config.proxy, oldConfig.proxy) + ); + if (isServerConnectionChanged) { + this.logger.debug("Server configurations updated, reconnecting..."); + this.connect(); // no await + } + }); + + const reconnectInterval = 1000 * 30; // 30s + this.reconnectTimer = setInterval(async () => { + if (this.status === "noConnection" || this.status === "unauthorized") { + this.logger.debug("Trying to reconnect..."); + await this.connect({ skipReset: true }); + } + }, reconnectInterval); + + const heartBeatInterval = 1000 * 60; // 1m + this.heartbeatTimer = setInterval(async () => { + if (this.status === "ready") { + this.logger.trace("Heartbeat..."); + await this.healthCheck({ background: true }); + } + }, heartBeatInterval); + } + + async shutdown() { + if (this.reconnectTimer) { + clearInterval(this.reconnectTimer); + } + if (this.heartbeatTimer) { + clearInterval(this.heartbeatTimer); + } + } + + private buildUserAgentString(clientInfo: ClientInfo | undefined): string { + const envInfo = isBrowser ? navigator?.userAgent : `Node.js/${process.version}`; + const tabbyAgentInfo = `${agentName}/${agentVersion}`; + const ideName = clientInfo?.name.replace(/ /g, "-"); + const ideVersion = clientInfo?.version; + const ideInfo = ideName ? `${ideName}/${ideVersion}` : ""; + const tabbyPluginName = clientInfo?.tabbyPlugin?.name; + const tabbyPluginVersion = clientInfo?.tabbyPlugin?.version; + const tabbyPluginInfo = tabbyPluginName ? `${tabbyPluginName}/${tabbyPluginVersion}` : ""; + return `${envInfo} ${tabbyAgentInfo} ${ideInfo} ${tabbyPluginInfo}`.trim(); + } + + private createTimeOutAbortSignal(): AbortSignal { + const config = this.configurations.getMergedConfig(); + const timeout = Math.min(0x7fffffff, config.server.requestTimeout); + return AbortSignal.timeout(timeout); + } + + private updateStatus(status: TabbyApiClientStatus) { + if (this.status != status) { + this.status = status; + this.logger.info(`Status updated: ${status}.`); + this.emit("statusUpdated", status); + } + } + + private updateIsConnecting(isConnecting: boolean) { + if (this.connecting != isConnecting) { + this.connecting = isConnecting; + this.emit("isConnectingUpdated", isConnecting); + } + } + + getStatus(): TabbyApiClientStatus { + return this.status; + } + + getHelpMessage(format?: "plaintext" | "markdown" | "html"): string | undefined { + if (this.status === "noConnection") { + const message = "Connect to Server Failed.\n" + this.connectionErrorMessage; + if (format == "html") { + return message?.replace(/\n/g, "
    "); + } else { + return message; + } + } + return undefined; + } + + isConnecting(): boolean { + return this.connecting; + } + + getServerHealth(): TabbyApiComponents["schemas"]["HealthState"] | undefined { + return this.serverHealth; + } + + async connect(options: { skipReset?: boolean } = {}): Promise { + if (!options.skipReset) { + this.connectionErrorMessage = undefined; + this.serverHealth = undefined; + this.updateStatus("noConnection"); + + const config = this.configurations.getMergedConfig(); + const endpoint = config.server.endpoint; + this.endpoint = endpoint; + + const auth = !isBlank(config.server.token) ? `Bearer ${config.server.token}` : undefined; + const proxyConfigs: ProxyConfig[] = [{ fromEnv: true }]; + if (!isBlank(config.proxy.url)) { + proxyConfigs.unshift(config.proxy); + } + this.api = createClient({ + baseUrl: endpoint, + headers: { + Authorization: auth, + "User-Agent": this.userAgentString, + ...config.server.requestHeaders, + }, + /** dispatcher do not exist in {@link RequestInit} in browser env. */ + /* @ts-expect-error TS-2353 */ + dispatcher: createProxyForUrl(endpoint, proxyConfigs), + }); + } + await this.healthCheck(); + if (this.status === "ready") { + await this.updateServerProvidedConfig(); + await this.anonymousUsageLogger.uniqueEvent("AgentConnected", this.serverHealth); + } + } + + private async healthCheck(options?: { + signal?: AbortSignal; + method?: "GET" | "POST"; + background?: boolean; + }): Promise { + const signal = options?.signal; + const method = options?.method ?? "GET"; + const background = options?.background; + + if (this.healthCheckMutexAbortController && !this.healthCheckMutexAbortController.signal.aborted) { + if (background) { + // there is a running check, skip background check + return; + } + this.healthCheckMutexAbortController.abort(new MutexAbortError()); + } + const abortController = new AbortController(); + this.healthCheckMutexAbortController = abortController; + if (!background) { + this.updateIsConnecting(true); + } + + const requestId = uuid(); + const requestPath = "/v1/health"; + const requestDescription = `${method} ${this.endpoint + requestPath}`; + const requestOptions = { + signal: abortSignalFromAnyOf([signal, abortController.signal, this.createTimeOutAbortSignal()]), + }; + try { + if (!this.api) { + throw new Error("http client not initialized"); + } + this.logger.debug(`Health check request: ${requestDescription}. [${requestId}]`); + let response; + if (method === "POST") { + response = await this.api.POST(requestPath, requestOptions); + } else { + response = await this.api.GET(requestPath, requestOptions); + } + this.logger.debug(`Health check response status: ${response.response.status}. [${requestId}]`); + if (response.error || !response.response.ok) { + throw new HttpError(response.response); + } + this.logger.trace(`Health check response data: [${requestId}]`, response.data); + this.connectionErrorMessage = undefined; + this.serverHealth = response.data; + this.updateStatus("ready"); + } catch (error) { + if (isCanceledError(error)) { + this.logger.debug(`Health check request canceled. [${requestId}]`); + } else if (error instanceof HttpError && error.status == 405 && method !== "POST") { + return await this.healthCheck({ signal, method: "POST" }); + } else if (isUnauthorizedError(error)) { + this.serverHealth = undefined; + this.updateStatus("unauthorized"); + } else if (isTimeoutError(error)) { + this.logger.error(`Health check request timed out. [${requestId}]`, error); + this.serverHealth = undefined; + this.connectionErrorMessage = `${requestDescription} timed out.`; + this.updateStatus("noConnection"); + } else { + this.logger.error(`Health check request failed. [${requestId}]`, error); + this.serverHealth = undefined; + this.connectionErrorMessage = `${requestDescription} failed: \n${formatErrorMessage(error)}`; + this.updateStatus("noConnection"); + } + } finally { + if (this.healthCheckMutexAbortController === abortController) { + this.healthCheckMutexAbortController = undefined; + if (!background) { + this.updateIsConnecting(false); + } + } + } + } + + private async updateServerProvidedConfig(): Promise { + const serverVersion = semver.coerce(this.serverHealth?.version.git_describe); + if (serverVersion && semver.lt(serverVersion, "0.9.0")) { + this.logger.debug(`Skip fetching server provided config due to server version: ${serverVersion}.`); + return; + } + const requestId = uuid(); + const requestPath = "/v1beta/server_setting"; + const requestDescription = `GET ${this.endpoint + requestPath}`; + const requestOptions = { + signal: this.createTimeOutAbortSignal(), + }; + try { + if (!this.api) { + throw new Error("http client not initialized"); + } + this.logger.debug(`Fetch server provided config request: ${requestDescription}. [${requestId}]`); + const response = await this.api.GET(requestPath, requestOptions); + this.logger.debug(`Fetch server provided config response status: ${response.response.status}. [${requestId}]`); + if (response.error || !response.response.ok) { + throw new HttpError(response.response); + } + this.logger.trace(`Fetch server provided config response data: [${requestId}]`, response.data); + const fetchedConfig = response.data; + await this.configurations.updateServerProvidedConfig(fetchedConfig, true); + } catch (error) { + if (isUnauthorizedError(error)) { + this.logger.debug(`Fetch server provided config request failed due to unauthorized. [${requestId}]`); + } else if (!(error instanceof HttpError)) { + this.logger.error(`Fetch server provided config request failed. [${requestId}]`, error); + } + } + } + + async fetchCompletion( + request: TabbyApiComponents["schemas"]["CompletionRequest"], + signal?: AbortSignal, + // set to track latency, the properties in latencyStats object will be updated in this function + latencyStats?: { + latency?: number; // ms, undefined means no data, timeout or canceled + canceled?: boolean; + timeout?: boolean; + }, + ): Promise { + const requestId = uuid(); + const requestPath = "/v1/completions"; + const requestDescription = `POST ${this.endpoint + requestPath}`; + const requestOptions = { + body: request, + signal: abortSignalFromAnyOf([signal, this.createTimeOutAbortSignal()]), + }; + + const requestStartedAt = performance.now(); + try { + if (!this.api) { + throw new Error("http client not initialized"); + } + this.logger.debug(`Completion request: ${requestDescription}. [${requestId}]`); + this.logger.trace(`Completion request body: [${requestId}]`, requestOptions.body); + const response = await this.api.POST(requestPath, requestOptions); + this.logger.debug(`Completion response status: ${response.response.status}. [${requestId}]`); + if (response.error || !response.response.ok) { + throw new HttpError(response.response); + } + this.logger.trace(`Completion response data: [${requestId}]`, response.data); + if (latencyStats) { + latencyStats.latency = performance.now() - requestStartedAt; + } + return response.data; + } catch (error) { + if (isCanceledError(error)) { + this.logger.debug(`Completion request canceled. [${requestId}]`); + if (latencyStats) { + latencyStats.canceled = true; + } + } else if (isTimeoutError(error)) { + this.logger.debug(`Completion request timed out. [${requestId}]`); + if (latencyStats) { + latencyStats.timeout = true; + } + } else if (isUnauthorizedError(error)) { + this.logger.debug(`Completion request failed due to unauthorized. [${requestId}]`); + this.healthCheck(); + } else { + this.logger.error(`Completion request failed. [${requestId}]`, error); + this.healthCheck(); + } + + if (latencyStats) { + latencyStats.latency = undefined; + } + throw error; // rethrow error + } + } + + // FIXME(@icycodes): use openai for nodejs instead of tabby-openapi schema + async fetchChatStream( + request: TabbyApiComponents["schemas"]["ChatCompletionRequest"], + signal?: AbortSignal, + useBetaVersion: boolean = false, + ): Promise { + const requestId = uuid(); + const requestPath = useBetaVersion ? "/v1beta/chat/completions" : "/v1/chat/completions"; + try { + if (!this.api) { + throw new Error("http client not initialized"); + } + const requestOptions = { + body: request, + signal: abortSignalFromAnyOf([signal, this.createTimeOutAbortSignal()]), + parseAs: "stream" as ParseAs, + }; + const requestDescription = `POST ${this.endpoint + requestPath}`; + this.logger.debug(`Chat request: ${requestDescription}. [${requestId}]`); + this.logger.trace(`Chat request body: [${requestId}]`, requestOptions.body); + const response = await this.api.POST(requestPath, requestOptions); + this.logger.debug(`Chat response status: ${response.response.status}. [${requestId}]`); + if (response.error || !response.response.ok) { + throw new HttpError(response.response); + } + if (!response.response.body) { + return null; + } + const readableStream = readChatStream(response.response.body, requestOptions.signal); + return readableStream; + } catch (error) { + if (error instanceof HttpError && error.status == 404 && !useBetaVersion) { + return await this.fetchChatStream(request, signal, true); + } + if (isCanceledError(error)) { + this.logger.debug(`Chat request canceled. [${requestId}]`); + } else if (isUnauthorizedError(error)) { + this.logger.debug(`Chat request failed due to unauthorized. [${requestId}]`); + this.healthCheck(); + } else { + this.logger.error(`Chat request failed. [${requestId}]`, error); + this.healthCheck(); + } + throw error; // rethrow error + } + } + + async postEvent( + request: TabbyApiComponents["schemas"]["LogEventRequest"] & { select_kind?: "line" }, + signal?: AbortSignal, + ): Promise { + const requestId = uuid(); + const requestPath = "/v1/events"; + const requestDescription = `POST ${this.endpoint + requestPath}`; + const requestOptions = { + body: request, + params: { + query: { + select_kind: request.select_kind, + }, + }, + signal: abortSignalFromAnyOf([signal, this.createTimeOutAbortSignal()]), + parseAs: "text" as ParseAs, + }; + + try { + if (!this.api) { + throw new Error("http client not initialized"); + } + this.logger.debug(`Event request: ${requestDescription}. [${requestId}]`); + this.logger.trace(`Event request body: [${requestId}]`, requestOptions.body); + const response = await this.api.POST(requestPath, requestOptions); + this.logger.debug(`Event response status: ${response.response.status}. [${requestId}]`); + if (response.error || !response.response.ok) { + throw new HttpError(response.response); + } + this.logger.trace(`Event response data: [${requestId}]`, response.data); + } catch (error) { + if (isCanceledError(error)) { + this.logger.debug(`Event request canceled. [${requestId}]`); + } + if (isUnauthorizedError(error)) { + this.logger.debug(`Event request failed due to unauthorized. [${requestId}]`); + this.healthCheck(); + } else { + this.logger.error(`Event request failed. [${requestId}]`, error); + this.healthCheck(); + } + throw error; // rethrow error + } + } +} diff --git a/clients/tabby-agent/src/index.ts b/clients/tabby-agent/src/index.ts index 09b10f322f33..b9bd32fa49d0 100644 --- a/clients/tabby-agent/src/index.ts +++ b/clients/tabby-agent/src/index.ts @@ -1,26 +1,12 @@ -export { TabbyAgent } from "./TabbyAgent"; -export { - Agent, - AgentStatus, - AgentFunction, - AgentEvent, - AgentEventEmitter, - AgentIssue, - StatusChangedEvent, - ConfigUpdatedEvent, - AuthRequiredEvent, - IssuesUpdatedEvent, - SlowCompletionResponseTimeIssue, - HighCompletionTimeoutRateIssue, - ConnectionFailedIssue, - ClientProperties, - AgentInitOptions, - ServerHealthState, - CompletionRequest, - CompletionResponse, - LogEventRequest, - AbortSignalOption, - agentEventNames, -} from "./Agent"; -export { AgentConfig, PartialAgentConfig } from "./AgentConfig"; -export { DataStore } from "./dataStore"; +#!/usr/bin/env node + +import * as dns from "node:dns"; +import { isBrowser } from "./env"; +import { Server } from "./server"; + +if (!isBrowser) { + dns.setDefaultResultOrder("ipv4first"); +} + +const server = new Server(); +server.listen(); diff --git a/clients/tabby-agent/src/logger.ts b/clients/tabby-agent/src/logger.ts deleted file mode 100644 index 183b461830eb..000000000000 --- a/clients/tabby-agent/src/logger.ts +++ /dev/null @@ -1,41 +0,0 @@ -import path from "path"; -import os from "os"; -import pino from "pino"; -import * as FileStreamRotator from "file-stream-rotator"; -import { isBrowser, isTest, testLogDebug } from "./env"; - -class LogFileStream implements pino.DestinationStream { - private streamOptions = { - // Rotating file locate at `~/.tabby-client/agent/logs/`. - filename: path.join(os.homedir(), ".tabby-client", "agent", "logs", "%DATE%"), - frequency: "daily", - size: "10M", - max_logs: "30d", - extension: ".log", - create_symlink: true, - }; - private stream?: pino.DestinationStream; - - write(data: string): void { - if (!this.stream) { - this.stream = FileStreamRotator.getStream(this.streamOptions); - } - this.stream.write(data); - } -} - -// LogFileStream not available in browser, will use default browser console output instead. -const stream = isBrowser || isTest ? undefined : new LogFileStream(); - -const options = { serializers: { error: pino.stdSerializers.err } }; -export const rootLogger = stream ? pino(options, stream) : pino(options); -if (isTest && testLogDebug) { - rootLogger.level = "debug"; -} else { - rootLogger.level = "silent"; -} - -export const allLoggers = [rootLogger]; -rootLogger.onChild = (child: pino.Logger) => { - allLoggers.push(child); -}; diff --git a/clients/tabby-agent/src/logger/fileLogger.ts b/clients/tabby-agent/src/logger/fileLogger.ts new file mode 100644 index 000000000000..211c60c1b4a8 --- /dev/null +++ b/clients/tabby-agent/src/logger/fileLogger.ts @@ -0,0 +1,80 @@ +import type { Logger } from "./type"; +import path from "path"; +import os from "os"; +import * as FileStreamRotator from "file-stream-rotator"; +import pino from "pino"; +import { isBrowser, isTest } from "../env"; + +export class LogFileStream implements pino.DestinationStream { + private stream?: ReturnType; + + write(data: string): void { + if (!this.stream) { + // Rotating file locate at `~/.tabby-client/agent/logs/`. + const logDir = path.join(os.homedir(), ".tabby-client", "agent", "logs"); + const now = new Date(); + const dateString = `${now.getFullYear()}${(now.getMonth() + 1).toString().padStart(2, "0")}${now.getDate().toString().padStart(2, "0")}`; + const timeString = `${now.getHours().toString().padStart(2, "0")}${now.getMinutes().toString().padStart(2, "0")}${now.getSeconds().toString().padStart(2, "0")}`; + const logFilePathName = path.join(logDir, dateString, `${timeString}-${process.pid.toString()}`); + this.stream = FileStreamRotator.getStream({ + filename: logFilePathName, + size: "10M", + max_logs: "30d", + end_stream: true, + audit_file: path.join(logDir, "audit.json"), + extension: ".log", + }); + } + this.stream.write(data); + } +} + +export class PinoLogger implements Logger { + private childLoggers: pino.Logger[] = []; + + constructor(private baseLogger: pino.Logger) { + this.baseLogger.onChild = (child: pino.Logger) => { + this.childLoggers.push(child); + }; + } + + get level(): string { + return this.baseLogger.level; + } + + set level(level: string) { + this.baseLogger.level = level; + this.childLoggers.forEach((child) => { + child.level = level; + }); + } + + child(tag: string): Logger { + return new PinoLogger(this.baseLogger.child({ tag })); + } + + error(msg: string, error: any) { + this.baseLogger.error({ error }, msg); + } + warn(msg: string) { + this.baseLogger.warn(msg); + } + info(msg: string) { + this.baseLogger.info(msg); + } + debug(msg: string) { + this.baseLogger.debug(msg); + } + trace(msg: string, verbose?: any) { + this.baseLogger.trace(verbose, msg); + } +} + +export function getFileLogger(): PinoLogger | undefined { + const fileLogger = + isBrowser || isTest + ? undefined + : new PinoLogger(pino({ serializers: { error: pino.stdSerializers.err } }, new LogFileStream())); + + return fileLogger; +} diff --git a/clients/tabby-agent/src/logger/index.ts b/clients/tabby-agent/src/logger/index.ts new file mode 100644 index 000000000000..121abf8bd01a --- /dev/null +++ b/clients/tabby-agent/src/logger/index.ts @@ -0,0 +1,150 @@ +import type { Logger } from "./type"; +import type { Connection } from "vscode-languageserver"; +import type { Configurations } from "../config"; +import type { ConfigData } from "../config/type"; +import EventEmitter from "events"; +import { getFileLogger, PinoLogger } from "./fileLogger"; + +class TaggedLogger implements Logger { + constructor( + private baseLogger: Logger, + private tag: string, + ) {} + + private tagMsg(msg: string): string { + return `[${this.tag}] ${msg}`; + } + + error(msg: string, error: any): void { + this.baseLogger.error(this.tagMsg(msg), { error }); + } + warn(msg: string): void { + this.baseLogger.warn(this.tagMsg(msg)); + } + info(msg: string): void { + this.baseLogger.info(this.tagMsg(msg)); + } + debug(msg: string): void { + this.baseLogger.debug(this.tagMsg(msg)); + } + trace(msg: string, verbose?: any): void { + this.baseLogger.trace(this.tagMsg(msg), verbose); + } +} + +class Destinations extends EventEmitter { + constructor(public readonly destinations: Logger[] = []) { + super(); + } + + attach(...destinations: Logger[]) { + this.destinations.push(...destinations); + this.emit("newAttached", destinations); + } +} + +class MultiDestinationLogger implements Logger { + private loggers: Logger[] = []; + + constructor( + public destinations: Destinations, + private readonly tag?: string, + ) { + this.loggers.push(...destinations.destinations.map((destination) => this.createTaggedLogger(destination))); + destinations.on("newAttached", (destinations: Logger[]) => { + this.loggers.push(...destinations.map((destination) => this.createTaggedLogger(destination))); + }); + } + + private createTaggedLogger(destination: Logger): Logger { + if (!this.tag) { + return destination; + } + if (destination instanceof PinoLogger) { + return destination.child(this.tag); + } + return new TaggedLogger(destination, this.tag); + } + + error(msg: string, error: any) { + this.loggers.forEach((logger) => logger.error(msg, error)); + } + warn(msg: string) { + this.loggers.forEach((logger) => logger.warn(msg)); + } + info(msg: string) { + this.loggers.forEach((logger) => logger.info(msg)); + } + debug(msg: string) { + this.loggers.forEach((logger) => logger.debug(msg)); + } + trace(msg: string, verbose?: any) { + this.loggers.forEach((logger) => logger.trace(msg, verbose)); + } +} + +export class LoggerManager { + private static instance: LoggerManager; + public static getInstance(): LoggerManager { + if (!LoggerManager.instance) { + LoggerManager.instance = new LoggerManager(); + } + return LoggerManager.instance; + } + + private logDestinations = new Destinations(); + private fileLogger: PinoLogger | undefined; + + getLogger(tag: string): Logger { + return new MultiDestinationLogger(this.logDestinations, tag); + } + + preInitialize(config: Configurations): void { + this.fileLogger = getFileLogger(); + if (this.fileLogger) { + this.logDestinations.attach(this.fileLogger); + this.setFileLoggerLevel(config.getMergedConfig().logs.level); + } + config.on("updated", (configData: ConfigData) => { + if (configData.logs) { + this.setFileLoggerLevel(configData.logs.level); + } + }); + } + + private setFileLoggerLevel(level: string) { + if (this.fileLogger) { + this.fileLogger.level = level; + } + } + + attachLogger(logger: Logger) { + this.logDestinations.attach(logger); + } + + attachLspConnection(connection: Connection) { + this.attachLogger({ + error: (msg: string, error: any) => { + const errorMsg = + error instanceof Error + ? `[${error.name}] ${error.message} \n${error.stack}` + : JSON.stringify(error, undefined, 2); + connection.console.error(`${msg} ${errorMsg}`); + }, + warn: (msg: string) => { + connection.console.warn(msg); + }, + info: (msg: string) => { + connection.console.info(msg); + }, + debug: (msg: string) => { + connection.console.debug(msg); + }, + trace: () => {}, + }); + } +} + +export function getLogger(tag: string): Logger { + return LoggerManager.getInstance().getLogger(tag); +} diff --git a/clients/tabby-agent/src/logger/type.d.ts b/clients/tabby-agent/src/logger/type.d.ts new file mode 100644 index 000000000000..ea545234461b --- /dev/null +++ b/clients/tabby-agent/src/logger/type.d.ts @@ -0,0 +1,7 @@ +export interface Logger { + error: (msg: string, error: any) => void; + warn: (msg: string) => void; + info: (msg: string) => void; + debug: (msg: string) => void; + trace: (msg: string, verbose?: any) => void; +} diff --git a/clients/tabby-agent/src/postprocess/base.ts b/clients/tabby-agent/src/postprocess/base.ts deleted file mode 100644 index 0617642cd03b..000000000000 --- a/clients/tabby-agent/src/postprocess/base.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { CompletionResponse, CompletionResponseChoice, CompletionContext } from "../CompletionContext"; -import { rootLogger } from "../logger"; - -type PostprocessFilterBase = ( - input: T, - context: CompletionContext, -) => T | null | Promise; - -export type PostprocessFilter = PostprocessFilterBase; -export type PostprocessChoiceFilter = PostprocessFilterBase; - -export const logger = rootLogger.child({ component: "Postprocess" }); - -declare global { - interface Array { - distinct(identity?: (x: T) => any): Array; - mapAsync(callbackfn: (value: T, index: number, array: T[]) => U | Promise, thisArg?: any): Promise; - } -} - -if (!Array.prototype.distinct) { - Array.prototype.distinct = function (this: T[], identity?: (x: T) => any): T[] { - return [...new Map(this.map((item) => [identity?.(item) ?? item, item])).values()]; - }; -} - -if (!Array.prototype.mapAsync) { - Array.prototype.mapAsync = async function ( - this: T[], - callbackfn: (value: T, index: number, array: T[]) => U | Promise, - thisArg?: any, - ): Promise { - return await Promise.all(this.map((item, index) => callbackfn.call(thisArg, item, index, this))); - }; -} - -export function applyFilter( - filter: PostprocessFilter, - context: CompletionContext, -): (response: CompletionResponse) => Promise { - return applyChoiceFilter(async (choice) => { - const replaceLength = context.position - choice.replaceRange.start; - const input = choice.text.slice(replaceLength); - const filtered = await filter(input, context); - choice.text = choice.text.slice(0, replaceLength) + (filtered ?? ""); - return choice; - }, context); -} - -export function applyChoiceFilter( - choiceFilter: PostprocessChoiceFilter, - context: CompletionContext, -): (response: CompletionResponse) => Promise { - return async (response: CompletionResponse) => { - response.choices = ( - await response.choices.mapAsync(async (choice) => { - return await choiceFilter(choice, context); - }) - ) - .filter((choice): choice is NonNullable => { - // Filter out empty choices. - return !!choice && !!choice.text; - }) - .distinct((choice) => choice.text); - return response; - }; -} diff --git a/clients/tabby-agent/src/postprocess/calculateReplaceRange.ts b/clients/tabby-agent/src/postprocess/calculateReplaceRange.ts deleted file mode 100644 index 181f641b2cd3..000000000000 --- a/clients/tabby-agent/src/postprocess/calculateReplaceRange.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { AgentConfig } from "../AgentConfig"; -import { isBrowser } from "../env"; -import { PostprocessChoiceFilter, logger } from "./base"; -import { calculateReplaceRangeByBracketStack } from "./calculateReplaceRangeByBracketStack"; -import { calculateReplaceRangeBySyntax } from "./calculateReplaceRangeBySyntax"; - -export function calculateReplaceRange( - config: AgentConfig["postprocess"]["calculateReplaceRange"], -): PostprocessChoiceFilter { - return async (choice, context) => { - const preferSyntaxParser = - !isBrowser && // syntax parser is not supported in browser yet - config.experimentalSyntax; - - if (preferSyntaxParser) { - try { - return await calculateReplaceRangeBySyntax(choice, context); - } catch (error) { - logger.debug({ error }, "Failed to calculate replace range by syntax parser"); - } - } - return calculateReplaceRangeByBracketStack(choice, context); - }; -} diff --git a/clients/tabby-agent/src/postprocess/calculateReplaceRangeByBracketStack.test.ts b/clients/tabby-agent/src/postprocess/calculateReplaceRangeByBracketStack.test.ts deleted file mode 100644 index c67708afd918..000000000000 --- a/clients/tabby-agent/src/postprocess/calculateReplaceRangeByBracketStack.test.ts +++ /dev/null @@ -1,229 +0,0 @@ -import { expect } from "chai"; -import { documentContext, inline } from "./testUtils"; -import { calculateReplaceRangeByBracketStack } from "./calculateReplaceRangeByBracketStack"; - -describe("postprocess", () => { - describe("calculateReplaceRangeByBracketStack", () => { - it("should handle auto closing quotes", () => { - const context = { - ...documentContext` - const hello = "║" - `, - language: "typescript", - }; - const choice = { - index: 0, - text: inline` - ├hello";┤ - `, - replaceRange: { - start: context.position, - end: context.position, - }, - }; - const expected = { - index: 0, - text: inline` - ├hello";┤ - `, - replaceRange: { - start: context.position, - end: context.position + 1, - }, - }; - expect(calculateReplaceRangeByBracketStack(choice, context)).to.deep.equal(expected); - }); - - it("should handle auto closing quotes", () => { - const context = { - ...documentContext` - let htmlMarkup = \`║\` - `, - language: "typescript", - }; - const choice = { - index: 0, - text: inline` - ├

    \${message}

    \`;┤ - `, - replaceRange: { - start: context.position, - end: context.position, - }, - }; - const expected = { - index: 0, - text: inline` - ├

    \${message}

    \`;┤ - `, - replaceRange: { - start: context.position, - end: context.position + 1, - }, - }; - expect(calculateReplaceRangeByBracketStack(choice, context)).to.deep.equal(expected); - }); - - it("should handle multiple auto closing brackets", () => { - const context = { - ...documentContext` - process.on('data', (data) => {║}) - `, - language: "typescript", - }; - const choice = { - index: 0, - text: inline` - ├ - console.log(data); - });┤ - `, - replaceRange: { - start: context.position, - end: context.position, - }, - }; - const expected = { - index: 0, - text: inline` - ├ - console.log(data); - });┤ - `, - replaceRange: { - start: context.position, - end: context.position + 2, - }, - }; - expect(calculateReplaceRangeByBracketStack(choice, context)).to.deep.equal(expected); - }); - - it("should handle multiple auto closing brackets", () => { - const context = { - ...documentContext` - let mat: number[][][] = [[[║]]] - `, - language: "typescript", - }; - const choice = { - index: 0, - text: inline` - ├1, 2], [3, 4]], [[5, 6], [7, 8]]];┤ - `, - replaceRange: { - start: context.position, - end: context.position, - }, - }; - const expected = { - index: 0, - text: inline` - ├1, 2], [3, 4]], [[5, 6], [7, 8]]];┤ - `, - replaceRange: { - start: context.position, - end: context.position + 3, - }, - }; - expect(calculateReplaceRangeByBracketStack(choice, context)).to.deep.equal(expected); - }); - - it("should handle html tags", () => { - const context = { - ...documentContext` - - `, - language: "html", - }; - const choice = { - index: 0, - text: inline` - ├tml>┤ - `, - replaceRange: { - start: context.position, - end: context.position, - }, - }; - const expected = { - index: 0, - text: inline` - ├tml>┤ - `, - replaceRange: { - start: context.position, - end: context.position + 1, - }, - }; - expect(calculateReplaceRangeByBracketStack(choice, context)).to.deep.equal(expected); - }); - - it("should handle jsx tags", () => { - const context = { - ...documentContext` - root.render( - - - - ); - `, - language: "javascriptreact", - }; - const choice = { - index: 0, - text: inline` - ├essage={message} />┤ - `, - replaceRange: { - start: context.position, - end: context.position, - }, - }; - const expected = { - index: 0, - text: inline` - ├essage={message} />┤ - `, - replaceRange: { - start: context.position, - end: context.position + 2, - }, - }; - expect(calculateReplaceRangeByBracketStack(choice, context)).to.deep.equal(expected); - }); - }); - - describe("calculateReplaceRangeByBracketStack: bad cases", () => { - it("cannot handle the case of completion bracket stack is same with suffix but should not be replaced", () => { - const context = { - ...documentContext` - function clamp(n: number, max: number, min: number): number { - return Math.max(Math.min(║); - } - `, - language: "typescript", - }; - const choice = { - index: 0, - text: inline` - ├n, max), min┤ - `, - replaceRange: { - start: context.position, - end: context.position, - }, - }; - const expected = { - index: 0, - text: inline` - ├n, max), min┤ - `, - replaceRange: { - start: context.position, - end: context.position, - }, - }; - expect(calculateReplaceRangeByBracketStack(choice, context)).not.to.deep.equal(expected); - }); - }); -}); diff --git a/clients/tabby-agent/src/postprocess/calculateReplaceRangeByBracketStack.ts b/clients/tabby-agent/src/postprocess/calculateReplaceRangeByBracketStack.ts deleted file mode 100644 index e86e37bbe682..000000000000 --- a/clients/tabby-agent/src/postprocess/calculateReplaceRangeByBracketStack.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { CompletionContext, CompletionResponseChoice } from "../CompletionContext"; -import { isBlank, findUnpairedAutoClosingChars } from "../utils"; -import { logger } from "./base"; - -export function calculateReplaceRangeByBracketStack( - choice: CompletionResponseChoice, - context: CompletionContext, -): CompletionResponseChoice { - const { currentLineSuffix } = context; - const suffixText = currentLineSuffix.trimEnd(); - if (isBlank(suffixText)) { - return choice; - } - const completionText = choice.text.slice(context.position - choice.replaceRange.start); - const unpaired = findUnpairedAutoClosingChars(completionText).join(""); - if (isBlank(unpaired)) { - return choice; - } - if (suffixText.startsWith(unpaired)) { - choice.replaceRange.end = context.position + unpaired.length; - logger.trace( - { context, completion: choice.text, range: choice.replaceRange, unpaired }, - "Adjust replace range by bracket stack", - ); - } else if (unpaired.startsWith(suffixText)) { - choice.replaceRange.end = context.position + suffixText.length; - logger.trace( - { context, completion: choice.text, range: choice.replaceRange, unpaired }, - "Adjust replace range by bracket stack", - ); - } - return choice; -} diff --git a/clients/tabby-agent/src/postprocess/calculateReplaceRangeBySyntax.test.ts b/clients/tabby-agent/src/postprocess/calculateReplaceRangeBySyntax.test.ts deleted file mode 100644 index 85c74fb7cb39..000000000000 --- a/clients/tabby-agent/src/postprocess/calculateReplaceRangeBySyntax.test.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { expect } from "chai"; -import { documentContext, inline } from "./testUtils"; -import { calculateReplaceRangeBySyntax } from "./calculateReplaceRangeBySyntax"; - -describe("postprocess", () => { - describe("calculateReplaceRangeBySyntax", () => { - it("should handle auto closing quotes", async () => { - const context = { - ...documentContext` - const hello = "║" - `, - language: "typescript", - }; - const choice = { - index: 0, - text: inline` - ├hello";┤ - `, - replaceRange: { - start: context.position, - end: context.position, - }, - }; - const expected = { - index: 0, - text: inline` - ├hello";┤ - `, - replaceRange: { - start: context.position, - end: context.position + 1, - }, - }; - expect(await calculateReplaceRangeBySyntax(choice, context)).to.deep.equal(expected); - }); - - it("should handle auto closing quotes", async () => { - const context = { - ...documentContext` - let htmlMarkup = \`║\` - `, - language: "typescript", - }; - const choice = { - index: 0, - text: inline` - ├

    \${message}

    \`;┤ - `, - replaceRange: { - start: context.position, - end: context.position, - }, - }; - const expected = { - index: 0, - text: inline` - ├

    \${message}

    \`;┤ - `, - replaceRange: { - start: context.position, - end: context.position + 1, - }, - }; - expect(await calculateReplaceRangeBySyntax(choice, context)).to.deep.equal(expected); - }); - - it("should handle multiple auto closing brackets", async () => { - const context = { - ...documentContext` - process.on('data', (data) => {║}) - `, - language: "typescript", - }; - const choice = { - index: 0, - text: inline` - ├ - console.log(data); - });┤ - `, - replaceRange: { - start: context.position, - end: context.position, - }, - }; - const expected = { - index: 0, - text: inline` - ├ - console.log(data); - });┤ - `, - replaceRange: { - start: context.position, - end: context.position + 2, - }, - }; - expect(await calculateReplaceRangeBySyntax(choice, context)).to.deep.equal(expected); - }); - - it("should handle multiple auto closing brackets", async () => { - const context = { - ...documentContext` - let mat: number[][][] = [[[║]]] - `, - language: "typescript", - }; - const choice = { - index: 0, - text: inline` - ├1, 2], [3, 4]], [[5, 6], [7, 8]]];┤ - `, - replaceRange: { - start: context.position, - end: context.position, - }, - }; - const expected = { - index: 0, - text: inline` - ├1, 2], [3, 4]], [[5, 6], [7, 8]]];┤ - `, - replaceRange: { - start: context.position, - end: context.position + 3, - }, - }; - expect(await calculateReplaceRangeBySyntax(choice, context)).to.deep.equal(expected); - }); - - it("should handle the bad case of calculateReplaceRangeByBracketStack", async () => { - const context = { - ...documentContext` - function clamp(n: number, max: number, min: number): number { - return Math.max(Math.min(║); - } - `, - language: "typescript", - }; - const choice = { - index: 0, - text: inline` - ├n, max), min┤ - `, - replaceRange: { - start: context.position, - end: context.position, - }, - }; - const expected = { - index: 0, - text: inline` - ├n, max), min┤ - `, - replaceRange: { - start: context.position, - end: context.position, - }, - }; - expect(await calculateReplaceRangeBySyntax(choice, context)).to.deep.equal(expected); - }); - }); -}); diff --git a/clients/tabby-agent/src/postprocess/calculateReplaceRangeBySyntax.ts b/clients/tabby-agent/src/postprocess/calculateReplaceRangeBySyntax.ts deleted file mode 100644 index 06932e56181c..000000000000 --- a/clients/tabby-agent/src/postprocess/calculateReplaceRangeBySyntax.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { getParser, languagesConfigs } from "../syntax/parser"; -import { CompletionContext, CompletionResponseChoice } from "../CompletionContext"; -import { isBlank, splitLines } from "../utils"; -import { logger } from "./base"; - -export const supportedLanguages = Object.keys(languagesConfigs); - -/** - * @throws {Error} if language is not supported - * @throws {Error} if syntax error when parsing completion - */ -export async function calculateReplaceRangeBySyntax( - choice: CompletionResponseChoice, - context: CompletionContext, -): Promise { - const { position, prefix, suffix, prefixLines, currentLinePrefix, currentLineSuffix, language } = context; - const suffixText = currentLineSuffix.trimEnd(); - if (isBlank(suffixText)) { - return choice; - } - - if (!supportedLanguages.includes(language)) { - throw new Error(`Language ${language} is not supported`); - } - const languageConfig = languagesConfigs[language]!; - const parser = await getParser(languageConfig); - - const completionText = choice.text.slice(position - choice.replaceRange.start); - const completionLines = splitLines(completionText); - let replaceLength = 0; - let tree = parser.parse(prefix + completionText + suffix); - let node = tree.rootNode.namedDescendantForIndex( - prefix.length + completionText.length, - prefix.length + completionText.length + suffixText.length - replaceLength, - ); - while (node.hasError() && replaceLength < suffixText.length) { - replaceLength++; - const row = prefixLines.length - 1 + completionLines.length - 1; - let column = completionLines[completionLines.length - 1]?.length ?? 0; - if (completionLines.length == 1) { - column += currentLinePrefix.length; - } - tree.edit({ - startIndex: prefix.length + completionText.length, - oldEndIndex: prefix.length + completionText.length + 1, - newEndIndex: prefix.length + completionText.length, - startPosition: { row, column }, - oldEndPosition: { row, column: column + 1 }, - newEndPosition: { row, column }, - }); - tree = parser.parse(prefix + completionText + suffix.slice(replaceLength), tree); - node = tree.rootNode.namedDescendantForIndex( - prefix.length + completionText.length, - prefix.length + completionText.length + suffixText.length - replaceLength, - ); - } - if (node.hasError()) { - throw new Error("Syntax error when parsing completion"); - } - - choice.replaceRange.end = position + replaceLength; - logger.trace({ context, completion: choice.text, range: choice.replaceRange }, "Adjust replace range by syntax"); - return choice; -} diff --git a/clients/tabby-agent/src/postprocess/dropBlank.test.ts b/clients/tabby-agent/src/postprocess/dropBlank.test.ts deleted file mode 100644 index b4ebe3aecf58..000000000000 --- a/clients/tabby-agent/src/postprocess/dropBlank.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { expect } from "chai"; -import { dropBlank } from "./dropBlank"; -import { documentContext } from "./testUtils"; - -describe("postprocess", () => { - describe("dropBlank", () => { - const dummyContext = { - ...documentContext` - dummy║ - `, - language: "plaintext", - }; - - it("should return null if input is blank", () => { - expect(dropBlank()("\n", dummyContext)).to.be.null; - expect(dropBlank()("\t\n", dummyContext)).to.be.null; - }); - it("should keep unchanged if input is not blank", () => { - expect(dropBlank()("Not blank", dummyContext)).to.eq("Not blank"); - }); - }); -}); diff --git a/clients/tabby-agent/src/postprocess/dropBlank.ts b/clients/tabby-agent/src/postprocess/dropBlank.ts deleted file mode 100644 index d9255300a374..000000000000 --- a/clients/tabby-agent/src/postprocess/dropBlank.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { PostprocessFilter } from "./base"; -import { isBlank } from "../utils"; - -export function dropBlank(): PostprocessFilter { - return (input: string) => { - return isBlank(input) ? null : input; - }; -} diff --git a/clients/tabby-agent/src/postprocess/dropDuplicated.test.ts b/clients/tabby-agent/src/postprocess/dropDuplicated.test.ts deleted file mode 100644 index 748b7fe32fa2..000000000000 --- a/clients/tabby-agent/src/postprocess/dropDuplicated.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { expect } from "chai"; -import { documentContext, inline } from "./testUtils"; -import { dropDuplicated } from "./dropDuplicated"; - -describe("postprocess", () => { - describe("dropDuplicated", () => { - it("should drop completion duplicated with suffix", () => { - const context = { - ...documentContext` - let sum = (a, b) => { - ║return a + b; - }; - `, - language: "javascript", - }; - // completion give a `;` at end but context have not - const completion = inline` - ├return a + b;┤ - `; - expect(dropDuplicated()(completion, context)).to.be.null; - }); - - it("should drop completion similar to suffix", () => { - const context = { - ...documentContext` - let sum = (a, b) => { - return a + b; - ║ - }; - `, - language: "javascript", - }; - // the difference is a `\n` - const completion = inline` - ├}┤ - `; - expect(dropDuplicated()(completion, context)).to.be.null; - }); - }); -}); diff --git a/clients/tabby-agent/src/postprocess/dropDuplicated.ts b/clients/tabby-agent/src/postprocess/dropDuplicated.ts deleted file mode 100644 index dbc8149a3b34..000000000000 --- a/clients/tabby-agent/src/postprocess/dropDuplicated.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { CompletionContext } from "../CompletionContext"; -import { PostprocessFilter, logger } from "./base"; -import { splitLines, isBlank, calcDistance } from "../utils"; - -export function dropDuplicated(): PostprocessFilter { - return (input: string, context: CompletionContext) => { - // get first n (n <= 3) lines of input and suffix, ignore blank lines - const { suffixLines } = context; - const inputLines = splitLines(input); - let inputIndex = 0; - while (inputIndex < inputLines.length && isBlank(inputLines[inputIndex]!)) { - inputIndex++; - } - let suffixIndex = 0; - while (suffixIndex < suffixLines.length && isBlank(suffixLines[suffixIndex]!)) { - suffixIndex++; - } - const lineCount = Math.min(3, inputLines.length - inputIndex, suffixLines.length - suffixIndex); - if (lineCount < 1) return input; - const inputToCompare = inputLines - .slice(inputIndex, inputIndex + lineCount) - .join("") - .trim(); - const suffixToCompare = suffixLines - .slice(suffixIndex, suffixIndex + lineCount) - .join("") - .trim(); - // if string distance is less than threshold (threshold = 1, or 5% of string length) - // drop this completion due to duplicated - const threshold = Math.max(1, 0.05 * inputToCompare.length, 0.05 * suffixToCompare.length); - const distance = calcDistance(inputToCompare, suffixToCompare); - if (distance <= threshold) { - logger.debug( - { inputLines, suffixLines, inputToCompare, suffixToCompare, distance, threshold }, - "Drop completion due to duplicated.", - ); - return null; - } - return input; - }; -} diff --git a/clients/tabby-agent/src/postprocess/formatIndentation.test.ts b/clients/tabby-agent/src/postprocess/formatIndentation.test.ts deleted file mode 100644 index 648462f6df9a..000000000000 --- a/clients/tabby-agent/src/postprocess/formatIndentation.test.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { expect } from "chai"; -import { documentContext, inline } from "./testUtils"; -import { formatIndentation } from "./formatIndentation"; - -describe("postprocess", () => { - describe("formatIndentation", () => { - it("should format indentation if first line of completion is over indented.", () => { - const context = { - ...documentContext` - function clamp(n: number, max: number, min: number): number { - ║ - } - `, - indentation: " ", - language: "typescript", - }; - const completion = inline` - ├ return Math.max(Math.min(n, max), min);┤ - `; - const expected = inline` - ├return Math.max(Math.min(n, max), min);┤ - `; - expect(formatIndentation()(completion, context)).to.eq(expected); - }); - - it("should format indentation if first line of completion is wrongly indented.", () => { - const context = { - ...documentContext` - function clamp(n: number, max: number, min: number): number { - ║ - } - `, - indentation: " ", - language: "typescript", - }; - const completion = inline` - ├ return Math.max(Math.min(n, max), min);┤ - `; - const expected = inline` - ├ return Math.max(Math.min(n, max), min);┤ - `; - expect(formatIndentation()(completion, context)).to.eq(expected); - }); - - it("should format indentation if completion lines is over indented.", () => { - const context = { - ...documentContext` - def findMax(arr):║ - `, - indentation: " ", - language: "python", - }; - const completion = inline` - ├ - max = arr[0] - for i in range(1, len(arr)): - if arr[i] > max: - max = arr[i] - return max - }┤ - `; - const expected = inline` - ├ - max = arr[0] - for i in range(1, len(arr)): - if arr[i] > max: - max = arr[i] - return max - }┤ - `; - expect(formatIndentation()(completion, context)).to.eq(expected); - }); - - it("should format indentation if completion lines is wrongly indented.", () => { - const context = { - ...documentContext` - def findMax(arr):║ - `, - indentation: " ", - language: "python", - }; - const completion = inline` - ├ - max = arr[0] - for i in range(1, len(arr)): - if arr[i] > max: - max = arr[i] - return max - }┤ - `; - const expected = inline` - ├ - max = arr[0] - for i in range(1, len(arr)): - if arr[i] > max: - max = arr[i] - return max - }┤ - `; - expect(formatIndentation()(completion, context)).to.eq(expected); - }); - - it("should keep it unchanged if it no indentation specified.", () => { - const context = { - ...documentContext` - def findMax(arr):║ - `, - indentation: undefined, - language: "python", - }; - const completion = inline` - ├ - max = arr[0] - for i in range(1, len(arr)): - if arr[i] > max: - max = arr[i] - return max - }┤ - `; - expect(formatIndentation()(completion, context)).to.eq(completion); - }); - - it("should keep it unchanged if there is indentation in the context.", () => { - const context = { - ...documentContext` - def hello(): - return "world" - - def findMax(arr):║ - `, - indentation: "\t", - language: "python", - }; - const completion = inline` - ├ - max = arr[0] - for i in range(1, len(arr)): - if arr[i] > max: - max = arr[i] - return max - }┤ - `; - expect(formatIndentation()(completion, context)).to.eq(completion); - }); - - it("should keep it unchanged if it is well indented.", () => { - const context = { - ...documentContext` - def findMax(arr):║ - `, - indentation: " ", - language: "python", - }; - const completion = inline` - ├ - max = arr[0] - for i in range(1, len(arr)): - if arr[i] > max: - max = arr[i] - return max - }┤ - `; - expect(formatIndentation()(completion, context)).to.eq(completion); - }); - }); -}); diff --git a/clients/tabby-agent/src/postprocess/golden/limit_scope/experimental_block_01.toml b/clients/tabby-agent/src/postprocess/golden/limit_scope/experimental_block_01.toml deleted file mode 100644 index c7d9c7ed2f55..000000000000 --- a/clients/tabby-agent/src/postprocess/golden/limit_scope/experimental_block_01.toml +++ /dev/null @@ -1,26 +0,0 @@ -description = 'Limit scope experimental: limit to block when completing a line: case 01' - -[config.limitScope.indentation] -experimentalKeepBlockScopeWhenCompletingLine = true - -[context] -filepath = 'foo.ts' -language = 'typescript' -# indentation = ' ' # not specified -text = ''' -export class Foo { - private _foo: number; - - constructor() { - this._foo = 1; - } - - update(value): Foo { - this._foo =├ value; - return this;┤ - } -} -''' - -[expected] -unchanged = true diff --git a/clients/tabby-agent/src/postprocess/golden/limit_scope/experimental_block_02.toml b/clients/tabby-agent/src/postprocess/golden/limit_scope/experimental_block_02.toml deleted file mode 100644 index 1fb08b4485f7..000000000000 --- a/clients/tabby-agent/src/postprocess/golden/limit_scope/experimental_block_02.toml +++ /dev/null @@ -1,21 +0,0 @@ -description = 'Limit scope experimental: limit to block when completing a line: case 02' - -[config.limitScope.indentation] -experimentalKeepBlockScopeWhenCompletingLine = true - -[context] -filepath = 'checks.js' -language = 'javascript' -# indentation = ' ' # not specified -text = ''' -function check(obj) { - // if obj.a is defined, then return t├rue - if (obj.a) { - return true; - }┤ - return false; -} -''' - -[expected] -unchanged = true \ No newline at end of file diff --git a/clients/tabby-agent/src/postprocess/golden/limit_scope/experimental_syntax_rust_01.toml b/clients/tabby-agent/src/postprocess/golden/limit_scope/experimental_syntax_rust_01.toml deleted file mode 100644 index e3f7689eea3e..000000000000 --- a/clients/tabby-agent/src/postprocess/golden/limit_scope/experimental_syntax_rust_01.toml +++ /dev/null @@ -1,26 +0,0 @@ -description = 'Limit scope experimental: syntax rust: case 01' - -[config.limitScope] -experimentalSyntax = true - -[context] -filepath = 'stop.rs' -language = 'rust' -# indentation = ' ' # not specified -text = ''' -pub struct StopCondition { - stop_re: Option, - max_decoding_length: ├usize, - max_decoding_time: Duration, -}┤ -} -''' - -[expected] -text = ''' -pub struct StopCondition { - stop_re: Option, - max_decoding_length: ├usize, - max_decoding_time: Duration,┤ -} -''' diff --git a/clients/tabby-agent/src/postprocess/golden/limit_scope/experimental_syntax_rust_02.toml b/clients/tabby-agent/src/postprocess/golden/limit_scope/experimental_syntax_rust_02.toml deleted file mode 100644 index 440e2c8fc286..000000000000 --- a/clients/tabby-agent/src/postprocess/golden/limit_scope/experimental_syntax_rust_02.toml +++ /dev/null @@ -1,25 +0,0 @@ -description = 'Limit scope experimental: syntax rust: case 02' - -[config.limitScope] -experimentalSyntax = true - -[context] -filepath = 'stop.rs' -language = 'rust' -# indentation = ' ' # not specified -text = ''' -fn add(x: i32, y: i32) -> i32 { - println!("x:├ {}", x); - println!("y: {}", y); - println!("x + y: {}┤", x); - return x + y; -} -''' - -[expected] -text = ''' -fn add(x: i32, y: i32) -> i32 { - println!("x:├ {}┤", x); - return x + y; -} -''' diff --git a/clients/tabby-agent/src/postprocess/golden/limit_scope/experimental_syntax_typescript_01.toml b/clients/tabby-agent/src/postprocess/golden/limit_scope/experimental_syntax_typescript_01.toml deleted file mode 100644 index 98482c67ef0e..000000000000 --- a/clients/tabby-agent/src/postprocess/golden/limit_scope/experimental_syntax_typescript_01.toml +++ /dev/null @@ -1,30 +0,0 @@ -description = 'Limit scope experimental: syntax typescript: case 01' - -[config.limitScope] -experimentalSyntax = true - -[context] -filepath = 'foo.ts' -language = 'typescript' -# indentation = ' ' # not specified -text = ''' -export class Foo { - private _foo: number; - - constructor() { - this._foo = 1; - } - - update(value): Foo { - ├this._foo = value; - return this;┤ - } - - get foo(): number { - return this._foo; - } -} -''' - -[expected] -unchanged = true diff --git a/clients/tabby-agent/src/postprocess/golden/replace_range/experimental_syntax_javascript_01.toml b/clients/tabby-agent/src/postprocess/golden/replace_range/experimental_syntax_javascript_01.toml deleted file mode 100644 index c681b8f652fa..000000000000 --- a/clients/tabby-agent/src/postprocess/golden/replace_range/experimental_syntax_javascript_01.toml +++ /dev/null @@ -1,27 +0,0 @@ -description = 'Replace range experimental: syntax javascript: case 01' - -[config.limitScope] -experimentalSyntax = true -[config.calculateReplaceRange] -experimentalSyntax = true - -[context] -filepath = 'listener.js' -language = 'javascript' -# indentation = ' ' # not specified -text = ''' -const stream = process.stdin; -// just print data string -stream.on('data', (data) => {├ - console.log(data.toString()); -});┤}) -''' - -[expected] -text = ''' -const stream = process.stdin; -// just print data string -stream.on('data', (data) => {├ - console.log(data.toString()); -});┤})╣ -''' diff --git a/clients/tabby-agent/src/postprocess/golden/replace_range/experimental_syntax_mismatched_01.toml b/clients/tabby-agent/src/postprocess/golden/replace_range/experimental_syntax_mismatched_01.toml deleted file mode 100644 index be5b6c26e76c..000000000000 --- a/clients/tabby-agent/src/postprocess/golden/replace_range/experimental_syntax_mismatched_01.toml +++ /dev/null @@ -1,31 +0,0 @@ -description = 'Replace range experimental: syntax mismatched: case 01' - -[config.limitScope] -experimentalSyntax = true -[config.calculateReplaceRange] -experimentalSyntax = true - -[context] -filepath = 'fibonacci.ts' -language = 'typescript' -# indentation = ' ' # not specified -text = ''' -def fibonacci(├n): - if n == 0: - return 0 - elif n == 1: - return 1 - else: - return fibonacci(n - 1) + fibonacci(n - 2)┤) -''' - -[expected] -text = ''' -def fibonacci(├n): - if n == 0: - return 0 - elif n == 1: - return 1 - else: - return fibonacci(n - 1) + fibonacci(n - 2)┤)╣ -''' diff --git a/clients/tabby-agent/src/postprocess/golden/replace_range/experimental_syntax_rust_01.toml b/clients/tabby-agent/src/postprocess/golden/replace_range/experimental_syntax_rust_01.toml deleted file mode 100644 index 3987255aac70..000000000000 --- a/clients/tabby-agent/src/postprocess/golden/replace_range/experimental_syntax_rust_01.toml +++ /dev/null @@ -1,30 +0,0 @@ -description = 'Replace range experimental: syntax rust: bad case 01' - -[config.limitScope] -experimentalSyntax = true -[config.calculateReplaceRange] -experimentalSyntax = true - -[context] -filepath = 'myTest.rs' -language = 'rust' -# indentation = ' ' # not specified -text = ''' -pub fun myTest(&self, text: String) { - let mut body = HashMap::new(); - body.insert("├text".to_string(), text);┤") - let mut headers = HashMap::new(); - headers.insert("Content-Type".to_string(), "application/json".to_string()); -} -''' - -[expected] -text = ''' -pub fun myTest(&self, text: String) { - let mut body = HashMap::new(); - body.insert("├text".to_string(), text);┤")╣ - let mut headers = HashMap::new(); - headers.insert("Content-Type".to_string(), "application/json".to_string()); -} -''' -notEqual = true diff --git a/clients/tabby-agent/src/postprocess/golden/replace_range/experimental_syntax_rust_01_ref.toml b/clients/tabby-agent/src/postprocess/golden/replace_range/experimental_syntax_rust_01_ref.toml deleted file mode 100644 index 7c649604dbbf..000000000000 --- a/clients/tabby-agent/src/postprocess/golden/replace_range/experimental_syntax_rust_01_ref.toml +++ /dev/null @@ -1,29 +0,0 @@ -description = 'Replace range experimental: syntax rust: case 01 (ref)' - -[config.limitScope] -experimentalSyntax = true -[config.calculateReplaceRange] -experimentalSyntax = false - -[context] -filepath = 'myTest.rs' -language = 'rust' -# indentation = ' ' # not specified -text = ''' -pub fun myTest(&self, text: String) { - let mut body = HashMap::new(); - body.insert("├text".to_string(), text);┤") - let mut headers = HashMap::new(); - headers.insert("Content-Type".to_string(), "application/json".to_string()); -} -''' - -[expected] -text = ''' -pub fun myTest(&self, text: String) { - let mut body = HashMap::new(); - body.insert("├text".to_string(), text);┤")╣ - let mut headers = HashMap::new(); - headers.insert("Content-Type".to_string(), "application/json".to_string()); -} -''' diff --git a/clients/tabby-agent/src/postprocess/golden/replace_range/experimental_syntax_typescript_01.toml b/clients/tabby-agent/src/postprocess/golden/replace_range/experimental_syntax_typescript_01.toml deleted file mode 100644 index dd0abc95eb10..000000000000 --- a/clients/tabby-agent/src/postprocess/golden/replace_range/experimental_syntax_typescript_01.toml +++ /dev/null @@ -1,23 +0,0 @@ -description = 'Replace range experimental: syntax typescript: case 01' - -[config.limitScope] -experimentalSyntax = true -[config.calculateReplaceRange] -experimentalSyntax = true - -[context] -filepath = 'print.ts' -language = 'typescript' -# indentation = ' ' # not specified -text = ''' -function print(obj: any) { - console.log("Obj: ├", obj);┤") -} -''' - -[expected] -text = ''' -function print(obj: any) { - console.log("Obj: ├", obj);┤")╣ -} -''' diff --git a/clients/tabby-agent/src/postprocess/index.ts b/clients/tabby-agent/src/postprocess/index.ts deleted file mode 100644 index e78130072f87..000000000000 --- a/clients/tabby-agent/src/postprocess/index.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { CompletionContext, CompletionResponse } from "../CompletionContext"; -import { AgentConfig } from "../AgentConfig"; -import { applyFilter, applyChoiceFilter } from "./base"; -import { removeRepetitiveBlocks } from "./removeRepetitiveBlocks"; -import { removeRepetitiveLines } from "./removeRepetitiveLines"; -import { removeLineEndsWithRepetition } from "./removeLineEndsWithRepetition"; -import { removeDuplicatedBlockClosingLine } from "./removeDuplicatedBlockClosingLine"; -import { limitScope } from "./limitScope"; -import { formatIndentation } from "./formatIndentation"; -import { trimSpace } from "./trimSpace"; -import { trimMultiLineInSingleLineMode } from "./trimMultiLineInSingleLineMode"; -import { dropDuplicated } from "./dropDuplicated"; -import { dropBlank } from "./dropBlank"; -import { calculateReplaceRange } from "./calculateReplaceRange"; - -export async function preCacheProcess( - context: CompletionContext, - _: AgentConfig["postprocess"], - response: CompletionResponse, -): Promise { - return Promise.resolve(response) - .then(applyFilter(trimMultiLineInSingleLineMode(), context)) - .then(applyFilter(removeLineEndsWithRepetition(), context)) - .then(applyFilter(dropDuplicated(), context)) - .then(applyFilter(trimSpace(), context)) - .then(applyFilter(dropBlank(), context)); -} - -export async function postCacheProcess( - context: CompletionContext, - config: AgentConfig["postprocess"], - response: CompletionResponse, -): Promise { - return Promise.resolve(response) - .then(applyFilter(removeRepetitiveBlocks(), context)) - .then(applyFilter(removeRepetitiveLines(), context)) - .then(applyFilter(limitScope(config["limitScope"]), context)) - .then(applyFilter(removeDuplicatedBlockClosingLine(), context)) - .then(applyFilter(formatIndentation(), context)) - .then(applyFilter(dropDuplicated(), context)) - .then(applyFilter(trimSpace(), context)) - .then(applyFilter(dropBlank(), context)) - .then(applyChoiceFilter(calculateReplaceRange(config["calculateReplaceRange"]), context)); -} diff --git a/clients/tabby-agent/src/postprocess/limitScope.ts b/clients/tabby-agent/src/postprocess/limitScope.ts deleted file mode 100644 index 8a5b6b8774b0..000000000000 --- a/clients/tabby-agent/src/postprocess/limitScope.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { CompletionContext } from "../CompletionContext"; -import { AgentConfig } from "../AgentConfig"; -import { isBrowser } from "../env"; -import { PostprocessFilter, logger } from "./base"; -import { limitScopeByIndentation } from "./limitScopeByIndentation"; -import { limitScopeBySyntax } from "./limitScopeBySyntax"; - -export function limitScope(config: AgentConfig["postprocess"]["limitScope"]): PostprocessFilter { - return async (input: string, context: CompletionContext) => { - const preferSyntaxParser = - !isBrowser && // syntax parser is not supported in browser yet - config.experimentalSyntax; - - if (preferSyntaxParser) { - try { - return await limitScopeBySyntax()(input, context); - } catch (error) { - logger.debug({ error }, "Failed to limit scope by syntax parser"); - } - } - return limitScopeByIndentation(config["indentation"])(input, context); - }; -} diff --git a/clients/tabby-agent/src/postprocess/limitScopeByIndentation.test.ts b/clients/tabby-agent/src/postprocess/limitScopeByIndentation.test.ts deleted file mode 100644 index 3a955b825e70..000000000000 --- a/clients/tabby-agent/src/postprocess/limitScopeByIndentation.test.ts +++ /dev/null @@ -1,374 +0,0 @@ -import { expect } from "chai"; -import { documentContext, inline } from "./testUtils"; -import { limitScopeByIndentation } from "./limitScopeByIndentation"; - -describe("postprocess", () => { - describe("limitScopeByIndentation: default config", () => { - const limitScopeByIndentationDefault = limitScopeByIndentation({ - experimentalKeepBlockScopeWhenCompletingLine: false, - }); - - it("should limit scope at sentence end, when completion is continuing uncompleted sentence in the prefix.", () => { - const context = { - ...documentContext` - let a =║ - `, - language: "javascript", - }; - const completion = inline` - ├ 1; - let b = 2;┤ - `; - const expected = inline` - ├ 1;┤ - `; - expect(limitScopeByIndentationDefault(completion, context)).to.eq(expected); - }); - - it("should limit scope at sentence end, when completion is continuing uncompleted sentence in the prefix.", () => { - const context = { - ...documentContext` - function safeParse(json) { - try { - console.log║ - } catch (error) { - console.error(error); - return null; - } - } - `, - language: "javascript", - }; - const completion = inline` - ├("Parsing", { json }); - return JSON.parse(json); - } catch (e) { - return null; - } - }┤ - `; - const expected = inline` - ├("Parsing", { json });┤ - `; - expect(limitScopeByIndentationDefault(completion, context)).to.eq(expected); - }); - - it("should limit scope at next indent level, including closing line, when completion is starting a new indent level in next line.", () => { - const context = { - ...documentContext` - function findMax(arr) {║} - `, - language: "javascript", - }; - const completion = inline` - ├ - let max = arr[0]; - for (let i = 1; i < arr.length; i++) { - if (arr[i] > max) { - max = arr[i]; - } - } - return max; - } - console.log(findMax([1, 2, 3, 4, 5]));┤ - `; - const expected = inline` - ├ - let max = arr[0]; - for (let i = 1; i < arr.length; i++) { - if (arr[i] > max) { - max = arr[i]; - } - } - return max; - }┤ - `; - expect(limitScopeByIndentationDefault(completion, context)).to.eq(expected); - }); - - it("should limit scope at next indent level, including closing line, when completion is continuing uncompleted sentence in the prefix, and starting a new indent level in next line.", () => { - const context = { - ...documentContext` - function findMax(arr) { - let max = arr[0]; - for║ - } - `, - language: "javascript", - }; - const completion = inline` - ├ (let i = 1; i < arr.length; i++) { - if (arr[i] > max) { - max = arr[i]; - } - } - return max; - } - console.log(findMax([1, 2, 3, 4, 5]));┤ - `; - const expected = inline` - ├ (let i = 1; i < arr.length; i++) { - if (arr[i] > max) { - max = arr[i]; - } - }┤ - ┴┴ - `; - expect(limitScopeByIndentationDefault(completion, context)).to.eq(expected); - }); - - it("should limit scope at current indent level, including closing line, when completion starts new sentences at same indent level.", () => { - const context = { - ...documentContext` - function findMax(arr) { - let max = arr[0];║ - } - `, - language: "javascript", - }; - const completion = inline` - ├ - for (let i = 1; i < arr.length; i++) { - if (arr[i] > max) { - max = arr[i]; - } - } - return max; - }┤ - `; - expect(limitScopeByIndentationDefault(completion, context)).to.eq(completion); - }); - - it("should allow only one level closing bracket", () => { - const context = { - ...documentContext` - function safeParse(json) { - try { - return JSON.parse(json); - } catch (e) { - return null;║ - `, - language: "javascript", - }; - const completion = inline` - ├ - } - }┤ - `; - const expected = inline` - ├ - }┤ - ┴┴ - `; - expect(limitScopeByIndentationDefault(completion, context)).to.eq(expected); - }); - - it("should allow level closing bracket at current line, it looks same as starts new sentences", () => { - const context = { - ...documentContext` - function helloworld() { - console.log("hello"); - ║ - `, - language: "javascript", - }; - const completion = inline` - ├}┤ - `; - expect(limitScopeByIndentationDefault(completion, context)).to.be.eq(completion); - }); - - it("should not allow level closing bracket, when the suffix lines have same indent level", () => { - const context = { - ...documentContext` - function helloworld() { - console.log("hello");║ - console.log("world"); - } - `, - language: "javascript", - }; - const completion = inline` - ├ - }┤ - `; - const expected = inline` - ├┤`; - expect(limitScopeByIndentationDefault(completion, context)).to.be.eq(expected); - }); - - it("should use indent level of previous line, when current line is empty.", () => { - const context = { - ...documentContext` - function safeParse(json) { - try { - ║ - } - } - `, - language: "javascript", - }; - const completion = inline` - ├return JSON.parse(json); - } catch (e) { - return null; - } - }┤ - `; - const expected = inline` - ├return JSON.parse(json); - } catch (e) { - return null; - }┤ - ┴┴ - `; - expect(limitScopeByIndentationDefault(completion, context)).to.eq(expected); - }); - }); - - describe("limitScopeByIndentation: bad cases", () => { - const limitScopeByIndentationDefault = limitScopeByIndentation({ - experimentalKeepBlockScopeWhenCompletingLine: false, - }); - it("cannot handle the case of indent that does'nt have a close line, e.g. chaining call", () => { - const context = { - ...documentContext` - function sortWords(input) { - const output = input.trim() - .split("\n") - .map((line) => line.split(" ")) - ║ - } - `, - language: "javascript", - }; - const completion = inline` - ├.flat() - .sort() - .join(" "); - console.log(output); - return output; - } - sortWords("world hello");┤ - `; - const expected = inline` - ├.flat() - .sort() - .join(" "); - console.log(output); - return output; - }┤ - `; - expect(limitScopeByIndentationDefault(completion, context)).not.to.eq(expected); - }); - - it("cannot handle the case of indent that does'nt have a close line, e.g. python def function", () => { - const context = { - ...documentContext` - def findMax(arr): - ║ - `, - language: "python", - }; - const completion = inline` - ├max = arr[0] - for i in range(1, len(arr)): - if arr[i] > max: - max = arr[i] - return max - findMax([1, 2, 3, 4, 5])┤ - `; - const expected = inline` - ├max = arr[0] - for i in range(1, len(arr)): - if arr[i] > max: - max = arr[i] - return max┤ - `; - expect(limitScopeByIndentationDefault(completion, context)).not.to.eq(expected); - }); - }); - - describe("limitScopeByIndentation: with experimentalKeepBlockScopeWhenCompletingLine on", () => { - const limitScopeByIndentationKeepBlock = limitScopeByIndentation({ - experimentalKeepBlockScopeWhenCompletingLine: true, - }); - - it("should limit scope at block end, when completion is continuing uncompleted sentence in the prefix.", () => { - const context = { - ...documentContext` - let a =║ - `, - language: "javascript", - }; - const completion = inline` - ├ 1; - let b = 2;┤ - `; - expect(limitScopeByIndentationKeepBlock(completion, context)).to.eq(completion); - }); - - it("should limit scope at block end, when completion is continuing uncompleted sentence in the prefix.", () => { - const context = { - ...documentContext` - function safeParse(json) { - try { - console.log║ - } - } - `, - language: "javascript", - }; - const completion = inline` - ├("Parsing", { json }); - return JSON.parse(json); - } catch (e) { - return null; - } - }┤ - `; - const expected = inline` - ├("Parsing", { json }); - return JSON.parse(json); - } catch (e) { - return null; - }┤ - ┴┴ - `; - expect(limitScopeByIndentationKeepBlock(completion, context)).to.eq(expected); - }); - - it("should limit scope at same indent level, including closing line, when completion is continuing uncompleted sentence in the prefix, and starting a new indent level in next line.", () => { - const context = { - ...documentContext` - function findMax(arr) { - let max = arr[0]; - for║ - } - `, - language: "javascript", - }; - const completion = inline` - ├ (let i = 1; i < arr.length; i++) { - if (arr[i] > max) { - max = arr[i]; - } - } - return max; - } - console.log(findMax([1, 2, 3, 4, 5]));┤ - `; - const expected = inline` - ├ (let i = 1; i < arr.length; i++) { - if (arr[i] > max) { - max = arr[i]; - } - } - return max; - }┤ - ┴┴ - `; - expect(limitScopeByIndentationKeepBlock(completion, context)).to.eq(expected); - }); - }); -}); diff --git a/clients/tabby-agent/src/postprocess/limitScopeBySyntax.test.ts b/clients/tabby-agent/src/postprocess/limitScopeBySyntax.test.ts deleted file mode 100644 index ee6d6f67d0b7..000000000000 --- a/clients/tabby-agent/src/postprocess/limitScopeBySyntax.test.ts +++ /dev/null @@ -1,410 +0,0 @@ -import { expect } from "chai"; -import { documentContext, inline } from "./testUtils"; -import { limitScopeBySyntax } from "./limitScopeBySyntax"; - -describe("postprocess", () => { - describe("limitScopeBySyntax javascript", () => { - it("should limit scope at function_declaration.", async () => { - const context = { - ...documentContext` - function findMax(arr) {║} - `, - language: "javascript", - }; - const completion = inline` - ├ - let max = arr[0]; - for (let i = 1; i < arr.length; i++) { - if (arr[i] > max) { - max = arr[i]; - } - } - return max; - } - console.log(findMax([1, 2, 3, 4, 5]));┤ - `; - const expected = inline` - ├ - let max = arr[0]; - for (let i = 1; i < arr.length; i++) { - if (arr[i] > max) { - max = arr[i]; - } - } - return max; - }┤ - `; - expect(await limitScopeBySyntax()(completion, context)).to.eq(expected); - }); - - it("should limit scope at function_declaration", async () => { - const context = { - ...documentContext` - function findMax(arr) { - let max = arr[0];║ - } - `, - language: "javascript", - }; - const completion = inline` - ├ - for (let i = 1; i < arr.length; i++) { - if (arr[i] > max) { - max = arr[i]; - } - } - return max; - }┤ - `; - expect(await limitScopeBySyntax()(completion, context)).to.eq(completion); - }); - - it("should limit scope at function_declaration", async () => { - const context = { - ...documentContext` - function findMax(arr) { - let max = arr[0]; - for (let i = 1; i < arr.length; i++) { - if (arr[i] > max) { - max = arr[i]; - } - }║ - } - `, - language: "javascript", - }; - const completion = inline` - ├ - return max; - }┤ - `; - expect(await limitScopeBySyntax()(completion, context)).to.eq(completion); - }); - - it("should limit scope at for_statement.", async () => { - const context = { - ...documentContext` - function findMax(arr) { - let max = arr[0]; - for║ - } - `, - language: "javascript", - }; - const completion = inline` - ├ (let i = 1; i < arr.length; i++) { - if (arr[i] > max) { - max = arr[i]; - } - } - return max; - } - console.log(findMax([1, 2, 3, 4, 5]));┤ - `; - const expected = inline` - ├ (let i = 1; i < arr.length; i++) { - if (arr[i] > max) { - max = arr[i]; - } - }┤ - ┴┴ - `; - expect(await limitScopeBySyntax()(completion, context)).to.eq(expected); - }); - - it("should limit scope at current node if no parent scope found.", async () => { - const context = { - ...documentContext` - let a =║ - `, - language: "javascript", - }; - const completion = inline` - ├ 1; - let b = 2;┤ - `; - const expected = inline` - ├ 1;┤ - `; - expect(await limitScopeBySyntax()(completion, context)).to.eq(expected); - }); - - it("should handle the bad case of limitScopeByIndentation", async () => { - const context = { - ...documentContext` - function sortWords(input) { - const output = input.trim() - .split("\n") - .map((line) => line.split(" ")) - ║ - } - `, - language: "javascript", - }; - const completion = inline` - ├.flat() - .sort() - .join(" "); - console.log(output); - return output; - } - sortWords("world hello");┤ - `; - const expected = inline` - ├.flat() - .sort() - .join(" "); - console.log(output); - return output; - }┤ - `; - expect(await limitScopeBySyntax()(completion, context)).to.eq(expected); - }); - }); - - describe("limitScopeBySyntax python", () => { - it("should limit scope at function_definition.", async () => { - const context = { - ...documentContext` - def find_min(arr):║ - `, - language: "python", - }; - const completion = inline` - ├ - min = arr[0] - for i in range(1, len(arr)): - if arr[i] < min: - min = arr[i] - return min - print(find_min([1, 2, 3, 4, 5]))┤ - `; - const expected = inline` - ├ - min = arr[0] - for i in range(1, len(arr)): - if arr[i] < min: - min = arr[i] - return min┤ - ┴┴ - `; - expect(await limitScopeBySyntax()(completion, context)).to.eq(expected); - }); - - it("should limit scope at function_definition.", async () => { - const context = { - ...documentContext` - def find_min(arr): - min = arr[0]║ - `, - language: "python", - }; - const completion = inline` - ├ - for i in range(1, len(arr)): - if arr[i] < min: - min = arr[i] - return min - print(find_min([1, 2, 3, 4, 5]))┤ - `; - const expected = inline` - ├ - for i in range(1, len(arr)): - if arr[i] < min: - min = arr[i] - return min┤ - ┴┴ - `; - expect(await limitScopeBySyntax()(completion, context)).to.eq(expected); - }); - - it("should limit scope at function_definition.", async () => { - const context = { - ...documentContext` - def find_min(arr): - min = arr[0] - for i in range(1, len(arr)): - if arr[i] < min: - min = arr[i]║ - `, - language: "python", - }; - const completion = inline` - ├ - return min┤ - `; - expect(await limitScopeBySyntax()(completion, context)).to.eq(completion); - }); - - it("should limit scope at for_statement.", async () => { - const context = { - ...documentContext` - def find_min(arr): - max = arr[0] - for║ - `, - language: "python", - }; - const completion = inline` - ├ i in range(1, len(arr)): - if arr[i] < min: - min = arr[i] - return min - ┤ - `; - const expected = inline` - ├ i in range(1, len(arr)): - if arr[i] < min: - min = arr[i]┤ - ┴┴┴┴ - `; - expect(await limitScopeBySyntax()(completion, context)).to.eq(expected); - }); - - it("should handle the bad case of limitScopeByIndentation", async () => { - const context = { - ...documentContext` - def findMax(arr): - ║ - `, - language: "python", - }; - const completion = inline` - ├max = arr[0] - for i in range(1, len(arr)): - if arr[i] > max: - max = arr[i] - return max - findMax([1, 2, 3, 4, 5])┤ - `; - const expected = inline` - ├max = arr[0] - for i in range(1, len(arr)): - if arr[i] > max: - max = arr[i] - return max┤ - ┴┴ - `; - expect(await limitScopeBySyntax()(completion, context)).to.eq(expected); - }); - }); - - describe("limitScopeBySyntax go", () => { - it("should limit scope at function_declaration.", async () => { - const context = { - ...documentContext` - func findMin(arr []int) int {║} - `, - language: "go", - }; - const completion = inline` - ├ - min := math.MaxInt64 - for _, v := range arr { - if v < min { - min = v - } - } - return min - } - - func main() { - arr := []int{5, 2, 9, 8, 1, 3} - fmt.Println(findMin(arr)) // Output: 1 - }┤ - `; - const expected = inline` - ├ - min := math.MaxInt64 - for _, v := range arr { - if v < min { - min = v - } - } - return min - }┤ - `; - expect(await limitScopeBySyntax()(completion, context)).to.eq(expected); - }); - - it("should limit scope at for_statement.", async () => { - const context = { - ...documentContext` - func findMin(arr []int) int { - min := math.MaxInt64 - for║ - `, - language: "go", - }; - const completion = inline` - ├ _, v := range arr { - if v < min { - min = v - } - } - return min - }┤ - `; - const expected = inline` - ├ _, v := range arr { - if v < min { - min = v - } - }┤ - ┴┴ - `; - expect(await limitScopeBySyntax()(completion, context)).to.eq(expected); - }); - }); - - describe("limitScopeBySyntax rust", () => { - it("should limit scope at function_item.", async () => { - const context = { - ...documentContext` - fn find_min(arr: &[i32]) -> i32 {║} - `, - language: "rust", - }; - const completion = inline` - ├ - *arr.iter().min().unwrap() - } - fn main() { - let arr = vec![5, 2, 9, 8, 1, 3]; - println!("{}", find_min(&arr)); // Output: 1 - }┤ - `; - const expected = inline` - ├ - *arr.iter().min().unwrap() - }┤ - `; - expect(await limitScopeBySyntax()(completion, context)).to.eq(expected); - }); - }); - - describe("limitScopeBySyntax ruby", () => { - it("should limit scope at for.", async () => { - const context = { - ...documentContext` - def fibonacci(n)║ - `, - language: "ruby", - }; - const completion = inline` - ├ - return n if n <= 1 - fibonacci(n - 1) + fibonacci(n - 2) - end - puts fibonacci(10)┤ - `; - const expected = inline` - ├ - return n if n <= 1 - fibonacci(n - 1) + fibonacci(n - 2) - end┤ - `; - expect(await limitScopeBySyntax()(completion, context)).to.eq(expected); - }); - }); -}); diff --git a/clients/tabby-agent/src/postprocess/limitScopeBySyntax.ts b/clients/tabby-agent/src/postprocess/limitScopeBySyntax.ts deleted file mode 100644 index 0fc2e46afde7..000000000000 --- a/clients/tabby-agent/src/postprocess/limitScopeBySyntax.ts +++ /dev/null @@ -1,93 +0,0 @@ -import type TreeSitterParser from "web-tree-sitter"; -import { getParser, languagesConfigs } from "../syntax/parser"; -import { typeList } from "../syntax/typeList"; -import { CompletionContext } from "../CompletionContext"; -import { PostprocessFilter, logger } from "./base"; - -export const supportedLanguages = Object.keys(languagesConfigs); - -function findLineBegin(text: string, position: number): number { - let lastNonBlankCharPos = position - 1; - while (lastNonBlankCharPos >= 0 && text[lastNonBlankCharPos]?.match(/\s/)) { - lastNonBlankCharPos--; - } - if (lastNonBlankCharPos < 0) { - return 0; - } - const lineBegin = text.lastIndexOf("\n", lastNonBlankCharPos); - if (lineBegin < 0) { - return 0; - } - const line = text.slice(lineBegin + 1, position); - const indentation = line.search(/\S/); - return lineBegin + 1 + indentation; -} - -function findLineEnd(text: string, position: number): number { - let firstNonBlankCharPos = position; - while (firstNonBlankCharPos < text.length && text[firstNonBlankCharPos]?.match(/\s/)) { - firstNonBlankCharPos++; - } - if (firstNonBlankCharPos >= text.length) { - return text.length; - } - const lineEnd = text.indexOf("\n", firstNonBlankCharPos); - if (lineEnd < 0) { - return text.length; - } - return lineEnd; -} - -function findScope(node: TreeSitterParser.SyntaxNode, typeList: string[][]): TreeSitterParser.SyntaxNode { - for (const types of typeList) { - let scope: TreeSitterParser.SyntaxNode | null = node; - while (scope) { - if (types.includes(scope.type)) { - return scope; - } - scope = scope.parent; - } - } - return node; -} - -export function limitScopeBySyntax(): PostprocessFilter { - return async (input: string, context: CompletionContext) => { - const { position, text, language, prefix, suffix } = context; - if (!supportedLanguages.includes(language)) { - throw new Error(`Language ${language} is not supported`); - } - const languageConfig = languagesConfigs[language]!; - const parser = await getParser(languageConfig); - - const updatedText = prefix + input + suffix; - const updatedTree = parser.parse(updatedText); - const lineBegin = findLineBegin(updatedText, position); - const lineEnd = findLineEnd(updatedText, position); - const scope = findScope( - updatedTree.rootNode.namedDescendantForIndex(lineBegin, lineEnd), - typeList[languageConfig] ?? [], - ); - - if (scope.type == "ERROR") { - throw new Error("Cannot determine syntax scope."); - } - - if (scope.endIndex < position + input.length) { - logger.debug( - { - languageConfig, - text, - updatedText, - position, - lineBegin, - lineEnd, - scope: { type: scope.type, start: scope.startIndex, end: scope.endIndex }, - }, - "Remove content out of syntax scope", - ); - return input.slice(0, scope.endIndex - position); - } - return input; - }; -} diff --git a/clients/tabby-agent/src/postprocess/postprocess.test.ts b/clients/tabby-agent/src/postprocess/postprocess.test.ts deleted file mode 100644 index 7a287d80052d..000000000000 --- a/clients/tabby-agent/src/postprocess/postprocess.test.ts +++ /dev/null @@ -1,130 +0,0 @@ -import path from "path"; -import fs from "fs-extra"; -import { v4 as uuid } from "uuid"; -import toml from "toml"; -import glob from "glob"; -import { expect } from "chai"; -import { deepmerge } from "deepmerge-ts"; -import { AgentConfig, defaultAgentConfig } from "../AgentConfig"; -import { CompletionContext, CompletionResponse } from "../CompletionContext"; -import { preCacheProcess, postCacheProcess } from "."; - -type PostprocessConfig = AgentConfig["postprocess"]; - -type DocumentContext = { - prefix: string; - prefixReplaceRange: string; - completion: string; - suffixReplaceRange: string; - suffix: string; -}; - -function parseDocContext(text: string): DocumentContext { - const insertStart = text.indexOf("├"); - const insertEnd = text.lastIndexOf("┤"); - let replaceStart = text.indexOf("╠"); - if (replaceStart < 0) { - replaceStart = insertStart; - } - let replaceEnd = text.lastIndexOf("╣"); - if (replaceEnd < 0) { - replaceEnd = insertEnd; - } - return { - prefix: text.slice(0, replaceStart), - prefixReplaceRange: text.slice(replaceStart + 1, insertStart), - completion: text.slice(insertStart + 1, insertEnd), - suffixReplaceRange: text.slice(insertEnd + 1, replaceEnd), - suffix: text.slice(replaceEnd + 1), - }; -} - -function getDoc(context: DocumentContext): string { - return context.prefix + context.prefixReplaceRange + context.suffixReplaceRange + context.suffix; -} - -function getPosition(context: DocumentContext): number { - return context.prefix.length + context.prefixReplaceRange.length; -} - -function getCompletion(context: DocumentContext): string { - return context.prefixReplaceRange + context.completion; -} - -function getReplaceRange(context: DocumentContext) { - return { - start: context.prefix.length, - end: context.prefix.length + context.prefixReplaceRange.length + context.suffixReplaceRange.length, - }; -} - -function buildChoices(context: DocumentContext) { - const text = getCompletion(context); - if (text.length === 0) { - return []; - } - return [ - { - index: 0, - text, - replaceRange: getReplaceRange(context), - }, - ]; -} - -describe("postprocess golden test", () => { - const postprocess = async (context: CompletionContext, config: PostprocessConfig, response: CompletionResponse) => { - let processed = await preCacheProcess(context, config, response); - processed = await postCacheProcess(context, config, processed); - return processed; - }; - - const files = glob.sync(path.join(__dirname, "golden/**/*.toml")); - files.sort().forEach((file) => { - const fileContent = fs.readFileSync(file, "utf8"); - const testCase = toml.parse(fileContent); - it(testCase["description"] ?? file, async () => { - const config = deepmerge(defaultAgentConfig["postprocess"], testCase["config"] ?? {}) as PostprocessConfig; - const docContext = parseDocContext(testCase["context"]?.["text"] ?? ""); - const completionContext = new CompletionContext({ - filepath: testCase["context"]?.["filepath"] ?? uuid(), - language: testCase["context"]?.["language"] ?? "plaintext", - text: getDoc(docContext), - position: getPosition(docContext), - indentation: testCase["context"]?.["indentation"], - }); - const completionId = "test-" + uuid(); - const completionResponse = { - id: completionId, - choices: buildChoices(docContext), - }; - const unchanged: CompletionResponse = JSON.parse(JSON.stringify(completionResponse)); - const output = await postprocess(completionContext, config, completionResponse); - - const checkExpected = (expected: CompletionResponse) => { - if (testCase["expected"]?.["notEqual"]) { - expect(output).to.not.deep.equal(expected); - } else { - expect(output).to.deep.equal(expected); - } - }; - - if (testCase["expected"]?.["unchanged"]) { - checkExpected(unchanged); - } else if (testCase["expected"]?.["discard"]) { - const expected = { - id: completionId, - choices: [], - }; - checkExpected(expected); - } else { - const expectedContext = parseDocContext(testCase["expected"]?.["text"] ?? ""); - const expected = { - id: completionId, - choices: buildChoices(expectedContext), - }; - checkExpected(expected); - } - }); - }); -}); diff --git a/clients/tabby-agent/src/postprocess/removeDuplicatedBlockClosingLine.test.ts b/clients/tabby-agent/src/postprocess/removeDuplicatedBlockClosingLine.test.ts deleted file mode 100644 index 06e67ffe741d..000000000000 --- a/clients/tabby-agent/src/postprocess/removeDuplicatedBlockClosingLine.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { expect } from "chai"; -import { documentContext, inline } from "./testUtils"; -import { removeDuplicatedBlockClosingLine } from "./removeDuplicatedBlockClosingLine"; - -describe("postprocess", () => { - describe("removeDuplicatedBlockClosingLine", () => { - it("should remove duplicated block closing line.", () => { - const context = { - ...documentContext` - function hello() { - ║ - } - `, - language: "javascript", - }; - const completion = inline` - ├console.log("hello"); - }┤ - `; - const expected = inline` - ├console.log("hello");┤ - ┴┴ - `; - expect(removeDuplicatedBlockClosingLine()(completion, context)).to.eq(expected); - }); - - it("should remove duplicated block closing line.", () => { - const context = { - ...documentContext` - function check(condition) { - if (!condition) { - ║ - } else { - return; - } - } - `, - language: "javascript", - }; - const completion = inline` - ├throw new Error("check not passed"); - }┤ - ┴┴ - `; - const expected = inline` - ├throw new Error("check not passed");┤ - ┴┴┴┴ - `; - expect(removeDuplicatedBlockClosingLine()(completion, context)).to.eq(expected); - }); - - it("should not remove non-duplicated block closing line.", () => { - const context = { - ...documentContext` - function check(condition) { - if (!condition) { - ║ - } - `, - language: "javascript", - }; - const completion = inline` - ├throw new Error("check not passed"); - }┤ - ┴┴ - `; - expect(removeDuplicatedBlockClosingLine()(completion, context)).to.eq(completion); - }); - }); -}); diff --git a/clients/tabby-agent/src/postprocess/removeDuplicatedBlockClosingLine.ts b/clients/tabby-agent/src/postprocess/removeDuplicatedBlockClosingLine.ts deleted file mode 100644 index 4dc591b121a7..000000000000 --- a/clients/tabby-agent/src/postprocess/removeDuplicatedBlockClosingLine.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { CompletionContext } from "../CompletionContext"; -import { PostprocessFilter, logger } from "./base"; -import { isBlank, splitLines, isBlockClosingLine } from "../utils"; - -// For remove duplicated block closing line at ( ending of input text ) and ( beginning of suffix text ) -// Should be useful after limitScope -export function removeDuplicatedBlockClosingLine(): PostprocessFilter { - return (input: string, context: CompletionContext) => { - const { suffixLines, currentLinePrefix } = context; - const inputLines = splitLines(input); - if (inputLines.length < 2) { - // If completion only has one line, don't continue process - return input; - } - - const inputLinesForDetection = inputLines.map((line, index) => { - return index === 0 ? currentLinePrefix + line : line; - }); - if (!isBlockClosingLine(inputLinesForDetection, inputLines.length - 1)) { - return input; - } - const inputEndingLine = inputLines[inputLines.length - 1]!; - - let suffixBeginningIndex = 1; - while (suffixBeginningIndex < suffixLines.length && isBlank(suffixLines[suffixBeginningIndex]!)) { - suffixBeginningIndex++; - } - if (suffixBeginningIndex >= suffixLines.length) { - return input; - } - const suffixBeginningLine = suffixLines[suffixBeginningIndex]!; - - if ( - inputEndingLine.startsWith(suffixBeginningLine.trimEnd()) || - suffixBeginningLine.startsWith(inputEndingLine.trimEnd()) - ) { - logger.debug({ inputLines, suffixLines }, "Removing duplicated block closing line"); - return inputLines - .slice(0, inputLines.length - 1) - .join("") - .trimEnd(); - } - return input; - }; -} diff --git a/clients/tabby-agent/src/postprocess/removeLineEndsWithRepetition.test.ts b/clients/tabby-agent/src/postprocess/removeLineEndsWithRepetition.test.ts deleted file mode 100644 index ace405c9cbf3..000000000000 --- a/clients/tabby-agent/src/postprocess/removeLineEndsWithRepetition.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { expect } from "chai"; -import { documentContext, inline } from "./testUtils"; -import { removeLineEndsWithRepetition } from "./removeLineEndsWithRepetition"; - -describe("postprocess", () => { - describe("removeLineEndsWithRepetition", () => { - it("should drop one line completion ends with repetition", () => { - const context = { - ...documentContext` - let foo = ║ - `, - language: "javascript", - }; - const completion = inline` - ├foo = foo = foo = foo = foo = foo = foo =┤ - `; - expect(removeLineEndsWithRepetition()(completion, context)).to.be.null; - }); - - it("should remove last line that ends with repetition", () => { - const context = { - ...documentContext` - let largeNumber = 1000000 - let veryLargeNumber = ║ - `, - language: "javascript", - }; - const completion = inline` - ├1000000000 - let superLargeNumber = 1000000000000000000000000000000000000000000000┤ - `; - const expected = inline` - ├1000000000┤ - `; - expect(removeLineEndsWithRepetition()(completion, context)).to.eq(expected); - }); - - it("should keep repetition less than threshold", () => { - const context = { - ...documentContext` - let largeNumber = 1000000 - let veryLargeNumber = ║ - `, - language: "javascript", - }; - const completion = inline` - ├1000000000000┤ - `; - const expected = completion; - expect(removeLineEndsWithRepetition()(completion, context)).to.eq(expected); - }); - }); -}); diff --git a/clients/tabby-agent/src/postprocess/removeLineEndsWithRepetition.ts b/clients/tabby-agent/src/postprocess/removeLineEndsWithRepetition.ts deleted file mode 100644 index 5fb15e99649a..000000000000 --- a/clients/tabby-agent/src/postprocess/removeLineEndsWithRepetition.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { PostprocessFilter, logger } from "./base"; -import { splitLines, isBlank } from "../utils"; - -const repetitionTests = [ - /(.{3,}?)\1{5,}$/g, // match a 3+ characters pattern repeating 5+ times - /(.{10,}?)\1{3,}$/g, // match a 10+ characters pattern repeating 3+ times -]; - -export function removeLineEndsWithRepetition(): PostprocessFilter { - return (input: string) => { - // only test last non-blank line - const inputLines = splitLines(input); - let index = inputLines.length - 1; - while (index >= 0 && isBlank(inputLines[index]!)) { - index--; - } - if (index < 0) return input; - // if matches repetition test, remove this line - for (const test of repetitionTests) { - const match = inputLines[index]!.match(test); - if (match) { - logger.debug( - { - inputLines, - lineNumber: index, - match, - }, - "Remove line ends with repetition.", - ); - if (index < 1) return null; - return inputLines.slice(0, index).join("").trimEnd(); - } - } - // no repetition found - return input; - }; -} diff --git a/clients/tabby-agent/src/postprocess/removeRepetitiveBlocks.test.ts b/clients/tabby-agent/src/postprocess/removeRepetitiveBlocks.test.ts deleted file mode 100644 index 512332255e24..000000000000 --- a/clients/tabby-agent/src/postprocess/removeRepetitiveBlocks.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { expect } from "chai"; -import { documentContext, inline } from "./testUtils"; -import { removeRepetitiveBlocks } from "./removeRepetitiveBlocks"; - -describe("postprocess", () => { - describe("removeRepetitiveBlocks", () => { - it("should remove repetitive blocks", () => { - const context = { - ...documentContext` - function myFuncA() { - console.log("myFuncA called."); - } - - ║ - `, - language: "javascript", - }; - const completion = inline` - ├function myFuncB() { - console.log("myFuncB called."); - } - - function myFuncC() { - console.log("myFuncC called."); - } - - function myFuncD() { - console.log("myFuncD called."); - } - - function myFuncE() { - console.log("myFuncE called."); - } - - function myFuncF() { - console.log("myFuncF called."); - } - - function myFuncG() { - console.log("myFuncG called."); - } - - function myFuncH() { - console.log("myFuncH ┤ - `; - const expected = inline` - ├function myFuncB() { - console.log("myFuncB called."); - }┤ - `; - expect(removeRepetitiveBlocks()(completion, context)).to.eq(expected); - }); - }); -}); diff --git a/clients/tabby-agent/src/postprocess/removeRepetitiveBlocks.ts b/clients/tabby-agent/src/postprocess/removeRepetitiveBlocks.ts deleted file mode 100644 index 4da2a47a099a..000000000000 --- a/clients/tabby-agent/src/postprocess/removeRepetitiveBlocks.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { CompletionContext } from "../CompletionContext"; -import { PostprocessFilter, logger } from "./base"; -import { isBlank, calcDistance } from "../utils"; - -function blockSplitter(_: string) { - // Have not implemented this for each language for now - // Return a blank line matcher should work for most cases - return /\n(\s*)\n/g; -} - -// FIXME: refactor this because it is very similar to `removeRepetitiveLines` -export function removeRepetitiveBlocks(): PostprocessFilter { - return (input: string, context: CompletionContext) => { - const inputBlocks = input.split(blockSplitter(context.language)); - let repetitionCount = 0; - const repetitionThreshold = 2; - // skip last block, it maybe cut - let index = inputBlocks.length - 2; - while (index >= 1) { - if (isBlank(inputBlocks[index]!)) { - index--; - continue; - } - let prev = index - 1; - while (prev >= 0 && isBlank(inputBlocks[prev]!)) { - prev--; - } - if (prev < 0) break; - // if distance between current and previous block is less than threshold (threshold = or 10% of string length) - const currentBlock = inputBlocks[index]!.trim(); - const previousBlock = inputBlocks[prev]!.trim(); - const threshold = Math.max(0.1 * currentBlock.length, 0.1 * previousBlock.length); - const distance = calcDistance(currentBlock, previousBlock); - if (distance <= threshold) { - repetitionCount++; - index--; - } else { - break; - } - } - if (repetitionCount >= repetitionThreshold) { - logger.debug( - { - inputBlocks, - repetitionCount, - }, - "Remove repetitive blocks.", - ); - return inputBlocks - .slice(0, index + 1) - .join("") - .trimEnd(); - } - return input; - }; -} diff --git a/clients/tabby-agent/src/postprocess/removeRepetitiveLines.test.ts b/clients/tabby-agent/src/postprocess/removeRepetitiveLines.test.ts deleted file mode 100644 index cf9e508b0c00..000000000000 --- a/clients/tabby-agent/src/postprocess/removeRepetitiveLines.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { expect } from "chai"; -import { documentContext, inline } from "./testUtils"; -import { removeRepetitiveLines } from "./removeRepetitiveLines"; - -describe("postprocess", () => { - describe("removeRepetitiveLines", () => { - it("should remove repetitive lines", () => { - const context = { - ...documentContext` - function hello() { - console.log("hello"); - } - hello(); - hello(); - ║ - `, - language: "javascript", - }; - const completion = inline` - ├hello(); - hello(); - hello(); - hello(); - hello(); - hello(); - hello(); - hello(); - hello(); - hello();┤ - `; - const expected = inline` - ├hello();┤ - `; - expect(removeRepetitiveLines()(completion, context)).to.eq(expected); - }); - - it("should remove repetitive lines with patterns", () => { - const context = { - ...documentContext` - const a = 1; - ║ - `, - language: "javascript", - }; - const completion = inline` - ├const b = 1; - const c = 1; - const d = 1; - const e = 1; - const f = 1; - const g = 1; - const h = 1; - const i = 1; - const j = 1; - const k =┤`; - const expected = inline` - ├const b = 1;┤ - `; - expect(removeRepetitiveLines()(completion, context)).to.eq(expected); - }); - }); -}); diff --git a/clients/tabby-agent/src/postprocess/removeRepetitiveLines.ts b/clients/tabby-agent/src/postprocess/removeRepetitiveLines.ts deleted file mode 100644 index 812e65f5eb49..000000000000 --- a/clients/tabby-agent/src/postprocess/removeRepetitiveLines.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { PostprocessFilter, logger } from "./base"; -import { splitLines, isBlank, calcDistance } from "../utils"; - -export function removeRepetitiveLines(): PostprocessFilter { - return (input: string) => { - const inputLines = splitLines(input); - let repetitionCount = 0; - const repetitionThreshold = 5; - // skip last line, it could be a not completed line - let index = inputLines.length - 2; - while (index >= 1) { - if (isBlank(inputLines[index]!)) { - index--; - continue; - } - let prev = index - 1; - while (prev >= 0 && isBlank(inputLines[prev]!)) { - prev--; - } - if (prev < 0) break; - // if distance between current and previous line is less than threshold (threshold = or 10% of string length) - const currentLine = inputLines[index]!.trim(); - const previousLine = inputLines[prev]!.trim(); - const threshold = Math.max(0.1 * currentLine.length, 0.1 * previousLine.length); - const distance = calcDistance(currentLine, previousLine); - if (distance <= threshold) { - repetitionCount++; - index = prev; - } else { - break; - } - } - if (repetitionCount >= repetitionThreshold) { - logger.debug( - { - inputLines, - repetitionCount, - }, - "Remove repetitive lines.", - ); - return inputLines - .slice(0, index + 1) - .join("") - .trimEnd(); - } - return input; - }; -} diff --git a/clients/tabby-agent/src/postprocess/testUtils.ts b/clients/tabby-agent/src/postprocess/testUtils.ts deleted file mode 100644 index 01e79c86ab42..000000000000 --- a/clients/tabby-agent/src/postprocess/testUtils.ts +++ /dev/null @@ -1,23 +0,0 @@ -import dedent from "dedent"; -import { v4 as uuid } from "uuid"; -import { CompletionContext } from "../CompletionContext"; - -// `║` is the cursor position -export function documentContext(literals: TemplateStringsArray, ...placeholders: any[]): CompletionContext { - const doc = dedent(literals, ...placeholders); - return new CompletionContext({ - filepath: uuid(), - language: "", - text: doc.replace(/║/, ""), - position: doc.indexOf("║"), - }); -} - -// `├` start of the inline completion to insert -// `┤` end of the inline completion to insert -// `┴` use for indent placeholder, should be placed at last line after `┤` - -export function inline(literals: TemplateStringsArray, ...placeholders: any[]): string { - const inline = dedent(literals, ...placeholders); - return inline.slice(inline.indexOf("├") + 1, inline.lastIndexOf("┤")); -} diff --git a/clients/tabby-agent/src/postprocess/trimMultiLineInSingleLineMode.test.ts b/clients/tabby-agent/src/postprocess/trimMultiLineInSingleLineMode.test.ts deleted file mode 100644 index f07edb8ed0eb..000000000000 --- a/clients/tabby-agent/src/postprocess/trimMultiLineInSingleLineMode.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { expect } from "chai"; -import { documentContext, inline } from "./testUtils"; -import { trimMultiLineInSingleLineMode } from "./trimMultiLineInSingleLineMode"; - -describe("postprocess", () => { - describe("trimMultiLineInSingleLineMode", () => { - it("should trim multiline completions, when the suffix have non-auto-closed chars in the current line.", () => { - const context = { - ...documentContext` - let error = new Error("Something went wrong"); - console.log(║message); - `, - language: "javascript", - }; - const completion = inline` - ├message); - throw error;┤ - `; - expect(trimMultiLineInSingleLineMode()(completion, context)).to.be.null; - }); - - it("should trim multiline completions, when the suffix have non-auto-closed chars in the current line.", () => { - const context = { - ...documentContext` - let error = new Error("Something went wrong"); - console.log(║message); - `, - language: "javascript", - }; - const completion = inline` - ├error, message); - throw error;┤ - `; - const expected = inline` - ├error, ┤ - `; - expect(trimMultiLineInSingleLineMode()(completion, context)).to.eq(expected); - }); - - it("should allow singleline completions, when the suffix have non-auto-closed chars in the current line.", () => { - const context = { - ...documentContext` - let error = new Error("Something went wrong"); - console.log(║message); - `, - language: "javascript", - }; - const completion = inline` - ├error, ┤ - `; - expect(trimMultiLineInSingleLineMode()(completion, context)).to.eq(completion); - }); - - it("should allow multiline completions, when the suffix only have auto-closed chars that will be replaced in the current line, such as `)]}`.", () => { - const context = { - ...documentContext` - function findMax(arr) {║} - `, - language: "javascript", - }; - const completion = inline` - ├ - let max = arr[0]; - for (let i = 1; i < arr.length; i++) { - if (arr[i] > max) { - max = arr[i]; - } - } - return max; - }┤ - `; - expect(trimMultiLineInSingleLineMode()(completion, context)).to.eq(completion); - }); - }); -}); diff --git a/clients/tabby-agent/src/postprocess/trimMultiLineInSingleLineMode.ts b/clients/tabby-agent/src/postprocess/trimMultiLineInSingleLineMode.ts deleted file mode 100644 index c841d49eb4d8..000000000000 --- a/clients/tabby-agent/src/postprocess/trimMultiLineInSingleLineMode.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { CompletionContext } from "../CompletionContext"; -import { PostprocessFilter, logger } from "./base"; -import { splitLines } from "../utils"; - -export function trimMultiLineInSingleLineMode(): PostprocessFilter { - return (input: string, context: CompletionContext) => { - const inputLines = splitLines(input); - if (context.mode === "fill-in-line" && inputLines.length > 1) { - const suffix = context.currentLineSuffix.trimEnd(); - const inputLine = inputLines[0]!.trimEnd(); - if (inputLine.endsWith(suffix)) { - const trimmedInputLine = inputLine.slice(0, -suffix.length); - if (trimmedInputLine.length > 0) { - logger.debug({ inputLines, trimmedInputLine }, "Trim content with multiple lines"); - return trimmedInputLine; - } - } - logger.debug({ inputLines }, "Drop content with multiple lines"); - return null; - } - return input; - }; -} diff --git a/clients/tabby-agent/src/postprocess/trimSpace.test.ts b/clients/tabby-agent/src/postprocess/trimSpace.test.ts deleted file mode 100644 index 2573ee75275e..000000000000 --- a/clients/tabby-agent/src/postprocess/trimSpace.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { expect } from "chai"; -import { documentContext, inline } from "./testUtils"; -import { trimSpace } from "./trimSpace"; - -describe("postprocess", () => { - describe("trimSpace", () => { - it("should remove trailing space", () => { - const context = { - ...documentContext` - let foo = new ║ - `, - language: "javascript", - }; - const completion = inline` - ├Foo(); ┤ - `; - const expected = inline` - ├Foo();┤ - `; - expect(trimSpace()(completion, context)).to.eq(expected); - }); - - it("should not remove trailing space if filling in line", () => { - const context = { - ...documentContext` - let foo = sum(║baz) - `, - language: "javascript", - }; - const completion = inline` - ├bar, ┤ - `; - expect(trimSpace()(completion, context)).to.eq(completion); - }); - - it("should remove trailing space if filling in line with suffix starts with space", () => { - const context = { - ...documentContext` - let foo = sum(║ baz) - `, - language: "javascript", - }; - const completion = inline` - ├bar, ┤ - `; - const expected = inline` - ├bar,┤ - `; - expect(trimSpace()(completion, context)).to.eq(expected); - }); - - it("should not remove leading space if current line is blank", () => { - const context = { - ...documentContext` - function sum(a, b) { - ║ - } - `, - language: "javascript", - }; - const completion = inline` - ├ return a + b;┤ - `; - expect(trimSpace()(completion, context)).to.eq(completion); - }); - - it("should remove leading space if current line is not blank and ends with space", () => { - const context = { - ...documentContext` - let foo = ║ - `, - language: "javascript", - }; - const completion = inline` - ├ sum(bar, baz);┤ - `; - const expected = inline` - ├sum(bar, baz);┤ - `; - expect(trimSpace()(completion, context)).to.eq(expected); - }); - }); -}); diff --git a/clients/tabby-agent/src/postprocess/trimSpace.ts b/clients/tabby-agent/src/postprocess/trimSpace.ts deleted file mode 100644 index f844e811ef9c..000000000000 --- a/clients/tabby-agent/src/postprocess/trimSpace.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { CompletionContext } from "../CompletionContext"; -import { PostprocessFilter } from "./base"; -import { isBlank } from "../utils"; - -export function trimSpace(): PostprocessFilter { - return (input: string, context: CompletionContext) => { - const { currentLinePrefix, currentLineSuffix } = context; - let trimmedInput = input; - if (!isBlank(currentLinePrefix) && currentLinePrefix.match(/\s$/)) { - trimmedInput = trimmedInput.trimStart(); - } - - if (isBlank(currentLineSuffix) || (!isBlank(currentLineSuffix) && currentLineSuffix.match(/^\s/))) { - trimmedInput = trimmedInput.trimEnd(); - } - return trimmedInput; - }; -} diff --git a/clients/tabby-agent/src/protocol.ts b/clients/tabby-agent/src/protocol.ts new file mode 100644 index 000000000000..d519e0518ebf --- /dev/null +++ b/clients/tabby-agent/src/protocol.ts @@ -0,0 +1,1031 @@ +/* eslint-disable @typescript-eslint/no-namespace */ + +import { + ProtocolRequestType, + ProtocolNotificationType, + RegistrationType, + MessageDirection, + LSPAny, + URI, + Range, + Location, + Command as LspCommand, + InitializeRequest as LspInitializeRequest, + InitializeParams as LspInitializeParams, + InitializeResult as LspInitializeResult, + InitializeError, + ClientCapabilities as LspClientCapabilities, + TextDocumentClientCapabilities, + CompletionClientCapabilities, + InlineCompletionClientCapabilities, + ServerCapabilities as LspServerCapabilities, + ConfigurationRequest as LspConfigurationRequest, + DidChangeConfigurationNotification as LspDidChangeConfigurationNotification, + DidChangeConfigurationParams as LspDidChangeConfigurationParams, + CodeLensRequest as LspCodeLensRequest, + CodeLensParams, + CodeLens as LspCodeLens, + CompletionRequest as LspCompletionRequest, + CompletionParams, + CompletionList as LspCompletionList, + CompletionItem as LspCompletionItem, + InlineCompletionRequest as LspInlineCompletionRequest, + InlineCompletionParams, + InlineCompletionList as LspInlineCompletionList, + InlineCompletionItem as LspInlineCompletionItem, + DeclarationParams, + Declaration, + LocationLink, + SemanticTokensRangeParams, + SemanticTokens, + SemanticTokensLegend, + WorkspaceEdit, +} from "vscode-languageserver-protocol"; + +/** + * Extends LSP method Initialize Request(↩️) + * + * - method: `initialize` + * - params: {@link InitializeParams} + * - result: {@link InitializeResult} + */ +export namespace InitializeRequest { + export const method = LspInitializeRequest.method; + export const messageDirection = LspInitializeRequest.messageDirection; + export const type = new ProtocolRequestType(method); +} + +export type InitializeParams = LspInitializeParams & { + clientInfo?: ClientInfo; + capabilities: ClientCapabilities; + initializationOptions?: InitializationOptions; +}; + +export type InitializationOptions = { + config?: ClientProvidedConfig; + /** + * ClientInfo also can be provided in InitializationOptions, will be merged with the one in InitializeParams. + * This is useful for the clients that don't support changing the ClientInfo in InitializeParams. + */ + clientInfo?: ClientInfo; + /** + * ClientCapabilities also can be provided in InitializationOptions, will be merged with the one in InitializeParams. + * This is useful for the clients that don't support changing the ClientCapabilities in InitializeParams. + */ + clientCapabilities?: ClientCapabilities; + /** + * The data store records that should be initialized when the server starts. This is useful for the clients that + * provides the dataStore capability. + */ + dataStoreRecords?: DataStoreRecords; +}; + +export type InitializeResult = LspInitializeResult & { + capabilities: ServerCapabilities; +}; + +/** + * [Tabby] Defines the name and version information of the IDE and the tabby plugin. + */ +export type ClientInfo = { + name: string; + version?: string; + tabbyPlugin?: { + name: string; + version?: string; + }; +}; + +export type ClientCapabilities = LspClientCapabilities & { + textDocument?: TextDocumentClientCapabilities & { + completion?: boolean | CompletionClientCapabilities; + inlineCompletion?: boolean | InlineCompletionClientCapabilities; + }; + tabby?: { + /** + * The client supports: + * - `tabby/config/didChange` + * This capability indicates that client support receiving notifications for configuration changes. + */ + configDidChangeListener?: boolean; + /** + * The client supports: + * - `tabby/status/didChange` + * This capability indicates that client support receiving notifications for status sync to display a status bar. + */ + statusDidChangeListener?: boolean; + /** + * The client supports: + * - `tabby/workspaceFileSystem/readFile` + * This capability improves the workspace code snippets context (RAG). + * When not provided, the server will try to fallback to NodeJS provided `fs` module, + * which is not available in the browser. + */ + workspaceFileSystem?: boolean; + /** + * The client provides a initial data store records for initialization and supports methods: + * - `tabby/dataStore/didUpdate` + * - `tabby/dataStore/update` + * When not provided, the server will try to fallback to the default data store, + * a file-based data store (~/.tabby-client/agent/data.json), which is not available in the browser. + */ + dataStore?: boolean; + /** + * The client supports: + * - `tabby/languageSupport/textDocument/declaration` + * - `tabby/languageSupport/textDocument/semanticTokens/range` + * This capability improves the workspace code snippets context (RAG). + */ + languageSupport?: boolean; + /** + * The client supports: + * - `tabby/git/repository` + * - `tabby/git/diff` + * This capability improves the workspace git repository context (RAG). + * When not provided, the server will try to fallback to the default git provider, + * which running system `git` command, not available if cannot execute `git` command, + * not available in the browser. + */ + gitProvider?: boolean; + /** + * The client supports: + * - `tabby/editorOptions` + * This capability improves the completion formatting. + */ + editorOptions?: boolean; + }; +}; + +export type ServerCapabilities = LspServerCapabilities & { + tabby?: Record; +}; + +export namespace ChatFeatures { + export const type = new RegistrationType("tabby/chat"); +} + +/** + * Extends LSP method Configuration Request(↪️) + * + * - method: `workspace/configuration` + * - params: any, not used + * - result: [{@link ClientProvidedConfig}] (the array should contains only one ClientProvidedConfig item) + */ +export namespace ConfigurationRequest { + export const method = LspConfigurationRequest.method; + export const messageDirection = LspConfigurationRequest.messageDirection; + export const type = new ProtocolRequestType(method); +} + +/** + * [Tabby] Defines the config supported to be changed on the client side (IDE). + */ +export type ClientProvidedConfig = { + /** + * Specifies the endpoint and token for connecting to the Tabby server. + */ + server?: { + endpoint?: string; + token?: string; + }; + /** + * Specifies the proxy for http/https requests. + */ + proxy?: { + url?: string; + authorization?: string; + }; + /** + * Trigger mode should be implemented on the client side. + * Sending this config to the server is for telemetry purposes. + */ + inlineCompletion?: { + triggerMode?: InlineCompletionTriggerMode; + }; + /** + * Keybindings should be implemented on the client side. + * Sending this config to the server is for telemetry purposes. + */ + keybindings?: "default" | "tabby-style" | "customize"; + /** + * Controls whether the telemetry is enabled or not. + */ + anonymousUsageTracking?: { + disable?: boolean; + }; +}; + +export type InlineCompletionTriggerMode = "auto" | "manual"; + +/** + * Extends LSP method DidChangeConfiguration Notification(➡️) + * - method: `workspace/didChangeConfiguration` + * - params: {@link DidChangeConfigurationParams} + * - result: void + */ +export namespace DidChangeConfigurationNotification { + export const method = LspDidChangeConfigurationNotification.method; + export const messageDirection = LspDidChangeConfigurationNotification.messageDirection; + export const type = new ProtocolNotificationType(method); +} + +export type DidChangeConfigurationParams = LspDidChangeConfigurationParams & { + settings?: ClientProvidedConfig; +}; + +/** + * Extends LSP method Code Lens Request(↩️) + * + * Tabby provides this method for preview changes applied in the Chat Edit feature, + * the client should render codelens and decorations to improve the readability of the pending changes. + * - method: `textDocument/codeLens` + * - params: {@link CodeLensParams} + * - result: {@link CodeLens}[] | null + * - partialResult: {@link CodeLens}[] + */ +export namespace CodeLensRequest { + export const method = LspCodeLensRequest.method; + export const messageDirection = LspCodeLensRequest.messageDirection; + export const type = new ProtocolRequestType(method); +} + +export type CodeLens = LspCodeLens & { + command?: ChatEditResolveCommand | LspCommand; + data?: { + type: CodeLensType; + line?: ChangesPreviewLineType; + text?: ChangesPreviewTextType; + }; +}; + +export type CodeLensType = "previewChanges"; +export type ChangesPreviewLineType = + | "header" + | "footer" + | "commentsFirstLine" + | "comments" + | "waiting" + | "inProgress" + | "unchanged" + | "inserted" + | "deleted"; + +export type ChangesPreviewTextType = "inserted" | "deleted"; + +/** + * Extends LSP method Completion Request(↩️) + * + * Note: Tabby provides this method capability when the client has `textDocument/completion` capability. + * - method: `textDocument/completion` + * - params: {@link CompletionParams} + * - result: {@link CompletionList} | null + */ +export namespace CompletionRequest { + export const method = LspCompletionRequest.method; + export const messageDirection = LspCompletionRequest.messageDirection; + export const type = new ProtocolRequestType(method); +} + +export type CompletionList = LspCompletionList & { + items: CompletionItem[]; +}; + +export type CompletionItem = LspCompletionItem & { + data?: { + /** + * The eventId is for telemetry purposes, should be used in `tabby/telemetry/event`. + */ + eventId?: CompletionEventId; + }; +}; + +export type CompletionEventId = { + completionId: string; + choiceIndex: number; +}; + +/** + * Extends LSP method Inline Completion Request(↩️) + * + * Note: Tabby provides this method capability when the client has `textDocument/inlineCompletion` capability. + * - method: `textDocument/inlineCompletion` + * - params: {@link InlineCompletionParams} + * - result: {@link InlineCompletionList} | null + */ +export namespace InlineCompletionRequest { + export const method = LspInlineCompletionRequest.method; + export const messageDirection = LspInlineCompletionRequest.messageDirection; + export const type = new ProtocolRequestType( + method, + ); +} + +export type InlineCompletionList = LspInlineCompletionList & { + isIncomplete: boolean; + items: InlineCompletionItem[]; +}; + +export type InlineCompletionItem = LspInlineCompletionItem & { + data?: { + /** + * The eventId is for telemetry purposes, should be used in `tabby/telemetry/event`. + */ + eventId?: CompletionEventId; + }; +}; + +/** + * [Tabby] Chat Edit Suggestion Command Request(↩️) + * + * This method is sent from the client to the server to get suggestion commands for the current context. + * - method: `tabby/chat/edit/command` + * - params: {@link ChatEditCommandParams} + * - result: {@link ChatEditCommand}[] | null + * - partialResult: {@link ChatEditCommand}[] + */ +export namespace ChatEditCommandRequest { + export const method = "tabby/chat/edit/command"; + export const messageDirection = MessageDirection.clientToServer; + export const type = new ProtocolRequestType< + ChatEditCommandParams, + ChatEditCommand[] | null, + ChatEditCommand[], + void, + void + >(method); +} + +export type ChatEditCommandParams = { + /** + * The document location to get suggestion commands for. + */ + location: Location; +}; + +export type ChatEditCommand = { + /** + * The display label of the command. + */ + label: string; + /** + * A string value for the command. + * If the command is a `preset` command, it always starts with `/`. + */ + command: string; + /** + * The source of the command. + */ + source: "preset"; +}; + +/** + * [Tabby] Chat Edit Request(↩️) + * + * This method is sent from the client to the server to edit the document content by user's command. + * The server will edit the document content using ApplyEdit(`workspace/applyEdit`) request, + * which requires the client to have this capability. + * - method: `tabby/chat/edit` + * - params: {@link ChatEditRequest} + * - result: {@link ChatEditToken} + * - error: {@link ChatFeatureNotAvailableError} + * | {@link ChatEditDocumentTooLongError} + * | {@link ChatEditCommandTooLongError} + * | {@link ChatEditMutexError} + */ +export namespace ChatEditRequest { + export const method = "tabby/chat/edit"; + export const messageDirection = MessageDirection.clientToServer; + export const type = new ProtocolRequestType< + ChatEditParams, + ChatEditToken, + void, + ChatFeatureNotAvailableError | ChatEditDocumentTooLongError | ChatEditCommandTooLongError | ChatEditMutexError, + void + >(method); +} + +export type ChatEditParams = { + /** + * The document location to edit. + */ + location: Location; + /** + * The command for this edit. + * If the command is a `preset` command, it should always start with "/". + * See {@link ChatEditCommand} + */ + command: string; + /** + * Select a edit format. + * - "previewChanges": The document will be edit to preview changes, + * use {@link ChatEditResolveRequest} to resolve it later. + */ + format: "previewChanges"; + + /** + * list of file contexts. + */ + context?: ChatEditFileContext[]; +}; + +/** + * Represents a file context use in {@link ChatEditParams}. + */ +export interface ChatEditFileContext { + /** + * The symbol in the user command that refer to this file context. + */ + referrer: string; + + /** + * The uri of the file. + */ + uri: URI; + + /** + * The context range in the file. + * If the range is not provided, the whole file is considered. + */ + range?: Range; +} + +export type ChatEditToken = string; + +export type ChatFeatureNotAvailableError = { + name: "ChatFeatureNotAvailableError"; +}; +export type ChatEditDocumentTooLongError = { + name: "ChatEditDocumentTooLongError"; +}; +export type ChatEditCommandTooLongError = { + name: "ChatEditCommandTooLongError"; +}; +export type ChatEditMutexError = { + name: "ChatEditMutexError"; +}; + +/** + * [Tabby] Chat Edit Resolve Request(↩️) + * + * This method is sent from the client to the server to accept or discard the changes in preview. + * - method: `tabby/chat/edit/resolve` + * - params: {@link ChatEditResolveParams} + * - result: boolean + */ +export namespace ChatEditResolveRequest { + export const method = "tabby/chat/edit/resolve"; + export const messageDirection = MessageDirection.clientToServer; + export const type = new ProtocolRequestType(method); +} + +export type ChatEditResolveParams = { + /** + * The document location to resolve the changes, should locate at the header line of the changes preview. + */ + location: Location; + /** + * The action to take for this edit. + */ + action: "accept" | "discard" | "cancel"; +}; + +/** + * [Tabby] Apply workspace edit request(↪️) + * + * This method is sent from the server to client to apply edit in workspace with options. + * - method: `tabby/workspace/applyEdit` + * - params: {@link ApplyWorkspaceEditParams} + * - result: boolean + */ +export namespace ApplyWorkspaceEditRequest { + export const method = "tabby/workspace/applyEdit"; + export const messageDirection = MessageDirection.serverToClient; + export const type = new ProtocolRequestType(method); +} + +export interface ApplyWorkspaceEditParams { + /** + * An optional label of the workspace edit. This label is + * presented in the user interface for example on an undo + * stack to undo the workspace edit. + */ + label?: string; + /** + * The edits to apply. + */ + edit: WorkspaceEdit; + options?: { + /** + * Add undo stop before making the edits. + */ + readonly undoStopBefore: boolean; + /** + * Add undo stop after making the edits. + */ + readonly undoStopAfter: boolean; + }; +} + +export type ChatEditResolveCommand = LspCommand & { + title: string; + tooltip?: string; + command: "tabby/chat/edit/resolve"; + arguments: [ChatEditResolveParams]; +}; + +/** + * [Tabby] Smart Apply Request(↩️) + * + * This method is sent from the client to the server to smart apply the text to the target location. + * The server will edit the document content using ApplyEdit(`workspace/applyEdit`) request, + * which requires the client to have this capability. + * - method: `tabby/chat/smartApply` + * - params: {@link SmartApplyParams} + * - result: boolean + * - error: {@link ChatFeatureNotAvailableError} + * | {@link ChatEditDocumentTooLongError} + * | {@link ChatEditMutexError} + */ +export namespace SmartApplyRequest { + export const method = "tabby/chat/smartApply"; + export const messageDirection = MessageDirection.clientToServer; + export const type = new ProtocolRequestType< + SmartApplyParams, + boolean, + void, + ChatFeatureNotAvailableError | ChatEditDocumentTooLongError | ChatEditMutexError, + void + >(method); +} + +export type SmartApplyParams = { + location: Location; + text: string; +}; + +/** + * [Tabby] Did Change Active Editor Notification(➡️) + * + * This method is sent from the client to server when the active editor changed. + * + * + * - method: `tabby/editors/didChangeActiveEditor` + * - params: {@link OpenedFileParams} + * - result: void + */ +export namespace DidChangeActiveEditorNotification { + export const method = "tabby/editors/didChangeActiveEditor"; + export const messageDirection = MessageDirection.clientToServer; + export const type = new ProtocolNotificationType(method); +} +export type DidChangeActiveEditorParams = { + activeEditor: Location; + visibleEditors: Location[] | undefined; +}; + +/** + * [Tabby] GenerateCommitMessage Request(↩️) + * + * This method is sent from the client to the server to generate a commit message for a git repository. + * - method: `tabby/chat/generateCommitMessage` + * - params: {@link GenerateCommitMessageParams} + * - result: {@link GenerateCommitMessageResult} | null + * - error: {@link ChatFeatureNotAvailableError} + */ +export namespace GenerateCommitMessageRequest { + export const method = "tabby/chat/generateCommitMessage"; + export const messageDirection = MessageDirection.clientToServer; + export const type = new ProtocolRequestType< + GenerateCommitMessageParams, + GenerateCommitMessageResult | null, + void, + ChatFeatureNotAvailableError, + void + >(method); +} + +export type GenerateCommitMessageParams = { + /** + * The root URI of the git repository. + */ + repository: URI; +}; + +export type GenerateCommitMessageResult = { + commitMessage: string; +}; + +/** + * [Tabby] GenerateBranchName Request(↩️) + * + * This method is sent from the client to the server to generate a branch name for a git repository. + * - method: `tabby/chat/generateBranchName` + * - params: {@link GenerateBranchNameParams} + * - result: {@link GenerateBranchNameResult} | null + * - error: {@link ChatFeatureNotAvailableError} + */ +export namespace GenerateBranchNameRequest { + export const method = "tabby/chat/generateBranchName"; + export const messageDirection = MessageDirection.clientToServer; + export const type = new ProtocolRequestType< + GenerateBranchNameParams, + GenerateBranchNameResult | null, + void, + ChatFeatureNotAvailableError, + void + >(method); +} + +export type GenerateBranchNameParams = { + /** + * The root URI of the git repository. + */ + repository: URI; + input: string; +}; + +export type GenerateBranchNameResult = { + branchNames: string[]; +}; + +/** + * [Tabby] Telemetry Event Notification(➡️) + * + * This method is sent from the client to the server for telemetry purposes. + * - method: `tabby/telemetry/event` + * - params: {@link EventParams} + * - result: void + */ +export namespace TelemetryEventNotification { + export const method = "tabby/telemetry/event"; + export const messageDirection = MessageDirection.clientToServer; + export const type = new ProtocolNotificationType(method); +} + +export type EventParams = { + type: "view" | "select" | "dismiss"; + selectKind?: "line"; + eventId: CompletionEventId; + viewId?: string; + elapsed?: number; +}; + +/** + * [Tabby] Config Request(↩️) + * + * This method is sent from the client to the server to get the current configuration. + * - method: `tabby/config` + * - params: any, not used + * - result: {@link Config} + */ +export namespace ConfigRequest { + export const method = "tabby/config"; + export const messageDirection = MessageDirection.clientToServer; + export const type = new ProtocolRequestType(method); +} + +export type Config = { + server: { + endpoint: string; + token: string; + requestHeaders: Record; + }; +}; + +/** + * [Tabby] Config DidChange Notification(⬅️) + * + * This method is sent from the server to the client to notify the client of the configuration changes. + * - method: `tabby/config/didChange` + * - params: {@link Config} + * - result: void + */ +export namespace ConfigDidChangeNotification { + export const method = "tabby/config/didChange"; + export const messageDirection = MessageDirection.serverToClient; + export const type = new ProtocolNotificationType(method); +} + +/** + * [Tabby] Status Request(↩️) + * + * This method is sent from the client to the server to check the current status of the server. + * - method: `tabby/status` + * - params: {@link StatusRequestParams} + * - result: {@link StatusInfo} + */ +export namespace StatusRequest { + export const method = "tabby/status"; + export const messageDirection = MessageDirection.clientToServer; + export const type = new ProtocolRequestType(method); +} + +export type StatusRequestParams = { + /** + * Forces a recheck of the connection to the Tabby server, and waiting for result. + */ + recheckConnection?: boolean; +}; + +/** + * [Tabby] StatusInfo is used to display the status bar in the editor. + */ +export type StatusInfo = { + status: + | "connecting" + | "unauthorized" + | "disconnected" + | "ready" + | "readyForAutoTrigger" + | "readyForManualTrigger" + | "fetching" + | "codeCompletionNotAvailable" + | "rateLimitExceeded" + | "completionResponseSlow"; + tooltip?: string; + /** + * The health information of the server if available. + */ + serverHealth?: Record; + /** + * The action to take for this status. + * - `disconnected`, `codeCompletionNotAvailable`, `rateLimitExceeded` or `completionResponseSlow` -> StatusShowHelpMessageCommand + * - others -> undefined + */ + command?: StatusShowHelpMessageCommand | LspCommand; + /** + * The help message if available. + * Only available when this status info is returned from {@link StatusRequest}, not provided in {@link StatusDidChangeNotification}. + * Only available when the status is `disconnected`, `codeCompletionNotAvailable`, `rateLimitExceeded` or `completionResponseSlow`. + */ + helpMessage?: string; +}; + +/** + * [Tabby] Status DidChange Notification(⬅️) + * + * This method is sent from the server to the client to notify the client of the status changes. + * - method: `tabby/status/didChange` + * - params: {@link StatusInfo} + * - result: void + */ +export namespace StatusDidChangeNotification { + export const method = "tabby/status/didChange"; + export const messageDirection = MessageDirection.serverToClient; + export const type = new ProtocolNotificationType(method); +} + +/** + * [Tabby] Status Show Help Message Request(↩️) + * + * This method is sent from the client to the server to request to show the help message for the current status. + * The server will callback client to show request using ShowMessageRequest (`window/showMessageRequest`). + * - method: `tabby/status/showHelpMessage` + * - params: any, not used + * - result: boolean + */ +export namespace StatusShowHelpMessageRequest { + export const method = "tabby/status/showHelpMessage"; + export const messageDirection = MessageDirection.clientToServer; + export const type = new ProtocolRequestType(method); +} + +export type StatusShowHelpMessageCommand = LspCommand & { + title: string; + command: "tabby/status/showHelpMessage"; + arguments: [LSPAny]; +}; + +/** + * [Tabby] Status Ignored Issues Edit Request(↩️) + * + * This method is sent from the client to the server to add or remove the issues that marked as ignored. + * - method: `tabby/status/ignoredIssues/edit` + * - params: {@link StatusIgnoredIssuesEditParams} + * - result: boolean + */ +export namespace StatusIgnoredIssuesEditRequest { + export const method = "tabby/status/ignoredIssues/edit"; + export const messageDirection = MessageDirection.clientToServer; + export const type = new ProtocolRequestType(method); +} + +export type StatusIssuesName = "completionResponseSlow"; + +export type StatusIgnoredIssuesEditParams = { + operation: "add" | "remove" | "removeAll"; + issues: StatusIssuesName | StatusIssuesName[]; +}; + +/** + * [Tabby] Read File Request(↪️) + * + * This method is sent from the server to the client to read the file contents. + * - method: `tabby/workspaceFileSystem/readFile` + * - params: {@link ReadFileParams} + * - result: {@link ReadFileResult} | null + */ +export namespace ReadFileRequest { + export const method = "tabby/workspaceFileSystem/readFile"; + export const messageDirection = MessageDirection.serverToClient; + export const type = new ProtocolRequestType(method); +} + +export type ReadFileParams = { + uri: URI; + /** + * If `text` is select, the result should try to decode the file contents to string, + * otherwise, the result should be a raw binary array. + */ + format: "text"; + /** + * When omitted, read the whole file. + */ + range?: Range; +}; + +export type ReadFileResult = { + /** + * If `text` is select, the result should be a string. + */ + text?: string; +}; + +/** + * [Tabby] DataStore DidUpdate Notification(➡️) + * + * This method is sent from the client to the server to notify that the data store records has been updated. + * - method: `tabby/dataStore/didUpdate` + * - params: {@link DataStoreDidChangeParams} + */ +export namespace DataStoreDidUpdateNotification { + export const method = "tabby/dataStore/didUpdate"; + export const messageDirection = MessageDirection.clientToServer; + export const type = new ProtocolNotificationType(method); +} + +/** + * [Tabby] DataStore Update Request(↪️) + * + * This method is sent from the server to the client to update the data store records. + * - method: `tabby/dataStore/update` + * - params: {@link DataStoreUpdateParams} + * - result: boolean + */ +export namespace DataStoreUpdateRequest { + export const method = "tabby/dataStore/update"; + export const messageDirection = MessageDirection.serverToClient; + export const type = new ProtocolRequestType(method); +} + +export type DataStoreRecords = Record; + +/** + * [Tabby] Language Support Declaration Request(↪️) + * + * This method is sent from the server to the client to request the support from another language server. + * See LSP `textDocument/declaration`. + * - method: `tabby/languageSupport/textDocument/declaration` + * - params: {@link DeclarationParams} + * - result: {@link Declaration} | {@link LocationLink}[] | null + */ +export namespace LanguageSupportDeclarationRequest { + export const method = "tabby/languageSupport/textDocument/declaration"; + export const messageDirection = MessageDirection.serverToClient; + export const type = new ProtocolRequestType< + DeclarationParams, + Declaration | LocationLink[] | null, + never, + void, + void + >(method); +} + +/** + * [Tabby] Semantic Tokens Range Request(↪️) + * + * This method is sent from the server to the client to request the support from another language server. + * See LSP `textDocument/semanticTokens/range`. + * - method: `tabby/languageSupport/textDocument/semanticTokens/range` + * - params: {@link SemanticTokensRangeParams} + * - result: {@link SemanticTokensRangeResult} | null + */ +export namespace LanguageSupportSemanticTokensRangeRequest { + export const method = "tabby/languageSupport/textDocument/semanticTokens/range"; + export const messageDirection = MessageDirection.serverToClient; + export const type = new ProtocolRequestType< + SemanticTokensRangeParams, + SemanticTokensRangeResult | null, + never, + void, + void + >(method); +} + +export type SemanticTokensRangeResult = { + legend: SemanticTokensLegend; + tokens: SemanticTokens; +}; + +/** + * [Tabby] Git Repository Request(↪️) + * + * This method is sent from the server to the client to get the git repository state of a file. + * - method: `tabby/git/repository` + * - params: {@link GitRepositoryParams} + * - result: {@link GitRepository} | null + */ +export namespace GitRepositoryRequest { + export const method = "tabby/git/repository"; + export const messageDirection = MessageDirection.serverToClient; + export const type = new ProtocolRequestType(method); +} + +export type GitRepositoryParams = { + /** + * The URI of the file to get the git repository state of. + */ + uri: URI; +}; + +export type GitRepository = { + /** + * The root URI of the git repository. + */ + root: URI; + /** + * The url of the default remote. + */ + remoteUrl?: string; + /** + * List of remotes in the git repository. + */ + remotes?: { + name: string; + url: string; + }[]; +}; + +/** + * [Tabby] Git Diff Request(↪️) + * + * This method is sent from the server to the client to get the diff of a git repository. + * - method: `tabby/git/diff` + * - params: {@link GitDiffParams} + * - result: {@link GitDiffResult} | null + */ +export namespace GitDiffRequest { + export const method = "tabby/git/diff"; + export const messageDirection = MessageDirection.serverToClient; + export const type = new ProtocolRequestType(method); +} + +export type GitDiffParams = { + /** + * The root URI of the git repository. + */ + repository: URI; + /** + * Returns the cached or uncached diff of the git repository. + */ + cached: boolean; +}; + +export type GitDiffResult = { + /** + * The diff of the git repository. + * - It could be the full diff. + * - It could be a list of diff for each single file, sorted by the priority. + * This will be useful when the full diff is too large, and we will select + * from the split diffs to generate a prompt under the tokens limit. + */ + diff: string | string[]; +}; + +/** + * [Tabby] Editor Options Request(↪️) + * + * This method is sent from the server to the client to get the diff of a git repository. + * - method: `tabby/editorOptions` + * - params: {@link EditorOptionsParams} + * - result: {@link EditorOptions} | null + */ +export namespace EditorOptionsRequest { + export const method = "tabby/editorOptions"; + export const messageDirection = MessageDirection.serverToClient; + export const type = new ProtocolRequestType(method); +} + +export type EditorOptionsParams = { + /** + * The uri of the document for which the editor options are requested. + */ + uri: URI; +}; + +export type EditorOptions = { + /** + * A string representing the indentation for the editor. It could be 2 or 4 spaces, or 1 tab. + */ + indentation: string; +}; diff --git a/clients/tabby-agent/src/server.ts b/clients/tabby-agent/src/server.ts new file mode 100644 index 000000000000..ed7f79e5a221 --- /dev/null +++ b/clients/tabby-agent/src/server.ts @@ -0,0 +1,240 @@ +import type { Feature } from "./feature"; +import { createConnection as nodeCreateConnection } from "vscode-languageserver/node"; +import { + createConnection as browserCreateConnection, + BrowserMessageReader, + BrowserMessageWriter, +} from "vscode-languageserver/browser"; +import { ProposedFeatures, TextDocumentSyncKind, NotebookDocuments } from "vscode-languageserver"; +import { + InitializeParams, + InitializeResult, + ClientInfo, + ClientCapabilities, + ServerCapabilities, + ClientProvidedConfig, + DataStoreRecords, +} from "./protocol"; +import { TextDocuments } from "./extensions/textDocuments"; +import { TextDocument } from "vscode-languageserver-textdocument"; +import { deepmerge } from "deepmerge-ts"; +import { isBrowser } from "./env"; +import { getLogger, LoggerManager } from "./logger"; +import { DataStore } from "./dataStore"; +import { Configurations } from "./config"; +import { CertsLoader } from "./certsLoader"; +import { AnonymousUsageLogger } from "./telemetry"; +import { TabbyApiClient } from "./http/tabbyApiClient"; +import { TextDocumentReader } from "./contextProviders/documentContexts"; +import { WorkspaceContextProvider } from "./contextProviders/workspace"; +import { GitContextProvider } from "./contextProviders/git"; +import { DeclarationSnippetsProvider } from "./contextProviders/declarationSnippets"; +import { RecentlyChangedCodeSearch } from "./contextProviders/recentlyChangedCodeSearch"; +import { EditorVisibleRangesTracker } from "./contextProviders/editorVisibleRanges"; +import { EditorOptionsProvider } from "./contextProviders/editorOptions"; +import { CodeLensProvider } from "./codeLens"; +import { CompletionProvider } from "./codeCompletion"; +import { ChatFeature } from "./chat"; +import { ChatEditProvider } from "./chat/inlineEdit"; +import { SmartApplyFeature } from "./chat/smartApply"; +import { CommitMessageGenerator } from "./chat/generateCommitMessage"; +import { BranchNameGenerator } from "./chat/generateBranchName"; +import { StatusProvider } from "./status"; +import { CommandProvider } from "./command"; +import { name as serverName, version as serverVersion } from "../package.json"; +import "./utils/array"; + +export class Server { + private readonly logger = getLogger("TabbyLSP"); + private readonly connection = isBrowser + ? browserCreateConnection(ProposedFeatures.all, new BrowserMessageReader(self), new BrowserMessageWriter(self)) + : nodeCreateConnection(ProposedFeatures.all); + + private readonly documents = new TextDocuments(TextDocument); + private readonly notebooks = new NotebookDocuments(this.documents); + + private readonly dataStore = new DataStore(); + private readonly configurations = new Configurations(this.dataStore); + + private readonly certsLoader = new CertsLoader(this.configurations); + private readonly anonymousUsageLogger = new AnonymousUsageLogger(this.dataStore, this.configurations); + private readonly tabbyApiClient = new TabbyApiClient(this.configurations, this.anonymousUsageLogger); + + private readonly textDocumentReader = new TextDocumentReader(this.documents); + private readonly workspaceContextProvider = new WorkspaceContextProvider(); + private readonly gitContextProvider = new GitContextProvider(); + private readonly declarationSnippetsProvider = new DeclarationSnippetsProvider(this.textDocumentReader); + private readonly recentlyChangedCodeSearch = new RecentlyChangedCodeSearch(this.configurations, this.documents); + private readonly editorVisibleRangesTracker = new EditorVisibleRangesTracker(this.configurations); + private readonly editorOptionsProvider = new EditorOptionsProvider(); + + private readonly codeLensProvider = new CodeLensProvider(this.documents); + private readonly completionProvider = new CompletionProvider( + this.configurations, + this.tabbyApiClient, + this.documents, + this.notebooks, + this.anonymousUsageLogger, + this.textDocumentReader, + this.workspaceContextProvider, + this.gitContextProvider, + this.declarationSnippetsProvider, + this.recentlyChangedCodeSearch, + this.editorVisibleRangesTracker, + this.editorOptionsProvider, + ); + private readonly chatFeature = new ChatFeature(this.tabbyApiClient); + private readonly chatEditProvider = new ChatEditProvider(this.chatFeature, this.configurations, this.documents); + private readonly commitMessageGenerator = new CommitMessageGenerator( + this.chatFeature, + this.configurations, + this.gitContextProvider, + ); + private readonly branchNameGenerator = new BranchNameGenerator( + this.chatFeature, + this.configurations, + this.gitContextProvider, + ); + private readonly smartApplyFeature = new SmartApplyFeature(this.chatFeature, this.configurations, this.documents); + + private readonly statusProvider = new StatusProvider( + this.dataStore, + this.configurations, + this.tabbyApiClient, + this.completionProvider, + ); + private readonly commandProvider = new CommandProvider(this.chatEditProvider, this.statusProvider); + + private readonly featureComponents = [ + this.textDocumentReader, + this.workspaceContextProvider, + this.gitContextProvider, + this.declarationSnippetsProvider, + this.recentlyChangedCodeSearch, + this.editorVisibleRangesTracker, + this.editorOptionsProvider, + this.completionProvider, + this.codeLensProvider, + this.chatFeature, + this.chatEditProvider, + this.commitMessageGenerator, + this.branchNameGenerator, + this.smartApplyFeature, + this.statusProvider, + this.commandProvider, + ]; + + async listen() { + await this.preInitialize(); + this.documents.listen(this.connection); + this.notebooks.listen(this.connection); + this.connection.listen(); + } + + private async preInitialize() { + // pre-initialize components + const loggerManager = LoggerManager.getInstance(); + loggerManager.preInitialize(this.configurations); + loggerManager.attachLspConnection(this.connection); + + await this.dataStore.preInitialize(); + await this.configurations.preInitialize(); + await this.certsLoader.preInitialize(); + + // Lifecycle methods + this.connection.onInitialize(async (params) => { + return this.initialize(params); + }); + this.connection.onInitialized(async () => { + return this.initialized(); + }); + this.connection.onShutdown(async () => { + return this.shutdown(); + }); + this.connection.onExit(async () => { + return this.exit(); + }); + } + + private async initialize(params: InitializeParams): Promise { + this.logger.info("Initializing..."); + const clientInfo: ClientInfo | undefined = deepmerge( + params.clientInfo, + params.initializationOptions?.clientInfo ?? {}, + ); + const clientCapabilities: ClientCapabilities = deepmerge( + params.capabilities, + params.initializationOptions?.clientCapabilities ?? {}, + ); + + const clientProvidedConfig: ClientProvidedConfig = params.initializationOptions?.config ?? {}; + const dataStoreRecords: DataStoreRecords | undefined = params.initializationOptions?.dataStoreRecords; + + const baseCapabilities: ServerCapabilities = { + textDocumentSync: { + openClose: true, + change: TextDocumentSyncKind.Incremental, + }, + notebookDocumentSync: { + notebookSelector: [ + { + notebook: "*", + }, + ], + }, + workspace: { + workspaceFolders: { + supported: true, + changeNotifications: true, + }, + }, + }; + + this.logger.debug("Initializing internal components..."); + await this.dataStore.initialize(this.connection, clientCapabilities, clientProvidedConfig, dataStoreRecords); + await this.configurations.initialize(this.connection, clientCapabilities, clientProvidedConfig); + await this.anonymousUsageLogger.initialize(clientInfo); + await this.tabbyApiClient.initialize(clientInfo); + this.logger.debug("Internal components initialized."); + + this.logger.debug("Initializing feature components..."); + const capabilities: ServerCapabilities[] = await this.featureComponents.mapAsync((feature: Feature) => { + return feature.initialize(this.connection, clientCapabilities, clientProvidedConfig, dataStoreRecords); + }); + this.logger.debug("Feature components initialized."); + + const serverCapabilities: ServerCapabilities = deepmerge(baseCapabilities, ...capabilities); + + const result: InitializeResult = { + capabilities: serverCapabilities, + serverInfo: { + name: serverName, + version: serverVersion, + }, + }; + + this.logger.info("Initialize done."); + this.anonymousUsageLogger.uniqueEvent("AgentInitialized"); // telemetry event, no wait + return result; + } + + private async initialized(): Promise { + this.logger.info("Received initialized notification."); + await [this.dataStore, this.configurations, ...this.featureComponents].mapAsync((feature: Feature) => { + return feature.initialized?.(this.connection); + }); + } + + private async shutdown() { + this.logger.info("Shutting down..."); + await this.featureComponents.mapAsync((feature: Feature) => { + return feature.shutdown?.(); + }); + await this.tabbyApiClient.shutdown(); + this.logger.info("Shutdown done."); + } + + private exit() { + return process.exit(0); + } +} diff --git a/clients/tabby-agent/src/status.ts b/clients/tabby-agent/src/status.ts new file mode 100644 index 000000000000..95358731f0eb --- /dev/null +++ b/clients/tabby-agent/src/status.ts @@ -0,0 +1,326 @@ +import type { Connection } from "vscode-languageserver"; +import type { Feature } from "./feature"; +import type { DataStore, StoredData } from "./dataStore"; +import type { Configurations } from "./config"; +import type { TabbyApiClient } from "./http/tabbyApiClient"; +import { EventEmitter } from "events"; +import { ShowMessageRequest, ShowMessageRequestParams, MessageType } from "vscode-languageserver"; +import deepEqual from "deep-equal"; +import { + ClientCapabilities, + ServerCapabilities, + ClientProvidedConfig, + StatusInfo, + StatusRequest, + StatusDidChangeNotification, + StatusShowHelpMessageRequest, + StatusIgnoredIssuesEditRequest, + StatusIgnoredIssuesEditParams, + StatusIssuesName, +} from "./protocol"; +import { getLogger } from "./logger"; +import "./utils/array"; +import { CompletionProvider } from "./codeCompletion"; + +export class StatusProvider extends EventEmitter implements Feature { + private readonly logger = getLogger("StatusProvider"); + + private lspConnection: Connection | undefined = undefined; + private clientCapabilities: ClientCapabilities | undefined = undefined; + + constructor( + private readonly dataStore: DataStore, + private readonly configurations: Configurations, + private readonly tabbyApiClient: TabbyApiClient, + private readonly completionProvider: CompletionProvider, + ) { + super(); + } + + initialize(connection: Connection, clientCapabilities: ClientCapabilities): ServerCapabilities { + this.lspConnection = connection; + this.clientCapabilities = clientCapabilities; + + connection.onRequest(StatusRequest.type, async (params) => { + if (params?.recheckConnection) { + await this.configurations.refreshClientProvidedConfig(); + await this.tabbyApiClient.connect(); + } + return this.buildStatusInfo({ includeHelpMessage: true }); + }); + connection.onRequest(StatusShowHelpMessageRequest.type, async () => { + return this.showStatusHelpMessage(); + }); + connection.onRequest(StatusIgnoredIssuesEditRequest.type, async (params) => { + return this.editStatusIgnoredIssues(params); + }); + if (clientCapabilities.tabby?.statusDidChangeListener) { + this.on("updated", (status: StatusInfo) => { + connection.sendNotification(StatusDidChangeNotification.type, status); + }); + } + + this.tabbyApiClient.on("statusUpdated", async () => { + this.notify(); + }); + this.tabbyApiClient.on("isConnectingUpdated", async () => { + this.notify(); + }); + this.completionProvider.on("isAvailableUpdated", async () => { + this.notify(); + }); + this.completionProvider.on("latencyIssueUpdated", async () => { + this.notify(); + }); + this.completionProvider.on("isRateLimitExceededUpdated", async () => { + this.notify(); + }); + this.completionProvider.on("isFetchingUpdated", async () => { + this.notify(); + }); + + this.configurations.on( + "clientProvidedConfigUpdated", + (config: ClientProvidedConfig, oldConfig: ClientProvidedConfig) => { + if (config.inlineCompletion?.triggerMode !== oldConfig.inlineCompletion?.triggerMode) { + this.notify(); + } + }, + ); + + this.dataStore.on("updated", async (data: Partial, old: Partial) => { + if (!deepEqual(data.statusIgnoredIssues, old.statusIgnoredIssues)) { + this.notify(); + } + }); + + return {}; + } + + async initialized(connection: Connection): Promise { + if (this.clientCapabilities?.tabby?.statusDidChangeListener) { + const statusInfo = this.buildStatusInfo(); + connection.sendNotification(StatusDidChangeNotification.type, statusInfo); + } + } + + private async notify() { + const statusInfo = this.buildStatusInfo(); + this.emit("updated", statusInfo); + } + + async showStatusHelpMessage(): Promise { + const connection = this.lspConnection; + if (!connection) { + return null; + } + + let params: ShowMessageRequestParams; + let issue: StatusIssuesName | undefined = undefined; + + const statusInfo = this.buildStatusInfo(); + switch (statusInfo.status) { + case "disconnected": + { + const message = this.tabbyApiClient.getHelpMessage(); + if (!message) { + return false; + } + params = { + type: MessageType.Error, + message, + actions: [{ title: "OK" }], + }; + } + break; + case "codeCompletionNotAvailable": + case "rateLimitExceeded": + { + const message = this.completionProvider.getHelpMessage(); + if (!message) { + return false; + } + params = { + type: MessageType.Error, + message, + actions: [{ title: "OK" }], + }; + } + break; + case "completionResponseSlow": + { + const message = this.completionProvider.getHelpMessage(); + if (!message) { + return false; + } + params = { + type: MessageType.Info, + message, + actions: [{ title: "OK" }, { title: "Never Show Again" }], + }; + issue = "completionResponseSlow"; + } + break; + default: + return false; + break; + } + + const result = await connection.sendRequest(ShowMessageRequest.type, params); + switch (result?.title) { + case "Never Show Again": + if (issue) { + await this.editStatusIgnoredIssues({ operation: "add", issues: [issue] }); + await this.notify(); + } + break; + case "OK": + break; + default: + break; + } + return true; + } + + private async editStatusIgnoredIssues(params: StatusIgnoredIssuesEditParams): Promise { + const issues = Array.isArray(params.issues) ? params.issues : [params.issues]; + const dataStore = this.dataStore; + switch (params.operation) { + case "add": { + const current = dataStore.data.statusIgnoredIssues ?? []; + dataStore.data.statusIgnoredIssues = current.concat(issues).distinct(); + this.logger.debug( + "Adding ignored issues: [" + + current.join(",") + + "] -> [" + + dataStore.data.statusIgnoredIssues.join(",") + + "].", + ); + await dataStore.save(); + return true; + } + case "remove": { + const current = dataStore.data.statusIgnoredIssues ?? []; + dataStore.data.statusIgnoredIssues = current.filter((item) => !issues.includes(item)); + this.logger.debug( + "Removing ignored issues: [" + + current.join(",") + + "] -> [" + + dataStore.data.statusIgnoredIssues.join(",") + + "].", + ); + + await dataStore.save(); + return true; + } + case "removeAll": { + dataStore.data.statusIgnoredIssues = []; + this.logger.debug("Removing all ignored issues."); + await dataStore.save(); + return true; + } + default: + break; + } + return false; + } + + private buildStatusInfo(options: { includeHelpMessage?: boolean } = {}): StatusInfo { + let statusInfo: StatusInfo; + const apiClientStatus = this.tabbyApiClient.getStatus(); + if (this.tabbyApiClient.isConnecting()) { + statusInfo = { status: "connecting" }; + } else { + switch (apiClientStatus) { + case "noConnection": + statusInfo = { status: "disconnected" }; + break; + case "unauthorized": + statusInfo = { status: "unauthorized" }; + break; + case "ready": + { + const ignored = this.dataStore.data.statusIgnoredIssues ?? []; + if (!this.completionProvider.isAvailable()) { + statusInfo = { status: "codeCompletionNotAvailable" }; + } else if (this.completionProvider.isRateLimitExceeded()) { + statusInfo = { status: "rateLimitExceeded" }; + } else if ( + this.completionProvider.getLatencyIssue() != undefined && + !ignored.includes("completionResponseSlow") + ) { + statusInfo = { status: "completionResponseSlow" }; + } else if (this.completionProvider.isFetching()) { + statusInfo = { status: "fetching" }; + } else { + switch (this.configurations.getClientProvidedConfig().inlineCompletion?.triggerMode) { + case "auto": + statusInfo = { status: "readyForAutoTrigger" }; + break; + case "manual": + statusInfo = { status: "readyForManualTrigger" }; + break; + default: + statusInfo = { status: "ready" }; + break; + } + } + } + break; + } + } + let hasHelpMessage = false; + switch (statusInfo.status) { + case "connecting": + statusInfo.tooltip = "Tabby: Connecting to Server..."; + break; + case "unauthorized": + statusInfo.tooltip = "Tabby: Authorization Required"; + break; + case "disconnected": + statusInfo.tooltip = "Tabby: Connect to Server Failed"; + hasHelpMessage = true; + break; + case "ready": + statusInfo.tooltip = "Tabby: Code Completion Enabled"; + break; + case "readyForAutoTrigger": + statusInfo.tooltip = "Tabby: Automatic Code Completion Enabled"; + break; + case "readyForManualTrigger": + statusInfo.tooltip = "Tabby: Manual Code Completion Enabled"; + break; + case "fetching": + statusInfo.tooltip = "Tabby: Generating Completions..."; + break; + case "codeCompletionNotAvailable": + statusInfo.tooltip = "Tabby: Code Completion Not Available"; + hasHelpMessage = true; + break; + case "rateLimitExceeded": + statusInfo.tooltip = "Tabby: Too Many Requests"; + hasHelpMessage = true; + break; + case "completionResponseSlow": + statusInfo.tooltip = "Tabby: Slow Completion Response Detected"; + hasHelpMessage = true; + break; + default: + break; + } + statusInfo.serverHealth = this.tabbyApiClient.getServerHealth(); + if (hasHelpMessage) { + statusInfo.command = { + title: "Detail", + command: "tabby/status/showHelpMessage", + arguments: [{}], + }; + + if (options.includeHelpMessage) { + statusInfo.helpMessage = this.tabbyApiClient.getHelpMessage() ?? this.completionProvider.getHelpMessage(); + } + } + + return statusInfo; + } +} diff --git a/clients/tabby-agent/src/syntax/typeList.ts b/clients/tabby-agent/src/syntax/typeList.ts deleted file mode 100644 index fbc1b8d6fabe..000000000000 --- a/clients/tabby-agent/src/syntax/typeList.ts +++ /dev/null @@ -1,113 +0,0 @@ -// https://github.com/tree-sitter/tree-sitter-typescript/blob/master/src/node-types.json -export const typeList: Record = { - tsx: [ - [ - "jsx_element", - "jsx_self_closing_element", - - "for_statement", - "for_in_statement", - "if_statement", - "while_statement", - "do_statement", - "switch_statement", - "try_statement", - "with_statement", - "labeled_statement", - - "class_declaration", - "abstract_class_declaration", - "interface_declaration", - "enum_declaration", - "type_alias_declaration", - "function_declaration", - "generator_function_declaration", - "ambient_declaration", - - "method_definition", - - "import_statement", - "export_statement", - "module", - ], - ["expression_statement", "lexical_declaration"], - ], - - // https://github.com/tree-sitter/tree-sitter-python/blob/master/src/node-types.json - python: [ - [ - "for_statement", - "if_statement", - "while_statement", - "match_statement", - "try_statement", - "with_statement", - - "function_definition", - "decorated_definition", - "class_definition", - - "import_statement", - "import_from_statement", - ], - ], - - // https://github.com/tree-sitter/tree-sitter-go/blob/master/src/node-types.json - go: [ - [ - "for_statement", - "if_statement", - "expression_switch_statement", - "type_switch_statement", - "select_statement", - "labeled_statement", - - "function_declaration", - "method_declaration", - "type_declaration", - - "import_declaration", - "package_clause", - ], - ], - - // https://github.com/tree-sitter/tree-sitter-rust/blob/master/src/node-types.json - rust: [ - [ - "for_expression", - "if_expression", - "while_expression", - "loop_expression", - "match_expression", - "try_expression", - - "function_item", - "type_item", - "enum_item", - "struct_item", - "union_item", - "trait_item", - "impl_item", - - "use_declaration", - ], - ], - - // https://github.com/tree-sitter/tree-sitter-ruby/blob/master/src/node-types.json - ruby: [ - [ - "for", - "if", - "unless", - "while", - "until", - "case", - - "class", - "singleton_class", - "method", - "singleton_method", - "module", - ], - ], -}; diff --git a/clients/tabby-agent/src/telemetry.ts b/clients/tabby-agent/src/telemetry.ts new file mode 100644 index 000000000000..85619a5b86b4 --- /dev/null +++ b/clients/tabby-agent/src/telemetry.ts @@ -0,0 +1,167 @@ +import type { paths as CloudApi } from "./http/cloudApi"; +import type { ClientInfo, ClientProvidedConfig } from "./protocol"; +import type { DataStore } from "./dataStore"; +import type { Configurations } from "./config"; +import type { ConfigData } from "./config/type"; +import os from "os"; +import createClient from "openapi-fetch"; +import { setProperty } from "dot-prop"; +import deepEqual from "deep-equal"; +import { v4 as uuid } from "uuid"; +import { name as agentName, version as agentVersion } from "../package.json"; +import { isBrowser } from "./env"; +import { ProxyConfig, createProxyForUrl } from "./http/proxy"; +import { getLogger } from "./logger"; +import { isBlank } from "./utils/string"; + +export class AnonymousUsageLogger { + private readonly logger = getLogger("Telemetry"); + private readonly systemData = { + agent: `${agentName}, ${agentVersion}`, + browser: isBrowser ? navigator?.userAgent || "browser" : undefined, + node: isBrowser ? undefined : `${process.version} ${process.platform} ${os.arch()} ${os.release()}`, + }; + + private api: ReturnType> | undefined; + private clientInfoProperties: Record = {}; + private userProperties: Record = {}; + private shouldUpdateUserProperties = false; + + private anonymousId: string | undefined = undefined; + private loggedUniqueEvents: string[] = []; + + private disabled: boolean = false; + + constructor( + private readonly dataStore: DataStore, + private readonly configurations: Configurations, + ) {} + + async initialize(clientInfo: ClientInfo | undefined) { + const config = this.configurations.getMergedConfig(); + const endpoint = "https://app.tabbyml.com/api"; + const proxyConfigs: ProxyConfig[] = [{ fromEnv: true }]; + if (!isBlank(config.proxy.url)) { + proxyConfigs.unshift(config.proxy); + } + this.api = createClient({ + baseUrl: endpoint, + /** dispatcher do not exist in {@link RequestInit} in browser env. */ + /* @ts-expect-error TS-2353 */ + dispatcher: createProxyForUrl(endpoint, proxyConfigs), + }); + + if (typeof this.dataStore.data["anonymousId"] === "string") { + this.anonymousId = this.dataStore.data["anonymousId"]; + } else { + this.anonymousId = uuid(); + this.dataStore.data["anonymousId"] = this.anonymousId; + try { + await this.dataStore.save(); + } catch (error) { + this.logger.error("Failed to save anonymous Id.", error); + } + } + + this.disabled = config.anonymousUsageTracking.disable; + this.logger.info("Anonymous usage tracking disabled: " + this.disabled); + this.configurations.on("updated", (config: ConfigData) => { + if (this.disabled !== config.anonymousUsageTracking.disable) { + this.disabled = config.anonymousUsageTracking.disable; + this.logger.info("Update anonymous usage tracking disabled: " + this.disabled); + } + }); + + const clientProvidedConfig = this.configurations.getClientProvidedConfig(); + this.updateUserProperties(clientInfo, clientProvidedConfig); + this.configurations.on("clientProvidedConfigUpdated", (clientProvidedConfig: ClientProvidedConfig) => { + this.updateUserProperties(clientInfo, clientProvidedConfig); + }); + + this.updateClientInfoProperties(clientInfo); + } + + async uniqueEvent(event: string, data: Record = {}) { + await this.event(event, data, true); + } + + async event(event: string, data: Record = {}, unique = false) { + if (this.disabled || !this.anonymousId || !this.api) { + return; + } + if (unique && this.loggedUniqueEvents.includes(event)) { + return; + } + if (unique) { + this.loggedUniqueEvents.push(event); + } + const properties = { + ...this.systemData, + ...this.clientInfoProperties, + ...data, + }; + if (this.shouldUpdateUserProperties) { + setProperty(properties, "$set", this.userProperties); + this.shouldUpdateUserProperties = false; + } + try { + const request = { + distinctId: this.anonymousId, + event, + properties, + }; + this.logger.trace("Sending anonymous usage data.", { request }); + await this.api.POST("/usage", { + body: request, + }); + } catch (error) { + this.logger.error("Failed to send anonymous usage data.", error); + this.loggedUniqueEvents = this.loggedUniqueEvents.filter((e) => e !== event); + } + } + + private updateUserProperties(clientInfo: ClientInfo | undefined, clientProvidedConfig: ClientProvidedConfig) { + const clientType = this.getClientType(clientInfo); + const properties = { + [clientType]: { + triggerMode: clientProvidedConfig?.inlineCompletion?.triggerMode, + keybindings: clientProvidedConfig?.keybindings, + }, + }; + if (!deepEqual(properties, this.userProperties)) { + this.userProperties = properties; + this.shouldUpdateUserProperties = true; + } + } + + private updateClientInfoProperties(clientInfo: ClientInfo | undefined) { + const properties = { + client: `${clientInfo?.name} ${clientInfo?.version ?? ""}`, + ide: { + name: clientInfo?.name, + version: clientInfo?.version, + }, + tabby_plugin: clientInfo?.tabbyPlugin ?? { + name: agentName, + version: agentVersion, + }, + }; + if (!deepEqual(properties, this.clientInfoProperties)) { + this.clientInfoProperties = properties; + } + } + + private getClientType(clientInfo: ClientInfo | undefined): string { + if (!clientInfo) { + return "unknown"; + } + if (clientInfo.tabbyPlugin?.name.includes("vscode")) { + return "vscode"; + } else if (clientInfo.tabbyPlugin?.name.includes("intellij")) { + return "intellij"; + } else if (clientInfo.tabbyPlugin?.name.includes("vim")) { + return "vim"; + } + return clientInfo.name; + } +} diff --git a/clients/tabby-agent/src/types/cloudApi.d.ts b/clients/tabby-agent/src/types/cloudApi.d.ts deleted file mode 100644 index e8998422d0f9..000000000000 --- a/clients/tabby-agent/src/types/cloudApi.d.ts +++ /dev/null @@ -1,101 +0,0 @@ -export interface paths { - "/device-token": { - post: operations["deviceToken"]; - }; - "/device-token/accept": { - post: operations["deviceTokenAccept"]; - }; - "/device-token/refresh": { - post: operations["deviceTokenRefresh"]; - }; - "/usage": { - post: operations["usage"]; - }; -} - -export type webhooks = Record; - -export interface components { - schemas: { - DeviceTokenRequest: { - auth_url: string; - }; - DeviceTokenResponse: { - data: { - code: string; - }; - }; - DeviceTokenAcceptResponse: { - data: { - jwt: string; - }; - }; - DeviceTokenRefreshResponse: { - data: { - jwt: string; - }; - }; - UsageRequest: object; - }; - responses: never; - parameters: never; - requestBodies: never; - headers: never; - pathItems: never; -} - -export type $defs = Record; - -export type external = Record; - -export interface operations { - deviceToken: { - requestBody: { - content: { - "application/json": components["schemas"]["DeviceTokenRequest"]; - }; - }; - responses: { - 200: { - content: { - "application/json": components["schemas"]["DeviceTokenResponse"]; - }; - }; - }; - }; - deviceTokenAccept: { - parameters: { - query: { - code: string; - }; - }; - responses: { - 200: { - content: { - "application/json": components["schemas"]["DeviceTokenAcceptResponse"]; - }; - }; - }; - }; - deviceTokenRefresh: { - responses: { - 200: { - content: { - "application/json": components["schemas"]["DeviceTokenRefreshResponse"]; - }; - }; - }; - }; - usage: { - requestBody: { - content: { - "application/json": components["schemas"]["UsageRequest"]; - }; - }; - responses: { - 200: { - content: never; - }; - }; - }; -} diff --git a/clients/tabby-agent/src/types/stats-logscale.d.ts b/clients/tabby-agent/src/types/stats-logscale.d.ts deleted file mode 100644 index bdac3bb3f1f0..000000000000 --- a/clients/tabby-agent/src/types/stats-logscale.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Ref: https://github.com/dallaylaen/stats-logscale-js/blob/main/lib/univariate.js -declare module "stats-logscale" { - export class Univariate { - constructor(args?: { base?: number; precision?: number; bins?: string }); - add(...data: number[]): this; - count(): number; - mean(): number | undefined; - percentile(p: number): number | undefined; - } -} diff --git a/clients/tabby-agent/src/types/tabbyApi.d.ts b/clients/tabby-agent/src/types/tabbyApi.d.ts deleted file mode 100644 index a848eaca53b4..000000000000 --- a/clients/tabby-agent/src/types/tabbyApi.d.ts +++ /dev/null @@ -1,320 +0,0 @@ -/** - * This file was auto-generated by openapi-typescript. - * Do not make direct changes to the file. - */ - -export interface paths { - "/v1/completions": { - post: operations["completion"]; - }; - "/v1/events": { - post: operations["event"]; - }; - "/v1/health": { - get: operations["health"]; - // back compatible for Tabby server 0.2.x and earlier - post: operations["health"]; - }; - "/v1beta/chat/completions": { - post: operations["chat_completions"]; - }; - "/v1beta/search": { - get: operations["search"]; - }; -} - -export type webhooks = Record; - -export interface components { - schemas: { - ChatCompletionChoice: { - index: number; - logprobs?: string | null; - finish_reason?: string | null; - delta: components["schemas"]["ChatCompletionDelta"]; - }; - ChatCompletionChunk: { - id: string; - /** Format: int64 */ - created: number; - system_fingerprint: string; - object: string; - model: string; - choices: components["schemas"]["ChatCompletionChoice"][]; - }; - ChatCompletionDelta: { - content: string; - }; - /** - * @example { - * "messages": [ - * { - * "role": "user", - * "content": "What is tail recursion?" - * }, - * { - * "role": "assistant", - * "content": "It's a kind of optimization in compiler?" - * }, - * { - * "role": "user", - * "content": "Could you share more details?" - * } - * ] - * } - */ - ChatCompletionRequest: { - messages: components["schemas"]["Message"][]; - /** Format: float */ - temperature?: number | null; - /** Format: int64 */ - seed?: number | null; - }; - Choice: { - /** Format: int32 */ - index: number; - text: string; - }; - /** - * @example { - * "language": "python", - * "segments": { - * "prefix": "def fib(n):\n ", - * "suffix": "\n return fib(n - 1) + fib(n - 2)" - * } - * } - */ - CompletionRequest: { - /** - * @description Language identifier, full list is maintained at - * https://code.visualstudio.com/docs/languages/identifiers - * @example python - */ - language?: string | null; - segments?: components["schemas"]["Segments"] | null; - /** - * @description A unique identifier representing your end-user, which can help Tabby to monitor & generating - * reports. - */ - user?: string | null; - debug_options?: components["schemas"]["DebugOptions"] | null; - /** - * Format: float - * @description The temperature parameter for the model, used to tune variance and "creativity" of the model output - */ - temperature?: number | null; - /** - * Format: int64 - * @description The seed used for randomly selecting tokens - */ - seed?: number | null; - }; - /** - * @example { - * "id": "string", - * "choices": [ - * { - * "index": 0, - * "text": "string" - * } - * ] - * } - */ - CompletionResponse: { - id: string; - choices: components["schemas"]["Choice"][]; - debug_data?: components["schemas"]["DebugData"] | null; - }; - DebugData: { - snippets?: components["schemas"]["Snippet"][] | null; - prompt?: string | null; - }; - DebugOptions: { - /** - * @description When `raw_prompt` is specified, it will be passed directly to the inference engine for completion. `segments` field in `CompletionRequest` will be ignored. - * - * This is useful for certain requests that aim to test the tabby's e2e quality. - */ - raw_prompt?: string | null; - /** @description When true, returns `snippets` in `debug_data`. */ - return_snippets?: boolean; - /** @description When true, returns `prompt` in `debug_data`. */ - return_prompt?: boolean; - /** @description When true, disable retrieval augmented code completion. */ - disable_retrieval_augmented_code_completion?: boolean; - }; - HealthState: { - model?: string | null; - chat_model?: string | null; - device: string; - arch: string; - cpu_info: string; - cpu_count: number; - cuda_devices: string[]; - version: components["schemas"]["Version"]; - }; - Hit: { - /** Format: float */ - score: number; - doc: components["schemas"]["HitDocument"]; - /** Format: int32 */ - id: number; - }; - HitDocument: { - body: string; - filepath: string; - git_url: string; - kind: string; - language: string; - name: string; - }; - LogEventRequest: { - /** - * @description Event type, should be `view`, `select` or `dismiss`. - * @example view - */ - type: string; - completion_id: string; - /** Format: int32 */ - choice_index: number; - view_id?: string | null; - /** Format: int32 */ - elapsed?: number | null; - }; - Message: { - role: string; - content: string; - }; - SearchResponse: { - num_hits: number; - hits: components["schemas"]["Hit"][]; - }; - Segments: { - /** @description Content that appears before the cursor in the editor window. */ - prefix: string; - /** @description Content that appears after the cursor in the editor window. */ - suffix?: string | null; - /** @description Clipboard content when requesting code completion. */ - clipboard?: string | null; - }; - Snippet: { - filepath: string; - body: string; - /** Format: float */ - score: number; - }; - Version: { - build_date: string; - build_timestamp: string; - git_sha: string; - git_describe: string; - }; - }; - responses: never; - parameters: never; - requestBodies: never; - headers: never; - pathItems: never; -} - -export type $defs = Record; - -export type external = Record; - -export interface operations { - completion: { - requestBody: { - content: { - "application/json": components["schemas"]["CompletionRequest"]; - }; - }; - responses: { - /** @description Success */ - 200: { - content: { - "application/json": components["schemas"]["CompletionResponse"]; - }; - }; - /** @description Bad Request */ - 400: { - content: never; - }; - }; - }; - event: { - parameters: { - query: { - select_kind?: string | null; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["LogEventRequest"]; - }; - }; - responses: { - /** @description Success */ - 200: { - content: never; - }; - /** @description Bad Request */ - 400: { - content: never; - }; - }; - }; - health: { - responses: { - /** @description Success */ - 200: { - content: { - "application/json": components["schemas"]["HealthState"]; - }; - }; - }; - }; - chat_completions: { - requestBody: { - content: { - "application/json": components["schemas"]["ChatCompletionRequest"]; - }; - }; - responses: { - /** @description Success */ - 200: { - content: { - "text/event-stream": components["schemas"]["ChatCompletionChunk"]; - }; - }; - /** @description When chat model is not specified, the endpoint returns 405 Method Not Allowed */ - 405: { - content: never; - }; - /** @description When the prompt is malformed, the endpoint returns 422 Unprocessable Entity */ - 422: { - content: never; - }; - }; - }; - search: { - parameters: { - query: { - q: string; - limit?: number | null; - offset?: number | null; - }; - }; - responses: { - /** @description Success */ - 200: { - content: { - "application/json": components["schemas"]["SearchResponse"]; - }; - }; - /** @description When code search is not enabled, the endpoint will returns 501 Not Implemented */ - 501: { - content: never; - }; - }; - }; -} diff --git a/clients/tabby-agent/src/utils.ts b/clients/tabby-agent/src/utils.ts deleted file mode 100644 index c76ce96bfc70..000000000000 --- a/clients/tabby-agent/src/utils.ts +++ /dev/null @@ -1,242 +0,0 @@ -export function splitLines(input: string) { - const lines = input.match(/.*(?:$|\r?\n)/g)?.filter(Boolean) ?? []; // Split lines and keep newline character - if (lines.length > 0 && lines[lines.length - 1]?.endsWith("\n")) { - // Keep last empty line - lines.push(""); - } - return lines; -} - -export function splitWords(input: string) { - return input.match(/\w+|\W+/g)?.filter(Boolean) ?? []; // Split consecutive words and non-words -} - -export function isBlank(input: string) { - return input.trim().length === 0; -} - -// Indentation - -export function getIndentationLevel(line: string, indentation?: string) { - if (indentation === undefined) { - return line.match(/^[ \t]*/)?.[0]?.length ?? 0; - } else if (indentation === "\t") { - return line.match(/^\t*/)?.[0].length ?? 0; - } else if (indentation.match(/^ *$/)) { - const spaces = line.match(/^ */)?.[0].length ?? 0; - return spaces / indentation.length; - } else { - throw new Error(`Invalid indentation: ${indentation}`); - } -} - -// function foo(a) { // <-- block opening line -// return a; -// } // <-- block closing line -export function isBlockOpeningLine(lines: string[], index: number): boolean { - if (index < 0 || index >= lines.length - 1) { - return false; - } - return getIndentationLevel(lines[index]!) < getIndentationLevel(lines[index + 1]!); -} - -export function isBlockClosingLine(lines: string[], index: number): boolean { - if (index <= 0 || index > lines.length - 1) { - return false; - } - return getIndentationLevel(lines[index - 1]!) > getIndentationLevel(lines[index]!); -} - -// Auto-closing chars -type AutoClosingCharPosition = "open" | "close" | "openOrClose"; -type AutoClosingCharPattern = { chars: string; reg: RegExp }; -type AutoClosingPairDifferent = { open: AutoClosingCharPattern; close: AutoClosingCharPattern }; -type AutoClosingPairSame = { openOrClose: AutoClosingCharPattern }; -type AutoClosingPair = AutoClosingPairDifferent | AutoClosingPairSame; - -// FIXME: use syntax parser instead -export const autoClosingPairs: AutoClosingPair[] = [ - { - open: { - chars: "(", - reg: /\(/, - }, - close: { - chars: ")", - reg: /\)/, - }, - }, - { - open: { - chars: "[", - reg: /\[/, - }, - close: { - chars: "]", - reg: /\]/, - }, - }, - { - open: { - chars: "{", - reg: /\{/, - }, - close: { - chars: "}", - reg: /\}/, - }, - }, - { - open: { - chars: "<", - reg: /<(?=\w)/, - }, - close: { - chars: "/>", - reg: /\/>/, - }, - }, - { - open: { - chars: "<", - reg: /<(?=[/\w])/, - }, - close: { - chars: ">", - reg: />/, - }, - }, - { - openOrClose: { - chars: '"', - reg: /"/, - }, - }, - { - openOrClose: { - chars: "'", - reg: /'/, - }, - }, - { - openOrClose: { - chars: "`", - reg: /`/, - }, - }, -]; - -export const regOnlyAutoClosingCloseChars = /^([)\]}>"'`]|(\/>))*$/g; - -// FIXME: This function is not good enough, it can not handle escaped characters. -export function findUnpairedAutoClosingChars(input: string): string[] { - const stack: string[] = []; - let index = 0; - while (index < input.length) { - const remain = input.slice(index); - let nextFound: { - index: number; - found: { pair: AutoClosingPair; pos: AutoClosingCharPosition; pattern: AutoClosingCharPattern } | undefined; - } = { - index: remain.length, - found: undefined, - }; - autoClosingPairs.forEach((pair) => { - Object.entries(pair).forEach(([pos, pattern]) => { - const match = remain.match(pattern.reg); - if (match && match.index !== undefined && match.index < nextFound.index) { - nextFound = { - index: match.index, - found: { pair, pos: pos as AutoClosingCharPosition, pattern }, - }; - } - }); - }); - if (!nextFound.found) { - break; - } - switch (nextFound.found.pos) { - case "openOrClose": { - const chars = nextFound.found.pattern.chars; - if (stack.length > 0 && stack.includes(chars)) { - stack.splice(stack.lastIndexOf(chars), stack.length - stack.lastIndexOf(chars)); - } else { - stack.push(chars); - } - break; - } - case "open": { - stack.push(nextFound.found.pattern.chars); - break; - } - case "close": { - const pair = nextFound.found.pair; - if (stack.length > 0 && "open" in pair && stack[stack.length - 1] === pair.open.chars) { - stack.pop(); - } else { - stack.push(nextFound.found.pattern.chars); - } - break; - } - } - index += nextFound.index + nextFound.found.pattern.chars.length; - } - return stack; -} - -// Using string levenshtein distance is not good, because variable name may create a large distance. -// Such as distance is 9 between `const fooFooFoo = 1;` and `const barBarBar = 1;`, but maybe 1 is enough. -// May be better to count distance based on words instead of characters. -import * as levenshtein from "fast-levenshtein"; -export function calcDistance(a: string, b: string) { - return levenshtein.get(a, b); -} - -// Polyfill for AbortSignal.any(signals) which added in Node.js v20. -export function abortSignalFromAnyOf(signals: (AbortSignal | undefined)[]) { - const controller = new AbortController(); - for (const signal of signals) { - if (signal?.aborted) { - controller.abort(signal.reason); - return signal; - } - signal?.addEventListener("abort", () => controller.abort(signal.reason), { - signal: controller.signal, - }); - } - return controller.signal; -} - -// Http Error -export class HttpError extends Error { - public readonly status: number; - public readonly statusText: string; - public readonly response: Response; - - constructor(response: Response) { - super(`${response.status} ${response.statusText}`); - this.name = "HttpError"; - this.status = response.status; - this.statusText = response.statusText; - this.response = response; - } -} - -export function isTimeoutError(error: any) { - return ( - (error instanceof Error && error.name === "TimeoutError") || - (error instanceof HttpError && [408, 499].includes(error.status)) - ); -} - -export function isCanceledError(error: any) { - return error instanceof Error && error.name === "AbortError"; -} - -export function errorToString(error: Error & { cause?: Error }) { - let message = error.message || error.toString(); - if (error.cause) { - message += "\nCaused by: " + errorToString(error.cause); - } - return message; -} diff --git a/clients/tabby-agent/src/utils/array.ts b/clients/tabby-agent/src/utils/array.ts new file mode 100644 index 000000000000..0fdefc4d3d1e --- /dev/null +++ b/clients/tabby-agent/src/utils/array.ts @@ -0,0 +1,24 @@ +declare global { + interface Array { + distinct(identity?: (x: T) => any): Array; + mapAsync(callbackfn: (value: T, index: number, array: T[]) => U | Promise, thisArg?: any): Promise; + } +} + +if (!Array.prototype.distinct) { + Array.prototype.distinct = function (this: T[], identity?: (x: T) => any): T[] { + return [...new Map(this.map((item) => [identity?.(item) ?? item, item])).values()]; + }; +} + +if (!Array.prototype.mapAsync) { + Array.prototype.mapAsync = async function ( + this: T[], + callbackfn: (value: T, index: number, array: T[]) => U | Promise, + thisArg?: any, + ): Promise { + return await Promise.all(this.map((item, index) => callbackfn.call(thisArg, item, index, this))); + }; +} + +export default {}; diff --git a/clients/tabby-agent/src/utils/diff.ts b/clients/tabby-agent/src/utils/diff.ts new file mode 100644 index 000000000000..ba0f497a6bf1 --- /dev/null +++ b/clients/tabby-agent/src/utils/diff.ts @@ -0,0 +1,94 @@ +import { Range, Position } from "vscode-languageserver"; +import { linesDiffComputers, Range as DiffRange } from "codiff"; + +interface CodeDiffResult { + originRanges: Range[]; + modifiedRanges: Range[]; +} + +function splitRangeToSingleLine(range: DiffRange, codeLines: string[]): DiffRange[] { + if (range.isSingleLine()) { + return [range]; + } + const resultRanges: DiffRange[] = []; + for (let i = range.startLineNumber; i <= range.endLineNumber; i++) { + const singlelineRange = new DiffRange( + i, + i === range.startLineNumber ? range.startColumn : 1, + i, + i === range.endLineNumber ? range.endColumn : codeLines[i - 1]?.length ?? 1, + ); + resultRanges.push(singlelineRange); + } + return resultRanges; +} + +function mapDiffRangeToEditorRange(diffRange: DiffRange, editorRanges: Range[]): Range | undefined { + if (diffRange.isEmpty()) { + return undefined; + } + + /** + * diff range must be splited to single line before being mapped to editor range. + */ + if (!diffRange.isSingleLine()) { + return undefined; + } + + const start: Position = { + line: editorRanges[diffRange.startLineNumber - 1]?.start.line ?? 0, + character: diffRange.startColumn - 1, + }; + + const end = { + line: editorRanges[diffRange.startLineNumber - 1]?.start.line ?? 0, + character: diffRange.endColumn - 1, + }; + + return { + start, + end, + }; +} + +/** + * Diff code and mapping the diff result range to editor range + */ +export function codeDiff( + originCode: string[], + originCodeRanges: Range[], + modifiedCode: string[], + modifiedCodeRanges: Range[], +): CodeDiffResult { + const originRanges: Range[] = []; + const modifiedRanges: Range[] = []; + + const diffResult = linesDiffComputers.getDefault().computeDiff(originCode, modifiedCode, { + computeMoves: false, + ignoreTrimWhitespace: true, + maxComputationTimeMs: 100, + }); + + diffResult.changes.forEach((change) => { + change.innerChanges?.forEach((innerChange) => { + splitRangeToSingleLine(innerChange.originalRange, originCode).forEach((singleLineRange) => { + const originRange = mapDiffRangeToEditorRange(singleLineRange, originCodeRanges); + if (originRange) { + originRanges.push(originRange); + } + }); + + splitRangeToSingleLine(innerChange.modifiedRange, modifiedCode).forEach((singleLineRange) => { + const modifiedRange = mapDiffRangeToEditorRange(singleLineRange, modifiedCodeRanges); + if (modifiedRange) { + modifiedRanges.push(modifiedRange); + } + }); + }); + }); + + return { + modifiedRanges, + originRanges, + }; +} diff --git a/clients/tabby-agent/src/utils/error.ts b/clients/tabby-agent/src/utils/error.ts new file mode 100644 index 000000000000..1442375e1a05 --- /dev/null +++ b/clients/tabby-agent/src/utils/error.ts @@ -0,0 +1,52 @@ +// Http Error +export class HttpError extends Error { + public readonly status: number; + public readonly statusText: string; + public readonly response: Response; + + constructor(response: Response) { + super(`${response.status} ${response.statusText}`); + this.name = "HttpError"; + this.status = response.status; + this.statusText = response.statusText; + this.response = response; + } +} + +export class MutexAbortError extends Error { + constructor() { + super("Aborted due to new request."); + this.name = "AbortError"; + } +} + +export function isTimeoutError(error: any) { + return ( + (error instanceof Error && error.name === "TimeoutError") || + (error instanceof HttpError && [408, 499].includes(error.status)) + ); +} + +export function isCanceledError(error: any) { + return error instanceof Error && error.name === "AbortError"; +} + +export function isUnauthorizedError(error: any) { + return error instanceof HttpError && [401, 403].includes(error.status); +} + +export function isRateLimitExceededError(error: any) { + return error instanceof HttpError && error.status === 429; +} + +function errorToString(error: Error) { + let message = error.message || error.toString(); + if (error.cause instanceof Error) { + message += "\nCaused by: " + errorToString(error.cause); + } + return message; +} + +export function formatErrorMessage(error: unknown) { + return error instanceof Error ? errorToString(error) : JSON.stringify(error); +} diff --git a/clients/tabby-agent/src/utils/languageId.ts b/clients/tabby-agent/src/utils/languageId.ts new file mode 100644 index 000000000000..ca729f6d376d --- /dev/null +++ b/clients/tabby-agent/src/utils/languageId.ts @@ -0,0 +1,93 @@ +import * as path from "path"; + +// https://code.visualstudio.com/docs/languages/identifiers + +export function getLanguageId(uri: string): string { + const extensionToLanguageId: { [key: string]: string } = { + ".abap": "abap", + ".bat": "bat", + ".bib": "bibtex", + ".clj": "clojure", + ".coffee": "coffeescript", + ".c": "c", + ".cpp": "cpp", + ".cs": "csharp", + ".css": "css", + ".cu": "cuda-cpp", + ".d": "d", + ".dart": "dart", + ".pas": "pascal", + ".diff": "diff", + ".dockerfile": "dockerfile", + ".erl": "erlang", + ".fs": "fsharp", + ".go": "go", + ".groovy": "groovy", + ".hbs": "handlebars", + ".haml": "haml", + ".hs": "haskell", + ".html": "html", + ".ini": "ini", + ".java": "java", + ".js": "javascript", + ".jsx": "javascriptreact", + ".json": "json", + ".jsonc": "jsonc", + ".jl": "julia", + ".tex": "latex", + ".less": "less", + ".lua": "lua", + ".m": "objective-c", + ".mm": "objective-cpp", + ".ml": "ocaml", + ".pl": "perl", + ".php": "php", + ".txt": "plaintext", + ".ps1": "powershell", + ".pug": "pug", + ".py": "python", + ".r": "r", + ".cshtml": "razor", + ".rb": "ruby", + ".rs": "rust", + ".scss": "scss", + ".sass": "sass", + ".shader": "shaderlab", + ".sh": "shellscript", + ".slim": "slim", + ".sql": "sql", + ".styl": "stylus", + ".svelte": "svelte", + ".swift": "swift", + ".ts": "typescript", + ".tsx": "typescriptreact", + ".vb": "vb", + ".vue": "vue", + ".xml": "xml", + ".xsl": "xsl", + ".yaml": "yaml", + ".yml": "yaml", + // Add more extensions as needed + }; + + const basenameToLanguageId: { [key: string]: string } = { + Dockerfile: "dockerfile", + Makefile: "makefile", + "git-commit": "git-commit", + "git-rebase": "git-rebase", + // Add more special filenames as needed + }; + + const basename = path.basename(uri); + if (basenameToLanguageId[basename]) { + return basenameToLanguageId[basename]; + } + + const ext = path.extname(uri).toLowerCase(); + if (extensionToLanguageId[ext]) { + return extensionToLanguageId[ext]; + } + + // Return extname without the dot as default + return ext ? ext.slice(1) : "plaintext"; +} diff --git a/clients/tabby-agent/src/utils/range.ts b/clients/tabby-agent/src/utils/range.ts new file mode 100644 index 000000000000..e00143e76e54 --- /dev/null +++ b/clients/tabby-agent/src/utils/range.ts @@ -0,0 +1,63 @@ +import { Position, Range } from "vscode-languageserver"; +import { TextDocument } from "vscode-languageserver-textdocument"; + +export function isPositionEqual(a: Position, b: Position): boolean { + return a.line === b.line && a.character === b.character; +} +export function isPositionBefore(a: Position, b: Position): boolean { + return a.line < b.line || (a.line === b.line && a.character < b.character); +} +export function isPositionAfter(a: Position, b: Position): boolean { + return a.line > b.line || (a.line === b.line && a.character > b.character); +} +export function isPositionBeforeOrEqual(a: Position, b: Position): boolean { + return a.line < b.line || (a.line === b.line && a.character <= b.character); +} +export function isPositionAfterOrEqual(a: Position, b: Position): boolean { + return a.line > b.line || (a.line === b.line && a.character >= b.character); +} +export function isPositionInRange(a: Position, b: Range): boolean { + return isPositionBeforeOrEqual(b.start, a) && isPositionBeforeOrEqual(a, b.end); +} +export function isRangeEqual(a: Range, b: Range): boolean { + return isPositionEqual(a.start, b.start) && isPositionEqual(a.end, b.end); +} +export function isEmptyRange(a: Range): boolean { + return isPositionAfterOrEqual(a.start, a.end); +} +export function unionRange(a: Range, b: Range): Range { + return { + start: isPositionBefore(a.start, b.start) + ? { line: a.start.line, character: a.start.character } + : { line: b.start.line, character: b.start.character }, + end: isPositionAfter(a.end, b.end) + ? { line: a.end.line, character: a.end.character } + : { line: b.end.line, character: b.end.character }, + }; +} +export function intersectionRange(a: Range, b: Range): Range | null { + const range = { + start: isPositionAfter(a.start, b.start) + ? { line: a.start.line, character: a.start.character } + : { line: b.start.line, character: b.start.character }, + end: isPositionBefore(a.end, b.end) + ? { line: a.end.line, character: a.end.character } + : { line: b.end.line, character: b.end.character }, + }; + return isEmptyRange(range) ? null : range; +} +export function documentRange(doc: TextDocument): Range { + return { + start: { + line: 0, + character: 0, + }, + end: { + line: doc.lineCount, + character: 0, + }, + }; +} +export function rangeInDocument(a: Range, doc: TextDocument): Range | null { + return intersectionRange(a, documentRange(doc)); +} diff --git a/clients/tabby-agent/src/utils/signal.ts b/clients/tabby-agent/src/utils/signal.ts new file mode 100644 index 000000000000..a81e26763b64 --- /dev/null +++ b/clients/tabby-agent/src/utils/signal.ts @@ -0,0 +1,14 @@ +// Polyfill for AbortSignal.any(signals) which added in Node.js v20. +export function abortSignalFromAnyOf(signals: (AbortSignal | undefined)[]) { + const controller = new AbortController(); + for (const signal of signals) { + if (signal?.aborted) { + controller.abort(signal.reason); + return signal; + } + signal?.addEventListener("abort", () => controller.abort(signal.reason), { + signal: controller.signal, + }); + } + return controller.signal; +} diff --git a/clients/tabby-agent/src/utils/string.ts b/clients/tabby-agent/src/utils/string.ts new file mode 100644 index 000000000000..78154bb4afc8 --- /dev/null +++ b/clients/tabby-agent/src/utils/string.ts @@ -0,0 +1,331 @@ +// Keywords appear in the code everywhere, but we don't want to use them for +// matching in code searching. +// Just filter them out before we start using a syntax parser. +const reservedKeywords = [ + // Typescript: https://github.com/microsoft/TypeScript/issues/2536 + "as", + "any", + "boolean", + "break", + "case", + "catch", + "class", + "const", + "constructor", + "continue", + "debugger", + "declare", + "default", + "delete", + "do", + "else", + "enum", + "export", + "extends", + "false", + "finally", + "for", + "from", + "function", + "get", + "if", + "implements", + "import", + "in", + "instanceof", + "interface", + "let", + "module", + "new", + "null", + "number", + "of", + "package", + "private", + "protected", + "public", + "require", + "return", + "set", + "static", + "string", + "super", + "switch", + "symbol", + "this", + "throw", + "true", + "try", + "typeof", + "var", + "void", + "while", + "with", + "yield", +]; +export function extractNonReservedWordList(text: string): string { + const re = /\w+/g; + return [ + ...new Set(text.match(re)?.filter((symbol) => symbol.length > 2 && !reservedKeywords.includes(symbol))).values(), + ].join(" "); +} + +export function splitLines(input: string) { + const lines = input.match(/.*(?:$|\r?\n)/g)?.filter(Boolean) ?? []; // Split lines and keep newline character + if (lines.length > 0 && lines[lines.length - 1]?.endsWith("\n")) { + // Keep last empty line + lines.push(""); + } + return lines; +} + +export function splitWords(input: string) { + return input.match(/\w+|\W+/g)?.filter(Boolean) ?? []; // Split consecutive words and non-words +} + +export function isBlank(input: string) { + return input.trim().length === 0; +} + +// Indentation + +export function getIndentationLevel(line: string, indentation?: string) { + if (indentation === undefined) { + return line.match(/^[ \t]*/)?.[0]?.length ?? 0; + } else if (indentation === "\t") { + return line.match(/^\t*/)?.[0].length ?? 0; + } else if (indentation.match(/^ *$/)) { + const spaces = line.match(/^ */)?.[0].length ?? 0; + return spaces / indentation.length; + } else { + throw new Error(`Invalid indentation: ${indentation}`); + } +} + +// function foo(a) { // <-- block opening line +// return a; +// } // <-- block closing line +export function isBlockOpeningLine(lines: string[], index: number): boolean { + if (index < 0 || index >= lines.length - 1) { + return false; + } + return getIndentationLevel(lines[index]!) < getIndentationLevel(lines[index + 1]!); +} + +export function isBlockClosingLine(lines: string[], index: number): boolean { + if (index <= 0 || index > lines.length - 1) { + return false; + } + return getIndentationLevel(lines[index - 1]!) > getIndentationLevel(lines[index]!); +} + +// Auto-closing chars +type AutoClosingCharPosition = "open" | "close" | "openOrClose"; +type AutoClosingCharPattern = { chars: string; reg: RegExp }; +type AutoClosingPairDifferent = { open: AutoClosingCharPattern; close: AutoClosingCharPattern }; +type AutoClosingPairSame = { openOrClose: AutoClosingCharPattern }; +type AutoClosingPair = AutoClosingPairDifferent | AutoClosingPairSame; + +// FIXME: use syntax parser instead +export const autoClosingPairs: AutoClosingPair[] = [ + { + open: { + chars: "(", + reg: /\(/, + }, + close: { + chars: ")", + reg: /\)/, + }, + }, + { + open: { + chars: "[", + reg: /\[/, + }, + close: { + chars: "]", + reg: /\]/, + }, + }, + { + open: { + chars: "{", + reg: /\{/, + }, + close: { + chars: "}", + reg: /\}/, + }, + }, + { + open: { + chars: "<", + reg: /<(?=\w)/, + }, + close: { + chars: "/>", + reg: /\/>/, + }, + }, + { + open: { + chars: "<", + reg: /<(?=[/\w])/, + }, + close: { + chars: ">", + reg: />/, + }, + }, + { + openOrClose: { + chars: '"', + reg: /"/, + }, + }, + { + openOrClose: { + chars: "'", + reg: /'/, + }, + }, + { + openOrClose: { + chars: "`", + reg: /`/, + }, + }, +]; + +export const regOnlyAutoClosingCloseChars = /^([)\]}>"'`]|(\/>)|[;,])*$/g; + +// FIXME: This function is not good enough, it can not handle escaped characters. +export function findUnpairedAutoClosingChars(input: string): string[] { + const stack: string[] = []; + let index = 0; + while (index < input.length) { + const remain = input.slice(index); + let nextFound: { + index: number; + found: { pair: AutoClosingPair; pos: AutoClosingCharPosition; pattern: AutoClosingCharPattern } | undefined; + } = { + index: remain.length, + found: undefined, + }; + autoClosingPairs.forEach((pair) => { + Object.entries(pair).forEach(([pos, pattern]) => { + const match = remain.match(pattern.reg); + if (match && match.index !== undefined && match.index < nextFound.index) { + nextFound = { + index: match.index, + found: { pair, pos: pos as AutoClosingCharPosition, pattern }, + }; + } + }); + }); + if (!nextFound.found) { + break; + } + switch (nextFound.found.pos) { + case "openOrClose": { + const chars = nextFound.found.pattern.chars; + if (stack.length > 0 && stack.includes(chars)) { + stack.splice(stack.lastIndexOf(chars), stack.length - stack.lastIndexOf(chars)); + } else { + stack.push(chars); + } + break; + } + case "open": { + stack.push(nextFound.found.pattern.chars); + break; + } + case "close": { + const pair = nextFound.found.pair; + if (stack.length > 0 && "open" in pair && stack[stack.length - 1] === pair.open.chars) { + stack.pop(); + } else { + stack.push(nextFound.found.pattern.chars); + } + break; + } + } + index += nextFound.index + nextFound.found.pattern.chars.length; + } + return stack; +} + +// Using string levenshtein distance is not good, because variable name may create a large distance. +// Such as distance is 9 between `const fooFooFoo = 1;` and `const barBarBar = 1;`, but maybe 1 is enough. +// May be better to count distance based on words instead of characters. +import * as levenshtein from "fast-levenshtein"; +export function calcDistance(a: string, b: string) { + return levenshtein.get(a, b); +} + +export function stringToRegExp(str: string): RegExp { + const parts = /\/(.*)\/(.*)/.exec(str); + if (parts && parts[1] && parts[2]) { + return new RegExp(parts[1], parts[2]); + } + return new RegExp(str); +} + +import type { Readable } from "readable-stream"; +export async function parseChatResponse(readableStream: Readable): Promise { + let output = ""; + try { + for await (const item of readableStream) { + const delta = typeof item === "string" ? item : ""; + output += delta; + } + } catch (error) { + if (error instanceof TypeError && error.message.startsWith("terminated")) { + // ignore server side close error + } else { + throw error; + } + } + return output; +} + +/** + * Format placeholders in the template string. + * Example: + * ``` + * formatPlaceholders("Hello {{name}}", { name: "World" }) + * ``` + * @param template a string with placeholders in the form {{key}} + * @param replacements a map of replacements for the placeholders + * @returns a string with the placeholders replaced + */ +export function formatPlaceholders(template: string, replacements: Record): string { + const patterns = Object.keys(replacements) + .map((key) => "{{" + key + "}}") + .join("|"); + const regexp = new RegExp(patterns, "g"); + return template.replace(regexp, (pattern: string) => { + const key = pattern.slice(2, -2); + return replacements[key] ?? ""; + }); +} + +/** + * Crop the text to fit within the specified character limit. + * If the text is cropped, it ensures the last line is complete by trimming at the last newline. + * + * @param text The input text to crop. + * @param maxChars The maximum number of characters allowed. + * @returns The cropped text. + */ +export function cropTextToMaxChars(text: string, maxChars: number): string { + if (text.length > maxChars) { + text = text.slice(0, maxChars); + const lastNewLine = text.lastIndexOf("\n"); + if (lastNewLine > 0) { + text = text.slice(0, lastNewLine + 1); + } + } + return text; +} diff --git a/clients/tabby-agent/src/utils/types.ts b/clients/tabby-agent/src/utils/types.ts new file mode 100644 index 000000000000..9d3193a166f4 --- /dev/null +++ b/clients/tabby-agent/src/utils/types.ts @@ -0,0 +1,7 @@ +import { Range } from "vscode-languageserver"; +import { TextDocument } from "vscode-languageserver-textdocument"; + +export interface DocumentRange { + document: TextDocument; + range: Range; +} diff --git a/clients/tabby-agent/tsconfig.json b/clients/tabby-agent/tsconfig.json index 930e87c9a09d..eeadb0ff996a 100644 --- a/clients/tabby-agent/tsconfig.json +++ b/clients/tabby-agent/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { "module": "commonjs", - "target": "ES2020", - "lib": ["ES2020", "dom"], + "lib": ["esnext", "webworker"], + "target": "es2020", "sourceMap": true, "esModuleInterop": true, "resolveJsonModule": true, @@ -15,5 +15,9 @@ "noUnusedParameters": true, "allowSyntheticDefaultImports": true }, - "include": ["./src", "./tests/**/*.test.ts"] + "include": ["./src"], + "ts-node": { + "files": true, + "require": ["./register-loader.js"] + } } diff --git a/clients/tabby-agent/tsup.config.ts b/clients/tabby-agent/tsup.config.ts index b5311019922e..eb5697473529 100644 --- a/clients/tabby-agent/tsup.config.ts +++ b/clients/tabby-agent/tsup.config.ts @@ -1,108 +1,135 @@ -import type { BuildOptions, Plugin } from "esbuild"; import { defineConfig } from "tsup"; -import { copy } from "esbuild-plugin-copy"; +import type { Plugin, PluginBuild } from "esbuild"; +import path from "path"; +import fs from "fs-extra"; import { polyfillNode } from "esbuild-plugin-polyfill-node"; -import { dependencies } from "./package.json"; import dedent from "dedent"; -function markSideEffects(value: boolean, packages: string[]): Plugin { +function processWinCa(copyRootsExe: boolean = false): Plugin { return { - name: "sideEffects", - setup: (build) => { - build.onResolve({ filter: /. */ }, async (args) => { - if (args.pluginData || !packages.includes(args.path)) { - return; + name: "processWinCa", + setup: (build: PluginBuild) => { + build.onLoad({ filter: /win-ca[\\/]lib[\\/]crypt32-\w*.node$/ }, async () => { + // As win-ca fallback is used, skip not required `.node` binaries + return { + contents: "", + loader: "empty", + }; + }); + build.onLoad({ filter: /win-ca[\\/]lib[\\/]fallback.js$/ }, async (args) => { + if (copyRootsExe) { + // Copy `roots.exe` binary to `dist/win-ca`, and the LICENSE + const binaryName = "roots.exe"; + const winCaPackagePath = path.join(path.dirname(args.path), ".."); + const license = await fs.readFile(path.join(winCaPackagePath, "LICENSE")); + const packageJson = await fs.readJSON(path.join(winCaPackagePath, "package.json")); + const exePath = path.join(path.dirname(args.path), binaryName); + const outDir = path.join(build.initialOptions.outdir ?? "", "win-ca"); + await fs.ensureDir(outDir); + await fs.copyFile(exePath, path.join(outDir, binaryName)); + await fs.writeFile( + path.join(outDir, "LICENSE"), + dedent` + win-ca v${packageJson.version} + ${packageJson.homepage} + + ${license} + `, + ); + return {}; } - const { path, ...rest } = args; - rest.pluginData = true; - const result = await build.resolve(path, rest); - result.sideEffects = value; - return result; }); }, }; } -function defineEnvs(targetOptions: BuildOptions, envs: { browser: boolean }) { - targetOptions["define"] = { - ...targetOptions["define"], - "process.env.IS_TEST": "false", - "process.env.IS_BROWSER": Boolean(envs?.browser).toString(), - }; - return targetOptions; -} +const banner = dedent` + /** + * Tabby Agent + * https://github.com/tabbyml/tabby/tree/main/clients/tabby-agent + * Copyright (c) 2023-2024 TabbyML, Inc. + * Licensed under the Apache License 2.0. + */`; -export default async () => [ - defineConfig({ - name: "node-cjs", - entry: ["src/index.ts"], - platform: "node", - target: "node18", - format: ["cjs"], - sourcemap: true, - esbuildOptions(options) { - defineEnvs(options, { browser: false }); - }, - clean: true, - }), - defineConfig({ - name: "browser-esm", - entry: ["src/index.ts"], - platform: "browser", - format: ["esm"], - treeshake: "smallest", // To remove unused libraries in browser. - sourcemap: true, - esbuildPlugins: [ - polyfillNode({ - polyfills: { fs: true }, - }), - // Mark sideEffects false for tree-shaking unused libraries in browser. - markSideEffects(false, ["chokidar", "file-stream-rotator"]), - ], - esbuildOptions(options) { - defineEnvs(options, { browser: true }); - }, - clean: true, - }), - defineConfig({ - name: "type-defs", - entry: ["src/index.ts"], - dts: { - only: true, +export default defineConfig(async () => { + return [ + { + name: "lsp-protocol", + entry: ["src/protocol.ts"], + dts: true, + external: ["vscode-languageserver-protocol"], + banner: { + js: banner, + }, }, - clean: true, - }), - defineConfig({ - name: "cli", - entry: ["src/cli.ts"], - platform: "node", - target: "node18", - noExternal: Object.keys(dependencies), - treeshake: "smallest", - minify: true, - sourcemap: true, - banner: { - js: dedent` - /** - * Tabby Agent - * https://github.com/tabbyml/tabby/tree/main/clients/tabby-agent - * Copyright (c) 2023-2024 TabbyML, Inc. - * Licensed under the Apache License 2.0. - */`, + { + name: "lsp-node", + loader: { + ".md": "text", + }, + entry: ["src/index.ts"], + outDir: "dist/node", + platform: "node", + target: "node18", + sourcemap: true, + clean: true, + define: { + "process.env.IS_TEST": "false", + "process.env.IS_BROWSER": "false", + }, + treeshake: { + preset: "smallest", + moduleSideEffects: "no-external", + }, + external: ["vscode-languageserver/browser"], + esbuildPlugins: [processWinCa(true)], + banner: { + js: banner, + }, }, - esbuildPlugins: [ - copy({ - assets: [ - { - from: "./wasm/*", - to: "./wasm", - }, - ], - }), - ], - esbuildOptions(options) { - defineEnvs(options, { browser: false }); + { + name: "lsp-browser", + loader: { + ".md": "text", + }, + entry: ["src/index.ts"], + outDir: "dist/browser", + platform: "browser", + format: "esm", + sourcemap: true, + clean: true, + define: { + "process.env.IS_TEST": "false", + "process.env.IS_BROWSER": "true", + }, + treeshake: { + preset: "smallest", + moduleSideEffects: "no-external", + }, + external: [ + "glob", + "fs-extra", + "chokidar", + "file-stream-rotator", + "win-ca", + "mac-ca", + "vscode-languageserver/node", + "undici", + ], + esbuildPlugins: [ + polyfillNode({ + polyfills: {}, + }), + ], + esbuildOptions: (options) => { + // disable warning for `import is undefined`: + // src/lsp/index.ts:9:6: + // 9 | dns.setDefaultResultOrder("ipv4first"); + options.logOverride = { "import-is-undefined": "info" }; + }, + banner: { + js: banner, + }, }, - clean: true, - }), -]; + ]; +}); diff --git a/clients/tabby-chat-panel/.gitignore b/clients/tabby-chat-panel/.gitignore new file mode 100644 index 000000000000..1521c8b7652b --- /dev/null +++ b/clients/tabby-chat-panel/.gitignore @@ -0,0 +1 @@ +dist diff --git a/clients/tabby-chat-panel/README.md b/clients/tabby-chat-panel/README.md new file mode 100644 index 000000000000..c9102a80e28a --- /dev/null +++ b/clients/tabby-chat-panel/README.md @@ -0,0 +1 @@ +# tabby-chat-panel diff --git a/clients/tabby-chat-panel/build.config.ts b/clients/tabby-chat-panel/build.config.ts new file mode 100644 index 000000000000..8a9eff3b7345 --- /dev/null +++ b/clients/tabby-chat-panel/build.config.ts @@ -0,0 +1,13 @@ +import { defineBuildConfig } from 'unbuild' + +export default defineBuildConfig({ + entries: [ + 'src/index', + 'src/react', + ], + declaration: true, + clean: true, + rollup: { + emitCJS: true, + }, +}) diff --git a/clients/tabby-chat-panel/eslint.config.js b/clients/tabby-chat-panel/eslint.config.js new file mode 100644 index 000000000000..fe05ab72be8b --- /dev/null +++ b/clients/tabby-chat-panel/eslint.config.js @@ -0,0 +1,7 @@ +import antfu from '@antfu/eslint-config' + +export default antfu({ + rules: { + curly: ['off', 'all'], + }, +}) diff --git a/clients/tabby-chat-panel/package.json b/clients/tabby-chat-panel/package.json new file mode 100644 index 000000000000..69d13be78b1e --- /dev/null +++ b/clients/tabby-chat-panel/package.json @@ -0,0 +1,80 @@ +{ + "name": "tabby-chat-panel", + "type": "module", + "version": "0.10.0", + "keywords": [], + "sideEffects": false, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.cjs" + }, + "./react": { + "types": "./dist/react.d.ts", + "import": "./dist/react.mjs", + "require": "./dist/react.cjs" + } + }, + "main": "./dist/index.mjs", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "./dist/*", + "./dist/index.d.ts" + ] + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "pnpm build:unbuild & pnpm build:rollup", + "build:unbuild": "unbuild", + "build:rollup": "rollup -c rollup.config.js", + "dev": "unbuild --stub", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "prepack": "nr build", + "preview": "node --watch bin/index.js", + "release": "bumpp && npm publish", + "start": "tsx src/index.ts", + "test": "vitest run", + "test:watch": "vitest", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "react": "18.2.0", + "semver": "^7.6.0", + "tabby-threads": "workspace:*" + }, + "devDependencies": { + "@antfu/eslint-config": "^2.16.0", + "@antfu/ni": "^0.21.12", + "@rollup/plugin-commonjs": "^28.0.3", + "@rollup/plugin-node-resolve": "^16.0.0", + "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-typescript": "^12.1.1", + "@types/node": "^20.12.7", + "@types/react": "18.2.23", + "@types/semver": "^7.5.8", + "bumpp": "^9.4.0", + "eslint": "^9.1.1", + "eslint-formatter-mo": "^1.2.0", + "lint-staged": "^15.2.2", + "rimraf": "^5.0.5", + "rollup": "^4.27.3", + "tsx": "^4.7.3", + "typescript": "^5.4.5", + "unbuild": "^2.0.0", + "vitest": "^1.5.2" + }, + "lint-staged": { + "*": "eslint --fix" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/clients/tabby-chat-panel/rollup.config.js b/clients/tabby-chat-panel/rollup.config.js new file mode 100644 index 000000000000..69e468f059d9 --- /dev/null +++ b/clients/tabby-chat-panel/rollup.config.js @@ -0,0 +1,27 @@ +import typescript from '@rollup/plugin-typescript' +import commonjs from '@rollup/plugin-commonjs' +import resolve from '@rollup/plugin-node-resolve' +import terser from '@rollup/plugin-terser' +import { defineConfig } from 'rollup' + +export default defineConfig([{ + input: 'src/browser.ts', + output: { + dir: 'dist', + format: 'iife', + entryFileNames: 'iife/tabby-chat-panel.min.js', + name: 'TabbyChatPanel', + }, + treeshake: true, + plugins: [ + resolve({ + browser: true, + }), + commonjs(), + terser(), + typescript({ + tsconfig: './tsconfig.json', + noEmitOnError: true, + }), + ], +}]) diff --git a/clients/tabby-chat-panel/src/browser.ts b/clients/tabby-chat-panel/src/browser.ts new file mode 100644 index 000000000000..a5ecb5af6145 --- /dev/null +++ b/clients/tabby-chat-panel/src/browser.ts @@ -0,0 +1,9 @@ +import { createThreadFromIframe } from 'tabby-threads' +import { createClient as createClientFromThread } from './thread' +import type { ClientApi } from './client' +import type { ServerApi, ServerApiList } from './server' + +export async function createClient(iframe: HTMLIFrameElement, api: ClientApi): Promise { + const thread = createThreadFromIframe(iframe, { expose: api }) + return await createClientFromThread(thread) +} diff --git a/clients/tabby-chat-panel/src/client.ts b/clients/tabby-chat-panel/src/client.ts new file mode 100644 index 000000000000..d67f9059806b --- /dev/null +++ b/clients/tabby-chat-panel/src/client.ts @@ -0,0 +1,287 @@ +import type { EditorFileContext, FileLocation, FileRange, Filepath, GitRepository, LineRange, Location, TerminalContext } from './types' + +export interface ClientApi { + /** + * @deprecated + * Notify the client that the server is ready. + * @param params {@link OnLoadedParams} + */ + onLoaded?: (params?: OnLoadedParams | undefined) => Promise + + /** + * Forces the webview to refresh. + */ + refresh: () => Promise + + /** + * Open the target file location in the editor. + * @param target The target {@link FileLocation} to open. + * @returns Whether the file location is opened successfully. + */ + openInEditor: (target: FileLocation) => Promise + + /** + * Open the target URL in the external browser. + * @param url The target URL to open. + */ + openExternal: (url: string) => Promise + + /** + * Get the active editor selection as context, or the whole file if no selection. + * @returns The {@link EditorFileContext} of the active editor, or `null` if no active editor is found. + */ + getActiveEditorSelection: () => Promise + + /** + * Get the active terminal selection as context. + * @returns The {@link TerminalContext} of the active terminal, or `null` if no active terminal selection is found. + * @since 0.10.0 + */ + getActiveTerminalSelection?: () => Promise + + /** + * Copy the content to the clipboard. + * @param content The content to copy. + */ + onCopy: (content: string) => Promise + + /** + * Apply the content into the active editor. + * @param content The content to apply. + * See {@link onApplyInEditorV2} for Smart Apply support. + */ + onApplyInEditor: (content: string) => Promise + + /** + * Apply the content into the active editor, with Smart Apply support. + * @param content The content to apply. + * @param options The optional {@link ApplyInEditorOptions} to control the behavior of the apply. + */ + onApplyInEditorV2?: (content: string, options?: ApplyInEditorOptions) => Promise + + /** + * Notify the client that a keyboard event has been triggered. + * @param type The type of the keyboard event. + * @param event The {@link KeyboardEventInit} to handle. + */ + onKeyboardEvent?: (type: 'keydown' | 'keyup' | 'keypress', event: KeyboardEventInit) => Promise + + /** + * Fetch all git repositories information for the current workspace. + * @returns The list of {@link GitRepository} objects, or an empty array if no repositories are found. + */ + readWorkspaceGitRepositories?: () => Promise + + /** + * Returns the content of a file within the specified range. + * @param fileRange The {@link FileRange} to read the content from. + * @returns The content of the file as a string, or `null` if the file or range cannot be accessed. + */ + readFileContent?: (fileRange: FileRange) => Promise + + /** + * Returns a list of file information matching the specified query. + * @param params An {@link ListFilesInWorkspaceParams} object that includes a search query and a limit for the results. + * @returns An array of {@link ListFileItem} objects that could be empty. + */ + listFileInWorkspace?: (params: ListFilesInWorkspaceParams) => Promise + + /** + * Returns active editor symbols when no query is provided. Otherwise, returns workspace symbols that match the query. + * @param params An {@link ListSymbolsParams} object that includes a search query and a limit for the results. + * @returns An array of {@link ListSymbolItem} objects that could be empty. + */ + listSymbols?: (params: ListSymbolsParams) => Promise + + /** + * Find the target symbol and return the symbol information. + * @param symbol The symbol to find. + * @param hints The optional {@link LookupSymbolHint} list to help find the symbol. The hints should be sorted by priority. + * @returns The {@link SymbolInfo} object if found, or `null` if not found. + */ + lookupSymbol?: (symbol: string, hints?: LookupSymbolHint[] | undefined) => Promise + + /** + * Fetch the saved session state from the client. + * When initialized, the chat panel attempts to fetch the saved session state to restore the session. + * @param keys The keys to be fetched. If not provided, all keys will be returned. + * @return The saved persisted state, or `null` if no state is found. + */ + fetchSessionState?: (keys?: string[] | undefined) => Promise | null> + + /** + * Save the session state of the chat panel. + * The client is responsible for maintaining the state in case of a webview reload. + * The saved state should be merged and updated by the record key. + * @param state The state to save. + */ + storeSessionState?: (state: Record) => Promise + + /** + * Retrieves the Git diff changes from the client IDE. + * @param params The parameters to filter or specify the changes to retrieve. + * @returns A Promise resolving to an array of {@link ChangeItem} objects representing the Git diff results. + */ + getChanges?: (params: GetChangesParams) => Promise + + /** + * Run a terminal command in the IDE Terminal shell. + * @param command The command to run in the terminal. + * @returns void + */ + runShell?: (command: string) => Promise +} + +/** + * @deprecated + * The params used in {@link ClientApi.onLoaded}. + */ +export interface OnLoadedParams { + /** + * The current version used by the server. + */ + apiVersion: string +} + +/** + * The options used in {@link ClientApi.applyInEditorV2}. + */ +export interface ApplyInEditorOptions { + /** + * The language of the content to apply. + */ + languageId?: string + + /** + * Controls whether use smart apply. + */ + smart?: boolean +} + +/** + * The options used in {@link ClientApi.lookupSymbol}. + */ +export interface LookupSymbolHint { + /** + * The filepath of the file to search the symbol. + */ + filepath?: Filepath + + /** + * The location in the file to search the symbol. + */ + location?: Location +} + +/** + * The result returned by {@link ClientApi.lookupSymbol}. + */ +export interface SymbolInfo { + /** + * Where the symbol is found. + */ + source: FileLocation + + /** + * The target location to navigate to when the symbol is clicked. + */ + target: FileLocation +} + +/** + * The params used in {@link ClientApi.listFileInWorkspace}. + */ +export interface ListFilesInWorkspaceParams { + /** + * The query string to filter the files. + * The query string could be an empty string. In this case, we do not read all files in the workspace, + * but only list the opened files in the editor. + */ + query: string + + /** + * The maximum number of files to list. + */ + limit?: number +} + +/** + * The item returned by {@link ClientApi.listFileInWorkspace}. + */ +export interface ListFileItem { + /** + * The filepath of the file. + */ + filepath: Filepath + + /** + * Source of the file, indicating whether it comes from an editor or search results. + * When undefined, the file is assumed to be from the workspace by default. + * This property helps determine how the file should be handled in the UI. + */ + source?: 'openedInEditor' | 'searchResult' +} + +/** + * The params used in {@link ClientApi.listSymbols}. + */ +export interface ListSymbolsParams { + /** + * The query string. + * When the query is empty, returns symbols in the current file. + */ + query: string + + /** + * The maximum number of items to return. + */ + limit?: number +} + +/** + * The item returned by {@link ClientApi.listSymbols}. + */ +export interface ListSymbolItem { + /** + * The symbol name. + */ + label: string + + /** + * The filepath of the containing file. + */ + filepath: Filepath + + /** + * The line range of the symbol definition in the file. + */ + range: LineRange +} + +/** + * Parameters for retrieving Git changes from the client IDE. + * Used with {@link ClientApi.getChanges} method. + */ +export interface GetChangesParams { + /** + * The maximum number of characters to return in the change content. + * If not provided, all changes will be returned without character limitation. + */ + maxChars?: number +} +/** + * Represents a Git change item returned by the {@link ClientApi.getChanges} method. + * Contains information about modifications detected in the Git workspace. + */ +export interface ChangeItem { + /** + * The content of the change, formatted in the same Diff format as the `git diff` command. + * This includes information about added, modified, and deleted lines in the affected files. + */ + content: string + /** + * Indicates whether the change has been staged in the Git index. + * True if the change is staged, false if it is unstaged. + */ + staged: boolean +} diff --git a/clients/tabby-chat-panel/src/index.ts b/clients/tabby-chat-panel/src/index.ts new file mode 100644 index 000000000000..698eed473df2 --- /dev/null +++ b/clients/tabby-chat-panel/src/index.ts @@ -0,0 +1,7 @@ +import { version } from '../package.json' + +export const TABBY_CHAT_PANEL_API_VERSION: string = version +export * from './types' +export * from './thread' +export * from './server' +export * from './client' diff --git a/clients/tabby-chat-panel/src/react.ts b/clients/tabby-chat-panel/src/react.ts new file mode 100644 index 000000000000..e28abeaa13fb --- /dev/null +++ b/clients/tabby-chat-panel/src/react.ts @@ -0,0 +1,36 @@ +import { type RefObject, useEffect, useState } from 'react' +import { createThreadFromIframe, createThreadFromInsideIframe } from 'tabby-threads' +import { createClient, createServer } from './thread' +import type { ClientApi } from './client' +import type { ServerApi, ServerApiList } from './server' + +export function useClient(iframeRef: RefObject, api: ClientApi) { + const [client, setClient] = useState(null) + let isCreated = false + + useEffect(() => { + if (iframeRef.current && !isCreated) { + isCreated = true + const thread = createThreadFromIframe(iframeRef.current, { expose: api }) + createClient(thread).then(setClient) + } + }, [iframeRef.current]) + + return client +} + +export function useServer(api: ServerApi) { + const [server, setServer] = useState(null) + let isCreated = false + + useEffect(() => { + const isInIframe = window.self !== window.top + if (isInIframe && !isCreated) { + isCreated = true + const thread = createThreadFromInsideIframe({ expose: api }) + createServer(thread).then(setServer) + } + }, []) + + return server +} diff --git a/clients/tabby-chat-panel/src/server.ts b/clients/tabby-chat-panel/src/server.ts new file mode 100644 index 000000000000..4b9b37cc1a28 --- /dev/null +++ b/clients/tabby-chat-panel/src/server.ts @@ -0,0 +1,132 @@ +import type { EditorContext } from './types' + +export const serverApiVersionList = ['0.8.0', '0.9.0', '0.10.0'] + +export type ServerApi = ServerApiV0_10 + +export interface ServerApiList { + '0.8.0': ServerApiV0_8 + '0.9.0': ServerApiV0_9 | undefined + '0.10.0': ServerApiV0_10 | undefined +} + +export interface ServerApiV0_8 { + /** + * The initialization request sent from the client to the server. + * @param request {@link InitRequest} + */ + init: (request: InitRequest) => Promise + + /** + * @deprecated + * Show an error message in the chat panel. + * @param error {@link ErrorMessage} + */ + showError: (error: ErrorMessage) => Promise + + /** + * @deprecated + * Clear the current error message. + */ + cleanError: () => Promise + + /** + * Update the style and theme. + * @param style css style string + * @param themeClass dark or light + */ + updateTheme: (style: string, themeClass: 'dark' | 'light') => Promise + + /** + * Execute a predefined command. + * @param command {@link ChatCommand} + */ + executeCommand: (command: ChatCommand) => Promise + + /** + * Navigate to the specified view. + * @param view {@link ChatView} + */ + navigate: (view: ChatView) => Promise + + /** + * Add a relevant editor context. + * @param context the {@link EditorContext} to add as the relevant context. + */ + addRelevantContext: (context: EditorContext) => Promise + + /** + * Notify the server that the user has changed the active selection in the editor. + * @param selection the active selected {@link EditorContext} in the editor. + */ + updateActiveSelection: (selection: EditorContext | undefined | null) => Promise +} + +export interface ServerApiV0_9 extends ServerApiV0_8 { + /** + * Get the api version of the server. + * @returns the version string. + */ + getVersion: () => Promise +} + +export interface ServerApiV0_10 extends ServerApiV0_9 { + /** + * @since 0.10.0 added 'explain-terminal' + */ + executeCommand: (command: ChatCommand) => Promise + + /** + * @since 0.10.0 added terminal context support + */ + addRelevantContext: (context: EditorContext) => Promise +} + +/** + * Predefined commands used in {@link ServerApiV0_8.executeCommand}. + * - 'explain': Explain the selected code. + * - 'fix': Fix bugs in the selected code. + * - 'generate-docs': Generate documentation for the selected code. + * - 'generate-tests': Generate tests for the selected code. + * - 'code-review': Review the selected code and provide feedback. + * - 'explain-terminal': Explain the selected text in the terminal. @since 0.10.0 + */ +export type ChatCommand = 'explain' | 'fix' | 'generate-docs' | 'generate-tests' | 'code-review' | 'explain-terminal' + +/** + * The views used in {@link ServerApiV0_8.navigate}. + */ +export type ChatView = 'new-chat' | 'history' + +/** + * The params used in {@link ServerApiV0_8.init}. + */ +export interface InitRequest { + fetcherOptions: { + /** + * The authorization token. + */ + authorization: string + + /** + * Optional http headers to be sent with every request. + */ + headers?: Record + } + + /** + * Enabled the keyboard event handler to workaround for vscode webview issue: + * shortcut (cmd+a, cmd+c, cmd+v, cmd+x) not work in nested iframe in vscode webview + * see https://github.com/microsoft/vscode/issues/129178 + */ + useMacOSKeyboardEventHandler?: boolean +} + +/** + * @deprecated + * The params used in {@link ServerApiV0_8.showError}. + */ +export interface ErrorMessage { + title?: string + content: string +} diff --git a/clients/tabby-chat-panel/src/thread.ts b/clients/tabby-chat-panel/src/thread.ts new file mode 100644 index 000000000000..256798af9310 --- /dev/null +++ b/clients/tabby-chat-panel/src/thread.ts @@ -0,0 +1,43 @@ +import type { Thread } from 'tabby-threads' +import * as semver from 'semver' +import type { ClientApi } from './client' +import { type ServerApi, type ServerApiList, serverApiVersionList } from './server' + +export type CreateThreadFunction = (expose: Self) => Thread + +export async function createServer(thread: Thread): Promise { + const methods = await thread._requestMethods() as (keyof ClientApi)[] + + const clientApi: Record = {} + for (const method of methods) { + clientApi[method] = thread[method] + } + return clientApi as ClientApi +} + +function isCompatible(current: string, target: string): boolean { + const currentSemver = semver.coerce(current) + const targetSemver = semver.coerce(target) + if (!currentSemver || !targetSemver) { + return false + } + return semver.gte(currentSemver, targetSemver) +} + +export async function createClient(thread: Thread): Promise { + const methods = await thread._requestMethods() as (keyof ServerApi)[] + + const serverApi: Record = {} + for (const method of methods) { + serverApi[method] = thread[method] + } + + const serverVersion = await thread.getVersion() + const serverApiList: Record = {} + for (const version of serverApiVersionList) { + if (isCompatible(serverVersion, version)) { + serverApiList[version] = serverApi + } + } + return serverApiList as ServerApiList +} diff --git a/clients/tabby-chat-panel/src/types.ts b/clients/tabby-chat-panel/src/types.ts new file mode 100644 index 000000000000..ec2d79180c36 --- /dev/null +++ b/clients/tabby-chat-panel/src/types.ts @@ -0,0 +1,207 @@ +/** + * Represents a position in a file. + */ +export interface Position { + /** + * 1-based line number + */ + line: number + /** + * 1-based character number + */ + character: number +} + +/** + * Represents a range in a file. + */ +export interface PositionRange { + /** + * The start position of the range. + */ + start: Position + /** + * The end position of the range. + */ + end: Position +} + +/** + * Represents a range of lines in a file. + */ +export interface LineRange { + /** + * 1-based line number + */ + start: number + /** + * 1-based line number + */ + end: number +} + +/** + * Represents a location in a file. + * It could be a 1-based line number, a line range, a position or a position range. + */ +export type Location = number | LineRange | Position | PositionRange + +/** + * This is used for files in a Git repository, and should be used in priority. + */ +export interface FilepathInGitRepository { + kind: 'git' + + /** + * A string that is a relative path in the repository. + */ + filepath: string + + /** + * A URL used to identify the Git repository in both the client and server. + * The URL should be valid for use as a git remote URL, for example: + * 1. 'https://github.com/TabbyML/tabby' + * 2. 'git://github.com/TabbyML/tabby.git' + */ + gitUrl: string + + /** + * An optional Git revision which the file is at. + */ + revision?: string +} + +/** + * This is used for files in the workspace, but not in a Git repository. + */ +export interface FilepathInWorkspace { + kind: 'workspace' + + /** + * A string that is a relative path to `baseDir`. + */ + filepath: string + + /** + * A string that can be parsed as a URI, used to identify the directory in the client. + * The scheme of the URI could be 'file' or some other protocol to access the directory. + */ + baseDir: string +} + +/** + * This is used for files not in a Git repository and not in the workspace. + * Also used for untitled files not saved. + */ +export interface FilepathUri { + kind: 'uri' + + /** + * A string that can be parsed as a URI, used to identify the file in the client. + * The scheme of the URI could be: + * - 'untitled' means a new file not saved. + * - 'file', 'vscode-vfs' or some other protocol to access the file. + */ + uri: string +} + +/** + * Represents a filepath to identify a file. + */ +export type Filepath = FilepathInGitRepository | FilepathInWorkspace | FilepathUri + +/** + * Represents a client-side file context. + * This type should only be used for sending context from client to server. + */ +export interface EditorFileContext { + kind: 'file' + + /** + * The filepath of the file. + */ + filepath: Filepath + + /** + * The range of the selected content in the file. + * If the range is not provided, the whole file is considered. + */ + range?: LineRange | PositionRange + + /** + * The content of the file context. + */ + content: string +} + +/** + * Represents a client-side terminal context. + * This type should only be used for sending context from client to server. + * @since 0.10.0 + */ +export interface TerminalContext { + kind: 'terminal' + + /** + * The terminal name. + */ + name: string + + /** + * The terminal process id + */ + processId: number | undefined + + /** + * The selected text in the terminal. + */ + selection: string +} + +/** + * Represents a client-side context. + * This type should only be used for sending context from client to server. + * @since 0.10.0 added TerminalContext + */ +export type EditorContext = EditorFileContext | TerminalContext + +/** + * Represents a file and a location in it. + */ +export interface FileLocation { + /** + * The filepath of the file. + */ + filepath: Filepath + + /** + * The location in the file. + * It could be a 1-based line number, a line range, a position or a position range. + * If the location is not provided, the whole file is considered. + */ + location?: Location +} + +/** + * Represents a file reference for retrieving file content. + * If `range` is not provided, the entire file is considered. + */ +export interface FileRange { + /** + * The file path of the file. + */ + filepath: Filepath + + /** + * The range of the selected content in the file. + * If the range is not provided, the whole file is considered. + */ + range?: LineRange | PositionRange +} + +/** + * Includes information about a git repository in workspace folder + */ +export interface GitRepository { + url: string +} diff --git a/clients/tabby-chat-panel/test/index.test.ts b/clients/tabby-chat-panel/test/index.test.ts new file mode 100644 index 000000000000..401553c6b32a --- /dev/null +++ b/clients/tabby-chat-panel/test/index.test.ts @@ -0,0 +1,7 @@ +import { describe, expect, it } from 'vitest' + +describe('should', () => { + it('exported', () => { + expect(1).toEqual(1) + }) +}) diff --git a/clients/tabby-chat-panel/tsconfig.json b/clients/tabby-chat-panel/tsconfig.json new file mode 100644 index 000000000000..8fa9a910f3df --- /dev/null +++ b/clients/tabby-chat-panel/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2020", + "jsx": "react", + "lib": ["ESNext", "DOM"], + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "strict": true, + "strictNullChecks": true, + "esModuleInterop": true, + "skipDefaultLibCheck": true, + "skipLibCheck": true + }, + "include": ["src", "test", "build.config.ts"] +} diff --git a/clients/tabby-openapi/.gitignore b/clients/tabby-openapi/.gitignore new file mode 100644 index 000000000000..3c3629e647f5 --- /dev/null +++ b/clients/tabby-openapi/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/clients/tabby-openapi/compatible/index.d.ts b/clients/tabby-openapi/compatible/index.d.ts new file mode 100644 index 000000000000..72df1530b2eb --- /dev/null +++ b/clients/tabby-openapi/compatible/index.d.ts @@ -0,0 +1,31 @@ +import { + paths as _paths, + webhooks as _webhooks, + components as _components, + $defs as _$defs, + operations as _operations, +} from "../lib"; + +export interface paths extends Omit<_paths, "/v1/health"> { + // backward compatible for Tabby server 0.2.x and earlier + "/v1/health": Omit<_paths["/v1/health"], "post"> & { + post: operations["health"]; + }; + // backward compatible for Tabby server 0.10.x and earlier + "/v1beta/chat/completions": _paths["/v1/chat/completions"]; +} + +export type webhooks = _webhooks; +export type components = _components; +export type $defs = _$defs; + +export interface operations extends Omit<_operations, "event"> { + event: Omit<_operations["event"], "parameters"> & { + // Add a query parameter to specify the select kind + parameters: Omit<_operations["event"]["parameters"], "query"> & { + query: { + select_kind?: string | undefined | null; + }; + }; + }; +} diff --git a/clients/tabby-openapi/lib/index.d.ts b/clients/tabby-openapi/lib/index.d.ts new file mode 100644 index 000000000000..822bdbb763ba --- /dev/null +++ b/clients/tabby-openapi/lib/index.d.ts @@ -0,0 +1 @@ +export * from "./tabby"; diff --git a/clients/tabby-openapi/lib/openai.d.ts b/clients/tabby-openapi/lib/openai.d.ts new file mode 100644 index 000000000000..58caf3c33245 --- /dev/null +++ b/clients/tabby-openapi/lib/openai.d.ts @@ -0,0 +1,14 @@ +import type { OpenAI } from "openai"; + +type ChatCompletionRequest = OpenAI.ChatCompletionCreateParams; + +type ChatCompletionChunk = OpenAI.ChatCompletionChunk; + +// Omit `name` and mark as optional. +// However, `name` is required when the `role` is `function`. +// This patch is for compatible with the type `Message` in https://www.npmjs.com/package/ai +type ChatCompletionRequestMessage = Omit & { + name?: string; +}; + +export { ChatCompletionRequest, ChatCompletionChunk, ChatCompletionRequestMessage }; diff --git a/clients/tabby-openapi/lib/tabby.d.ts b/clients/tabby-openapi/lib/tabby.d.ts new file mode 100644 index 000000000000..01e4cb46537e --- /dev/null +++ b/clients/tabby-openapi/lib/tabby.d.ts @@ -0,0 +1,430 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + + +import { ChatCompletionRequest, ChatCompletionChunk, ChatCompletionRequestMessage } from './openai'; + +export interface paths { + "/v1/chat/completions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["chat_completions"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/completions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["completion"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/events": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["event"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/health": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["health"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1beta/server_setting": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["config"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + Choice: { + /** Format: int32 */ + index: number; + text: string; + }; + CodeSearchDocument: { + body: string; + filepath: string; + git_url: string; + language: string; + start_line: number; + }; + CodeSearchHit: { + scores: components["schemas"]["CodeSearchScores"]; + doc: components["schemas"]["CodeSearchDocument"]; + }; + CodeSearchQuery: { + git_url: string; + filepath?: string | null; + language?: string | null; + content: string; + }; + CodeSearchScores: { + /** + * Format: float + * @description Reciprocal rank fusion score: https://www.elastic.co/guide/en/elasticsearch/reference/current/rrf.html + */ + rrf: number; + /** Format: float */ + bm25: number; + /** Format: float */ + embedding: number; + }; + /** @example { + * "language": "python", + * "segments": { + * "prefix": "def fib(n):\n ", + * "suffix": "\n return fib(n - 1) + fib(n - 2)" + * } + * } */ + CompletionRequest: { + /** + * @description Language identifier, full list is maintained at + * https://code.visualstudio.com/docs/languages/identifiers + * @example python + */ + language?: string | null; + segments?: components["schemas"]["Segments"] | null; + /** @description A unique identifier representing your end-user, which can help Tabby to monitor & generating + * reports. */ + user?: string | null; + debug_options?: components["schemas"]["DebugOptions"] | null; + /** + * Format: float + * @description The temperature parameter for the model, used to tune variance and "creativity" of the model output + */ + temperature?: number | null; + /** + * Format: int64 + * @description The seed used for randomly selecting tokens + */ + seed?: number | null; + }; + /** @example { + * "choices": [ + * { + * "index": 0, + * "text": "string" + * } + * ], + * "id": "string" + * } */ + CompletionResponse: { + id: string; + choices: components["schemas"]["Choice"][]; + debug_data?: components["schemas"]["DebugData"] | null; + }; + DebugData: { + snippets?: components["schemas"]["Snippet"][] | null; + prompt?: string | null; + }; + DebugOptions: { + /** @description When `raw_prompt` is specified, it will be passed directly to the inference engine for completion. `segments` field in `CompletionRequest` will be ignored. + * + * This is useful for certain requests that aim to test the tabby's e2e quality. */ + raw_prompt?: string | null; + /** @description When true, returns `snippets` in `debug_data`. */ + return_snippets?: boolean; + /** @description When true, returns `prompt` in `debug_data`. */ + return_prompt?: boolean; + /** @description When true, disable retrieval augmented code completion. */ + disable_retrieval_augmented_code_completion?: boolean; + }; + /** @description A snippet of declaration code that is relevant to the current completion request. */ + Declaration: { + /** @description Filepath of the file where the snippet is from. + * - When the file belongs to the same workspace as the current file, + * this is a relative filepath, use the same rule as [Segments::filepath]. + * - When the file located outside the workspace, such as in a dependency package, + * this is a file URI with an absolute filepath. */ + filepath: string; + /** @description Body of the snippet. */ + body: string; + }; + DocSearchDocument: { + title: string; + link: string; + snippet: string; + }; + DocSearchHit: { + /** Format: float */ + score: number; + doc: components["schemas"]["DocSearchDocument"]; + }; + HealthState: { + model?: string | null; + chat_model?: string | null; + chat_device?: string | null; + device: string; + arch: string; + cpu_info: string; + cpu_count: number; + cuda_devices: string[]; + version: components["schemas"]["Version"]; + webserver?: boolean | null; + }; + LogEventRequest: { + /** + * @description Event type, should be `view`, `select` or `dismiss`. + * @example view + */ + type: string; + completion_id: string; + /** Format: int32 */ + choice_index: number; + view_id?: string | null; + /** Format: int32 */ + elapsed?: number | null; + }; + Segments: { + /** @description Content that appears before the cursor in the editor window. */ + prefix: string; + /** @description Content that appears after the cursor in the editor window. */ + suffix?: string | null; + /** @description The relative path of the file that is being edited. + * - When [Segments::git_url] is set, this is the path of the file in the git repository. + * - When [Segments::git_url] is empty, this is the path of the file in the workspace. */ + filepath?: string | null; + /** @description The remote URL of the current git repository. + * Leave this empty if the file is not in a git repository, + * or the git repository does not have a remote URL. */ + git_url?: string | null; + /** @description The relevant declaration code snippets provided by the editor's LSP, + * contain declarations of symbols extracted from [Segments::prefix]. */ + declarations?: components["schemas"]["Declaration"][] | null; + /** @description The relevant code snippets extracted from recently edited files. + * These snippets are selected from candidates found within code chunks + * based on the edited location. + * The current editing file is excluded from the search candidates. + * + * When provided alongside [Segments::declarations], the snippets have + * already been deduplicated to ensure no duplication with entries + * in [Segments::declarations]. + * + * Sorted in descending order of [Snippet::score]. */ + relevant_snippets_from_changed_files?: components["schemas"]["Snippet"][] | null; + /** @description The relevant code snippets extracted from recently opened files. These snippets are selected from candidates found within code chunks based on the last visited location. Current Active file is excluded from the search candidates. When provided with [Segments::relevant_snippets_from_changed_files], the snippets have already been deduplicated to ensure no duplication with entries in [Segments::relevant_snippets_from_changed_files]. */ + relevant_snippets_from_recently_opened_files?: components["schemas"]["Snippet"][] | null; + /** @description Clipboard content when requesting code completion. */ + clipboard?: string | null; + }; + ServerSetting: { + disable_client_side_telemetry: boolean; + }; + Snippet: { + filepath: string; + body: string; + /** Format: float */ + score: number; + }; + Version: { + build_date: string; + build_timestamp: string; + git_sha: string; + git_describe: string; + }; + ChatCompletionRequest: ChatCompletionRequest; + ChatCompletionChunk: ChatCompletionChunk; + ChatCompletionRequestMessage: ChatCompletionRequestMessage; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + chat_completions: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ChatCompletionRequest"]; + }; + }; + responses: { + /** @description Success */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "text/event-stream": components["schemas"]["ChatCompletionChunk"]; + }; + }; + /** @description When chat model is not specified, the endpoint returns 405 Method Not Allowed */ + 405: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description When the prompt is malformed, the endpoint returns 422 Unprocessable Entity */ + 422: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + completion: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CompletionRequest"]; + }; + }; + responses: { + /** @description Success */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CompletionResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + event: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["LogEventRequest"]; + }; + }; + responses: { + /** @description Success */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + health: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Success */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HealthState"]; + }; + }; + }; + }; + config: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Success */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServerSetting"]; + }; + }; + }; + }; +} diff --git a/clients/tabby-openapi/openapi.json b/clients/tabby-openapi/openapi.json new file mode 100644 index 000000000000..2382e69deb6e --- /dev/null +++ b/clients/tabby-openapi/openapi.json @@ -0,0 +1 @@ +{"openapi":"3.0.3","info":{"title":"Tabby Server","description":"\n[![tabby stars](https://img.shields.io/github/stars/TabbyML/tabby)](https://github.com/TabbyML/tabby)\n[![Join Slack](https://shields.io/badge/Join-Tabby%20Slack-red?logo=slack)](https://links.tabbyml.com/join-slack)\n\nInstall following IDE / Editor extensions to get started with [Tabby](https://github.com/TabbyML/tabby).\n* [VSCode Extension](https://github.com/TabbyML/tabby/tree/main/clients/vscode) – Install from the [marketplace](https://marketplace.visualstudio.com/items?itemName=TabbyML.vscode-tabby), or [open-vsx.org](https://open-vsx.org/extension/TabbyML/vscode-tabby)\n* [VIM Extension](https://github.com/TabbyML/tabby/tree/main/clients/vim)\n* [IntelliJ Platform Plugin](https://github.com/TabbyML/tabby/tree/main/clients/intellij) – Install from the [marketplace](https://plugins.jetbrains.com/plugin/22379-tabby)\n","contact":{"name":"TabbyML Team"},"license":{"name":"Apache 2.0","url":"https://github.com/TabbyML/tabby/blob/main/LICENSE"},"version":"0.17.0-dev.0"},"servers":[{"url":"/","description":"Server"}],"paths":{"/v1/chat/completions":{"post":{"tags":["v1"],"operationId":"chat_completions","requestBody":{"description":"","content":{"application/json":{"schema":{}}},"required":true},"responses":{"200":{"description":"Success"},"405":{"description":"When chat model is not specified, the endpoint returns 405 Method Not Allowed"},"422":{"description":"When the prompt is malformed, the endpoint returns 422 Unprocessable Entity"}},"security":[{"token":[]}]}},"/v1/completions":{"post":{"tags":["v1"],"operationId":"completion","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompletionRequest"}}},"required":true},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompletionResponse"}}}},"400":{"description":"Bad Request"}},"security":[{"token":[]}]}},"/v1/events":{"post":{"tags":["v1"],"operationId":"event","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogEventRequest"}}},"required":true},"responses":{"200":{"description":"Success"},"400":{"description":"Bad Request"}},"security":[{"token":[]}]}},"/v1/health":{"get":{"tags":["v1"],"operationId":"health","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthState"}}}}},"security":[{"token":[]}]}},"/v1beta/server_setting":{"get":{"tags":["v1beta"],"operationId":"config","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerSetting"}}}}},"security":[{"token":[]}]}}},"components":{"schemas":{"Choice":{"type":"object","required":["index","text"],"properties":{"index":{"type":"integer","format":"int32","minimum":0},"text":{"type":"string"}}},"CodeSearchDocument":{"type":"object","required":["body","filepath","git_url","language","start_line"],"properties":{"body":{"type":"string"},"filepath":{"type":"string"},"git_url":{"type":"string"},"language":{"type":"string"},"start_line":{"type":"integer","minimum":0}}},"CodeSearchHit":{"type":"object","required":["scores","doc"],"properties":{"scores":{"$ref":"#/components/schemas/CodeSearchScores"},"doc":{"$ref":"#/components/schemas/CodeSearchDocument"}}},"CodeSearchQuery":{"type":"object","required":["git_url","content"],"properties":{"git_url":{"type":"string"},"filepath":{"type":"string","nullable":true},"language":{"type":"string","nullable":true},"content":{"type":"string"}}},"CodeSearchScores":{"type":"object","required":["rrf","bm25","embedding"],"properties":{"rrf":{"type":"number","format":"float","description":"Reciprocal rank fusion score: https://www.elastic.co/guide/en/elasticsearch/reference/current/rrf.html"},"bm25":{"type":"number","format":"float"},"embedding":{"type":"number","format":"float"}}},"CompletionRequest":{"type":"object","properties":{"language":{"type":"string","description":"Language identifier, full list is maintained at\nhttps://code.visualstudio.com/docs/languages/identifiers","example":"python","nullable":true},"segments":{"allOf":[{"$ref":"#/components/schemas/Segments"}],"nullable":true},"user":{"type":"string","description":"A unique identifier representing your end-user, which can help Tabby to monitor & generating\nreports.","nullable":true},"debug_options":{"allOf":[{"$ref":"#/components/schemas/DebugOptions"}],"nullable":true},"temperature":{"type":"number","format":"float","description":"The temperature parameter for the model, used to tune variance and \"creativity\" of the model output","nullable":true},"seed":{"type":"integer","format":"int64","description":"The seed used for randomly selecting tokens","nullable":true,"minimum":0}},"example":{"language":"python","segments":{"prefix":"def fib(n):\n ","suffix":"\n return fib(n - 1) + fib(n - 2)"}}},"CompletionResponse":{"type":"object","required":["id","choices"],"properties":{"id":{"type":"string"},"choices":{"type":"array","items":{"$ref":"#/components/schemas/Choice"}},"debug_data":{"allOf":[{"$ref":"#/components/schemas/DebugData"}],"nullable":true}},"example":{"choices":[{"index":0,"text":"string"}],"id":"string"}},"DebugData":{"type":"object","properties":{"snippets":{"type":"array","items":{"$ref":"#/components/schemas/Snippet"},"nullable":true},"prompt":{"type":"string","nullable":true}}},"DebugOptions":{"type":"object","properties":{"raw_prompt":{"type":"string","description":"When `raw_prompt` is specified, it will be passed directly to the inference engine for completion. `segments` field in `CompletionRequest` will be ignored.\n\nThis is useful for certain requests that aim to test the tabby's e2e quality.","nullable":true},"return_snippets":{"type":"boolean","description":"When true, returns `snippets` in `debug_data`."},"return_prompt":{"type":"boolean","description":"When true, returns `prompt` in `debug_data`."},"disable_retrieval_augmented_code_completion":{"type":"boolean","description":"When true, disable retrieval augmented code completion."}}},"Declaration":{"type":"object","description":"A snippet of declaration code that is relevant to the current completion request.","required":["filepath","body"],"properties":{"filepath":{"type":"string","description":"Filepath of the file where the snippet is from.\n- When the file belongs to the same workspace as the current file,\nthis is a relative filepath, use the same rule as [Segments::filepath].\n- When the file located outside the workspace, such as in a dependency package,\nthis is a file URI with an absolute filepath."},"body":{"type":"string","description":"Body of the snippet."}}},"DocSearchDocument":{"type":"object","required":["title","link","snippet"],"properties":{"title":{"type":"string"},"link":{"type":"string"},"snippet":{"type":"string"}}},"DocSearchHit":{"type":"object","required":["score","doc"],"properties":{"score":{"type":"number","format":"float"},"doc":{"$ref":"#/components/schemas/DocSearchDocument"}}},"HealthState":{"type":"object","required":["device","arch","cpu_info","cpu_count","cuda_devices","version"],"properties":{"model":{"type":"string","nullable":true},"chat_model":{"type":"string","nullable":true},"chat_device":{"type":"string","nullable":true},"device":{"type":"string"},"arch":{"type":"string"},"cpu_info":{"type":"string"},"cpu_count":{"type":"integer","minimum":0},"cuda_devices":{"type":"array","items":{"type":"string"}},"version":{"$ref":"#/components/schemas/Version"},"webserver":{"type":"boolean","nullable":true}}},"LogEventRequest":{"type":"object","required":["type","completion_id","choice_index"],"properties":{"type":{"type":"string","description":"Event type, should be `view`, `select` or `dismiss`.","example":"view"},"completion_id":{"type":"string"},"choice_index":{"type":"integer","format":"int32","minimum":0},"view_id":{"type":"string","nullable":true},"elapsed":{"type":"integer","format":"int32","nullable":true,"minimum":0}}},"Segments":{"type":"object","required":["prefix"],"properties":{"prefix":{"type":"string","description":"Content that appears before the cursor in the editor window."},"suffix":{"type":"string","description":"Content that appears after the cursor in the editor window.","nullable":true},"filepath":{"type":"string","description":"The relative path of the file that is being edited.\n- When [Segments::git_url] is set, this is the path of the file in the git repository.\n- When [Segments::git_url] is empty, this is the path of the file in the workspace.","nullable":true},"git_url":{"type":"string","description":"The remote URL of the current git repository.\nLeave this empty if the file is not in a git repository,\nor the git repository does not have a remote URL.","nullable":true},"declarations":{"type":"array","items":{"$ref":"#/components/schemas/Declaration"},"description":"The relevant declaration code snippets provided by the editor's LSP,\ncontain declarations of symbols extracted from [Segments::prefix].","nullable":true},"relevant_snippets_from_changed_files":{"type":"array","items":{"$ref":"#/components/schemas/Snippet"},"description":"The relevant code snippets extracted from recently edited files.\nThese snippets are selected from candidates found within code chunks\nbased on the edited location.\nThe current editing file is excluded from the search candidates.\n\nWhen provided alongside [Segments::declarations], the snippets have\nalready been deduplicated to ensure no duplication with entries\nin [Segments::declarations].\n\nSorted in descending order of [Snippet::score].","nullable":true},"relevant_snippets_from_recently_opened_files": {"type": "array","items": {"$ref": "#/components/schemas/Snippet"},"description": "The relevant code snippets extracted from recently opened files. These snippets are selected from candidates found within code chunks based on the last visited location. Current Active file is excluded from the search candidates. When provided with [Segments::relevant_snippets_from_changed_files], the snippets have already been deduplicated to ensure no duplication with entries in [Segments::relevant_snippets_from_changed_files].","nullable": true},"clipboard":{"type":"string","description":"Clipboard content when requesting code completion.","nullable":true}}},"ServerSetting":{"type":"object","required":["disable_client_side_telemetry"],"properties":{"disable_client_side_telemetry":{"type":"boolean"}}},"Snippet":{"type":"object","required":["filepath","body","score"],"properties":{"filepath":{"type":"string"},"body":{"type":"string"},"score":{"type":"number","format":"float"}}},"Version":{"type":"object","required":["build_date","build_timestamp","git_sha","git_describe"],"properties":{"build_date":{"type":"string"},"build_timestamp":{"type":"string"},"git_sha":{"type":"string"},"git_describe":{"type":"string"}}}},"securitySchemes":{"token":{"type":"http","scheme":"bearer","bearerFormat":"token"}}}} \ No newline at end of file diff --git a/clients/tabby-openapi/package.json b/clients/tabby-openapi/package.json new file mode 100644 index 000000000000..4e0716c8ad82 --- /dev/null +++ b/clients/tabby-openapi/package.json @@ -0,0 +1,19 @@ +{ + "name": "tabby-openapi", + "version": "0.12.0-dev", + "description": "Tabby API schema for typescript.", + "files": [ + "lib/**/*.d.ts", + "compatible/**/*.d.ts" + ], + "types": "./lib/index.d.ts", + "scripts": { + "codegen": "node scripts/codegen.js", + "codegen:fetch": "node scripts/codegen.js --fetch" + }, + "devDependencies": { + "openai": "^4.52.7", + "openapi-typescript": "^7.0.2", + "typescript": "^5.3.2" + } +} diff --git a/clients/tabby-openapi/scripts/codegen.js b/clients/tabby-openapi/scripts/codegen.js new file mode 100644 index 000000000000..4bcff240a803 --- /dev/null +++ b/clients/tabby-openapi/scripts/codegen.js @@ -0,0 +1,62 @@ +#!/usr/bin/env node + +/** + * Usage: node scripts/codegen.js [--fetch []] + * - no-fetch: run codegen only + * - fetch: fetch openapi schema json from url, then run codegen + */ + +const path = require("path"); +const fs = require("fs"); +const openapiTS = require("openapi-typescript"); +const ts = require("typescript"); + +const shouldDownload = process.argv[2] === "--fetch"; +const schemaUrl = new URL(process.argv[3] || "http://127.0.0.1:8080/api-docs/openapi.json"); +const jsonFile = path.join(__dirname, "../openapi.json"); +const dtsFile = path.join(__dirname, "../lib/tabby.d.ts"); + + +function patchOpenAiTypes(schema) { + schema["paths"]["/v1/chat/completions"]["post"]["requestBody"]["content"]["application/json"]["schema"] = { "$ref": "#/components/schemas/ChatCompletionRequest" }; + schema["paths"]["/v1/chat/completions"]["post"]["responses"]["200"]["content"] = { "text/event-stream": { "schema": { "$ref": "#/components/schemas/ChatCompletionChunk" } } }; + schema["components"]["schemas"]["ChatCompletionRequest"] = { "type": "object" }; + schema["components"]["schemas"]["ChatCompletionChunk"] = { "type": "object" }; + schema["components"]["schemas"]["ChatCompletionRequestMessage"] = { "type": "object" }; + return schema; +} + +async function download(url, outputFile) { + const response = await fetch(url); + const data = await response.text(); + fs.writeFileSync(outputFile, data); +} + +async function main() { + if (shouldDownload) { + await download(schemaUrl, jsonFile); + } + const schema = JSON.parse(fs.readFileSync(jsonFile, "utf-8")); + const patched = patchOpenAiTypes(schema); + const ast = await openapiTS.default(patched, { + transform: (schemaObject, metadata) => { + if (metadata.path === "#/components/schemas/ChatCompletionRequest") { + return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("ChatCompletionRequest")); + } + if (metadata.path === "#/components/schemas/ChatCompletionChunk") { + return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("ChatCompletionChunk")); + } + if (metadata.path === "#/components/schemas/ChatCompletionRequestMessage") { + return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("ChatCompletionRequestMessage")); + } + } + }); + const typeContents = openapiTS.astToString(ast); + const importContents = "import { ChatCompletionRequest, ChatCompletionChunk, ChatCompletionRequestMessage } from './openai';\n"; + const contents = openapiTS.COMMENT_HEADER + "\n" + importContents + "\n" + typeContents; + fs.writeFileSync(dtsFile, contents); + + console.log("✅ Generated types successfully.") +} + +main(); diff --git a/clients/tabby-threads/.gitignore b/clients/tabby-threads/.gitignore new file mode 100644 index 000000000000..7d1b0d18274c --- /dev/null +++ b/clients/tabby-threads/.gitignore @@ -0,0 +1,2 @@ +.rollup.cache +dist \ No newline at end of file diff --git a/clients/tabby-threads/README.md b/clients/tabby-threads/README.md new file mode 100644 index 000000000000..52eddae68a1e --- /dev/null +++ b/clients/tabby-threads/README.md @@ -0,0 +1,18 @@ +# Tabby-Threads + +**Tabby-Threads** is an internally customized version of [`@quilted/threads`](https://github.com/lemonmade/quilt/tree/%40quilted/threads%402.2.0/packages/threads) 2.2.0, specifically designed for the Tabby project. This library adapts and extends the functionality of `@quilted/threads` to better meet the unique requirements of the project. + +## Purpose + +- **Internal Use Only**: This library is strictly for use within the Tabby project and is not intended for external distribution or public usage. +- **Custom Enhancements**: Includes modifications and optimizations tailored for Tabby’s specific use cases. +- **No External Support**: As an internal tool, `tabby-threads` does not include usage documentation or external support. + +## Foundation + +Tabby-Threads builds upon `@quilted/threads` 2.2.0, inheriting its core features: + +- Cross-thread communication via multiple mechanisms like `Worker`, `MessagePort`, and `BroadcastChannel`. +- Robust TypeScript support with thread-safe APIs. + +For further details on the underlying framework, refer to the official [`@quilted/threads`](https://github.com/lemonmade/quilt/tree/main/packages/threads) documentation. diff --git a/clients/tabby-threads/package.json b/clients/tabby-threads/package.json new file mode 100644 index 000000000000..cb92f0b2eb0d --- /dev/null +++ b/clients/tabby-threads/package.json @@ -0,0 +1,27 @@ +{ + "name": "tabby-threads", + "type": "module", + "version": "1.0.0", + "exports": { + ".": { + "types": "./dist/types/index.d.ts", + "import": "./dist/esm/index.mjs", + "require": "./dist/cjs/index.cjs" + } + }, + "main": "./dist/cjs/index.cjs", + "module": "./dist/esm/index.mjs", + "types": "./dist/types/index.d.ts", + "scripts": { + "build": "tsc --emitDeclarationOnly && rollup -c rollup.config.js" + }, + "dependencies": { + "@quilted/events": "2.0.0", + "@quilted/signals": "0.2.1" + }, + "devDependencies": { + "@rollup/plugin-typescript": "^12.1.1", + "rollup": "^4.27.3", + "typescript": "^5.3.3" + } +} \ No newline at end of file diff --git a/clients/tabby-threads/rollup.config.js b/clients/tabby-threads/rollup.config.js new file mode 100644 index 000000000000..3be16400b13f --- /dev/null +++ b/clients/tabby-threads/rollup.config.js @@ -0,0 +1,29 @@ +import typescript from "@rollup/plugin-typescript"; +import { defineConfig } from "rollup"; + +export default defineConfig([{ + input: "source/index.ts", + output: [ + { + dir: "dist", + format: "esm", + entryFileNames: "esm/[name].mjs", + preserveModules: true, + preserveModulesRoot: "source", + }, + { + dir: "dist", + format: "cjs", + entryFileNames: "cjs/[name].cjs", + preserveModules: true, + preserveModulesRoot: "source", + }, + ], + plugins: [ + typescript({ + tsconfig: "./tsconfig.json", + noEmitOnError: true, + }), + ], + external: ["@quilted/events", "@preact/signals"], +}]); diff --git a/clients/tabby-threads/source/abort-signal.ts b/clients/tabby-threads/source/abort-signal.ts new file mode 100644 index 000000000000..b701ebbe5370 --- /dev/null +++ b/clients/tabby-threads/source/abort-signal.ts @@ -0,0 +1,3 @@ +export { acceptThreadAbortSignal } from "./abort-signal/accept"; +export { createThreadAbortSignal } from "./abort-signal/create"; +export type { ThreadAbortSignal } from "./abort-signal/types"; diff --git a/clients/tabby-threads/source/abort-signal/accept.ts b/clients/tabby-threads/source/abort-signal/accept.ts new file mode 100644 index 000000000000..bfad16a649ac --- /dev/null +++ b/clients/tabby-threads/source/abort-signal/accept.ts @@ -0,0 +1,48 @@ +import { retain, release } from "../memory"; +import type { ThreadAbortSignal } from "./types"; + +/** + * Call this function in a thread receiving a `ThreadAbortSignal` to + * turn it into a "live" `AbortSignal`. The resulting signal will + * connect the thread to its sending pair, and will abort it when the + * original `AbortSignal` is aborted. + */ +export function acceptThreadAbortSignal( + signal: AbortSignal | ThreadAbortSignal +): AbortSignal { + if (isAbortSignal(signal)) return signal; + + const abort = new AbortController(); + + const { aborted, start } = signal; + + if (aborted) { + abort.abort(); + return abort.signal; + } + + if (start) { + retain(start); + start((aborted) => { + if (aborted) abort.abort(); + }); + } + + abort.signal.addEventListener( + "abort", + async () => { + release(start); + }, + { once: true } + ); + + return abort.signal; +} + +function isAbortSignal(value?: unknown): value is AbortSignal { + return ( + value != null && + (value as any).aborted != null && + typeof (value as any).addEventListener === "function" + ); +} diff --git a/clients/tabby-threads/source/abort-signal/create.ts b/clients/tabby-threads/source/abort-signal/create.ts new file mode 100644 index 000000000000..efca82c92318 --- /dev/null +++ b/clients/tabby-threads/source/abort-signal/create.ts @@ -0,0 +1,45 @@ +import { retain, release } from "../memory"; +import type { ThreadAbortSignal } from "./types.ts"; + +/** + * Converts an `AbortSignal` into a version of that signal that can + * be transferred to a target `Thread`. The resulting object can be + * transferred to the paired thread, and turned into an actual `AbortSignal` + * object using `acceptThreadAbortSignal()`. + */ +export function createThreadAbortSignal( + signal: AbortSignal +): ThreadAbortSignal { + const listeners = new Set<(aborted: boolean) => void>(); + + if (signal.aborted) { + return { + aborted: true, + }; + } + + signal.addEventListener( + "abort", + () => { + for (const listener of listeners) { + listener(signal.aborted); + release(listener); + } + + listeners.clear(); + }, + { once: true } + ); + + return { + aborted: false, + start(listener) { + if (signal.aborted) { + listener(true); + } else { + retain(listener); + listeners.add(listener); + } + }, + }; +} diff --git a/clients/tabby-threads/source/abort-signal/types.ts b/clients/tabby-threads/source/abort-signal/types.ts new file mode 100644 index 000000000000..7be479215a7e --- /dev/null +++ b/clients/tabby-threads/source/abort-signal/types.ts @@ -0,0 +1,19 @@ +/** + * A representation of an `AbortSignal` that can be serialized between + * two threads. + */ +export interface ThreadAbortSignal { + /** + * Whether the signal was already aborted at the time it was + * sent to the sibling thread. + */ + aborted: boolean; + + /** + * A function to connect the signal between the two threads. This + * function should be called by the sibling thread when the abort + * state changes (including changes since the thread-safe abort signal + * was created). + */ + start?(listener: (aborted: boolean) => void): void; +} diff --git a/clients/tabby-threads/source/constants.ts b/clients/tabby-threads/source/constants.ts new file mode 100644 index 000000000000..ec34371d9d1a --- /dev/null +++ b/clients/tabby-threads/source/constants.ts @@ -0,0 +1,5 @@ +export const RETAIN_METHOD = Symbol.for('quilt.threads.retain'); +export const RELEASE_METHOD = Symbol.for('quilt.threads.release'); +export const RETAINED_BY = Symbol.for('quilt.threads.retained-by'); +export const ENCODE_METHOD = Symbol.for('quilt.threads.encode'); +export const TRANSFERABLE = Symbol.for('quilt.threads.transferable'); diff --git a/clients/tabby-threads/source/encoding.ts b/clients/tabby-threads/source/encoding.ts new file mode 100644 index 000000000000..4739a5f64da7 --- /dev/null +++ b/clients/tabby-threads/source/encoding.ts @@ -0,0 +1 @@ +export { createBasicEncoder } from "./encoding/basic"; diff --git a/clients/tabby-threads/source/encoding/basic.ts b/clients/tabby-threads/source/encoding/basic.ts new file mode 100644 index 000000000000..3253e7ed191f --- /dev/null +++ b/clients/tabby-threads/source/encoding/basic.ts @@ -0,0 +1,320 @@ +import { ENCODE_METHOD, TRANSFERABLE } from "../constants"; +import type { + ThreadEncoder, + ThreadEncoderApi, + ThreadEncodable, +} from "../types"; +import { + isBasicObject, + isMemoryManageable, + type MemoryRetainer, +} from "../memory"; + +const FUNCTION = "_@f"; +const MAP = "_@m"; +const SET = "_@s"; +const URL_ID = "_@u"; +const DATE = "_@d"; +const REGEXP = "_@r"; +const ASYNC_ITERATOR = "_@i"; + +export interface ThreadEncoderOptions { + /** + * Customizes the encoding of each value in a passed message. + */ + encode?( + value: unknown, + defaultEncode: (value: unknown) => [any, Transferable[]?] + ): [any, Transferable[]?]; + + /** + * Customizes the decoding of each value in a passed message. + */ + decode?( + value: unknown, + defaultDecode: ( + value: unknown, + retainedBy?: Iterable + ) => unknown, + retainedBy?: Iterable + ): unknown; +} + +/** + * Creates an encoder that converts most common JavaScript types into a format + * that can be transferred via message passing. + */ +export function createBasicEncoder({ + encode: encodeOverride, + decode: decodeOverride, +}: ThreadEncoderOptions = {}): ThreadEncoder { + return { + encode, + decode, + }; + + type EncodeResult = ReturnType; + + interface EncodeContext { + api: ThreadEncoderApi; + seen: Map; + encode: Parameters>[1]; + } + + function encode(value: unknown, api: ThreadEncoderApi): EncodeResult { + const context: EncodeContext = { + api, + seen: new Map(), + encode: (value: any) => encodeInternal(value, context, true), + }; + + return encodeInternal(value, context); + } + + function encodeInternal( + value: unknown, + context: EncodeContext, + isFromOverride = false + ): EncodeResult { + const { seen, api, encode } = context; + + if (!isFromOverride && encodeOverride) { + return encodeOverride(value, encode); + } + + if (value == null) return [value]; + + const seenValue = seen.get(value); + if (seenValue) return seenValue; + + seen.set(value, [undefined]); + + if (typeof value === "object") { + if ((value as any)[TRANSFERABLE]) { + const result: EncodeResult = [value, [value as any]]; + seen.set(value, result); + return result; + } + + const transferables: Transferable[] = []; + const encodeValue = (value: any) => { + const [fieldValue, nestedTransferables = []] = encodeInternal( + value, + context + ); + transferables.push(...nestedTransferables); + return fieldValue; + }; + + if (typeof (value as any)[ENCODE_METHOD] === "function") { + const result = (value as ThreadEncodable)[ENCODE_METHOD]({ + encode: encodeValue, + }); + + const fullResult: EncodeResult = [result, transferables]; + seen.set(value, fullResult); + + return fullResult; + } + + if (Array.isArray(value)) { + const result = value.map((item) => encodeValue(item)); + const fullResult: EncodeResult = [result, transferables]; + seen.set(value, fullResult); + return fullResult; + } + + // TODO: avoid this if using a `structuredClone` postMessage-ing object? + if (value instanceof RegExp) { + const result = { [REGEXP]: [value.source, value.flags] }; + const fullResult: EncodeResult = [result, transferables]; + seen.set(value, fullResult); + return fullResult; + } + + if (value instanceof URL) { + const result = { [URL_ID]: value.href }; + const fullResult: EncodeResult = [result, transferables]; + seen.set(value, fullResult); + return fullResult; + } + + if (value instanceof Date) { + const result = { [DATE]: value.toISOString() }; + const fullResult: EncodeResult = [result, transferables]; + seen.set(value, fullResult); + return fullResult; + } + + if (value instanceof Map) { + const entries = [...value.entries()].map(([key, value]) => { + return [encodeValue(key), encodeValue(value)]; + }); + const result = { [MAP]: entries }; + const fullResult: EncodeResult = [result, transferables]; + seen.set(value, fullResult); + return fullResult; + } + + if (value instanceof Set) { + const entries = [...value].map((entry) => encodeValue(entry)); + const result = { [SET]: entries }; + const fullResult: EncodeResult = [result, transferables]; + seen.set(value, fullResult); + return fullResult; + } + + const valueIsIterator = isIterator(value); + + if (isBasicObject(value) || valueIsIterator) { + const result: Record = {}; + + for (const key of Object.keys(value)) { + result[key] = encodeValue((value as any)[key]); + } + + if (valueIsIterator) { + result.next ??= encodeValue((value as any).next.bind(value)); + result.return ??= encodeValue((value as any).return.bind(value)); + result.throw ??= encodeValue((value as any).throw.bind(value)); + result[ASYNC_ITERATOR] = true; + } + + const fullResult: EncodeResult = [result, transferables]; + seen.set(value, fullResult); + + return fullResult; + } + } + + if (typeof value === "function") { + const id = api.functions?.add(value); + + if (id == null) return [id]; + + const result: EncodeResult = [{ [FUNCTION]: id }]; + seen.set(value, result); + + return result; + } + + const result: EncodeResult = [value]; + seen.set(value, result); + + return result; + } + + interface DecodeContext { + api: ThreadEncoderApi; + decode: Parameters>[1]; + } + + function decode( + value: unknown, + api: ThreadEncoderApi, + retainedBy?: Iterable + ) { + const context: DecodeContext = { + api, + decode: (value: any) => decodeInternal(value, context, retainedBy, true), + }; + + return decodeInternal(value, context); + } + + function decodeInternal( + value: unknown, + context: DecodeContext, + retainedBy?: Iterable, + isFromOverride = false + ): any { + const { api, decode } = context; + + if (!isFromOverride && decodeOverride) { + return decodeOverride(value, decode, retainedBy); + } + + if (typeof value === "object") { + if (value == null) { + return value as any; + } + + if (Array.isArray(value)) { + return value.map((value) => decodeInternal(value, context, retainedBy)); + } + + if (REGEXP in value) { + return new RegExp(...(value as { [REGEXP]: [string, string] })[REGEXP]); + } + + if (URL_ID in value) { + return new URL((value as { [URL_ID]: string })[URL_ID]); + } + + if (DATE in value) { + return new Date((value as { [DATE]: string })[DATE]); + } + + if (MAP in value) { + return new Map( + (value as { [MAP]: [any, any] })[MAP].map(([key, value]) => [ + decodeInternal(key, context, retainedBy), + decodeInternal(value, context, retainedBy), + ]) + ); + } + + if (SET in value) { + return new Set( + (value as { [SET]: any[] })[SET].map((entry) => + decodeInternal(entry, context, retainedBy) + ) + ); + } + + if (FUNCTION in value) { + const id = (value as { [FUNCTION]: string })[FUNCTION]; + + const func = api.functions?.get(id); + + if (retainedBy && isMemoryManageable(func)) { + for (const retainer of retainedBy) { + retainer.add(func); + } + } + + return func; + } + + if (!isBasicObject(value)) { + return value; + } + + const result: Record = {}; + + for (const key of Object.keys(value)) { + if (key === ASYNC_ITERATOR) { + result[Symbol.asyncIterator] = () => result; + } else { + result[key] = decodeInternal( + (value as any)[key], + context, + retainedBy + ); + } + } + + return result; + } + + return value; + } +} + +function isIterator(value: any) { + return ( + value != null && + (Symbol.asyncIterator in value || Symbol.iterator in value) && + typeof (value as any).next === "function" + ); +} diff --git a/clients/tabby-threads/source/index.ts b/clients/tabby-threads/source/index.ts new file mode 100644 index 000000000000..4b740cede19b --- /dev/null +++ b/clients/tabby-threads/source/index.ts @@ -0,0 +1,39 @@ +export { + retain, + release, + StackFrame, + isMemoryManageable, + markAsTransferable, +} from "./memory"; +export type { MemoryManageable, MemoryRetainer } from "./memory.ts"; +export { + RELEASE_METHOD, + RETAIN_METHOD, + RETAINED_BY, + ENCODE_METHOD, + TRANSFERABLE, +} from "./constants"; +export { + createThread, + createThreadFromBroadcastChannel, + createThreadFromBrowserWebSocket, + createThreadFromIframe, + createThreadFromInsideIframe, + createThreadFromMessagePort, + createThreadFromWebWorker, + type ThreadOptions, +} from "./targets"; +export { createBasicEncoder } from "./encoding"; +export { + createThreadAbortSignal, + acceptThreadAbortSignal, + type ThreadAbortSignal, +} from "./abort-signal"; +export type { + Thread, + ThreadTarget, + ThreadEncoder, + ThreadEncoderApi, + ThreadEncodable, + AnyFunction, +} from "./types.ts"; diff --git a/clients/tabby-threads/source/memory.ts b/clients/tabby-threads/source/memory.ts new file mode 100644 index 000000000000..07a8869130c3 --- /dev/null +++ b/clients/tabby-threads/source/memory.ts @@ -0,0 +1,186 @@ +import { + RETAINED_BY, + RETAIN_METHOD, + RELEASE_METHOD, + TRANSFERABLE, +} from "./constants"; +import type { MemoryRetainer, MemoryManageable } from "./types.ts"; + +export { RETAINED_BY, RETAIN_METHOD, RELEASE_METHOD }; +export type { MemoryRetainer, MemoryManageable }; + +/** + * Marks the value as being transferable between threads. The memory for this object + * will be moved to the target thread once the object is sent. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects + */ +export function markAsTransferable(value: T) { + Object.assign(value as any, { [TRANSFERABLE]: true }); + return value; +} + +/** + * A simple representation of a called function. This object allows this library to + * release references to functions immediately when the function call that transferred + * them into this thread is completed. + */ +export class StackFrame { + private readonly memoryManaged = new Set(); + + add(memoryManageable: MemoryManageable) { + this.memoryManaged.add(memoryManageable); + memoryManageable[RETAINED_BY].add(this); + memoryManageable[RETAIN_METHOD](); + } + + release() { + for (const memoryManaged of this.memoryManaged) { + memoryManaged[RETAINED_BY].delete(this); + memoryManaged[RELEASE_METHOD](); + } + + this.memoryManaged.clear(); + } +} + +/** + * Indicates that a value is being manually memory-managed across threads by this library. + */ +export function isMemoryManageable(value: unknown): value is MemoryManageable { + return Boolean( + value && (value as any)[RETAIN_METHOD] && (value as any)[RELEASE_METHOD] + ); +} + +/** + * Marks a value as being used so it will not be automatically released. Calling `retain` will, + * by default, deeply retain the value — that is, it will traverse into nested array elements + * and object properties, and retain every `retain`-able thing it finds. + * + * You will typically use this alongside also storing that value in a variable that lives outside + * the context of the function where that value was received. + * + * @example + * import {retain} from '@quilted/threads'; + * + * const allUsers = new Set(); + * + * async function sayHello(user: User) { + * allUsers.add(user); + * retain(user); + * return `Hey, ${await user.fullName()}!`; + * } + */ +export function retain(value: any, { deep = true } = {}): boolean { + return retainInternal(value, deep, new Map()); +} + +function retainInternal( + value: any, + deep: boolean, + seen: Map +): boolean { + const seenValue = seen.get(value); + if (seenValue) return seenValue; + + const canRetain = isMemoryManageable(value); + + if (canRetain) { + value[RETAIN_METHOD](); + } + + seen.set(value, canRetain); + + if (deep) { + if (Array.isArray(value)) { + const nestedCanRetain = value.reduce( + (canRetain, item) => retainInternal(item, deep, seen) || canRetain, + canRetain + ); + + seen.set(value, nestedCanRetain); + return nestedCanRetain; + } + + if (isBasicObject(value)) { + const nestedCanRetain = Object.keys(value).reduce( + (canRetain, key) => retainInternal(value[key], deep, seen) || canRetain, + canRetain + ); + + seen.set(value, nestedCanRetain); + return nestedCanRetain; + } + } + + return canRetain; +} + +/** + * Once you are no longer using the a `retain`-ed value, you can use this function to mark it as + * being unused. Like `retain()`, this function will apply to all nested array elements and object + * properties. + * + * @example + * import {retain} from '@quilted/threads'; + * + * const allUsers = new Set(); + * + * function removeUser(user: User) { + * allUsers.delete(user); + * release(user); + * } + */ +export function release(value: any, { deep = true } = {}): boolean { + return releaseInternal(value, deep, new Map()); +} + +function releaseInternal( + value: any, + deep: boolean, + seen: Map +): boolean { + const seenValue = seen.get(value); + if (seenValue) return seenValue; + + const canRelease = isMemoryManageable(value); + + if (canRelease) { + value[RELEASE_METHOD](); + } + + seen.set(value, canRelease); + + if (deep) { + if (Array.isArray(value)) { + const nestedCanRelease = value.reduce( + (canRelease, item) => releaseInternal(item, deep, seen) || canRelease, + canRelease + ); + + seen.set(value, nestedCanRelease); + return nestedCanRelease; + } + + if (isBasicObject(value)) { + const nestedCanRelease = Object.keys(value).reduce( + (canRelease, key) => + releaseInternal(value[key], deep, seen) || canRelease, + canRelease + ); + + seen.set(value, nestedCanRelease); + return nestedCanRelease; + } + } + + return canRelease; +} + +export function isBasicObject(value: unknown) { + if (value == null || typeof value !== "object") return false; + + const prototype = Object.getPrototypeOf(value); + return prototype == null || prototype === Object.prototype; +} diff --git a/clients/tabby-threads/source/signals.ts b/clients/tabby-threads/source/signals.ts new file mode 100644 index 000000000000..a3a04cc16f32 --- /dev/null +++ b/clients/tabby-threads/source/signals.ts @@ -0,0 +1,3 @@ +export { createThreadSignal } from "./signals/create.js"; +export { acceptThreadSignal, isThreadSignal } from "./signals/accept.js"; +export type { ThreadSignal } from "./signals/types.js"; diff --git a/clients/tabby-threads/source/signals/accept.ts b/clients/tabby-threads/source/signals/accept.ts new file mode 100644 index 000000000000..1952589741aa --- /dev/null +++ b/clients/tabby-threads/source/signals/accept.ts @@ -0,0 +1,72 @@ +import { signal as createSignal, type Signal } from "@quilted/signals"; + +import { createThreadAbortSignal } from "../abort-signal"; + +import { type ThreadSignal } from "./types"; + +/** + * Call this function in a thread receiving a `ThreadSignal` to + * turn it into a "live" Preact signal. The resulting signal will + * connect the thread to its sending pair, and will update it as the + * signal value changes. If the thread signal is writable, writing + * the value of the resulting signal will also update it on the paired + * thread. + */ +export function acceptThreadSignal( + threadSignal: ThreadSignal, + { + signal: abortSignal, + }: { + /** + * An optional `AbortSignal` that can cancel synchronizing the + * signal to its paired thread. + */ + signal?: AbortSignal; + } = {} +): Signal { + const signal = createSignal(threadSignal.initial); + const threadAbortSignal = abortSignal && createThreadAbortSignal(abortSignal); + + const valueDescriptor = Object.getOwnPropertyDescriptor( + Object.getPrototypeOf(signal), + "value" + )!; + + Object.defineProperty(signal, "value", { + ...valueDescriptor, + get() { + return valueDescriptor.get?.call(this); + }, + set(value) { + if (threadSignal.set == null) { + throw new Error(`You can’t set the value of a readonly thread signal.`); + } + + threadSignal.set(value); + return valueDescriptor.set?.call(this, value); + }, + }); + + threadSignal.start( + (value) => { + valueDescriptor.set?.call(signal, value); + }, + { signal: threadAbortSignal } + ); + + return signal; +} + +/** + * Returns `true` if the passed object is a `ThreadSignal`. + */ +export function isThreadSignal( + value?: unknown +): value is ThreadSignal { + return ( + value != null && + typeof value === "object" && + "initial" in value && + typeof (value as any).start === "function" + ); +} diff --git a/clients/tabby-threads/source/signals/create.ts b/clients/tabby-threads/source/signals/create.ts new file mode 100644 index 000000000000..0b39a879e725 --- /dev/null +++ b/clients/tabby-threads/source/signals/create.ts @@ -0,0 +1,81 @@ +import { type Signal } from "@quilted/signals"; +import { NestedAbortController } from "@quilted/events"; + +import { retain, release } from "../memory"; +import { acceptThreadAbortSignal } from "../abort-signal"; + +import { type ThreadSignal } from "./types"; + +/** + * Converts a Preact signal into a version of that signal that can + * be transferred to a target `Thread`. On the paired thread, this + * "thread-safe" version of the signal can be turned into an actual, + * live Preact signal using `acceptThreadSignal()`. + */ +export function createThreadSignal( + signal: Signal, + { + /** + * Whether the thread signal should have a method to write a value + * back to the original signal. This allows you to create two-way + * synchronization between the two threads, which can be useful, but + * can also be hard to reason about. + * + * @default false + */ + writable = false, + + /** + * An optional `AbortSignal` that can cancel synchronizing the + * signal to its paired thread. + */ + signal: teardownAbortSignal, + }: { writable?: boolean; signal?: AbortSignal } = {} +): ThreadSignal { + let initialVersion: number; + + return { + get initial() { + // @see https://github.com/preactjs/signals/blob/main/mangle.json#L56 + initialVersion = (signal as any).i; + return signal.peek(); + }, + set: + writable && !isReadonlySignal(signal) + ? (value) => { + signal.value = value; + } + : undefined, + start(subscriber, { signal: threadAbortSignal } = {}) { + retain(subscriber); + + const abortSignal = + threadAbortSignal && acceptThreadAbortSignal(threadAbortSignal); + + const finalAbortSignal = + abortSignal && teardownAbortSignal + ? new NestedAbortController(abortSignal, teardownAbortSignal).signal + : abortSignal ?? teardownAbortSignal; + + const teardown = signal.subscribe((value: any) => { + if ((signal as any).i === initialVersion) { + return; + } + + subscriber(value); + }); + + finalAbortSignal?.addEventListener("abort", () => { + teardown(); + release(subscriber); + }); + }, + }; +} + +function isReadonlySignal(signal: Signal): boolean { + return ( + Object.getOwnPropertyDescriptor(Object.getPrototypeOf(signal), "value") + ?.set == null + ); +} diff --git a/clients/tabby-threads/source/signals/types.ts b/clients/tabby-threads/source/signals/types.ts new file mode 100644 index 000000000000..1ec84316451d --- /dev/null +++ b/clients/tabby-threads/source/signals/types.ts @@ -0,0 +1,36 @@ +import type {ThreadAbortSignal} from '../abort-signal.ts'; + +/** + * A representation of a Preact signal that can be serialized between + * two threads. + */ +export interface ThreadSignal { + /** + * The initial value of the signal. + */ + initial: T; + + /** + * Sets the value of the original signal. + */ + set?(value: T): void; + + /** + * A function to connect the signal between the two threads. This + * function should be called by the sibling thread when the abort + * state changes. It must also respond with a boolean indicating + * whether the original signal was aborted at the time the listener + * was attached, which is used to detect a change in state that happened + * during the message passing process. + */ + start( + listener: (value: T) => void, + options?: { + /** + * An `AbortSignal` that can be used to stop synchronizing the signal + * between the two threads. + */ + signal?: AbortSignal | ThreadAbortSignal; + }, + ): void; +} diff --git a/clients/tabby-threads/source/targets.ts b/clients/tabby-threads/source/targets.ts new file mode 100644 index 000000000000..ee6fe4a05932 --- /dev/null +++ b/clients/tabby-threads/source/targets.ts @@ -0,0 +1,7 @@ +export { createThread, type ThreadOptions } from "./targets/target"; +export { createThreadFromBroadcastChannel } from "./targets/broadcast-channel"; +export { createThreadFromIframe } from "./targets/iframe/iframe"; +export { createThreadFromInsideIframe } from "./targets/iframe/nested"; +export { createThreadFromMessagePort } from "./targets/message-port"; +export { createThreadFromBrowserWebSocket } from "./targets/web-socket-browser"; +export { createThreadFromWebWorker } from "./targets/web-worker"; diff --git a/clients/tabby-threads/source/targets/broadcast-channel.ts b/clients/tabby-threads/source/targets/broadcast-channel.ts new file mode 100644 index 000000000000..d5ce250edde9 --- /dev/null +++ b/clients/tabby-threads/source/targets/broadcast-channel.ts @@ -0,0 +1,36 @@ +import { createThread, type ThreadOptions } from "./target"; + +/** + * Creates a thread from a `BroadcastChannel` instance in the browser. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel + * + * @example + * import {createThreadFromBroadcastChannel} from '@quilted/threads'; + * + * const channel = new BroadcastChannel('my-channel');; + * const thread = createThreadFromBroadcastChannel(channel); + * await thread.sendMessage('Hello world!'); + */ +export function createThreadFromBroadcastChannel< + Self = Record, + Target = Record, +>(channel: BroadcastChannel, options?: ThreadOptions) { + return createThread( + { + send(message) { + channel.postMessage(message); + }, + listen(listener, { signal }) { + channel.addEventListener( + "message", + (event) => { + listener(event.data); + }, + { signal } + ); + }, + }, + options + ); +} diff --git a/clients/tabby-threads/source/targets/iframe/iframe.ts b/clients/tabby-threads/source/targets/iframe/iframe.ts new file mode 100644 index 000000000000..b7c510aa4a34 --- /dev/null +++ b/clients/tabby-threads/source/targets/iframe/iframe.ts @@ -0,0 +1,98 @@ +import { NestedAbortController } from "@quilted/events"; +import { createThread, type ThreadTarget, type ThreadOptions } from "../target"; +import { CHECK_MESSAGE, RESPONSE_MESSAGE } from "./shared"; + +/** + * Creates a thread from an iframe nested on a top-level document. To create + * a thread from the contents of this iframe, use `createThreadFromInsideIframe()` + * instead. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe + * + * @example + * import {createThreadFromIframe} from '@quilted/threads'; + * + * const iframe = document.createElement('iframe'); + * const thread = createThreadFromInsideIframe(iframe); + * await thread.sendMessage('Hello world!'); + */ +export function createThreadFromIframe< + Self = Record, + Target = Record, +>( + iframe: HTMLIFrameElement, + { + targetOrigin = "*", + ...options + }: ThreadOptions & { + /** + * The target origin to use when sending `postMessage` events to the child frame. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage#targetorigin + * @default '*' + */ + targetOrigin?: string; + } = {} +) { + let connected = false; + + const sendMessage: ThreadTarget["send"] = function send(message, transfer) { + iframe.contentWindow?.postMessage(message, targetOrigin, transfer); + }; + + const connectedPromise = new Promise((resolve) => { + const abort = options.signal + ? new NestedAbortController(options.signal) + : new AbortController(); + + window.addEventListener( + "message", + (event) => { + if (event.source !== iframe.contentWindow) return; + + if (event.data === RESPONSE_MESSAGE) { + connected = true; + abort.abort(); + resolve(); + } + }, + { signal: abort.signal } + ); + + abort.signal.addEventListener( + "abort", + () => { + resolve(); + }, + { once: true } + ); + + sendMessage(CHECK_MESSAGE); + }); + + return createThread( + { + send(message, transfer) { + if (!connected) { + return connectedPromise.then(() => { + if (connected) return sendMessage(message, transfer); + }); + } + + return sendMessage(message, transfer); + }, + listen(listen, { signal }) { + self.addEventListener( + "message", + (event) => { + if (event.source !== iframe.contentWindow) return; + if (event.data === RESPONSE_MESSAGE) return; + listen(event.data); + }, + { signal } + ); + }, + }, + options + ); +} diff --git a/clients/tabby-threads/source/targets/iframe/nested.ts b/clients/tabby-threads/source/targets/iframe/nested.ts new file mode 100644 index 000000000000..9890e543ca19 --- /dev/null +++ b/clients/tabby-threads/source/targets/iframe/nested.ts @@ -0,0 +1,95 @@ +import { NestedAbortController } from "@quilted/events"; +import { createThread, type ThreadOptions } from "../target"; +import { CHECK_MESSAGE, RESPONSE_MESSAGE } from "./shared"; + +/** + * Creates a thread from within an iframe nested in a top-level document. To create + * a thread from this iframe in the top-level document, use `createThreadFromIframe()` + * instead. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe + * + * @example + * import {createThreadFromInsideIframe} from '@quilted/threads'; + * + * const thread = createThreadFromInsideIframe(); + * await thread.sendMessage('Hello world!'); + */ +export function createThreadFromInsideIframe< + Self = Record, + Target = Record, +>({ + targetOrigin = "*", + ...options +}: ThreadOptions & { + /** + * The target origin to use when sending `postMessage` events to the parent frame. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage#targetorigin + * @default '*' + */ + targetOrigin?: string; +} = {}) { + if (typeof self === "undefined" || self.parent == null) { + throw new Error( + "You are not inside an iframe, because there is no parent window." + ); + } + + const { parent } = self; + + const abort = options.signal + ? new NestedAbortController(options.signal) + : new AbortController(); + + const ready = () => { + const respond = () => parent.postMessage(RESPONSE_MESSAGE, targetOrigin); + + // Handles wrappers that want to connect after the page has already loaded + self.addEventListener( + "message", + ({ data }) => { + if (data === CHECK_MESSAGE) respond(); + }, + { signal: options.signal } + ); + + respond(); + }; + + // Listening to `readyState` in iframe, though the child iframe could probably + // send a `postMessage` that it is ready to receive messages sooner than that. + if (document.readyState === "complete") { + ready(); + } else { + document.addEventListener( + "readystatechange", + () => { + if (document.readyState === "complete") { + ready(); + abort.abort(); + } + }, + { signal: abort.signal } + ); + } + + return createThread( + { + send(message, transfer) { + return parent.postMessage(message, targetOrigin, transfer); + }, + listen(listen, { signal }) { + self.addEventListener( + "message", + (event) => { + if (event.data === CHECK_MESSAGE) return; + listen(event.data); + }, + { signal } + ); + }, + }, + options + ); +} diff --git a/clients/tabby-threads/source/targets/iframe/shared.ts b/clients/tabby-threads/source/targets/iframe/shared.ts new file mode 100644 index 000000000000..1e7b696b2520 --- /dev/null +++ b/clients/tabby-threads/source/targets/iframe/shared.ts @@ -0,0 +1,2 @@ +export const CHECK_MESSAGE = 'quilt.threads.ping'; +export const RESPONSE_MESSAGE = 'quilt.threads.pong'; diff --git a/clients/tabby-threads/source/targets/message-port.ts b/clients/tabby-threads/source/targets/message-port.ts new file mode 100644 index 000000000000..037dccaab189 --- /dev/null +++ b/clients/tabby-threads/source/targets/message-port.ts @@ -0,0 +1,44 @@ +import { createThread, type ThreadOptions } from "./target"; + +/** + * Creates a thread from a `MessagePort` instance in the browser. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/MessagePort + * + * @example + * import {createThreadFromMessagePort} from '@quilted/threads'; + * + * const channel = new MessageChannel(); + * const threadOne = createThreadFromMessagePort(channel.port1); + * const threadTwo = createThreadFromMessagePort(channel.port2, { + * expose: { + * sendMessage: (message) => console.log(message), + * }, + * }); + * + * await threadOne.sendMessage('Hello world!'); + */ +export function createThreadFromMessagePort< + Self = Record, + Target = Record, +>(port: MessagePort, options?: ThreadOptions) { + return createThread( + { + send(...args: [any, Transferable[]]) { + port.postMessage(...args); + }, + listen(listener, { signal }) { + port.addEventListener( + "message", + (event) => { + listener(event.data); + }, + { signal } + ); + + port.start(); + }, + }, + options + ); +} diff --git a/clients/tabby-threads/source/targets/target.ts b/clients/tabby-threads/source/targets/target.ts new file mode 100644 index 000000000000..639367428474 --- /dev/null +++ b/clients/tabby-threads/source/targets/target.ts @@ -0,0 +1,506 @@ +import type { + Thread, + ThreadTarget, + ThreadEncoder, + ThreadEncoderApi, + AnyFunction, +} from "../types.ts"; + +import { + RELEASE_METHOD, + RETAINED_BY, + RETAIN_METHOD, + StackFrame, + isMemoryManageable, +} from "../memory"; +import { createBasicEncoder } from "../encoding/basic"; + +export type { ThreadTarget }; + +/** + * Options to customize the creation of a `Thread` instance. + */ +export interface ThreadOptions< + Self = Record, + Target = Record, +> { + /** + * Methods to expose on this thread, so that they are callable on the paired thread. + * This should be an object, with each member of that object being a function. Remember + * that these functions will become asynchronous when called over the thread boundary. + */ + expose?: Self; + + /** + * An `AbortSignal` that controls whether the thread is active or not. When aborted, + * the thread will no longer send any messages to the underlying object, will stop + * listening for messages from that object, and will clean up any memory associated + * with in-progress communication between the threads. + */ + signal?: AbortSignal; + + /** + * An object that will encode and decode messages sent between threads. If not + * provided, a default implementation created with `createBasicEncoder()` will be used instead. + */ + encoder?: ThreadEncoder; + + /** + * A list of callable methods exposed on the paired `Thread`. This option is + * required if you want to call methods and your environment does not support + * the `Proxy` constructor. When the `Proxy` constructor is available, `createThread()` + * will forward all method calls to the paired `Thread` by default. + */ + callable?: (keyof Target)[]; + + /** + * A function for generating unique identifiers. Unique identifiers are used by + * some encoding and decoding operations to maintain stable references to objects + * transferred between the threads. If not provided, a simple default implementation + * will be used instead. + */ + uuid?(): string; +} + +const CALL = 0; +const RESULT = 1; +const TERMINATE = 2; +const RELEASE = 3; +const FUNCTION_APPLY = 5; +const FUNCTION_RESULT = 6; +const CHECK_CAPABILITY = 7; +const EXPOSE_LIST = 8; + +interface MessageMap { + [CALL]: [string, string | number, any]; + [RESULT]: [string, Error?, any?]; + [TERMINATE]: []; + [RELEASE]: [string]; + [FUNCTION_APPLY]: [string, string, any]; + [FUNCTION_RESULT]: [string, Error?, any?]; + [CHECK_CAPABILITY]: [string, string]; + [EXPOSE_LIST]: [string]; +} + +type MessageData = { + [K in keyof MessageMap]: [K, MessageMap[K]]; +}[keyof MessageMap]; + +/** + * Creates a thread from any object that conforms to the `ThreadTarget` + * interface. + */ +export function createThread< + Self = Record, + Target = Record, +>( + target: ThreadTarget, + { + expose, + callable, + signal, + uuid = defaultUuid, + encoder = createBasicEncoder(), + }: ThreadOptions = {} +): Thread { + let terminated = false; + const activeApi = new Map(); + const functionsToId = new Map(); + const idsToFunction = new Map(); + const idsToProxy = new Map(); + + if (expose) { + for (const key of Object.keys(expose)) { + const value = expose[key as keyof typeof expose]; + if (typeof value === "function") activeApi.set(key, value); + } + } + + const callIdsToResolver = new Map< + string, + ( + ...args: MessageMap[typeof FUNCTION_RESULT] | MessageMap[typeof RESULT] + ) => void + >(); + + const call = createCallable>(handlerForCall, callable, { + _requestMethods, + }); + + const encoderApi: ThreadEncoderApi = { + functions: { + add(func) { + let id = functionsToId.get(func); + + if (id == null) { + id = uuid(); + functionsToId.set(func, id); + idsToFunction.set(id, func); + } + + return id; + }, + get(id) { + let proxy = idsToProxy.get(id); + + if (proxy) return proxy; + + let retainCount = 0; + let released = false; + + const release = () => { + retainCount -= 1; + + if (retainCount === 0) { + released = true; + idsToProxy.delete(id); + send(RELEASE, [id]); + } + }; + + const retain = () => { + retainCount += 1; + }; + + proxy = (...args: any[]) => { + if (released) { + throw new Error( + "You attempted to call a function that was already released." + ); + } + + if (!idsToProxy.has(id)) { + throw new Error( + "You attempted to call a function that was already revoked." + ); + } + + const [encoded, transferable] = encoder.encode(args, encoderApi); + + const callId = uuid(); + const done = waitForResult(callId); + + send(FUNCTION_APPLY, [callId, id, encoded], transferable); + + return done; + }; + + Object.defineProperties(proxy, { + [RELEASE_METHOD]: { value: release, writable: false }, + [RETAIN_METHOD]: { value: retain, writable: false }, + [RETAINED_BY]: { value: new Set(), writable: false }, + }); + + idsToProxy.set(id, proxy); + + return proxy; + }, + }, + }; + + const terminate = () => { + if (terminated) return; + + for (const id of callIdsToResolver.keys()) { + resolveCall(id, new ThreadTerminatedError()); + } + + terminated = true; + activeApi.clear(); + callIdsToResolver.clear(); + functionsToId.clear(); + idsToFunction.clear(); + idsToProxy.clear(); + }; + + signal?.addEventListener( + "abort", + () => { + send(TERMINATE, []); + terminate(); + }, + { once: true } + ); + + target.listen(listener, { signal }); + + return call; + + function send( + type: Type, + args: MessageMap[Type], + transferables?: Transferable[] + ) { + if (terminated) return; + target.send([type, args], transferables); + } + + async function listener(rawData: unknown) { + const isThreadMessageData = + Array.isArray(rawData) && + typeof rawData[0] === "number" && + (rawData[1] == null || Array.isArray(rawData[1])); + + if (!isThreadMessageData) { + return; + } + + const data = rawData as MessageData; + + switch (data[0]) { + case TERMINATE: { + terminate(); + break; + } + case CALL: { + const stackFrame = new StackFrame(); + const [id, property, args] = data[1]; + const func = activeApi.get(property); + + try { + if (func == null) { + throw new Error( + `No '${property}' method is exposed on this endpoint` + ); + } + + const result = await func( + ...(encoder.decode(args, encoderApi, [stackFrame]) as any[]) + ); + const [encoded, transferables] = encoder.encode(result, encoderApi); + send(RESULT, [id, undefined, encoded], transferables); + } catch (error) { + const { name, message, stack } = error as Error; + send(RESULT, [id, { name, message, stack }]); + } finally { + stackFrame.release(); + } + + break; + } + case RESULT: { + resolveCall(...data[1]); + break; + } + case RELEASE: { + const [id] = data[1]; + const func = idsToFunction.get(id); + + if (func) { + idsToFunction.delete(id); + functionsToId.delete(func); + } + + break; + } + case FUNCTION_RESULT: { + resolveCall(...data[1]); + break; + } + case FUNCTION_APPLY: { + const [callId, funcId, args] = data[1]; + + const stackFrame = new StackFrame(); + + try { + const func = idsToFunction.get(funcId); + + if (func == null) { + throw new Error( + "You attempted to call a function that was already released." + ); + } + + const result = await func( + ...(encoder.decode( + args, + encoderApi, + isMemoryManageable(func) + ? [...func[RETAINED_BY], stackFrame] + : [stackFrame] + ) as any[]) + ); + const [encoded, transferables] = encoder.encode(result, encoderApi); + send(FUNCTION_RESULT, [callId, undefined, encoded], transferables); + } catch (error) { + const { name, message, stack } = error as Error; + send(FUNCTION_RESULT, [callId, { name, message, stack }]); + } finally { + stackFrame.release(); + } + + break; + } + case CHECK_CAPABILITY: { + const [id, methodToCheck] = data[1]; + const hasMethod = activeApi.has(methodToCheck); + send(RESULT, [id, undefined, encoder.encode(hasMethod, encoderApi)[0]]); + break; + } + + case EXPOSE_LIST: { + // return our list of exposed methods + const [id] = data[1]; + const exposedMethods = Array.from(activeApi.keys()); + send(RESULT, [ + id, + undefined, + encoder.encode(exposedMethods, encoderApi)[0], + ]); + break; + } + } + } + + function handlerForCall(property: string | number | symbol) { + return (...args: any[]) => { + try { + if (terminated) { + throw new ThreadTerminatedError(); + } + + if (typeof property !== "string" && typeof property !== "number") { + throw new Error( + `Can’t call a symbol method on a thread: ${property.toString()}` + ); + } + + // hasCapability is a special method that checks if a method is exposed on the other thread + if (property === "hasCapability") { + const methodToCheck = args[0]; + const id = uuid(); + const done = waitForResult(id); + send(CHECK_CAPABILITY, [id, methodToCheck]); + return done; + } + + //normal call + const id = uuid(); + const done = waitForResult(id); + const [encoded, transferables] = encoder.encode(args, encoderApi); + + send(CALL, [id, property, encoded], transferables); + + return done; + } catch (error) { + return Promise.reject(error); + } + }; + } + + function waitForResult(id: string) { + const promise = new Promise((resolve, reject) => { + callIdsToResolver.set(id, (_, errorResult, value) => { + if (errorResult == null) { + resolve(encoder.decode(value, encoderApi)); + } else { + const error = new Error(); + Object.assign(error, errorResult); + reject(error); + } + }); + }); + + Object.defineProperty(promise, Symbol.asyncIterator, { + async *value() { + const result = await promise; + + Object.defineProperty(result, Symbol.asyncIterator, { + value: () => result, + }); + + yield* result; + }, + }); + + return promise; + } + + function resolveCall(...args: MessageMap[typeof RESULT]) { + const callId = args[0]; + + const resolver = callIdsToResolver.get(callId); + + if (resolver) { + resolver(...args); + callIdsToResolver.delete(callId); + } + } + + async function _requestMethods() { + const id = uuid(); + const done = waitForResult(id); + send(EXPOSE_LIST, [id]); + return done; + } +} + +class ThreadTerminatedError extends Error { + constructor() { + super("You attempted to call a function on a terminated thread."); + } +} + +function defaultUuid() { + return `${uuidSegment()}-${uuidSegment()}-${uuidSegment()}-${uuidSegment()}`; +} + +function uuidSegment() { + return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(16); +} + +function createCallable( + handlerForCall: ( + property: string | number | symbol + ) => AnyFunction | undefined, + callable?: (keyof T)[], + methods?: { + _requestMethods(): Promise; + } +): T { + let call: any; + + if (callable == null) { + if (typeof Proxy !== "function") { + throw new Error( + `You must pass an array of callable methods in environments without Proxies.` + ); + } + + const cache = new Map(); + + call = new Proxy( + {}, + { + get(_target, property) { + if (property === "then") { + return undefined; + } + if (property === "_requestMethods") { + return methods?._requestMethods; + } + if (cache.has(property)) { + return cache.get(property); + } + + const handler = handlerForCall(property); + cache.set(property, handler); + return handler; + }, + } + ); + } else { + call = {}; + + for (const method of callable) { + Object.defineProperty(call, method, { + value: handlerForCall(method), + writable: false, + configurable: true, + enumerable: true, + }); + } + } + + return call; +} diff --git a/clients/tabby-threads/source/targets/web-socket-browser.ts b/clients/tabby-threads/source/targets/web-socket-browser.ts new file mode 100644 index 000000000000..8330ee7b885f --- /dev/null +++ b/clients/tabby-threads/source/targets/web-socket-browser.ts @@ -0,0 +1,48 @@ +import { createThread, type ThreadOptions } from "./target"; + +/** + * Creates a thread from a `WebSocket` instance in the browser. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket + * + * @example + * import {createThreadFromBrowserWebSocket} from '@quilted/threads'; + * + * const websocket = new WebSocket('ws://localhost:8080'); + * const thread = createThreadFromBrowserWebSocket(websocket); + * await thread.sendMessage('Hello world!'); + */ +export function createThreadFromBrowserWebSocket< + Self = Record, + Target = Record, +>(websocket: WebSocket, options?: ThreadOptions) { + return createThread( + { + async send(message) { + if (websocket.readyState !== websocket.OPEN) { + await new Promise((resolve) => { + websocket.addEventListener( + "open", + () => { + resolve(); + }, + { once: true } + ); + }); + } + + websocket.send(JSON.stringify(message)); + }, + listen(listener, { signal }) { + websocket.addEventListener( + "message", + (event) => { + listener(JSON.parse(event.data)); + }, + { signal } + ); + }, + }, + options + ); +} diff --git a/clients/tabby-threads/source/targets/web-worker.ts b/clients/tabby-threads/source/targets/web-worker.ts new file mode 100644 index 000000000000..90b9f683b936 --- /dev/null +++ b/clients/tabby-threads/source/targets/web-worker.ts @@ -0,0 +1,43 @@ +import { createThread, type ThreadOptions } from "./target"; + +/** + * Creates a thread from a web worker. This function can be used either from a JavaScript + * environment that *created* a web worker, or from within a web worker that has been + * created. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers + * + * @example + * import {createThreadFromWebWorker} from '@quilted/threads'; + * + * // If inside a web worker: + * const thread = createThreadFromWebWorker(self); + * + * // If in an environment that creates a worker: + * const worker = new Worker('worker.js'); + * const thread = createThreadFromWebWorker(worker); + * + * await thread.sendMessage('Hello world!'); + */ +export function createThreadFromWebWorker< + Self = Record, + Target = Record, +>(worker: Worker, options?: ThreadOptions) { + return createThread( + { + send(...args: [any, Transferable[]]) { + worker.postMessage(...args); + }, + listen(listener, { signal }) { + worker.addEventListener( + "message", + (event) => { + listener(event.data); + }, + { signal } + ); + }, + }, + options + ); +} diff --git a/clients/tabby-threads/source/types.ts b/clients/tabby-threads/source/types.ts new file mode 100644 index 000000000000..1cc5005fc8cc --- /dev/null +++ b/clients/tabby-threads/source/types.ts @@ -0,0 +1,124 @@ +import type { + RELEASE_METHOD, + RETAIN_METHOD, + ENCODE_METHOD, + RETAINED_BY, +} from "./constants.ts"; + +/** + * A thread represents a target JavaScript environment that exposes a set + * of callable, asynchronous methods. The thread takes care of automatically + * encoding and decoding its arguments and return values, so you can interact + * with it as if its methods were implemented in the same environment as your + * own code. + */ +export type Thread = { + [K in keyof Target]: Target[K] extends (...args: any[]) => infer ReturnType + ? ReturnType extends Promise | AsyncGenerator + ? Target[K] + : never + : never; +} & { + + /** + * A method that used to get all exposed methods of the opposite thread. + */ + _requestMethods(): Promise; +}; + +/** + * An object backing a `Thread` that provides the message-passing interface + * that allows communication to flow between environments. This message-passing + * interface is based on the [`postMessage` interface](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage), + * which is easily adaptable to many JavaScript objects and environments. + */ +export interface ThreadTarget { + /** + * Sends a message to the target thread. The message will be encoded before sending, + * and the consumer may also pass an array of "transferable" objects that should be + * transferred (rather than copied) to the other environment, if supported. + */ + send(message: any, transferables?: Transferable[]): void; + + /** + * Listens for messages coming in to the thread. This method must call the provided + * listener for each message as it is received. The thread will then decode the message + * and handle its content. This method may be passed an `AbortSignal` to abort the + * listening process. + */ + listen(listener: (value: any) => void, options: {signal?: AbortSignal}): void; +} + +/** + * A mapped object type that takes an object with methods, and converts it into the + * an object with the same methods that can be called over a thread. + */ + +/** + * An object that can retain a reference to a `MemoryManageable` object. + */ +export interface MemoryRetainer { + add(manageable: MemoryManageable): void; +} + +/** + * An object transferred between threads that must have its memory manually managed, + * in order to release the reference to a corresponding object on the original thread. + */ +export interface MemoryManageable { + readonly [RETAINED_BY]: Set; + [RETAIN_METHOD](): void; + [RELEASE_METHOD](): void; +} + +/** + * An object that can encode and decode values communicated between two threads. + */ +export interface ThreadEncoder { + /** + * Encodes a value before sending it to another thread. Should return a tuple where + * the first item is the encoded value, and the second item is an array of elements + * that can be transferred to the other thread, instead of being copied. + */ + encode(value: unknown, api: ThreadEncoderApi): [any, Transferable[]?]; + + /** + * Decodes a value received from another thread. + */ + decode( + value: unknown, + api: ThreadEncoderApi, + retainedBy?: Iterable, + ): unknown; +} + +export interface ThreadEncoderApi { + /** + * Controls how the thread encoder will handle functions. + */ + functions?: { + /** + * Retrieve a function by its serialized ID. This function will be called while + * decoding responses from the other "side" of a thread. The implementer of this + * API should return a proxy function that will call the function on the other + * thread, or `undefined` to prevent the function from being being decoded. + */ + get(id: string): AnyFunction | undefined; + + /** + * Stores a function during encoding. The implementer of this API should return + * a unique ID for the function, or `undefined` to prevent the function from + * being encoded. + */ + add(func: AnyFunction): string | undefined; + }; +} + +/** + * An object that provides a custom process to encode its value. + */ +export interface ThreadEncodable { + [ENCODE_METHOD](api: {encode(value: any): unknown}): any; +} + +export type AnyFunction = Function; diff --git a/clients/tabby-threads/tsconfig.json b/clients/tabby-threads/tsconfig.json new file mode 100644 index 000000000000..e16b7740b4c6 --- /dev/null +++ b/clients/tabby-threads/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "composite": true, + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": false, + "pretty": true, + "allowSyntheticDefaultImports": true, + "allowImportingTsExtensions": false, + "esModuleInterop": true, + "moduleResolution": "bundler", + "module": "esnext", + "target": "esnext", + "jsx": "preserve", + "strict": true, + "noImplicitAny": true, + "noImplicitReturns": false, + "noImplicitThis": true, + "noPropertyAccessFromIndexSignature": false, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "skipLibCheck": true, + "sourceMap": false, + "noEmitOnError": false, + "isolatedModules": true, + "lib": ["dom", "dom.iterable", "esnext"], + "rootDir": "source", + "outDir": "dist/types" + }, + "include": ["source"], + "exclude": ["*.test.ts", "*.test.tsx"] +} diff --git a/clients/tabby-threads/tsconfig.tsbuildinfo b/clients/tabby-threads/tsconfig.tsbuildinfo new file mode 100644 index 000000000000..f9d2123fe658 --- /dev/null +++ b/clients/tabby-threads/tsconfig.tsbuildinfo @@ -0,0 +1 @@ +{"program":{"fileNames":["../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es5.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2015.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2016.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2017.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2018.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2019.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2020.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2021.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2022.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2023.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.esnext.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.dom.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.dom.iterable.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2015.core.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2015.collection.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2015.generator.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2015.promise.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2016.intl.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2017.date.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2017.object.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2017.string.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2017.intl.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2018.intl.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2018.promise.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2019.array.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2019.object.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2019.string.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2019.intl.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2020.date.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2020.promise.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2020.string.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2020.intl.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2020.number.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2021.promise.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2021.string.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2021.weakref.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2021.intl.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2022.array.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2022.error.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2022.intl.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2022.object.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2022.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2022.string.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2022.regexp.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2023.array.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.es2023.collection.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.esnext.collection.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.esnext.intl.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.esnext.disposable.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.esnext.promise.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.esnext.decorators.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.esnext.object.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.decorators.d.ts","../../node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/lib/lib.decorators.legacy.d.ts","./source/constants.ts","./source/types.ts","./source/memory.ts","./source/abort-signal/types.ts","./source/abort-signal/accept.ts","./source/abort-signal/create.ts","./source/abort-signal.ts","./source/encoding/basic.ts","./source/encoding.ts","./source/targets/target.ts","./source/targets/broadcast-channel.ts","../../node_modules/.pnpm/@quilted+events@2.0.0/node_modules/@quilted/events/build/typescript/types.d.ts","../../node_modules/.pnpm/@quilted+events@2.0.0/node_modules/@quilted/events/build/typescript/on.d.ts","../../node_modules/.pnpm/@quilted+events@2.0.0/node_modules/@quilted/events/build/typescript/once.d.ts","../../node_modules/.pnpm/@quilted+events@2.0.0/node_modules/@quilted/events/build/typescript/abort/aborterror.d.ts","../../node_modules/.pnpm/@quilted+events@2.0.0/node_modules/@quilted/events/build/typescript/abort/nestedabortcontroller.d.ts","../../node_modules/.pnpm/@quilted+events@2.0.0/node_modules/@quilted/events/build/typescript/abort/timedabortcontroller.d.ts","../../node_modules/.pnpm/@quilted+events@2.0.0/node_modules/@quilted/events/build/typescript/abort/race.d.ts","../../node_modules/.pnpm/@quilted+events@2.0.0/node_modules/@quilted/events/build/typescript/abort.d.ts","../../node_modules/.pnpm/@quilted+events@2.0.0/node_modules/@quilted/events/build/typescript/handler.d.ts","../../node_modules/.pnpm/@quilted+events@2.0.0/node_modules/@quilted/events/build/typescript/emitter.d.ts","../../node_modules/.pnpm/@quilted+events@2.0.0/node_modules/@quilted/events/build/typescript/sleep.d.ts","../../node_modules/.pnpm/@quilted+events@2.0.0/node_modules/@quilted/events/build/typescript/index.d.ts","./source/targets/iframe/shared.ts","./source/targets/iframe/iframe.ts","./source/targets/iframe/nested.ts","./source/targets/message-port.ts","./source/targets/web-socket-browser.ts","./source/targets/web-worker.ts","./source/targets.ts","./source/index.ts","../../node_modules/.pnpm/@preact+signals-core@1.8.0/node_modules/@preact/signals-core/dist/signals-core.d.ts","../../node_modules/.pnpm/@quilted+signals@0.2.1/node_modules/@quilted/signals/build/typescript/signal-or-value.d.ts","../../node_modules/.pnpm/@quilted+signals@0.2.1/node_modules/@quilted/signals/build/typescript/iterator.d.ts","../../node_modules/.pnpm/@quilted+signals@0.2.1/node_modules/@quilted/signals/build/typescript/index.d.ts","./source/signals/types.ts","./source/signals/create.ts","./source/signals/accept.ts","./source/signals.ts","../../node_modules/.pnpm/@types+eslint@8.56.10/node_modules/@types/eslint/helpers.d.ts","../../node_modules/.pnpm/@types+estree@1.0.6/node_modules/@types/estree/index.d.ts","../../node_modules/.pnpm/@types+json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts","../../node_modules/.pnpm/@types+eslint@8.56.10/node_modules/@types/eslint/index.d.ts","../../node_modules/.pnpm/@types+eslint-scope@3.7.7/node_modules/@types/eslint-scope/index.d.ts"],"fileInfos":[{"version":"824cb491a40f7e8fdeb56f1df5edf91b23f3e3ee6b4cde84d4a99be32338faee","affectsGlobalScope":true},"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","9a68c0c07ae2fa71b44384a839b7b8d81662a236d4b9ac30916718f7510b1b2d","5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","feecb1be483ed332fad555aff858affd90a48ab19ba7272ee084704eb7167569","5514e54f17d6d74ecefedc73c504eadffdeda79c7ea205cf9febead32d45c4bc","1c0cdb8dc619bc549c3e5020643e7cf7ae7940058e8c7e5aefa5871b6d86f44b","886e50ef125efb7878f744e86908884c0133e7a6d9d80013f421b0cd8fb2af94",{"version":"87d693a4920d794a73384b3c779cadcb8548ac6945aa7a925832fe2418c9527a","affectsGlobalScope":true},{"version":"76f838d5d49b65de83bc345c04aa54c62a3cfdb72a477dc0c0fce89a30596c30","affectsGlobalScope":true},{"version":"138fb588d26538783b78d1e3b2c2cc12d55840b97bf5e08bca7f7a174fbe2f17","affectsGlobalScope":true},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true},{"version":"4443e68b35f3332f753eacc66a04ac1d2053b8b035a0e0ac1d455392b5e243b3","affectsGlobalScope":true},{"version":"bc47685641087c015972a3f072480889f0d6c65515f12bd85222f49a98952ed7","affectsGlobalScope":true},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true},{"version":"93495ff27b8746f55d19fcbcdbaccc99fd95f19d057aed1bd2c0cafe1335fbf0","affectsGlobalScope":true},{"version":"6fc23bb8c3965964be8c597310a2878b53a0306edb71d4b5a4dfe760186bcc01","affectsGlobalScope":true},{"version":"ea011c76963fb15ef1cdd7ce6a6808b46322c527de2077b6cfdf23ae6f5f9ec7","affectsGlobalScope":true},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true},{"version":"bb42a7797d996412ecdc5b2787720de477103a0b2e53058569069a0e2bae6c7e","affectsGlobalScope":true},{"version":"4738f2420687fd85629c9efb470793bb753709c2379e5f85bc1815d875ceadcd","affectsGlobalScope":true},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true},{"version":"9fc46429fbe091ac5ad2608c657201eb68b6f1b8341bd6d670047d32ed0a88fa","affectsGlobalScope":true},{"version":"61c37c1de663cf4171e1192466e52c7a382afa58da01b1dc75058f032ddf0839","affectsGlobalScope":true},{"version":"b541a838a13f9234aba650a825393ffc2292dc0fc87681a5d81ef0c96d281e7a","affectsGlobalScope":true},{"version":"b20fe0eca9a4e405f1a5ae24a2b3290b37cf7f21eba6cbe4fc3fab979237d4f3","affectsGlobalScope":true},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true},{"version":"49ed889be54031e1044af0ad2c603d627b8bda8b50c1a68435fe85583901d072","affectsGlobalScope":true},{"version":"e93d098658ce4f0c8a0779e6cab91d0259efb88a318137f686ad76f8410ca270","affectsGlobalScope":true},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true},{"version":"bf14a426dbbf1022d11bd08d6b8e709a2e9d246f0c6c1032f3b2edb9a902adbe","affectsGlobalScope":true},{"version":"5e07ed3809d48205d5b985642a59f2eba47c402374a7cf8006b686f79efadcbd","affectsGlobalScope":true},{"version":"2b72d528b2e2fe3c57889ca7baef5e13a56c957b946906d03767c642f386bbc3","affectsGlobalScope":true},{"version":"8073890e29d2f46fdbc19b8d6d2eb9ea58db9a2052f8640af20baff9afbc8640","affectsGlobalScope":true},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true},{"version":"af3dd424cf267428f30ccfc376f47a2c0114546b55c44d8c0f1d57d841e28d74","affectsGlobalScope":true},{"version":"995c005ab91a498455ea8dfb63aa9f83fa2ea793c3d8aa344be4a1678d06d399","affectsGlobalScope":true},{"version":"51e547984877a62227042850456de71a5c45e7fe86b7c975c6e68896c86fa23b","affectsGlobalScope":true},{"version":"956d27abdea9652e8368ce029bb1e0b9174e9678a273529f426df4b3d90abd60","affectsGlobalScope":true},{"version":"4fa6ed14e98aa80b91f61b9805c653ee82af3502dc21c9da5268d3857772ca05","affectsGlobalScope":true},{"version":"e6633e05da3ff36e6da2ec170d0d03ccf33de50ca4dc6f5aeecb572cedd162fb","affectsGlobalScope":true},{"version":"d8670852241d4c6e03f2b89d67497a4bbefe29ecaa5a444e2c11a9b05e6fccc6","affectsGlobalScope":true},{"version":"8444af78980e3b20b49324f4a16ba35024fef3ee069a0eb67616ea6ca821c47a","affectsGlobalScope":true},{"version":"caccc56c72713969e1cfe5c3d44e5bab151544d9d2b373d7dbe5a1e4166652be","affectsGlobalScope":true},{"version":"3287d9d085fbd618c3971944b65b4be57859f5415f495b33a6adc994edd2f004","affectsGlobalScope":true},{"version":"50d53ccd31f6667aff66e3d62adf948879a3a16f05d89882d1188084ee415bbc","affectsGlobalScope":true},{"version":"08a58483392df5fcc1db57d782e87734f77ae9eab42516028acbfe46f29a3ef7","affectsGlobalScope":true},{"version":"436aaf437562f276ec2ddbee2f2cdedac7664c1e4c1d2c36839ddd582eeb3d0a","affectsGlobalScope":true},{"version":"b1cb28af0c891c8c96b2d6b7be76bd394fddcfdb4709a20ba05a7c1605eea0f9","affectsGlobalScope":true},{"version":"13f6e6380c78e15e140243dc4be2fa546c287c6d61f4729bc2dd7cf449605471","affectsGlobalScope":true},{"version":"15b98a533864d324e5f57cd3cfc0579b231df58c1c0f6063ea0fcb13c3c74ff9","affectsGlobalScope":true},{"version":"ac77cb3e8c6d3565793eb90a8373ee8033146315a3dbead3bde8db5eaf5e5ec6","affectsGlobalScope":true},{"version":"d4b1d2c51d058fc21ec2629fff7a76249dec2e36e12960ea056e3ef89174080f","affectsGlobalScope":true},{"version":"2fef54945a13095fdb9b84f705f2b5994597640c46afeb2ce78352fab4cb3279","affectsGlobalScope":true},{"version":"33358442698bb565130f52ba79bfd3d4d484ac85fe33f3cb1759c54d18201393","affectsGlobalScope":true},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true},{"version":"6abefd7cf9951773b6cf6c1e2e854882b8e60529ac231ac651018355656e5340","signature":"93604360dd3cb5c597f043bd25b4f19dc568ee017d421c9fbed9861894169dce"},{"version":"144d294b0ccdfa50288aa231f78677fa6ed9e8ed615613a06049ca615b81069c","signature":"e0943fe40b305c2e54c9800e5071fcaebf11ff98f204f1fe09fccbaff84f33c4"},{"version":"491c6eb1c3a49b8761d57a496b1dc418234ed17fa357053426adc27b9d7e963c","signature":"d6df5f6fa62a043199e23ec8deda36940e216cfb2b12021ae629e3c91da1d0cc"},{"version":"4b6b6378650409a2812134ce0b8c39b1e68f2cfa3e4bcd66a12a7cce0a4a5628","signature":"d29ae2ff63172838ad1514a457313a58d706f450c5385fbba4967167431d3496"},{"version":"0988f7272449b354e6b65c586530abf5ce576049afe52a7ea9cff64ede31872c","signature":"04d1f4e5565f6408802a1a4d63b45954553ae2ed2f080874c6194f13df6d1956"},{"version":"7ff9ca164861284927bb7b1494305ae96e907d2a828b1bc73e952a12a05e9427","signature":"db859c912c93a963284c5f33f2cf81309b18e6beefeb066a4b9c5c9339b40f41"},"e8542d2d508c661cee8e640044c392948ebc22175718fe3f0eafc3c74fcd26af",{"version":"f43fca1a0191ebb0b266e67530b07e90dbdbd64ef1b410d3337d5d8ac42d5402","signature":"494d22135859073099a042658a77a32a61f569dcff343cca0e2948e6062f969a"},"dbaa4237e685eea2cabc591807ad023ea8c5bddf7e2305875c08f8ca0889d30f",{"version":"c69b5a40c53848ae9078bef30bc7e6c7f4416fd238c1c3cda4b815afa1d3e796","signature":"c252092044f6fb2155e7c5108dfd14937acd48faa95cd2a396dda700e26ce1d6"},{"version":"7e4e24e104c16e0faaeee5bfbcb94e4a25d8b7e7b1ff31a74458e9cd74e35d1a","signature":"1d5b9be725f8d4676e59f83004e665874e2f84dbea8fc52d0fe5350308d6e476"},"41f3a76f048488216ccf77f19e26495b0ed621dccb5183625b56a94ce3afe7b1","153f97b0659257f89aa02d89c591433a9e6b8d139fd0db4536f7a10e02c3cad9","b50fd46bb5d839360b5735175cea4f7bd73c14181b42f0713a12d5bab3115b7c","9249a2db228a66d9f2a930e79e73ab2509113e53eb350c07de01aba23e30e9b8","f5058de03ccf29a3e7ad5a1631221ad787a4b51408406b5a1392a3bc618488e4","73f55daafe3dd962b882c068e4d48e5d2a1563364a57725fffae9289c80498f4","a124c90f365a06d5626b62113044831181c08543aa5a64fcde73fa54e82fd792","76e829c980431942cd9ac591c90110f2b4bfe67397f504c1aa68324d2810e0b4","40e8ccb33d46f6b9cf55a7690085f9fcf81bd9510e34aafe4676e816b1ceec9c","405344c05db80ce07a83e8804d906bff009abd831867d1bfd72a762ac4bb04a2","62fabf26f5abf27b6754e9d168b0eb31d76e072dd7eb2cd483ea73aa99528dfa","87703da4961922343e7947932a94733d5086ff42619ed74d9961ac9e2fd2cb11",{"version":"d0c286cef87830a76eb6641fc974a8a95736030567b25f4f3abd804ba59ecb47","signature":"d8b89aea1b6a1cf139310b93879ef97aca1aed903741c4ebe1b70a5e977b8c6c"},{"version":"05aef3a03c124e5a972c81f99c6159bb3e0a19b0e2dcb3e2794d3d781062d09a","signature":"19c94e8d17169d77d61edd07845a1545255041e5addbc0b1dbc052e83a20ffd5"},{"version":"da33095eee4aa96bd3132bf5d0ff90b33e4fad5553acd2c52ff317c08b2a1235","signature":"4e9ccd724ef5dfe1fb0374efd1a171f4acb345e52eaaa17d689b499a56b788e9"},{"version":"c61e92957deb58e65b16b54459ba91f084501c913199268c64c075babb89e3db","signature":"62556c03de0f5e2bd3850c23cee5bd017a819bb87b6803128a1756825307fe8d"},{"version":"16ce05f79a8af1bc56ed18889cae7a51974133bfa90f546935118db42141723f","signature":"ea349755a67c8e3248a8040f12499e23144c80754ede2036ef34ea89967be8d1"},{"version":"e66f20faf2a7a8d37a3392fd8e35edea05e5d5e0509e93d6be83cf7946e1ec14","signature":"8f15d6b029d0818da1b1cc43815127f887ddd7d6080b7e68be9916cadec5ce30"},"d8fac3c77c131743cc12f0acb6509f583b1feed49923a2c465c6b01b1811143e",{"version":"29616f14c2f0c2684d238bbd93d85ede6d9b8bc76b91eaa7f62f14950dc29848","signature":"bfaf88bcf001f9921f110bf7ae321ff0d8edc5669aeec3d01f9beb0c122be116"},"ae576b4a68f96cd1c6e423ff41f4169ecdc7c70dad07fc41d535444a294c7e26","8c2e9e5890815a3943c416da4152bebaee7ce4e72cdc69a7f35d6a4e9cd74487","f0c3d97601fc1528bfa58606e51137b6e1ba9f9c5f25bccb91d2ddee3111dbcf","65090712afd608cc6450f9f02dd75e0a21ba61948b8f73f3ee13af4a7239cba1",{"version":"a0cdcf9b9ece8fb25b4d9cd52a0a87be0a85db6c18cbdcd5ce53f1c0783e4344","signature":"41fcc00960e16a0faa46d34515d08891aafff99fdb86f285bd1c0ebaaa75928b"},{"version":"a02efbef37eef7ef89cf5bac3275382405d974af648616cd63f0fb4b43dc990a","signature":"41fbe68bd01aae8012ca990872ebf2ed95a3884e44ecb2bdfea6b103254dd9f5"},{"version":"2e51fa7ea33d625301279652dcc6b0bbe9c4a8c5087efa28052c703e9a3140c4","signature":"a85d5d0bcf9e02b192f21fd8cfe0f3dd3df3b97eab4e580d161ab80aedc249a3"},"fdd2342750b0808dc5c6cf7f8f9340cf4b34ee29ddd637bcb115fbcd781973b8",{"version":"64d4b35c5456adf258d2cf56c341e203a073253f229ef3208fc0d5020253b241","affectsGlobalScope":true},"785b9d575b49124ce01b46f5b9402157c7611e6532effa562ac6aebec0074dfc","f3d8c757e148ad968f0d98697987db363070abada5f503da3c06aefd9d4248c1","7852500a7dc3f9cb6b73d619f6e0249119211ea662fd5e16c59ee5aba3deeb80","1f68ab0e055994eb337b67aa87d2a15e0200951e9664959b3866ee6f6b11a0fe"],"root":[[70,80],[93,100],[105,108]],"options":{"allowImportingTsExtensions":false,"allowSyntheticDefaultImports":true,"composite":true,"declaration":true,"declarationMap":true,"emitDeclarationOnly":false,"esModuleInterop":true,"importHelpers":true,"jsx":1,"module":99,"noEmitHelpers":true,"noEmitOnError":true,"noImplicitAny":true,"noImplicitReturns":false,"noImplicitThis":true,"noPropertyAccessFromIndexSignature":false,"noUncheckedIndexedAccess":true,"noUnusedLocals":true,"noUnusedParameters":true,"outDir":"./dist","rootDir":"./source","skipLibCheck":true,"sourceMap":false,"strict":true,"target":99},"fileIdsList":[[73,74,75],[72,73],[77],[70,71,72],[70,71,72,76,78,99],[70,71],[105,106,107],[76,104,105],[72,76,92,104,105],[76],[79,80,94,95,96,97,98],[79],[79,92,93],[71,72,77],[70],[84,85,86,87],[81],[81,82,83,88,89,90,91],[101,102,103],[101],[110,112],[109,110,111],[73],[71,72],[104,105],[79,100],[71]],"referencedMap":[[76,1],[74,2],[75,2],[78,3],[77,4],[100,5],[72,6],[108,7],[107,8],[106,9],[105,10],[99,11],[80,12],[94,13],[95,13],[96,12],[79,14],[97,12],[98,12],[71,15],[88,16],[90,17],[89,17],[92,18],[82,17],[83,17],[104,19],[103,20],[102,20],[113,21],[112,22]],"exportedModulesMap":[[76,1],[74,23],[75,23],[78,3],[77,24],[100,5],[72,6],[108,7],[107,25],[106,25],[105,10],[99,11],[80,26],[94,26],[95,26],[96,26],[79,27],[97,26],[98,26],[71,15],[88,16],[90,17],[89,17],[92,18],[82,17],[83,17],[104,19],[103,20],[102,20],[113,21],[112,22]],"semanticDiagnosticsPerFile":[76,74,75,73,70,78,77,100,72,108,107,106,105,99,80,94,95,93,96,79,97,98,71,101,88,84,85,87,86,90,89,92,82,83,91,81,104,103,102,113,109,112,110,111,68,69,12,13,15,14,2,16,17,18,19,20,21,22,23,3,24,4,25,29,26,27,28,30,31,32,5,33,34,35,36,6,40,37,38,39,41,7,42,47,48,43,44,45,46,8,52,49,50,51,53,9,54,55,56,59,57,58,60,61,10,1,62,11,66,64,63,67,65],"latestChangedDtsFile":"./dist/signals/accept.d.ts"},"version":"5.4.5"} \ No newline at end of file diff --git a/clients/vim/.gitattributes b/clients/vim/.gitattributes deleted file mode 100644 index 5f2fd032fb30..000000000000 --- a/clients/vim/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -*.wasm filter=lfs diff=lfs merge=lfs -text \ No newline at end of file diff --git a/clients/vim/.gitignore b/clients/vim/.gitignore index c705c365f812..0a56e3fc5d8c 100644 --- a/clients/vim/.gitignore +++ b/clients/vim/.gitignore @@ -1,2 +1 @@ /doc/tags -node_modules diff --git a/clients/vim/CHANGELOG.md b/clients/vim/CHANGELOG.md index ef63dca9055d..f6703d0035ba 100644 --- a/clients/vim/CHANGELOG.md +++ b/clients/vim/CHANGELOG.md @@ -1,76 +1,10 @@ -## 1.3.2 - -### Fixes: - -- Disabled experimental features by default: - - Stripping auto-closing characters in prompt suffix. - - Syntax-based code completion scope limit. - - Syntax-based replace range calculation. - -## 1.3.0 - -### Features: - -- Removed the completion request timeout limit. -- Enabled the experimental feature of stripping auto-closing characters in the prompt suffix by default. -- Enabled the experimental feature of syntax-based post-processing by default. -- Added support for logging completion dismiss events. - -### Fixes: - -- Fixed health checking to be compatible with Tabby server version 0.2.0 or earlier. - -## 1.2.0 - -### Features: - -- The status "Authorization required" has been updated to be more commonly used, as it was previously only used for Tabby Cloud users. -- Removed command `:Tabby auth` for opening the authentication page and fetching the token when using Tabby Cloud. - - To connect to Tabby Cloud server, you need to manually set the token instead. The token already in use will remain usable. - -### Fixes: - -- Fixed a bug that caused a script error when attempting to map the `` key with a fallback to an original mapping that had an empty `rhs`. -- Corrected invalid online documentation links. -- Resolved a bug that resulted in empty log files being generated even when the logging level is set to `silent`. -- Fixed bugs related to the experimental syntax-based post-processing. - -## 1.1.1 - -### Fixes: - -- Fix a bug cause the completion does not show up if the completion cache is missing. - -## 1.1.0 - -### Features: - -- Updated the config.toml file to include new configuration options: `server.auth` and `completion.timeout`. -- Added a command `:Tabby version` to print the current version of Tabby plugin. -- Added experimental features aimed at fine-tuning completion quality. These features are disabled by default but can be enabled by setting the corresponding config flag to `true` in the `config.toml` file, include: - - `completion.prompt.experimentalStripAutoClosingCharacters`: Strip auto-closing brackets and quotes in prompt suffix, to generate more lines in FIM mode. - - `postprocess.limitScope.indentation.experimentalKeepBlockScopeWhenCompletingLine`: Use the block scope instead of line scope when using indentation to limit the completion scope and the completion is continuing the current line. - - `postprocess.limitScope.experimentalSyntax`: Use syntax parser to limit the completion scope. - - `postprocess.calculateReplaceRange.experimentalSyntax`: Use syntax parser to calculate the completion replace range, to avoid duplicated auto-closing brackets and quotes. - -### Fixes: - -- Fixed a bug causing the `` key to input unexpected characters when fallback to another plugin script. - -## 1.0.2 - -### Fixes: - -- Fixed a bug causing status stuck in 'initializing' when script not found. - -## 1.0.1 - -### Fixes: - -- Fixed when resolving the server address, it will now try to prefer to use ipv4 over ipv6. -- Fixed a bug causing the `` key can not fallback to the default behavior. -- Fixed a bug causing the completion replace range is rendered incorrectly. - -## 1.0.0 - -### Initial release +## 2.0.0 + +Since version 2.0, the vim-tabby plugin is designed as two parts: +1. **LSP Client Extension**: + - Relies on an LSP client and extends it with methods (such as `textDocument/inlineCompletion`) to communicate with the tabby-agent. + - Note: The Node.js script of tabby-agent is no longer a built-in part of the vim-tabby plugin. You need to install tabby-agent separately via npm, and the LSP client will launch it using the command `npx tabby-agent --stdio`. +2. **Inline Completion UI**: + - Automatically triggers inline completion requests when typing. + - Renders the inline completion text as ghost text. + - Sets up actions with keyboard shortcuts to accept or dismiss the inline completion. diff --git a/clients/vim/README.md b/clients/vim/README.md index 605230664430..4479651b76c1 100644 --- a/clients/vim/README.md +++ b/clients/vim/README.md @@ -1,164 +1,100 @@ -# Tabby Plugin for Vim and NeoVim - -Tabby is a self-hosted AI coding assistant that can suggest multi-line code or full functions in real-time. For more information, please check out our [website](https://tabbyml.com/) and [github](https://github.com/TabbyML/tabby). -If you encounter any problem or have any suggestion, please [open an issue](https://github.com/TabbyML/tabby/issues/new) or join our [Slack community](https://links.tabbyml.com/join-slack) for support. - -## Table of Contents - -- [Tabby Plugin for Vim and NeoVim](#tabby-plugin-for-vim-and-neovim) - - [Table of Contents](#table-of-contents) - - [Requirements](#requirements) - - [Installation](#installation) - - [🔌 Vim-plug](#-vim-plug) - - [📦 Packer.nvim](#-packernvim) - - [💤 Lazy.nvim](#-lazynvim) - - [Usage](#usage) - - [Configuration](#configuration) - - [Tabby Server](#tabby-server) - - [Node.js Binary Path](#nodejs-binary-path) - - [Completion Trigger Mode](#completion-trigger-mode) - - [KeyBindings](#keybindings) - - [Contributing](#contributing) - - [License](#license) +# Tabby Plugin for Vim and Neovim -## Requirements - -Tabby plugin requires the following dependencies: - -- Vim 9.0+ with `+job` and `+textprop` features enabled, or NeoVim 0.6.0+. -- Tabby server. You can install Tabby server locally or have it hosted on a remote server. For Tabby server installation, please refer to this [documentation](https://tabby.tabbyml.com/docs/installation/). -- [Node.js](https://nodejs.org/en/download/) version v18.0+. - - If you need have multiple Node.js versions installed, you can use Node.js version manager such as [nvm](https://github.com/nvm-sh/nvm). -- Vim filetype plugin enabled. You can add following lines in vim config file (`~/.vimrc`). For NeoVim, filetype plugin is enabled by default, you don't need to add these lines. - - ```vim - filetype plugin on - ``` - -## Installation - -You can install Tabby plugin using your favorite plugin manager. Here are some examples using popular plugin managers, you can choose one to follow. - -### 🔌 [Vim-plug](https://github.com/junegunn/vim-plug) +Tabby is a self-hosted AI coding assistant that can suggest multi-line code or full functions in real-time. For more information, please check out our [website](https://tabbyml.com/) and [GitHub](https://github.com/TabbyML/tabby). +If you encounter any problems or have any suggestions, please [open an issue](https://github.com/TabbyML/tabby/issues/new) or join our [Slack community](https://links.tabbyml.com/join-slack) for support. -```vim -" Example ~/.vimrc configuration -filetype plugin on +## Notable Changes in vim-tabby Plugin 2.0 -" Section for plugins managed by vim-plug -plug#begin() +Since version 2.0, the vim-tabby plugin is designed as two parts: +1. **LSP Client Extension**: + - Relies on an LSP client and extends it with methods (such as `textDocument/inlineCompletion`) to communicate with the tabby-agent. + - Note: The Node.js script of tabby-agent is no longer a built-in part of the vim-tabby plugin. You need to install tabby-agent separately via npm, and the LSP client will launch it using the command `npx tabby-agent --stdio`. +2. **Inline Completion UI**: + - Automatically triggers inline completion requests when typing. + - Renders the inline completion text as ghost text. + - Sets up actions with keyboard shortcuts to accept or dismiss the inline completion. -" Tabby plugin -Plug 'TabbyML/vim-tabby' -" Add config here. Example config: -let g:tabby_keybinding_accept = '' - -plug#end() -``` +## Requirements -### 📦 [Packer.nvim](https://github.com/wbthomason/packer.nvim) +The Tabby plugin requires the following dependencies: -```lua ---- Example Packer plugin specification +- **Tabby Server**: The backend LLM server. You can install the Tabby server locally or have it hosted on a remote server. For Tabby server installation, please refer to this [documentation](https://tabby.tabbyml.com/docs/installation/). +- **Tabby Agent (LSP server)**: Requires [Node.js](https://nodejs.org/en/download/) version v18.0+ and [tabby-agent](https://www.npmjs.com/package/tabby-agent) installed. + ```sh + npm install --global tabby-agent + ``` +- **LSP Client**: The Neovim built-in LSP client, or a Vim plugin that provides an LSP client. Supported LSP clients include: + - The Neovim built-in LSP client, with the [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig) plugin installed. + - More clients are in development. +- **Textprop Support**: Neovim, or Vim v9.0+ with `+textprop` features enabled. This is required for inline completion ghost text rendering. ---- Add config here. Example config: -vim.g.tabby_keybinding_accept = '' +## Installation -return require('packer').startup(function(use) - --- Tabby plugin - use { 'TabbyML/vim-tabby' } -end) -``` +You can install the Tabby plugin using your favorite plugin manager by simply adding `TabbyML/vim-tabby` to the registry. -### 💤 [Lazy.nvim](https://github.com/folke/lazy.nvim) +Here is a detailed example setup with advanced options, based on [Neovim](https://neovim.io/), [Lazy.nvim](https://github.com/folke/lazy.nvim), and [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig). ```lua ---- Example Lazy plugin specification - ---- Add config here. Example config: -vim.g.tabby_keybinding_accept = '' - -return { - { 'TabbyML/vim-tabby' } -} -``` - -## Usage - -After installation, please exit and restart Vim or NeoVim. Then you can check the Tabby plugin status by running `:Tabby` in your vim command line. If you see any message reported by Tabby, it means the plugin is installed successfully. If you see `Not an editor command: Tabby` or any other error message, please check the installation steps. - -In insert mode, Tabby plugin will show inline completion automatically when you stop typing. You can simply press `` to accept the completion. If you want to dismiss the completion manually, you can press `` to dismiss, and press `` again to show the completion again. - -## Configuration - -### Tabby Server - -You need to start the Tabby server before using the plugin. For Tabby server installation, please refer to this [documentation](https://tabby.tabbyml.com/docs/installation/). - -If your Tabby server endpoint is different from the default `http://localhost:8080`, please set the endpoint in `~/.tabby-client/agent/config.toml`. - -If your Tabby server requires an authentication token, remember to set it here. - -```toml -# Server -# You can set the server endpoint here. -[server] -endpoint = "http://localhost:8080" # http or https URL -token = "your-auth-token" -``` - -### Node.js Binary Path - -Normally, this config is not required as the Tabby plugin will try to find the Node.js binary in your `PATH` environment variable. -But if you have installed Node.js in a non-standard location, or you are using a Node.js version manager such as nvm, you can set the Node.js binary path in your vim config file (`~/.vimrc` for Vim and `~/.config/nvim/init.vim` or `~/.config/nvim/init.lua` for NeoVim). - -```vim -let g:tabby_node_binary = '/path/to/node' +-- ~/.config/nvim/init.lua +require("lazy").setup({ + -- other plugins + -- ... + -- Tabby plugin + { + "TabbyML/vim-tabby", + lazy = false, + dependencies = { + "neovim/nvim-lspconfig", + }, + init = function() + vim.g.tabby_agent_start_command = {"npx", "tabby-agent", "--stdio"} + vim.g.tabby_inline_completion_trigger = "auto" + end, + }, +}) ``` +After setting up the plugin, you can open a file in Neovim and use `:LspInfo` to check if the Tabby plugin is successfully connected. -```lua ---- lua -vim.g.tabby_node_binary = '/path/to/node' -``` +## Getting Started -### Completion Trigger Mode +### 1. Setup Tabby Server +The Tabby plugin requires a Tabby server to work. Follow the [documentation](https://tabby.tabbyml.com/docs/installation/) to install and [create your account](https://tabby.tabbyml.com/docs/quick-start/register-account/). -Completion trigger mode is set to `auto` by default, Tabby plugin will show inline completion automatically when you stop typing. -If you prefer to trigger code completion manually, add this config in your vim config file. Tabby plugin will not show inline completion automatically, you can trigger the completion manually by pressing ``. +### 2. Connect to the Server +Edit the tabby-agent config file located at `~/.tabby-client/agent/config.toml` to set up the server endpoint and token. This file may have been auto-created if you have previously used the tabby-agent or Tabby plugin for other IDEs. You can also manually create this file. -```vim -let g:tabby_trigger_mode = 'manual' -``` + ```toml + [server] + endpoint = "http://localhost:8080" + token = "your-auth-token" + ``` -```lua ---- lua -vim.g.tabby_trigger_mode = 'manual' -``` +### 3. Code Completion +Tabby suggests code completions in real-time as you write code. You can also trigger the completion manually by pressing ``. To accept suggestions, simply press the `` key. You can also continue typing or explicitly press `` again to dismiss it. -### KeyBindings +## Known Conflicts -The default key bindings for accept completion(``), manual trigger/dismiss(``) can be customized with the following global settings. +- Tabby will attempt to set up the `` key mapping to accept the inline completion and will fall back to the original function mapped to it. There could be a conflict with other plugins that also map the `` key. In such cases, you can use a different keybinding to accept the completion to avoid conflicts. -```vim -let g:tabby_keybinding_accept = '' -let g:tabby_keybinding_trigger_or_dismiss = '' -``` +- Tabby internally utilizes the `` command to insert the completion. If you have mapped it to other functions, the insertion of the completion text may fail. -```lua ---- lua -vim.g.tabby_keybinding_accept = '' -vim.g.tabby_keybinding_trigger_or_dismiss = '' -``` +## Configurations -## Known Conflicts +You can find a detailed explanation of tabby-agent configurations in the [Tabby online documentation](https://tabby.tabbyml.com/docs/extensions/configurations/). -- For the default settings, Tabby will attempt to set up the `` key mapping. If Tabby's inline completion is not displayed, it will fall back to the original mapping. However, this approach might not work when there is a conflict with other plugins that also map the `` key, as they could overwrite Tabby's mapping. In such cases, you can use a different keybinding to accept the completion and avoid conflicts. +Here is a table of all configuration variables that can be set when the Tabby plugin initializes: -- Tabby internally utilizes the `` command to insert the completion. If you have mapped `` to other functions, you won't be able to accept the completion. In such scenarios, you may need to manually modify the function `tabby#Accept()` in [`autoload/tabby.vim`](https://github.com/TabbyML/tabby/tree/main/clients/vim/autoload/tabby.vim). +| Variable | Default | Description | +| --- | --- | --- | +| `g:tabby_agent_start_command` | `["npx", "tabby-agent", "--stdio"]` | The command to start the tabby-agent | +| `g:tabby_inline_completion_trigger` | `"auto"` | The trigger mode of inline completion, can be `"auto"` or `"manual"` | +| `g:tabby_inline_completion_keybinding_accept` | `""` | The keybinding to accept the inline completion | +| `g:tabby_inline_completion_keybinding_trigger_or_dismiss` | `""` | The keybinding to trigger or dismiss the inline completion | +| `g:tabby_inline_completion_insertion_leading_key` | `"\\="` | The leading key sequence to insert the inline completion text | ## Contributing -Repository [TabbyML/vim-tabby](https://github.com/TabbyML/vim-tabby) is for releasing Tabby plugin for Vim and NeoVim. If you want to contribute to Tabby plugin, please check our main repository [TabbyML/tabby](https://github.com/TabbyML/tabby/tree/main/clients/vim). +Repository [TabbyML/vim-tabby](https://github.com/TabbyML/vim-tabby) is for releasing Tabby plugin for Vim and Neovim. If you want to contribute to Tabby plugin, please check our main repository [TabbyML/tabby](https://github.com/TabbyML/tabby/tree/main/clients/vim). ## License diff --git a/clients/vim/autoload/tabby.vim b/clients/vim/autoload/tabby.vim index a4bf80e845a1..655cf1bdf227 100644 --- a/clients/vim/autoload/tabby.vim +++ b/clients/vim/autoload/tabby.vim @@ -1,299 +1,11 @@ -" Main functions - if exists('g:autoloaded_tabby') finish endif let g:autoloaded_tabby = 1 -let s:status = "initializing" -let s:message = "" - -function! tabby#Status() - if s:status == "initializing" - echo 'Tabby is initializing.' - elseif s:status == "initialization_failed" - echo 'Tabby initialization failed.' - echo s:message - elseif s:status == "initialization_done" - let agent_status = tabby#agent#Status() - if agent_status == 'notInitialized' - echo 'Tabby is initializing.' - elseif agent_status == 'exited' - echo 'Tabby agent exited unexpectedly.' - elseif agent_status == 'ready' - echo 'Tabby is online.' - let agent_issues = tabby#agent#Issues() - if len(agent_issues) > 0 - if agent_issues[0] == 'slowCompletionResponseTime' - echo 'Completion requests appear to take too much time.' - elseif agent_issues[0] == 'highCompletionTimeoutRate' - echo 'Most completion requests timed out.' - endif - elseif g:tabby_trigger_mode == 'manual' - echo 'You can use ' . g:tabby_keybinding_trigger_or_dismiss . - \ ' in insert mode to trigger completion manually.' - elseif g:tabby_trigger_mode == 'auto' - echo 'Automatic inline completion is enabled.' - endif - elseif agent_status == 'disconnected' - echo 'Tabby cannot connect to server. Please check your settings.' - elseif agent_status == 'unauthorized' - echo 'Authorization required. Please set your personal token in settings.' - endif - endif -endfunction - -function! tabby#OnVimEnter() - call tabby#globals#Load() - - let check_job = tabby#job#Check() - if !check_job.ok - let s:status = "initialization_failed" - let s:message = check_job.message - return - endif - - let check_virtual_text = tabby#virtual_text#Check() - if !check_virtual_text.ok - let s:status = "initialization_failed" - let s:errmsg = check_virtual_text.message - return - endif - call tabby#virtual_text#Init() - - let node_binary = expand(g:tabby_node_binary) - if !executable(node_binary) - let s:status = "initialization_failed" - let s:message = 'Node.js binary not found. Please install Node.js version >= 18.0.' - return - endif - - let node_version_command = node_binary . ' --version' - let version_output = system(node_version_command) - let node_version = matchstr(version_output, '\d\+\.\d\+\.\d\+') - let major_version = str2nr(split(node_version, '\.')[0]) - if major_version < 18 - let s:status = "initialization_failed" - let s:message = 'Node.js version is too old: ' . node_version . '. Please install Node.js version >= 18.0.' - return - endif - - if !filereadable(g:tabby_node_script) - let s:status = "initialization_failed" - let s:message = 'Tabby agent script not found. Please reinstall Tabby plugin.' - return - endif - - let command = node_binary . ' --dns-result-order=ipv4first ' . g:tabby_node_script - call tabby#agent#Open(command) - - call tabby#keybindings#Map() - - let s:status = "initialization_done" -endfunction +let g:tabby_version = "2.0.0-dev" -function! tabby#OnVimLeave() - call tabby#agent#Close() +function! tabby#Setup() + call tabby#lsp#Setup() + call tabby#inline_completion#Setup() endfunction - -function! tabby#OnTextChanged() - if s:status != "initialization_done" - return - endif - if g:tabby_trigger_mode == 'auto' - " FIXME: Do not dismiss when type over the same as the completion text, or backspace in replace range. - call tabby#Dismiss() - call tabby#Trigger(v:false) - endif -endfunction - -function! tabby#OnCursorMoved() - if s:current_completion_request == s:GetCompletionContext(v:false) - return - endif - call tabby#Dismiss() - if s:ongoing_request_id != 0 - call tabby#agent#CancelRequest(s:ongoing_request_id) - endif -endfunction - -function! tabby#OnInsertLeave() - call tabby#Dismiss() - if s:ongoing_request_id != 0 - call tabby#agent#CancelRequest(s:ongoing_request_id) - endif -endfunction - -function! tabby#TriggerOrDismiss() - if s:status != "initialization_done" - return '' - endif - if s:current_completion_response != {} - call tabby#Dismiss() - else - call tabby#Trigger(v:true) - endif - return '' -endfunction - -" Store the context of ongoing completion request -let s:current_completion_request = {} -let s:ongoing_request_id = 0 - -function! tabby#Trigger(is_manual) - if s:status != "initialization_done" - return - endif - if s:ongoing_request_id != 0 - call tabby#agent#CancelRequest(s:ongoing_request_id) - endif - let s:current_completion_request = s:GetCompletionContext(a:is_manual) - let request = s:current_completion_request - let OnResponse = { response -> s:HandleCompletionResponse(request, response) } - let s:ongoing_request_id = tabby#agent#ProvideCompletions(request, OnResponse) -endfunction - -" Store the completion response that is shown as inline completion. -let s:current_completion_response = {} -let s:current_completion_display_at = 0 -let s:current_completion_display_id = "" - -function! s:HandleCompletionResponse(request, response) - if s:current_completion_request != a:request - return - endif - let s:ongoing_request_id = 0 - if (type(a:response) != v:t_dict) || !has_key(a:response, 'choices') || - \ (type(a:response.choices) != v:t_list) - return - endif - call tabby#Dismiss() - if (len(a:response.choices) == 0) - return - endif - " Only support single choice completion for now - let choice = a:response.choices[0] - call tabby#virtual_text#Render(s:current_completion_request, choice) - let s:current_completion_response = a:response - let s:current_completion_display_at = s:GetTimestamp() - let cmplId = substitute(a:response.id, 'cmpl-', '', '') - let s:current_completion_display_id = "view-" . cmplId . "-at-" . string(s:current_completion_display_at) - - call tabby#agent#PostEvent(#{ - \ type: "view", - \ completion_id: a:response.id, - \ choice_index: choice.index, - \ view_id: s:current_completion_display_id, - \ }) -endfunction - -" Used as a buffer to store the text that should be inserted when user accepts -" the completion. -let s:text_to_insert = '' - -function! tabby#ConsumeInsertion() - let text = s:text_to_insert - let s:text_to_insert = '' - return text -endfunction - -function! tabby#Accept(...) - if s:current_completion_response == {} - " keybindings fallback - if a:0 < 1 - return "\" - elseif type(a:1) == v:t_string - return a:1 - elseif type(a:1) == v:t_func - return call(a:1, []) - endif - endif - - let accept_at = s:GetTimestamp() - let response = s:current_completion_response - let choice = response.choices[0] - if (type(choice.text) != v:t_string) || (len(choice.text) == 0) - return - endif - let prefix_replace_chars = s:current_completion_request.position - choice.replaceRange.start - let suffix_replace_chars = choice.replaceRange.end - s:current_completion_request.position - let s:text_to_insert = strcharpart(choice.text, prefix_replace_chars) - let insertion = repeat("\", suffix_replace_chars) . "\\=tabby#ConsumeInsertion()\" - - if s:text_to_insert[-1:] == "\n" - " Add a char and remove, workaround for insertion bug if ends with newline - let s:text_to_insert .= "_" - let insertion .= "\" - endif - - call tabby#agent#PostEvent(#{ - \ type: "select", - \ completion_id: response.id, - \ choice_index: choice.index, - \ view_id: s:current_completion_display_id, - \ elapsed: accept_at - s:current_completion_display_at, - \ }) - - call tabby#ClearDisplayedCompletion() - return insertion -endfunction - -function! tabby#Dismiss() - if s:current_completion_response == {} - return - endif - - let dismiss_at = s:GetTimestamp() - let response = s:current_completion_response - let choice = response.choices[0] - call tabby#agent#PostEvent(#{ - \ type: "dismiss", - \ completion_id: response.id, - \ choice_index: choice.index, - \ view_id: s:current_completion_display_id, - \ elapsed: dismiss_at - s:current_completion_display_at, - \ }) - - call tabby#ClearDisplayedCompletion() -endfunction - -function! tabby#ClearDisplayedCompletion() - let s:current_completion_response = {} - let s:current_completion_display_at = 0 - let s:current_completion_display_id = "" - call tabby#virtual_text#Clear() -endfunction - -function! s:GetCompletionContext(is_manual) - return #{ - \ filepath: expand('%:p'), - \ language: s:GetLanguage(), - \ text: join(getbufline('%', 1, '$'), "\n"), - \ position: s:GetCursorPosition(), - \ manually: a:is_manual, - \ } -endfunction - -" Count the number of characters from the beginning of the buffer to the cursor. -function! s:GetCursorPosition() - let lines = getline(1, line('.') - 1) - if col('.') > 1 - let lines += [strpart(getline(line('.')), 0, col('.') - 1)] - else - let lines += [''] - endif - return strchars(join(lines, "\n")) -endfunction - -function! s:GetLanguage() - let filetype = getbufvar('%', '&filetype') - if has_key(g:tabby_filetype_dict, filetype) - return g:tabby_filetype_dict[filetype] - else - return filetype - endif -endfunction - -function! s:GetTimestamp() - return float2nr(reltimefloat(reltime()) * 1000) -endfunction \ No newline at end of file diff --git a/clients/vim/autoload/tabby/agent.vim b/clients/vim/autoload/tabby/agent.vim deleted file mode 100644 index 6b035e333902..000000000000 --- a/clients/vim/autoload/tabby/agent.vim +++ /dev/null @@ -1,157 +0,0 @@ -" Implementation of agent interface - -if exists('g:autoloaded_tabby_agent') - finish -endif -let g:autoloaded_tabby_agent = 1 - -" Stores the job of the current tabby agent node process -let s:tabby = 0 - -" Stores the status of the tabby agent -let s:tabby_status = 'notInitialized' - -" Stores the name of issues if any -let s:tabby_issues = [] - -function! tabby#agent#Status() - return s:tabby_status -endfunction - -function! tabby#agent#Issues() - return s:tabby_issues -endfunction - -function! tabby#agent#Open(command) - if type(s:tabby) != v:t_number || s:tabby != 0 - return - endif - - let s:tabby = tabby#job#Start(a:command, #{ - \ out_cb: { _, data -> s:OnNotification(data) }, - \ err_cb: { _, data -> s:OnError(data) }, - \ exit_cb: { _ -> s:OnExit() }, - \ }) - - call tabby#agent#Initialize() -endfunction - -function! s:OnNotification(data) - if (type(a:data) == v:t_dict) && has_key(a:data, 'event') - if a:data.event == 'statusChanged' - let s:tabby_status = a:data.status - elseif a:data.event == 'issuesUpdated' - let s:tabby_issue = a:data.issues - endif - endif -endfunction - -function! s:OnError(data) - " For Debug - " echoerr "OnError: " . string(a:data) -endfunction - -function! s:OnExit() - let s:tabby = {} - let s:tabby_status = 'exited' -endfunction - -function! tabby#agent#Close() - if type(s:tabby) == v:t_number && s:tabby == 0 - return - endif - call tabby#job#Stop(s:tabby) - let s:tabby = {} - let s:tabby_status = 'exited' -endfunction - -function! tabby#agent#Initialize() - if type(s:tabby) == v:t_number && s:tabby == 0 - return - endif - call tabby#job#Send(s:tabby, #{ - \ func: 'initialize', - \ args: [#{ - \ clientProperties: s:GetClientProperties(), - \ }], - \ }) -endfunction - -function! tabby#agent#RequestAuthUrl(OnResponse) - if type(s:tabby) == v:t_number && s:tabby == 0 - return - endif - call tabby#job#Send(s:tabby, #{ - \ func: 'requestAuthUrl', - \ args: [], - \ }, #{ - \ callback: { _, data -> a:OnResponse(data) }, - \ }) -endfunction - -function! tabby#agent#WaitForAuthToken(code) - if type(s:tabby) == v:t_number && s:tabby == 0 - return - endif - call tabby#job#Send(s:tabby, #{ - \ func: 'waitForAuthToken', - \ args: [a:code], - \ }) -endfunction - -function! tabby#agent#ProvideCompletions(request, OnResponse) - if type(s:tabby) == v:t_number && s:tabby == 0 - return - endif - let requestId = tabby#job#Send(s:tabby, #{ - \ func: 'provideCompletions', - \ args: [a:request, { "signal": v:true }], - \ }, #{ - \ callback: { _, data -> a:OnResponse(data) }, - \ }) - return requestId -endfunction - -function! tabby#agent#CancelRequest(requestId) - if type(s:tabby) == v:t_number && s:tabby == 0 - return - endif - call tabby#job#Send(s:tabby, #{ - \ func: 'cancelRequest', - \ args: [a:requestId], - \ }) -endfunction - -function! tabby#agent#PostEvent(event) - if type(s:tabby) == v:t_number && s:tabby == 0 - return - endif - call tabby#job#Send(s:tabby, #{ - \ func: 'postEvent', - \ args: [a:event], - \ }) -endfunction - -function! s:GetClientProperties() - let version_output = execute('version') - let client = split(version_output, "\n")[0] - let name = split(client, ' ')[0] - return #{ - \ user: #{ - \ vim: #{ - \ triggerMode: g:tabby_trigger_mode - \ } - \ }, - \ session: #{ - \ client: client, - \ ide: #{ - \ name: name, - \ version: client, - \ }, - \ tabby_plugin: #{ - \ name: 'TabbyML/vim-tabby', - \ version: g:tabby_version, - \ }, - \ } - \ } -endfunction diff --git a/clients/vim/autoload/tabby/commands.vim b/clients/vim/autoload/tabby/commands.vim deleted file mode 100644 index d24db7512541..000000000000 --- a/clients/vim/autoload/tabby/commands.vim +++ /dev/null @@ -1,77 +0,0 @@ -" Commands for Tabby - -if exists('g:autoloaded_tabby_commands') - finish -endif -let g:autoloaded_tabby_commands = 1 - - -" See `*Tabby-commands*` section in `doc/tabby.txt` for more details. - -" A dictionary contains all commands. Use name as key and function as value. -let s:commands = {} - -function! s:commands.status(...) - call tabby#Status() -endfunction - -function! s:commands.version(...) - echo g:tabby_version -endfunction - -function! s:commands.help(...) - let args = get(a:, 1, []) - if len(args) < 1 - execute 'help Tabby' - return - endif - try - execute 'help Tabby-' . join(args, '-') - return - catch - endtry - try - execute 'help tabby_' . join(args, '_') - return - catch - endtry - execute 'help Tabby' -endfunction - -function! tabby#commands#Main(args) - let args = split(a:args, ' ') - if len(args) < 1 - call tabby#Status() - echo 'Use `:help Tabby` to see available commands.' - return - endif - if has_key(s:commands, args[0]) - call s:commands[args[0]](args[1:]) - else - echo 'Unknown command.' - echo 'Use `:help Tabby` to see available commands.' - endif -endfunction - -function! tabby#commands#Complete(arglead, cmd, pos) - let words = split(a:cmd[0:a:pos].'#', ' ') - if len(words) > 3 - return [] - endif - if len(words) == 3 - if words[1] == 'help' - let candidates = ['compatibility', 'commands', 'options', 'keybindings'] - else - return [] - endif - else - let candidates = keys(s:commands) - endif - - let end_index = len(a:arglead) - 1 - if end_index < 0 - return candidates - else - return filter(candidates, { idx, val -> val[0:end_index] == a:arglead }) - endif -endfunction diff --git a/clients/vim/autoload/tabby/globals.vim b/clients/vim/autoload/tabby/globals.vim deleted file mode 100644 index ed38e00f3505..000000000000 --- a/clients/vim/autoload/tabby/globals.vim +++ /dev/null @@ -1,54 +0,0 @@ - -" Global variables of Tabby plugin. Include options and internal variables. - -if exists('g:autoloaded_tabby_globals') - finish -endif - -function! tabby#globals#Load() - let g:autoloaded_tabby_globals = 1 - - " See *Tabby-options* section in `doc/tabby.txt` for more details about options. - - " The trigger mode of compleiton, default is "auto". - " - auto: Tabby automatically show inline completion when you stop typing. - " - manual: You need to press to show inline completion. - let g:tabby_trigger_mode = get(g:, 'tabby_trigger_mode', 'auto') - - - " Tabby requires Node.js version 18.0 or higher to run the tabby agent. - " Specify the binary of Node.js, default is "node", which means search in $PATH. - let g:tabby_node_binary = get(g:, 'tabby_node_binary', 'node') - - " The script of tabby agent. - let g:tabby_node_script = expand(' + + diff --git a/clients/vscode/src/chat/sidePanel.ts b/clients/vscode/src/chat/sidePanel.ts new file mode 100644 index 000000000000..84fc124ea01f --- /dev/null +++ b/clients/vscode/src/chat/sidePanel.ts @@ -0,0 +1,36 @@ +import { ExtensionContext, WebviewViewProvider, WebviewView } from "vscode"; +import { ChatWebview } from "./webview"; +import type { ContextVariables } from "../ContextVariables"; +import type { Client } from "../lsp/client"; +import { GitProvider } from "../git/GitProvider"; + +export class ChatSidePanelProvider implements WebviewViewProvider { + readonly chatWebview: ChatWebview; + + constructor( + private readonly context: ExtensionContext, + private readonly client: Client, + private readonly contextVariables: ContextVariables, + private readonly gitProvider: GitProvider, + ) { + this.chatWebview = new ChatWebview(this.context, this.client, this.gitProvider); + this.contextVariables.chatSidePanelStatus = undefined; + this.chatWebview.on("didChangedStatus", (status: "loading" | "error" | "ready") => { + this.contextVariables.chatSidePanelStatus = status; + this.contextVariables.terminalContextEnabled = this.chatWebview.isTerminalContextEnabled; + }); + } + + async resolveWebviewView(webviewView: WebviewView) { + this.chatWebview.init(webviewView.webview); + + this.contextVariables.chatSidePanelVisible = webviewView.visible; + webviewView.onDidChangeVisibility(() => { + this.contextVariables.chatSidePanelVisible = webviewView.visible; + }); + + webviewView.onDidDispose(() => { + this.chatWebview.dispose(); + }); + } +} diff --git a/clients/vscode/src/chat/utils.ts b/clients/vscode/src/chat/utils.ts new file mode 100644 index 000000000000..9b1b3ebe1c2a --- /dev/null +++ b/clients/vscode/src/chat/utils.ts @@ -0,0 +1,234 @@ +import path from "path"; +import { TextEditor, Position as VSCodePosition, Range as VSCodeRange, Uri, workspace } from "vscode"; +import type { Filepath, Position as ChatPanelPosition, LineRange, PositionRange, Location } from "tabby-chat-panel"; +import type { GitProvider } from "../git/GitProvider"; +import { getLogger } from "../logger"; +import * as semver from "semver"; + +const logger = getLogger("chat/utils"); + +enum DocumentSchemes { + file = "file", + untitled = "untitled", + vscodeNotebookCell = "vscode-notebook-cell", + vscodeVfs = "vscode-vfs", +} + +export function isValidForSyncActiveEditorSelection(editor: TextEditor): boolean { + const supportedSchemes: string[] = [ + DocumentSchemes.file, + DocumentSchemes.untitled, + DocumentSchemes.vscodeNotebookCell, + DocumentSchemes.vscodeVfs, + ]; + return supportedSchemes.includes(editor.document.uri.scheme); +} + +export function localUriToChatPanelFilepath(uri: Uri, gitProvider: GitProvider): Filepath { + let localUri = uri; + if (localUri.scheme === DocumentSchemes.vscodeNotebookCell) { + localUri = convertFromNotebookCellUri(localUri); + } + + const uriFilePath = localUri.toString(true); + const workspaceFolder = workspace.getWorkspaceFolder(localUri); + + let repo = gitProvider.getRepository(localUri); + if (!repo && workspaceFolder) { + repo = gitProvider.getRepository(workspaceFolder.uri); + } + const gitRemoteUrl = repo ? gitProvider.getDefaultRemoteUrl(repo) : undefined; + if (repo && gitRemoteUrl) { + const relativeFilePath = path.relative(repo.rootUri.toString(true), uriFilePath); + if (!relativeFilePath.startsWith("..")) { + return { + kind: "git", + filepath: relativeFilePath, + gitUrl: gitRemoteUrl, + }; + } + } + + if (workspaceFolder) { + const baseDir = workspaceFolder.uri.toString(true); + const relativeFilePath = path.relative(baseDir, uriFilePath); + if (!relativeFilePath.startsWith("..")) { + return { + kind: "workspace", + filepath: relativeFilePath, + baseDir: baseDir, + }; + } + } + + return { + kind: "uri", + uri: uriFilePath, + }; +} + +export function chatPanelFilepathToLocalUri(filepath: Filepath, gitProvider: GitProvider): Uri | null { + let result: Uri | null = null; + if (filepath.kind === "uri") { + try { + result = Uri.parse(filepath.uri, true); + } catch (e) { + const workspaceRoot = workspace.workspaceFolders?.[0]; + if (workspaceRoot) { + result = Uri.joinPath(workspaceRoot.uri, filepath.uri); + } + } + } else if (filepath.kind === "workspace") { + try { + const workspaceFolder = workspace.getWorkspaceFolder(Uri.parse(filepath.baseDir, true)); + if (workspaceFolder) { + result = Uri.joinPath(workspaceFolder.uri, filepath.filepath); + } + } catch (e) { + // do nothing + } + } else if (filepath.kind === "git") { + const localGitRoot = gitProvider.findLocalRootUriByRemoteUrl(filepath.gitUrl); + if (localGitRoot) { + result = Uri.joinPath(localGitRoot, filepath.filepath); + } + } + + if (result == null) { + logger.warn(`Invalid filepath params.`, filepath); + return null; + } + + if (isJupyterNotebookFilepath(result)) { + result = convertToNotebookCellUri(result); + } + return result; +} + +export function vscodePositionToChatPanelPosition(position: VSCodePosition): ChatPanelPosition { + return { + line: position.line + 1, + character: position.character + 1, + }; +} + +export function chatPanelPositionToVSCodePosition(position: ChatPanelPosition): VSCodePosition { + return new VSCodePosition(Math.max(0, position.line - 1), Math.max(0, position.character - 1)); +} + +export function vscodeRangeToChatPanelPositionRange(range: VSCodeRange): PositionRange { + return { + start: vscodePositionToChatPanelPosition(range.start), + end: vscodePositionToChatPanelPosition(range.end), + }; +} + +export function chatPanelPositionRangeToVSCodeRange(positionRange: PositionRange): VSCodeRange { + return new VSCodeRange( + chatPanelPositionToVSCodePosition(positionRange.start), + chatPanelPositionToVSCodePosition(positionRange.end), + ); +} + +export function chatPanelLineRangeToVSCodeRange(lineRange: LineRange): VSCodeRange { + // Do not minus 1 from end line number, as we want to include the last line. + return new VSCodeRange(Math.max(0, lineRange.start - 1), 0, lineRange.end, 0); +} + +export function vscodeRangeToChatPanelLineRange(range: VSCodeRange): LineRange { + return { + start: range.start.line + 1, + end: range.end.line + 1, + }; +} + +export function chatPanelLocationToVSCodeRange(location: Location | undefined): VSCodeRange | null { + if (!location) { + return null; + } + if (typeof location === "number") { + const position = new VSCodePosition(Math.max(0, location - 1), 0); + return new VSCodeRange(position, position); + } else if ("line" in location) { + const position = chatPanelPositionToVSCodePosition(location); + return new VSCodeRange(position, position); + } else if ("start" in location) { + if (typeof location.start === "number") { + return chatPanelLineRangeToVSCodeRange(location as LineRange); + } else { + return chatPanelPositionRangeToVSCodeRange(location as PositionRange); + } + } + logger.warn(`Invalid location params.`, location); + return null; +} + +// Notebook cell uri conversion + +function isJupyterNotebookFilepath(uri: Uri): boolean { + const extname = path.extname(uri.fsPath); + return extname.startsWith(".ipynb"); +} + +function convertToNotebookCellUri(uri: Uri): Uri { + let handle: number | undefined; + + const searchParams = new URLSearchParams(uri.fragment); + const cellString = searchParams.get("cell"); + if (cellString) { + handle = parseInt(cellString, 10); + } + handle = handle || 0; + + searchParams.set("cell", handle.toString()); + return generateNotebookCellUri(uri, handle); +} + +function convertFromNotebookCellUri(uri: Uri): Uri { + const parsed = parseNotebookCellUri(uri); + if (!parsed) { + return uri; + } + return uri.with({ scheme: parsed.notebook.scheme, fragment: `cell=${parsed.handle}` }); +} + +const nb_lengths = ["W", "X", "Y", "Z", "a", "b", "c", "d", "e", "f"]; +const nb_padRegexp = new RegExp(`^[${nb_lengths.join("")}]+`); +const nb_radix = 7; + +function parseNotebookCellUri(cell: Uri): { notebook: Uri; handle: number } | undefined { + if (cell.scheme !== DocumentSchemes.vscodeNotebookCell) { + return undefined; + } + + const idx = cell.fragment.indexOf("s"); + if (idx < 0) { + return undefined; + } + + const handle = parseInt(cell.fragment.substring(0, idx).replace(nb_padRegexp, ""), nb_radix); + const _scheme = Buffer.from(cell.fragment.substring(idx + 1), "base64").toString("utf-8"); + if (isNaN(handle)) { + return undefined; + } + return { + handle, + notebook: cell.with({ scheme: _scheme, fragment: "" }), + }; +} + +function generateNotebookCellUri(notebook: Uri, handle: number): Uri { + const s = handle.toString(nb_radix); + const p = s.length < nb_lengths.length ? nb_lengths[s.length - 1] : "z"; + const fragment = `${p}${s}s${Buffer.from(notebook.scheme).toString("base64")}`; + return notebook.with({ scheme: DocumentSchemes.vscodeNotebookCell, fragment }); +} + +export function isCompatible(current: string, target: string): boolean { + const currentSemver = semver.coerce(current); + const targetSemver = semver.coerce(target); + if (!currentSemver || !targetSemver) { + return false; + } + return semver.gte(currentSemver, targetSemver); +} diff --git a/clients/vscode/src/chat/webview.ts b/clients/vscode/src/chat/webview.ts new file mode 100644 index 000000000000..7cda756525e9 --- /dev/null +++ b/clients/vscode/src/chat/webview.ts @@ -0,0 +1,1036 @@ +import EventEmitter from "events"; +import { + commands, + env, + window, + workspace, + Disposable, + ExtensionContext, + Uri, + TextEditor, + Range, + TextDocument, + Webview, + ColorThemeKind, + ProgressLocation, + Location, + LocationLink, + SymbolInformation, + DocumentSymbol, +} from "vscode"; +import type { + ServerApiList, + ChatView, + EditorContext, + LookupSymbolHint, + SymbolInfo, + FileLocation, + GitRepository, + ListFilesInWorkspaceParams, + ListFileItem, + FileRange, + Filepath, + ListSymbolsParams, + ListSymbolItem, + ChangeItem, + GetChangesParams, + EditorFileContext, + TerminalContext, + ChatCommand, +} from "tabby-chat-panel"; +import * as semver from "semver"; +import debounce from "debounce"; +import { v4 as uuid } from "uuid"; +import type { StatusInfo, Config } from "tabby-agent"; +import type { GitProvider, Repository } from "../git/GitProvider"; +import type { Client as LspClient } from "../lsp/client"; +import { createClient } from "./createClient"; +import { isBrowser } from "../env"; +import { getLogger } from "../logger"; +import { getEditorContext } from "./context"; +import { + localUriToChatPanelFilepath, + chatPanelFilepathToLocalUri, + vscodePositionToChatPanelPosition, + vscodeRangeToChatPanelPositionRange, + chatPanelLocationToVSCodeRange, + isValidForSyncActiveEditorSelection, + vscodeRangeToChatPanelLineRange, + isCompatible, +} from "./utils"; +import { listFiles } from "../findFiles"; +import { wrapCancelableFunction } from "../cancelableFunction"; +import mainHtml from "./html/main.html"; +import errorHtml from "./html/error.html"; +import { getTerminalContext } from "../terminal"; + +export class ChatWebview extends EventEmitter { + private readonly logger = getLogger("ChatWebView"); + private disposables: Disposable[] = []; + private webview: Webview | undefined = undefined; + private client: ServerApiList | undefined = undefined; + + // The current server config used to load the chat panel. + private currentConfig: Config["server"] | undefined = undefined; + + // A number to ensure the html is reloaded when assigned a new value + private reloadCount = 0; + + // Once the chat iframe is loaded, the `createChatPanelApiClient` should be resolved, + // and we can start to `initChatPanel`. + // So we set a timeout here to ensure the `createChatPanelApiClient` is resolved, + // otherwise we will show an error. + private createClientTimeout: NodeJS.Timeout | undefined = undefined; + + // Pending actions to perform after the chat panel is initialized. + private pendingActions: (() => Promise)[] = []; + + // A callback list for invoke javascript function by postMessage + private pendingCallbacks = new Map void>(); + + // Store the chat state to be reload when webview is reloaded + private sessionStateMap = new Map>(); + + constructor( + private readonly context: ExtensionContext, + private readonly lspClient: LspClient, + private readonly gitProvider: GitProvider, + ) { + super(); + } + + async init(webview: Webview) { + webview.options = { + enableScripts: true, + enableCommandUris: true, + }; + this.webview = webview; + + const statusListener = () => { + this.checkStatusAndLoadContent(); + }; + this.lspClient.status.on("didChange", statusListener); + this.disposables.push( + new Disposable(() => { + this.lspClient.status.off("didChange", statusListener); + }), + ); + this.checkStatusAndLoadContent(); + + this.disposables.push( + window.onDidChangeActiveTextEditor((editor) => { + if (this.client) { + this.debouncedNotifyActiveEditorSelectionChange(editor); + } + }), + ); + this.disposables.push( + window.onDidChangeTextEditorSelection((event) => { + if (event.textEditor === window.activeTextEditor && this.client) { + this.debouncedNotifyActiveEditorSelectionChange(event.textEditor); + } + }), + ); + + this.disposables.push( + webview.onDidReceiveMessage((event) => { + switch (event.action) { + case "chatIframeLoaded": { + this.createClientTimeout = setTimeout(() => { + const endpoint = this.currentConfig?.endpoint ?? ""; + if (!endpoint) { + this.checkStatusAndLoadContent(); + } else { + const command = `command:tabby.openExternal?${encodeURIComponent(`["${endpoint}/chat"]`)}`; + this.loadErrorPage( + `Failed to load the chat panel.
    Please check your network to ensure access to ${endpoint}/chat.

    Reload`, + ); + } + }, 10000); + return; + } + case "syncStyle": { + this.client?.["0.8.0"].updateTheme(event.style, this.getColorThemeString()); + return; + } + case "jsCallback": { + this.pendingCallbacks.get(event.id)?.(...event.args); + this.pendingCallbacks.delete(event.id); + return; + } + } + }), + ); + } + + async dispose() { + this.disposables.forEach((d) => d.dispose()); + this.disposables = []; + this.webview = undefined; + this.client = undefined; + if (this.createClientTimeout) { + clearTimeout(this.createClientTimeout); + this.createClientTimeout = undefined; + } + this.currentConfig = undefined; + } + + async isFocused(): Promise { + const webview = this.webview; + if (!webview) { + return false; + } + return new Promise((resolve) => { + const id = uuid(); + this.pendingCallbacks.set(id, (...args) => { + resolve(args[0] as boolean); + }); + webview.postMessage({ id, action: "checkFocused" }); + }); + } + + getApiVersions(): string[] | undefined { + return Object.keys(this.client ?? {}).filter((key) => semver.valid(key)); + } + + get isTerminalContextEnabled(): boolean { + return this.getApiVersions()?.some((version) => isCompatible(version, "0.10.0")) ?? false; + } + + setActiveSelection(selection: EditorContext) { + if (this.client) { + this.logger.info(`Set active selection: ${selection}`); + this.client["0.8.0"].updateActiveSelection(selection); + } else { + this.pendingActions.push(async () => { + this.logger.info(`Set pending active selection: ${selection}`); + await this.client?.["0.8.0"].updateActiveSelection(selection); + }); + } + } + + async addRelevantContext(context: EditorContext) { + if (this.client) { + this.logger.info(`Adding relevant context: ${context}`); + if (context.kind === "terminal") { + this.client["0.10.0"]?.addRelevantContext(context); + } else { + this.client["0.8.0"].addRelevantContext(context); + } + } else { + this.pendingActions.push(async () => { + this.logger.info(`Adding pending relevant context: ${context}`); + if (context.kind === "terminal") { + await this.client?.["0.10.0"]?.addRelevantContext(context); + } else { + await this.client?.["0.8.0"].addRelevantContext(context); + } + }); + } + } + + async executeCommand(command: ChatCommand) { + if (this.client) { + this.logger.info(`Executing command: ${command}`); + if (command === "explain-terminal") { + this.client["0.10.0"]?.executeCommand(command); + } else { + this.client["0.8.0"].executeCommand(command); + } + } else { + this.pendingActions.push(async () => { + this.logger.info(`Executing pending command: ${command}`); + if (command === "explain-terminal") { + await this.client?.["0.10.0"]?.executeCommand(command); + } else { + await this.client?.["0.8.0"].executeCommand(command); + } + }); + } + } + + async navigate(view: ChatView) { + if (this.client) { + this.logger.info(`Navigate: ${view}`); + this.client["0.8.0"].navigate(view); + } + } + + private async initChatPanel(webview: Webview) { + const client = await this.createChatPanelApiClient(webview); + this.client = client; + this.emit("didChangedStatus", "ready"); + + if (this.createClientTimeout) { + clearTimeout(this.createClientTimeout); + this.createClientTimeout = undefined; + } + + // 1. Send pending actions + // 2. Call the client's init method + // 3. Show the chat panel (call syncStyle underlay) + this.pendingActions.forEach(async (fn) => { + await fn(); + }); + this.pendingActions = []; + + const isMac = isBrowser + ? navigator.userAgent.toLowerCase().includes("mac") + : process.platform.toLowerCase().includes("darwin"); + + await client["0.8.0"].init({ + fetcherOptions: { + authorization: this.currentConfig?.token ?? "", + }, + useMacOSKeyboardEventHandler: isMac, + }); + + webview.postMessage({ action: "showChatPanel" }); + } + + private async createChatPanelApiClient(webview: Webview): Promise { + return await createClient(webview, { + refresh: async () => { + commands.executeCommand("tabby.reconnectToServer"); + return; + }, + + onApplyInEditor: async (content: string) => { + const editor = window.activeTextEditor; + if (!editor) { + window.showErrorMessage("No active editor found."); + return; + } + await this.applyInEditor(editor, content); + }, + + onApplyInEditorV2: async (content: string, options?: { languageId?: string; smart?: boolean }) => { + const editor = window.activeTextEditor; + if (!editor) { + window.showErrorMessage("No active editor found."); + return; + } + if (!options || !options.smart) { + await this.applyInEditor(editor, content); + } else if (editor.document.languageId !== options.languageId) { + this.logger.debug("Editor's languageId:", editor.document.languageId, "opts.languageId:", options.languageId); + await this.applyInEditor(editor, content); + window.showInformationMessage("The active editor is not in the correct language. Did normal apply."); + } else { + await this.smartApplyInEditor(editor, content); + } + }, + + onCopy: async (content) => { + env.clipboard.writeText(content); + }, + + onKeyboardEvent: async (type: string, event: KeyboardEventInit) => { + this.logger.debug(`Dispatching keyboard event: ${type} ${JSON.stringify(event)}`); + this.webview?.postMessage({ action: "dispatchKeyboardEvent", type, event }); + }, + + lookupSymbol: async (symbol: string, hints?: LookupSymbolHint[] | undefined): Promise => { + if (!symbol.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/)) { + // Do not process invalid symbols + return null; + } + /// FIXME: When no hints provided, try to use `vscode.executeWorkspaceSymbolProvider` to find the symbol. + + // Find the symbol in the hints + for (const hint of hints ?? []) { + if (!hint.filepath) { + this.logger.debug("No filepath in the hint:", hint); + continue; + } + const uri = chatPanelFilepathToLocalUri(hint.filepath, this.gitProvider); + if (!uri) { + continue; + } + + let document: TextDocument; + try { + document = await workspace.openTextDocument(uri); + } catch (error) { + this.logger.debug("Failed to open document:", uri, error); + continue; + } + if (!document) { + continue; + } + + const findSymbolInContent = async ( + content: string, + offsetInDocument: number, + ): Promise => { + // Add word boundary to perform exact match + const matchRegExp = new RegExp(`\\b${symbol}\\b`, "g"); + let match; + while ((match = matchRegExp.exec(content)) !== null) { + const offset = offsetInDocument + match.index; + const position = document.positionAt(offset); + const locations = await commands.executeCommand( + "vscode.executeDefinitionProvider", + document.uri, + position, + ); + if (locations && locations.length > 0) { + const location = locations[0]; + if (location) { + if ("targetUri" in location) { + const targetLocation = location.targetSelectionRange ?? location.targetRange; + return { + source: { + filepath: localUriToChatPanelFilepath(document.uri, this.gitProvider), + location: vscodePositionToChatPanelPosition(position), + }, + target: { + filepath: localUriToChatPanelFilepath(location.targetUri, this.gitProvider), + location: vscodeRangeToChatPanelPositionRange(targetLocation), + }, + }; + } else if ("uri" in location) { + return { + source: { + filepath: localUriToChatPanelFilepath(document.uri, this.gitProvider), + location: vscodePositionToChatPanelPosition(position), + }, + target: { + filepath: localUriToChatPanelFilepath(location.uri, this.gitProvider), + location: vscodeRangeToChatPanelPositionRange(location.range), + }, + }; + } + } + } + } + return undefined; + }; + + let symbolInfo: SymbolInfo | undefined; + if (hint.location) { + // Find in the hint location + const location = chatPanelLocationToVSCodeRange(hint.location); + if (location) { + let range: Range; + if (!location.isEmpty) { + range = location; + } else { + // a empty range, create a new range with this line to the end of the file + range = new Range(location.start.line, 0, document.lineCount, 0); + } + const content = document.getText(range); + const offset = document.offsetAt(range.start); + symbolInfo = await findSymbolInContent(content, offset); + } + } + if (!symbolInfo) { + // Fallback to find in full content + const content = document.getText(); + symbolInfo = await findSymbolInContent(content, 0); + } + if (symbolInfo) { + // Symbol found + this.logger.debug( + `Symbol found: ${symbol} with hints: ${JSON.stringify(hints)}: ${JSON.stringify(symbolInfo)}`, + ); + return symbolInfo; + } + } + this.logger.debug(`Symbol not found: ${symbol} with hints: ${JSON.stringify(hints)}`); + return null; + }, + + openInEditor: async (fileLocation: FileLocation): Promise => { + const uri = chatPanelFilepathToLocalUri(fileLocation.filepath, this.gitProvider); + if (!uri) { + return false; + } + + if (uri.scheme === "output") { + try { + await commands.executeCommand(`workbench.action.output.show.${uri.fsPath}`); + return true; + } catch (error) { + this.logger.error("Failed to open output channel:", fileLocation, error); + return false; + } + } + + const targetRange = chatPanelLocationToVSCodeRange(fileLocation.location) ?? new Range(0, 0, 0, 0); + try { + await commands.executeCommand( + "editor.action.goToLocations", + uri, + targetRange.start, + [new Location(uri, targetRange)], + "goto", + ); + return true; + } catch (error) { + this.logger.error("Failed to go to location:", fileLocation, error); + return false; + } + }, + + openExternal: async (url: string) => { + await env.openExternal(Uri.parse(url)); + }, + + readWorkspaceGitRepositories: async (): Promise => { + const activeTextEditor = window.activeTextEditor; + const infoList: GitRepository[] = []; + let activeGitUrl: string | undefined; + if (activeTextEditor) { + const repo = this.gitProvider.getRepository(activeTextEditor.document.uri); + if (repo) { + const gitRemoteUrl = this.gitProvider.getDefaultRemoteUrl(repo); + if (gitRemoteUrl) { + infoList.push({ + url: gitRemoteUrl, + }); + } + } + } + + const workspaceFolder = workspace.workspaceFolders ?? []; + for (const folder of workspaceFolder) { + const repo = this.gitProvider.getRepository(folder.uri); + if (repo) { + const gitRemoteUrl = this.gitProvider.getDefaultRemoteUrl(repo); + if (gitRemoteUrl && gitRemoteUrl !== activeGitUrl) { + infoList.push({ + url: gitRemoteUrl, + }); + } + } + } + return infoList; + }, + + getActiveEditorSelection: async (): Promise => { + const editor = window.activeTextEditor; + if (!editor || !isValidForSyncActiveEditorSelection(editor)) { + return null; + } + + return await getEditorContext(editor, this.gitProvider); + }, + + getActiveTerminalSelection: async (): Promise => { + const terminalContext = await getTerminalContext(); + if (!terminalContext) { + this.logger.warn("No active terminal selection found."); + return null; + } + return terminalContext; + }, + + fetchSessionState: async (keys?: string[] | undefined): Promise | null> => { + const sessionStateKey = this.currentConfig?.endpoint ?? ""; + const sessionState = this.sessionStateMap.get(sessionStateKey) ?? {}; + + if (!keys) { + return { ...sessionState }; + } + + const filtered: Record = {}; + for (const key of keys) { + if (key in sessionState) { + filtered[key] = sessionState[key]; + } + } + return filtered; + }, + + storeSessionState: async (state: Record) => { + const sessionStateKey = this.currentConfig?.endpoint ?? ""; + const sessionState = this.sessionStateMap.get(sessionStateKey) ?? {}; + this.sessionStateMap.set(sessionStateKey, { + ...sessionState, + ...state, + }); + }, + + listFileInWorkspace: async (params: ListFilesInWorkspaceParams): Promise => { + try { + const files = await this.listFiles(params.query, params.limit); + return files.map((item) => { + return { + filepath: localUriToChatPanelFilepath(item.uri, this.gitProvider), + source: item.isOpenedInEditor ? "openedInEditor" : "searchResult", + }; + }); + } catch (error) { + this.logger.warn("Failed to list files:", error); + return []; + } + }, + + readFileContent: async (info: FileRange): Promise => { + const uri = chatPanelFilepathToLocalUri(info.filepath, this.gitProvider); + if (!uri) { + this.logger.warn(`Could not resolve URI from filepath: ${JSON.stringify(info.filepath)}`); + return null; + } + const document = await workspace.openTextDocument(uri); + return document.getText(chatPanelLocationToVSCodeRange(info.range) ?? undefined); + }, + listSymbols: async (params: ListSymbolsParams): Promise => { + const { query } = params; + let { limit } = params; + const editor = window.activeTextEditor; + + if (!editor) { + this.logger.warn("listActiveSymbols: No active editor found."); + return []; + } + if (!limit || limit < 0) { + limit = 20; + } + + const getDocumentSymbols = async (editor: TextEditor): Promise => { + this.logger.debug(`getDocumentSymbols: Fetching document symbols for ${editor.document.uri.toString()}`); + const symbols = + (await commands.executeCommand( + "vscode.executeDocumentSymbolProvider", + editor.document.uri, + )) || []; + + const result: SymbolInformation[] = []; + const queue: (DocumentSymbol | SymbolInformation)[] = [...symbols]; + + // BFS to get all symbols up to the limit + while (queue.length > 0 && result.length < limit) { + const current = queue.shift(); + if (!current) { + continue; + } + + if (current instanceof DocumentSymbol) { + const converted = new SymbolInformation( + current.name, + current.kind, + current.detail, + new Location(editor.document.uri, current.range), + ); + + result.push(converted); + + if (result.length >= limit) { + break; + } + + queue.push(...current.children); + } else { + result.push(current); + + if (result.length >= limit) { + break; + } + } + } + + this.logger.debug(`getDocumentSymbols: Found ${result.length} symbols.`); + return result; + }; + + const getWorkspaceSymbols = async (query: string): Promise => { + this.logger.debug(`getWorkspaceSymbols: Fetching workspace symbols for query "${query}"`); + try { + const symbols = + (await commands.executeCommand("vscode.executeWorkspaceSymbolProvider", query)) || + []; + + const items = symbols.map((symbol) => ({ + filepath: localUriToChatPanelFilepath(symbol.location.uri, this.gitProvider), + range: vscodeRangeToChatPanelLineRange(symbol.location.range), + label: symbol.name, + })); + this.logger.debug(`getWorkspaceSymbols: Found ${items.length} symbols.`); + return items; + } catch (error) { + this.logger.error(`Workspace symbols failed: ${error}`); + return []; + } + }; + + const filterSymbols = (symbols: SymbolInformation[], query: string): SymbolInformation[] => { + const lowerQuery = query.toLowerCase(); + const filtered = symbols.filter( + (s) => s.name.toLowerCase().includes(lowerQuery) || s.containerName?.toLowerCase().includes(lowerQuery), + ); + this.logger.debug(`filterSymbols: Filtered down to ${filtered.length} symbols with query "${query}"`); + return filtered; + }; + + const mergeResults = ( + local: ListSymbolItem[], + workspace: ListSymbolItem[], + query: string, + limit = 20, + ): ListSymbolItem[] => { + this.logger.debug( + `mergeResults: Merging ${local.length} local symbols and ${workspace.length} workspace symbols with query "${query}" and limit ${limit}`, + ); + + const seen = new Set(); + const allItems = [...local, ...workspace]; + const uniqueItems: ListSymbolItem[] = []; + + for (const item of allItems) { + const key = `${item.filepath}-${item.label}-${item.range.start}-${item.range.end}`; + if (!seen.has(key)) { + seen.add(key); + uniqueItems.push(item); + } + } + + // Sort all items by the match score + const getMatchScore = (label: string): number => { + const lowerLabel = label.toLowerCase(); + const lowerQuery = query.toLowerCase(); + + if (lowerLabel === lowerQuery) return 3; + if (lowerLabel.startsWith(lowerQuery)) return 2; + if (lowerLabel.includes(lowerQuery)) return 1; + return 0; + }; + + uniqueItems.sort((a, b) => { + const scoreA = getMatchScore(a.label); + const scoreB = getMatchScore(b.label); + + if (scoreB !== scoreA) return scoreB - scoreA; + return a.label.length - b.label.length; + }); + + this.logger.debug(`mergeResults: Returning ${Math.min(uniqueItems.length, limit)} sorted symbols.`); + return uniqueItems.slice(0, limit); + }; + + const symbolToItem = (symbol: SymbolInformation, filepath: Filepath): ListSymbolItem => { + return { + filepath, + range: vscodeRangeToChatPanelLineRange(symbol.location.range), + label: symbol.name, + }; + }; + + try { + this.logger.info("listActiveSymbols: Starting to fetch symbols."); + const defaultSymbols = await getDocumentSymbols(editor); + const filepath = localUriToChatPanelFilepath(editor.document.uri, this.gitProvider); + + if (!query) { + const items = defaultSymbols.slice(0, limit).map((symbol) => symbolToItem(symbol, filepath)); + this.logger.debug(`listActiveSymbols: Returning ${items.length} symbols.`); + return items; + } + + const [filteredDefault, workspaceSymbols] = await Promise.all([ + Promise.resolve(filterSymbols(defaultSymbols, query)), + getWorkspaceSymbols(query), + ]); + this.logger.info( + `listActiveSymbols: Found ${filteredDefault.length} filtered local symbols and ${workspaceSymbols.length} workspace symbols.`, + ); + + const mergedItems = mergeResults( + filteredDefault.map((s) => symbolToItem(s, filepath)), + workspaceSymbols, + query, + limit, + ); + this.logger.info(`listActiveSymbols: Returning ${mergedItems.length} merged symbols.`); + return mergedItems; + } catch (error) { + this.logger.error(`listActiveSymbols: Failed - ${error}`); + return []; + } + }, + + getChanges: async (params: GetChangesParams): Promise => { + if (!this.gitProvider.isApiAvailable()) { + return []; + } + + const maxChars = params.maxChars ?? undefined; + let remainingChars = maxChars; + + const repositories = this.gitProvider.getRepositories(); + if (!repositories) { + return []; + } + + const getRepoChanges = async ( + repos: Repository[], + staged: boolean, + charLimit?: number, + ): Promise => { + if (charLimit !== undefined && charLimit <= 0) { + return []; + } + + const res: ChangeItem[] = []; + let currentCharCount = 0; + + for (const repo of repos) { + const diffs = await this.gitProvider.getDiff(repo, staged); + if (!diffs) { + continue; + } + + for (const diff of diffs) { + const diffChars = diff.length; + + if (charLimit !== undefined && currentCharCount + diffChars > charLimit) { + break; + } + + res.push({ + content: diff, + staged: staged, + } as ChangeItem); + + currentCharCount += diffChars; + } + + if (charLimit !== undefined && currentCharCount >= charLimit) { + break; + } + } + + return res; + }; + + const stagedChanges: ChangeItem[] = await getRepoChanges(repositories, true, remainingChars); + + const stagedCharCount = stagedChanges.reduce((count, item) => count + item.content.length, 0); + + remainingChars = maxChars !== undefined ? maxChars - stagedCharCount : undefined; + + const unstagedChanges: ChangeItem[] = await getRepoChanges(repositories, false, remainingChars); + + const res = [...stagedChanges, ...unstagedChanges]; + + this.logger.info(`Found ${res.length} changed files.`); + + return res; + }, + runShell: async (command: string) => { + const terminal = window.createTerminal("Tabby"); + terminal.show(); + terminal.sendText(command); + }, + }); + } + + private checkStatusAndLoadContent() { + const statusInfo = this.lspClient.status.current; + const error = this.checkStatusInfo(statusInfo); + if (error) { + this.currentConfig = undefined; + this.loadErrorPage(error); + return; + } + const config = this.lspClient.agentConfig.current; + if (!config) { + this.currentConfig = undefined; + this.loadErrorPage("Cannot get the server configuration."); + return; + } + if (this.currentConfig?.endpoint !== config.server.endpoint || this.currentConfig?.token !== config.server.token) { + this.currentConfig = config.server; + this.loadChatPanel(); + } + } + + private getUriStylesheet() { + return ( + this.webview?.asWebviewUri(Uri.joinPath(this.context.extensionUri, "assets", "chat-panel.css")).toString() ?? "" + ); + } + + private getUriAvatarTabby() { + return this.webview?.asWebviewUri(Uri.joinPath(this.context.extensionUri, "assets", "tabby.png")).toString() ?? ""; + } + + private loadChatPanel() { + const webview = this.webview; + if (!webview) { + return; + } + this.client = undefined; + this.emit("didChangedStatus", "loading"); + this.reloadCount += 1; + webview.html = mainHtml + .replace(/{{RELOAD_COUNT}}/g, this.reloadCount.toString()) + .replace(/{{SERVER_ENDPOINT}}/g, this.currentConfig?.endpoint ?? "") + .replace(/{{URI_STYLESHEET}}/g, this.getUriStylesheet()) + .replace(/{{URI_AVATAR_TABBY}}/g, this.getUriAvatarTabby()); + + this.initChatPanel(webview); + } + + private loadErrorPage(message: string) { + const webview = this.webview; + if (!webview) { + return; + } + this.client = undefined; + this.emit("didChangedStatus", "error"); + this.reloadCount += 1; + webview.html = errorHtml + .replace(/{{RELOAD_COUNT}}/g, this.reloadCount.toString()) + .replace(/{{URI_STYLESHEET}}/g, this.getUriStylesheet()) + .replace(/{{URI_AVATAR_TABBY}}/g, this.getUriAvatarTabby()) + .replace(/{{ERROR_MESSAGE}}/g, message); + } + + // Returns undefined if no error, otherwise returns the error message + private checkStatusInfo(statusInfo: StatusInfo | undefined): string | undefined { + if (!statusInfo || statusInfo.status === "connecting") { + return 'Connecting to the Tabby server...
    '; + } + + if (statusInfo.status === "unauthorized") { + return "Your token is invalid.
    Update Token"; + } + + if (statusInfo.status === "disconnected") { + return "Failed to connect to the Tabby server.
    Connect To Server"; + } + + const health = statusInfo.serverHealth; + if (!health) { + return "Cannot get the health status of the Tabby server."; + } + + if (!health["webserver"] || !health["chat_model"]) { + return "You need to launch the server with the chat model enabled; for example, use `--chat-model Qwen2-1.5B-Instruct`."; + } + + const MIN_VERSION = "0.27.0"; + + if (health["version"]) { + let version: semver.SemVer | undefined | null = undefined; + if (typeof health["version"] === "string") { + version = semver.coerce(health["version"]); + } else if ( + typeof health["version"] === "object" && + "git_describe" in health["version"] && + typeof health["version"]["git_describe"] === "string" + ) { + version = semver.coerce(health["version"]["git_describe"]); + } + if (version && semver.lt(version, MIN_VERSION)) { + return `Tabby Chat requires Tabby server version ${MIN_VERSION} or later. Your server is running version ${version}.`; + } + } + + return undefined; + } + + private async applyInEditor(editor: TextEditor, content: string) { + const document = editor.document; + const selection = editor.selection; + + // Determine the indentation for the content + // The calculation is based solely on the indentation of the first line + const lineText = document.lineAt(selection.start.line).text; + const match = lineText.match(/^(\s*)/); + const indent = match ? match[0] : ""; + + // Determine the indentation for the content's first line + // Note: + // If using spaces, selection.start.character = 1 means 1 space + // If using tabs, selection.start.character = 1 means 1 tab + const indentUnit = indent[0]; + const indentAmountForTheFirstLine = Math.max(indent.length - selection.start.character, 0); + const indentForTheFirstLine = indentUnit?.repeat(indentAmountForTheFirstLine) ?? ""; + // Indent the content + const indentedContent = indentForTheFirstLine + content.replaceAll("\n", "\n" + indent); + + // Apply into the editor + await editor.edit((editBuilder) => { + editBuilder.replace(selection, indentedContent); + }); + } + + private async smartApplyInEditor(editor: TextEditor, content: string) { + this.logger.info("Smart apply in editor started."); + this.logger.trace("Smart apply in editor with content:", { content }); + + window.withProgress( + { + location: ProgressLocation.Notification, + title: "Smart Apply in Progress", + cancellable: true, + }, + async (progress, token) => { + progress.report({ increment: 0, message: "Applying smart edit..." }); + try { + await this.lspClient.chat.provideSmartApplyEdit( + { + text: content, + location: { + uri: editor.document.uri.toString(), + range: { + start: { line: editor.selection.start.line, character: editor.selection.start.character }, + end: { line: editor.selection.end.line, character: editor.selection.end.character }, + }, + }, + }, + token, + ); + } catch (error) { + if (error instanceof Error) { + window.showErrorMessage(error.message); + } else { + window.showErrorMessage("An unknown error occurred"); + } + } + }, + ); + } + + private async notifyActiveEditorSelectionChange(editor: TextEditor | undefined) { + if (editor && editor.document.uri.scheme === "output") { + // do not update when the active editor is an output channel + return; + } + + if (!editor || !isValidForSyncActiveEditorSelection(editor)) { + await this.client?.["0.8.0"].updateActiveSelection(null); + return; + } + + const fileContext = await getEditorContext(editor, this.gitProvider); + await this.client?.["0.8.0"].updateActiveSelection(fileContext); + } + + private debouncedNotifyActiveEditorSelectionChange = debounce(async (editor: TextEditor | undefined) => { + await this.notifyActiveEditorSelectionChange(editor); + }, 100); + + private listFiles = wrapCancelableFunction( + listFiles, + (args) => args[2], + (args, token) => { + args[2] = token; + return args; + }, + ); + + private getColorThemeString() { + switch (window.activeColorTheme.kind) { + case ColorThemeKind.Light: + case ColorThemeKind.HighContrastLight: + return "light"; + case ColorThemeKind.Dark: + case ColorThemeKind.HighContrast: + return "dark"; + } + } +} diff --git a/clients/vscode/src/code-action/InlineEdit.ts b/clients/vscode/src/code-action/InlineEdit.ts new file mode 100644 index 000000000000..12801795f6d1 --- /dev/null +++ b/clients/vscode/src/code-action/InlineEdit.ts @@ -0,0 +1,38 @@ +import { + CancellationToken, + CodeAction, + CodeActionContext, + CodeActionKind, + CodeActionProvider as CodeActionProviderInterface, + Range, + Selection, + TextDocument, +} from "vscode"; +import { ContextVariables } from "../ContextVariables"; + +export class InlineEditCodeActionProvider implements CodeActionProviderInterface { + constructor(private readonly contextVariables: ContextVariables) {} + + provideCodeActions( + _document: TextDocument, + _range: Range | Selection, + _context: CodeActionContext, + token: CancellationToken, + ): CodeAction[] | undefined { + if (token.isCancellationRequested) { + return; + } + + if (!this.contextVariables.chatEnabled) { + return; + } + + const inlineEditing = new CodeAction("Edit using Tabby", CodeActionKind.RefactorRewrite); + inlineEditing.command = { + command: "tabby.chat.edit.start", + title: "Edit using Tabby", + }; + + return [inlineEditing]; + } +} diff --git a/clients/vscode/src/code-action/QuickFix.ts b/clients/vscode/src/code-action/QuickFix.ts new file mode 100644 index 000000000000..8358908c2beb --- /dev/null +++ b/clients/vscode/src/code-action/QuickFix.ts @@ -0,0 +1,81 @@ +import { + CancellationToken, + CodeAction, + CodeActionContext, + CodeActionKind, + CodeActionProvider as CodeActionProviderInterface, + Range, + Selection, + TextDocument, +} from "vscode"; +import { ContextVariables } from "../ContextVariables"; +import { getLogger } from "../logger"; + +export class QuickFixCodeActionProvider implements CodeActionProviderInterface { + private readonly logger = getLogger("QuickFixCodeActionProvider"); + constructor(private readonly contextVariables: ContextVariables) {} + + provideCodeActions( + _document: TextDocument, + _range: Range | Selection, + context: CodeActionContext, + token: CancellationToken, + ): CodeAction[] | undefined { + if (token.isCancellationRequested) { + return; + } + if (!this.contextVariables.chatEnabled) { + return; + } + if (context.diagnostics.length === 0) { + return []; + } + + const getMergedDiagnosticRange = () => { + return context.diagnostics.reduce( + (mergedRange, diagnostic) => { + if (!mergedRange) { + return diagnostic.range; + } + return mergedRange.union(diagnostic.range); + }, + null as Range | null, + ); + }; + + const mergedRange = getMergedDiagnosticRange(); + if (!mergedRange) { + return []; + } + + const lspErrors = context.diagnostics + .map((diagnostic, idx) => "Error " + idx + ": " + diagnostic.message) + .join("\n"); + + const quickFixCmd = `Here is some error information that occurred in the selection: + ${lspErrors} + Please provide the correct command to fix the error.`; + this.logger.trace("LSP Errors collections: ", lspErrors); + this.logger.debug("QuickFix Range: ", mergedRange); + + const quickFixEditing = new CodeAction("Fix using Tabby", CodeActionKind.QuickFix); + + quickFixEditing.command = { + command: "tabby.chat.edit.start", + title: "Fix using Tabby", + arguments: [undefined, mergedRange, quickFixCmd], + }; + + const explainErrorCmd = `\nHere is some error information that occurred in the selection: + ${lspErrors} + Please provide an explanation for the error.`; + const explainError = new CodeAction("Explain using Tabby", CodeActionKind.QuickFix); + explainError.command = { + command: "tabby.chat.explainCodeBlock", + title: "Explain using Tabby", + arguments: [explainErrorCmd], + }; + + return [quickFixEditing, explainError]; + } +} diff --git a/clients/vscode/src/commands.ts b/clients/vscode/src/commands.ts deleted file mode 100644 index a7f6a2fe76a1..000000000000 --- a/clients/vscode/src/commands.ts +++ /dev/null @@ -1,346 +0,0 @@ -import { - ConfigurationTarget, - InputBoxValidationSeverity, - ProgressLocation, - Uri, - ThemeIcon, - ExtensionContext, - workspace, - window, - env, - commands, -} from "vscode"; -import os from "os"; -import { strict as assert } from "assert"; -import { agent } from "./agent"; -import { notifications } from "./notifications"; -import { TabbyCompletionProvider } from "./TabbyCompletionProvider"; -import { TabbyStatusBarItem } from "./TabbyStatusBarItem"; - -const configTarget = ConfigurationTarget.Global; - -type Command = { - command: string; - callback: (...args: any[]) => any; - thisArg?: any; -}; - -const toggleInlineCompletionTriggerMode: Command = { - command: "tabby.toggleInlineCompletionTriggerMode", - callback: (value: "automatic" | "manual" | undefined) => { - const configuration = workspace.getConfiguration("tabby"); - let target = value; - if (!target) { - const current = configuration.get("inlineCompletion.triggerMode", "automatic"); - if (current === "automatic") { - target = "manual"; - } else { - target = "automatic"; - } - } - configuration.update("inlineCompletion.triggerMode", target, configTarget, false); - }, -}; - -const setApiEndpoint: Command = { - command: "tabby.setApiEndpoint", - callback: () => { - const configuration = workspace.getConfiguration("tabby"); - window - .showInputBox({ - prompt: "Enter the URL of your Tabby Server", - value: configuration.get("api.endpoint", ""), - validateInput: (input: string) => { - try { - const url = new URL(input); - assert(url.protocol == "http:" || url.protocol == "https:"); - } catch (_) { - return { - message: "Please enter a validate http or https URL.", - severity: InputBoxValidationSeverity.Error, - }; - } - return null; - }, - }) - .then((url) => { - if (url) { - console.debug("Set Tabby Server URL: ", url); - configuration.update("api.endpoint", url, configTarget, false); - } - }); - }, -}; - -const setApiToken = (context: ExtensionContext): Command => { - return { - command: "tabby.setApiToken", - callback: () => { - const currentToken = agent().getConfig()["server"]["token"].trim(); - window - .showInputBox({ - prompt: "Enter your personal token", - value: currentToken.length > 0 ? currentToken : undefined, - password: true, - }) - .then((token) => { - if (token === undefined) { - return; // User canceled - } - if (token.length > 0) { - console.debug("Set token: ", token); - context.globalState.update("server.token", token); - agent().updateConfig("server.token", token); - } else { - console.debug("Clear token."); - context.globalState.update("server.token", undefined); - agent().clearConfig("server.token"); - } - }); - }, - }; -}; - -const openSettings: Command = { - command: "tabby.openSettings", - callback: () => { - commands.executeCommand("workbench.action.openSettings", "@ext:TabbyML.vscode-tabby"); - }, -}; - -const openTabbyAgentSettings: Command = { - command: "tabby.openTabbyAgentSettings", - callback: () => { - if (env.appHost !== "desktop") { - window.showWarningMessage("Tabby Agent config file is not supported on web.", { modal: true }); - return; - } - const agentUserConfig = Uri.joinPath(Uri.file(os.homedir()), ".tabby-client", "agent", "config.toml"); - workspace.fs.stat(agentUserConfig).then( - () => { - workspace.openTextDocument(agentUserConfig).then((document) => { - window.showTextDocument(document); - }); - }, - () => { - window.showWarningMessage("Tabby Agent config file not found.", { modal: true }); - }, - ); - }, -}; - -const openKeybindings: Command = { - command: "tabby.openKeybindings", - callback: () => { - commands.executeCommand("workbench.action.openGlobalKeybindings", "tabby.inlineCompletion"); - }, -}; - -const gettingStarted: Command = { - command: "tabby.gettingStarted", - callback: () => { - commands.executeCommand("workbench.action.openWalkthrough", "TabbyML.vscode-tabby#gettingStarted"); - }, -}; - -/** @deprecated Tabby Cloud auth */ -const openAuthPage: Command = { - command: "tabby.openAuthPage", - callback: (callbacks?: { onAuthStart?: () => void; onAuthEnd?: () => void }) => { - window.withProgress( - { - location: ProgressLocation.Notification, - title: "Tabby Server Authorization", - cancellable: true, - }, - async (progress, token) => { - const abortController = new AbortController(); - token.onCancellationRequested(() => { - abortController.abort(); - }); - const signal = abortController.signal; - try { - callbacks?.onAuthStart?.(); - progress.report({ message: "Generating authorization url..." }); - const authUrl = await agent().requestAuthUrl({ signal }); - if (authUrl) { - env.openExternal(Uri.parse(authUrl.authUrl)); - progress.report({ message: "Waiting for authorization from browser..." }); - await agent().waitForAuthToken(authUrl.code, { signal }); - assert(agent().getStatus() === "ready"); - notifications.showInformationAuthSuccess(); - } else if (agent().getStatus() === "ready") { - notifications.showInformationWhenStartAuthButAlreadyAuthorized(); - } else { - notifications.showInformationWhenAuthFailed(); - } - } catch (error: any) { - if (error.name === "AbortError") { - return; - } - console.debug("Error auth", { error }); - notifications.showInformationWhenAuthFailed(); - } finally { - callbacks?.onAuthEnd?.(); - } - }, - ); - }, -}; - -const applyCallback: Command = { - command: "tabby.applyCallback", - callback: (callback) => { - callback?.(); - }, -}; - -const triggerInlineCompletion: Command = { - command: "tabby.inlineCompletion.trigger", - callback: () => { - commands.executeCommand("editor.action.inlineSuggest.trigger"); - }, -}; - -const acceptInlineCompletion: Command = { - command: "tabby.inlineCompletion.accept", - callback: () => { - commands.executeCommand("editor.action.inlineSuggest.commit"); - }, -}; - -const acceptInlineCompletionNextWord = (completionProvider: TabbyCompletionProvider): Command => { - return { - command: "tabby.inlineCompletion.acceptNextWord", - callback: () => { - completionProvider.handleEvent("accept_word"); - commands.executeCommand("editor.action.inlineSuggest.acceptNextWord"); - }, - }; -}; - -const acceptInlineCompletionNextLine = (completionProvider: TabbyCompletionProvider): Command => { - return { - command: "tabby.inlineCompletion.acceptNextLine", - callback: () => { - completionProvider.handleEvent("accept_line"); - // FIXME: this command move cursor to next line, but we want to move cursor to the end of current line - commands.executeCommand("editor.action.inlineSuggest.acceptNextLine"); - }, - }; -}; - -const dismissInlineCompletion = (completionProvider: TabbyCompletionProvider): Command => { - return { - command: "tabby.inlineCompletion.dismiss", - callback: () => { - completionProvider.handleEvent("dismiss"); - commands.executeCommand("editor.action.inlineSuggest.hide"); - }, - }; -}; - -const openOnlineHelp: Command = { - command: "tabby.openOnlineHelp", - callback: () => { - window - .showQuickPick([ - { - label: "Online Documentation", - iconPath: new ThemeIcon("book"), - alwaysShow: true, - }, - { - label: "Model Registry", - description: "Explore more recommend models from Tabby's model registry", - iconPath: new ThemeIcon("library"), - alwaysShow: true, - }, - { - label: "Tabby Slack Community", - description: "Join Tabby's Slack community to get help or feed back", - iconPath: new ThemeIcon("comment-discussion"), - alwaysShow: true, - }, - { - label: "Tabby GitHub Repository", - description: "View the source code for Tabby, and open issues", - iconPath: new ThemeIcon("github"), - alwaysShow: true, - }, - ]) - .then((selection) => { - if (selection) { - switch (selection.label) { - case "Online Documentation": - env.openExternal(Uri.parse("https://tabby.tabbyml.com/")); - break; - case "Model Registry": - env.openExternal(Uri.parse("https://tabby.tabbyml.com/docs/models/")); - break; - case "Tabby Slack Community": - env.openExternal(Uri.parse("https://links.tabbyml.com/join-slack-extensions/")); - break; - case "Tabby GitHub Repository": - env.openExternal(Uri.parse("https://github.com/tabbyml/tabby")); - break; - } - } - }); - }, -}; - -const muteNotifications = (context: ExtensionContext, statusBarItem: TabbyStatusBarItem): Command => { - return { - command: "tabby.notifications.mute", - callback: (type: string) => { - const notifications = context.globalState.get("notifications.muted", []); - notifications.push(type); - context.globalState.update("notifications.muted", notifications); - statusBarItem.refresh(); - }, - }; -}; - -const resetMutedNotifications = (context: ExtensionContext, statusBarItem: TabbyStatusBarItem): Command => { - return { - command: "tabby.notifications.resetMuted", - callback: (type?: string) => { - const notifications = context.globalState.get("notifications.muted", []); - if (type) { - context.globalState.update( - "notifications.muted", - notifications.filter((t) => t !== type), - ); - } else { - context.globalState.update("notifications.muted", []); - } - statusBarItem.refresh(); - }, - }; -}; - -export const tabbyCommands = ( - context: ExtensionContext, - completionProvider: TabbyCompletionProvider, - statusBarItem: TabbyStatusBarItem, -) => - [ - toggleInlineCompletionTriggerMode, - setApiEndpoint, - setApiToken(context), - openSettings, - openTabbyAgentSettings, - openKeybindings, - gettingStarted, - openAuthPage, - applyCallback, - triggerInlineCompletion, - acceptInlineCompletion, - acceptInlineCompletionNextWord(completionProvider), - acceptInlineCompletionNextLine(completionProvider), - dismissInlineCompletion(completionProvider), - openOnlineHelp, - muteNotifications(context, statusBarItem), - resetMutedNotifications(context, statusBarItem), - ].map((command) => commands.registerCommand(command.command, command.callback, command.thisArg)); diff --git a/clients/vscode/src/commands/branchQuickPick.ts b/clients/vscode/src/commands/branchQuickPick.ts new file mode 100644 index 000000000000..851d373ee3e5 --- /dev/null +++ b/clients/vscode/src/commands/branchQuickPick.ts @@ -0,0 +1,156 @@ +import { QuickPickItem, window, QuickPickItemKind, CancellationTokenSource, ThemeIcon } from "vscode"; +import { Deferred } from "../deferred"; +import { Client } from "../lsp/client"; +import { getLogger } from "../logger"; + +interface BranchQuickPickItem extends QuickPickItem { + name: string; +} + +const logger = getLogger("BranchQuickPick"); + +export class BranchQuickPick { + quickPick = window.createQuickPick(); + private suggestedItems: BranchQuickPickItem[] = []; + private resultDeferred = new Deferred(); + private cancellationTokenSource = new CancellationTokenSource(); + private debounceTimer: NodeJS.Timeout | null = null; + private cache = new Map(); + + constructor( + private readonly client: Client, + private readonly repository: string, + ) { + this.quickPick.title = "Enter name to create new branch"; + this.quickPick.placeholder = "Type to filter branch names or create new"; + // Quick pick items are always sorted by label. issue: https://github.com/microsoft/vscode/issues/73904 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (this.quickPick as any).sortByLabel = false; + + this.quickPick.onDidChangeValue((value) => { + this.quickPick.items = this.buildBranchList(); + + if (this.debounceTimer) { + clearTimeout(this.debounceTimer); + } + // Generate branch names even with a single character to improve responsiveness + this.debounceTimer = setTimeout(() => { + this.generateBranchNames(value); + }, 300); + }); + + this.quickPick.onDidAccept(() => { + this.handleAccept(); + }); + + this.quickPick.onDidHide(() => { + this.handleHidden(); + }); + } + + start() { + this.generateBranchNames(""); + this.quickPick.show(); + return this.resultDeferred.promise; + } + + private async generateBranchNames(input: string) { + if (this.cancellationTokenSource) { + this.cancellationTokenSource.cancel(); + this.cancellationTokenSource = new CancellationTokenSource(); + } + + const cacheKey = input.trim().toLowerCase(); + const cachedItems = this.cache.get(cacheKey); + if (cachedItems) { + this.suggestedItems = cachedItems; + this.quickPick.items = this.buildBranchList(); + return; + } + + if (this.quickPick) { + this.quickPick.busy = true; + } + + try { + const result = await this.client.chat.generateBranchName( + { + repository: this.repository, + input: input, + }, + this.cancellationTokenSource.token, + ); + + if (result?.branchNames) { + const uniqueBranches = result.branchNames.filter((name) => name.toLowerCase() !== input.toLowerCase()); + + const branchItems = uniqueBranches.map((name) => ({ + label: name, + name: name, + iconPath: new ThemeIcon("sparkle"), + })); + + this.cache.set(cacheKey, branchItems); + logger.trace("Cached branch names for key:", cacheKey); + + this.suggestedItems = branchItems; + this.quickPick.items = this.buildBranchList(); + } + } catch (error) { + if (!(error instanceof Error && error.name === "CancellationError")) { + logger.error("Error generating branch names:", error); + } + } finally { + if (this.quickPick) { + this.quickPick.busy = false; + } + } + } + + private buildBranchList(): BranchQuickPickItem[] { + const input = this.quickPick.value; + const list: BranchQuickPickItem[] = []; + + if (input) { + list.push({ + label: input, + name: input, + iconPath: new ThemeIcon("add"), + alwaysShow: true, + }); + } + + list.push({ + label: "suggested names", + name: "", + kind: QuickPickItemKind.Separator, + }); + list.push(...this.suggestedItems); + + return list; + } + + private handleAccept() { + const selection = this.quickPick.selectedItems[0]; + if (selection) { + this.resultDeferred.resolve(selection.name); + this.quickPick.hide(); + } + } + + private handleHidden() { + if (this.cancellationTokenSource) { + this.cancellationTokenSource.cancel(); + } + this.resultDeferred.resolve(undefined); + } + + dispose() { + if (this.cancellationTokenSource) { + this.cancellationTokenSource.dispose(); + } + if (this.quickPick) { + this.quickPick.dispose(); + } + } +} diff --git a/clients/vscode/src/commands/commandPalette.ts b/clients/vscode/src/commands/commandPalette.ts new file mode 100644 index 000000000000..f015e912bf43 --- /dev/null +++ b/clients/vscode/src/commands/commandPalette.ts @@ -0,0 +1,245 @@ +import { commands, window, Command, QuickPick, QuickPickItem, QuickPickItemKind, ThemeIcon } from "vscode"; +import { State as LanguageClientState } from "vscode-languageclient"; +import { Client } from "../lsp/client"; +import { Config } from "../Config"; +import { isBrowser } from "../env"; + +interface CommandPaletteItem extends QuickPickItem { + command?: string | Command | (() => void | Promise); + picked?: boolean; +} + +export class CommandPalette { + constructor( + private readonly client: Client, + private readonly config: Config, + ) {} + + show() { + const quickPick: QuickPick = window.createQuickPick(); + quickPick.title = "Tabby Command Palette"; + quickPick.items = this.buildMenuItems(); + + this.client.status.on("didChange", () => { + quickPick.items = this.buildMenuItems(); + }); + + quickPick.onDidAccept(async () => { + quickPick.hide(); + const command = quickPick.activeItems[0]?.command; + if (command) { + if (typeof command === "string") { + await commands.executeCommand(command); + } else if (typeof command === "function") { + await command(); + } else if (command.arguments) { + await commands.executeCommand(command.command, ...command.arguments); + } else { + await commands.executeCommand(command.command); + } + } + }); + quickPick.show(); + } + + private buildMenuItems(): CommandPaletteItem[] { + const items: CommandPaletteItem[] = []; + const status = this.client.status.current?.status; + + // Status section + items.push({ + label: "status", + kind: QuickPickItemKind.Separator, + }); + items.push(this.itemForStatus()); + + // Chat section + items.push({ + label: "chat", + kind: QuickPickItemKind.Separator, + }); + if (this.client.chat.isAvailable) { + items.push({ + label: "Chat", + command: "tabby.chatView.focus", + iconPath: new ThemeIcon("comment"), + }); + } + + // Completion section + items.push({ + label: "code completion", + kind: QuickPickItemKind.Separator, + }); + const invalidStatuses = ["connecting", "unauthorized", "disconnected"]; + if (status !== undefined && !invalidStatuses.includes(status)) { + const isAutomatic = this.config.inlineCompletionTriggerMode === "automatic"; + + const currentLanguageId = window.activeTextEditor?.document.languageId; + const isLanguageDisabled = currentLanguageId ? this.config.disabledLanguages.includes(currentLanguageId) : false; + + items.push({ + label: (isAutomatic ? "Disable" : "Enable") + " auto completions", + picked: isAutomatic, + command: "tabby.toggleInlineCompletionTriggerMode", + alwaysShow: true, + }); + + if (currentLanguageId) { + items.push({ + label: (isLanguageDisabled ? "Enable" : "Disable") + ` completions for ${currentLanguageId}`, + picked: !isLanguageDisabled, + command: { + title: "toggleLanguageInlineCompletion", + command: "tabby.toggleLanguageInlineCompletion", + arguments: [currentLanguageId], + }, + alwaysShow: true, + }); + } + } + + // Settings section + items.push( + { + label: "settings", + kind: QuickPickItemKind.Separator, + }, + { + label: "Connect to Server", + command: "tabby.connectToServer", + iconPath: new ThemeIcon("plug"), + }, + ); + if (status === "unauthorized") { + items.push({ + label: "Update Token", + command: "tabby.updateToken", + iconPath: new ThemeIcon("key"), + }); + } + items.push({ + label: "Settings", + command: "tabby.openSettings", + iconPath: new ThemeIcon("settings"), + }); + if (!isBrowser) { + items.push({ + label: "Agent Settings", + command: "tabby.openTabbyAgentSettings", + iconPath: new ThemeIcon("tools"), + }); + } + items.push({ + label: "Show Logs", + command: "tabby.outputPanel.focus", + iconPath: new ThemeIcon("output"), + }); + + // Help section + items.push( + { + label: "help & support", + kind: QuickPickItemKind.Separator, + }, + { + label: "Help", + description: "Open online documentation", + command: "tabby.openOnlineHelp", + iconPath: new ThemeIcon("question"), + }, + ); + + return items; + } + + private itemForStatus(): CommandPaletteItem { + const STATUS_PREFIX = "Status: "; + const languageClientState = this.client.languageClient.state; + switch (languageClientState) { + case LanguageClientState.Stopped: + case LanguageClientState.Starting: { + return { + label: `${STATUS_PREFIX}Initializing...`, + }; + } + case LanguageClientState.Running: { + const statusInfo = this.client.status.current; + switch (statusInfo?.status) { + case "connecting": { + return { + label: `${STATUS_PREFIX}Connecting...`, + }; + } + case "unauthorized": { + return { + label: `${STATUS_PREFIX}Unauthorized`, + description: "Update your token to connect to Tabby Server", + command: "tabby.updateToken", + }; + } + case "disconnected": { + return { + label: `${STATUS_PREFIX}Disconnected`, + description: "Update the settings to connect to Tabby Server", + command: "tabby.connectToServer", + }; + } + case "ready": + case "readyForAutoTrigger": + case "readyForManualTrigger": + case "fetching": { + return { + label: `${STATUS_PREFIX}Ready`, + description: this.client.agentConfig.current?.server.endpoint, + command: "tabby.outputPanel.focus", + }; + } + case "completionResponseSlow": { + return { + label: `${STATUS_PREFIX}Slow Response`, + description: "Completion requests appear to take too much time.", + command: async () => { + const currentStatusInfo = await this.client.status.fetchAgentStatusInfo(); + window + .showWarningMessage( + "Completion requests appear to take too much time.", + { + modal: true, + detail: currentStatusInfo.helpMessage, + }, + "Online Help...", + "Don't Show Again", + ) + .then((selection) => { + switch (selection) { + case "Online Help...": + commands.executeCommand("tabby.openOnlineHelp"); + break; + case "Don't Show Again": + commands.executeCommand("tabby.status.addIgnoredIssues", "completionResponseSlow"); + break; + } + }); + }, + }; + } + case "rateLimitExceeded": { + return { + label: `${STATUS_PREFIX}Too Many Requests`, + description: "Request limit exceeded", + command: "tabby.outputPanel.focus", + }; + } + default: { + return { + label: `${STATUS_PREFIX}Unknown Status`, + description: "Please check the logs for more information.", + command: "tabby.outputPanel.focus", + }; + } + } + } + } + } +} diff --git a/clients/vscode/src/commands/connectToServer.ts b/clients/vscode/src/commands/connectToServer.ts new file mode 100644 index 000000000000..4ddf02f3e38c --- /dev/null +++ b/clients/vscode/src/commands/connectToServer.ts @@ -0,0 +1,246 @@ +import { + commands, + window, + ThemeIcon, + QuickPickItem, + QuickPickItemKind, + QuickPickItemButtonEvent, + ProgressLocation, +} from "vscode"; +import { isBrowser } from "../env"; +import { Client } from "../lsp/client"; +import { Config } from "../Config"; + +interface ServerQuickPickItem extends QuickPickItem { + endpoint: string; +} + +export class ConnectToServerWidget { + private quickPick = window.createQuickPick(); + + constructor( + private client: Client, + private config: Config, + ) {} + + /** + * Show the widget to connect to a Tabby Server: + * 1. Input the URL of the Tabby Server, show the recent server list for quick selection. + * 2. If no token saved for the server, ask for the token. + * 3. Ensure the connection to the server. + */ + async show(): Promise { + return new Promise((resolve) => { + const quickPick = this.quickPick; + quickPick.title = "Enter the URL of your Tabby Server"; + quickPick.items = this.buildQuickPickItems(); + quickPick.onDidChangeValue(() => { + quickPick.items = this.buildQuickPickItems(); + }); + quickPick.onDidAccept(async () => { + quickPick.hide(); + const selected = quickPick.activeItems[0]; + if (selected) { + if (selected.endpoint == "") { + // Use config in tabby agent config file + await this.config.updateServerEndpoint(""); + } else { + const serverRecords = this.config.serverRecords; + const record = serverRecords.get(selected.endpoint); + if (!record) { + const token = await window.showInputBox({ + title: "Enter your token", + placeHolder: "auth_" + "*".repeat(32), + password: true, + ignoreFocusOut: true, + }); + if (token == undefined) { + // User canceled + resolve(); + return; + } + serverRecords.set(selected.endpoint, { token, updatedAt: Date.now() }); + } else { + record.updatedAt = Date.now(); + } + await this.config.updateServerRecords(serverRecords); + await this.config.updateServerEndpoint(selected.endpoint); + } + await this.ensureConnection(); + } + resolve(); + }); + quickPick.onDidTriggerItemButton(async (event: QuickPickItemButtonEvent) => { + const item = event.item; + const button = event.button; + if (button.iconPath instanceof ThemeIcon) { + if (button.iconPath.id == "settings-remove") { + const serverRecords = this.config.serverRecords; + serverRecords.delete(item.endpoint); + await this.config.updateServerRecords(serverRecords); + quickPick.items = this.buildQuickPickItems(); + } else if (button.iconPath.id == "settings-edit") { + commands.executeCommand("tabby.openTabbyAgentSettings"); + } + } + }); + quickPick.show(); + }); + } + + /** + * Show the widget to update the token for the current server: + * 1. Ask for the new token. + * 2. Ensure the connection to the server. + */ + async showUpdateTokenWidget(): Promise { + const serverRecords = this.config.serverRecords; + const endpoint = this.config.serverEndpoint; + + if (endpoint == "") { + // Should not reach here + throw new Error("This method should not be called when using the config from Tabby Agent Settings."); + } + + const token = await window.showInputBox({ + title: "Your token is invalid. Please update your token", + placeHolder: "auth_" + "*".repeat(32), + password: true, + ignoreFocusOut: true, + }); + if (token == undefined) { + // User canceled + return; + } + serverRecords.set(endpoint, { token, updatedAt: Date.now() }); + await this.config.updateServerRecords(serverRecords); + await this.ensureConnection(); + } + + private async ensureConnection(): Promise { + const endpoint = this.config.serverEndpoint; + const statusInfo = await window.withProgress( + { + location: ProgressLocation.Notification, + title: "Connecting to Tabby Server...", + cancellable: true, + }, + async () => { + return await this.client.status.fetchAgentStatusInfo({ recheckConnection: true }); + }, + ); + + if (statusInfo.status == "disconnected") { + const selected = await window.showErrorMessage( + "Failed to connect to Tabby Server.", + { + modal: true, + detail: statusInfo.helpMessage, + }, + "Select Server", + ); + if (selected == "Select Server") { + const newWidget = new ConnectToServerWidget(this.client, this.config); + await newWidget.show(); + } + } else if (statusInfo.status == "unauthorized") { + if (endpoint == "") { + const selected = await window.showErrorMessage( + "Your token is invalid. Please update your token in Tabby Agent Settings.", + { modal: true }, + "Tabby Agent Settings...", + ); + if (selected == "Tabby Agent Settings...") { + await commands.executeCommand("tabby.openTabbyAgentSettings"); + } + } else { + const selected = await window.showErrorMessage( + "Your token is invalid. Please update your token.", + { modal: true }, + "Update Token", + ); + if (selected == "Update Token") { + await this.showUpdateTokenWidget(); + } + } + } + } + + private buildQuickPickItems(): ServerQuickPickItem[] { + const serverRecords = this.config.serverRecords; + const defaultEndpoint = "http://localhost:8080"; + + const items: ServerQuickPickItem[] = Array.from(serverRecords.entries()) + .map(([endpoint, record]) => { + const isCurrent = endpoint == this.config.serverEndpoint; + return { + label: endpoint, + description: isCurrent ? "Current" : "", + iconPath: new ThemeIcon("server"), + buttons: isCurrent + ? [] + : [ + { + iconPath: new ThemeIcon("settings-remove"), + tooltip: "Remove from Recent Server List", + }, + ], + endpoint, + ...record, + }; + }) + .toSorted((a, b) => b.updatedAt - a.updatedAt); + + if (!items.find((i) => i.label == defaultEndpoint)) { + items.push({ + label: defaultEndpoint, + iconPath: new ThemeIcon("server"), + endpoint: defaultEndpoint, + }); + } + + if (!isBrowser) { + items.push({ + label: "", + endpoint: "", + kind: QuickPickItemKind.Separator, + }); + + items.push({ + label: "Use Configuration in Tabby Agent Settings", + endpoint: "", + description: this.config.serverEndpoint == "" ? "Current" : "", + iconPath: new ThemeIcon("settings"), + buttons: [ + { + iconPath: new ThemeIcon("settings-edit"), + tooltip: "Edit Tabby Agent Settings", + }, + ], + alwaysShow: true, + }); + } + + const input = this.quickPick.value; + if (this.isValidateServerEndpoint(input) && !items.find((i) => i.label === input)) { + items.unshift({ + label: input, + endpoint: input, + iconPath: new ThemeIcon("add"), + description: "Add New Server", + alwaysShow: true, + }); + } + + return items; + } + + private isValidateServerEndpoint(input: string): boolean { + try { + const url = new URL(input); + return url.protocol == "http:" || url.protocol == "https:"; + } catch { + return false; + } + } +} diff --git a/clients/vscode/src/commands/index.ts b/clients/vscode/src/commands/index.ts new file mode 100644 index 000000000000..ca62f09b85c5 --- /dev/null +++ b/clients/vscode/src/commands/index.ts @@ -0,0 +1,547 @@ +import { + workspace, + window, + env, + commands, + ExtensionContext, + CancellationTokenSource, + Uri, + ProgressLocation, + ThemeIcon, + TextEditor, + Range, + CodeAction, + CodeActionKind, +} from "vscode"; +import os from "os"; +import path from "path"; +import { StatusIssuesName } from "tabby-agent"; +import { Client } from "../lsp/client"; +import { Config } from "../Config"; +import { ContextVariables } from "../ContextVariables"; +import { InlineCompletionProvider } from "../InlineCompletionProvider"; +import { ChatSidePanelProvider } from "../chat/sidePanel"; +import { createChatPanel } from "../chat/chatPanel"; +import { getEditorContext } from "../chat/context"; +import { GitProvider, Repository } from "../git/GitProvider"; +import { showOutputPanel } from "../logger"; +import { InlineEditController } from "../inline-edit"; +import { CommandPalette } from "./commandPalette"; +import { ConnectToServerWidget } from "./connectToServer"; +import { BranchQuickPick } from "./branchQuickPick"; +import { getTerminalContext } from "../terminal"; + +export class Commands { + private chatEditCancellationTokenSource: CancellationTokenSource | null = null; + + constructor( + private readonly context: ExtensionContext, + private readonly client: Client, + private readonly config: Config, + private readonly contextVariables: ContextVariables, + private readonly inlineCompletionProvider: InlineCompletionProvider, + private readonly chatSidePanelProvider: ChatSidePanelProvider, + private readonly gitProvider: GitProvider, + ) {} + + register() { + const registrations = Object.entries(this.commands).map(([key, handler]) => { + const commandName = `tabby.${key}`; + return commands.registerCommand(commandName, handler, this); + }); + this.context.subscriptions.push(...registrations); + } + + commands: Record void> = { + applyCallback: (callback: (() => void) | undefined) => { + callback?.(); + }, + toggleInlineCompletionTriggerMode: async (value: "automatic" | "manual" | undefined) => { + let target = value; + if (!target) { + if (this.config.inlineCompletionTriggerMode === "automatic") { + target = "manual"; + } else { + target = "automatic"; + } + } + await this.config.updateInlineCompletionTriggerMode(target); + }, + connectToServer: async (endpoint?: string | undefined) => { + if (endpoint !== undefined) { + await this.config.updateServerEndpoint(endpoint); + } else { + const widget = new ConnectToServerWidget(this.client, this.config); + widget.show(); + } + }, + reconnectToServer: async () => { + await this.client.status.fetchAgentStatusInfo({ recheckConnection: true }); + }, + updateToken: async (token?: string | undefined) => { + const endpoint = this.config.serverEndpoint; + if (token) { + if (endpoint == "") { + return; + } + const serverRecords = this.config.serverRecords; + serverRecords.set(endpoint, { token, updatedAt: Date.now() }); + await this.config.updateServerRecords(serverRecords); + } else { + if (endpoint == "") { + await commands.executeCommand("tabby.openTabbyAgentSettings"); + } else { + const widget = new ConnectToServerWidget(this.client, this.config); + widget.showUpdateTokenWidget(); + } + } + }, + openSettings: () => { + commands.executeCommand("workbench.action.openSettings", "@ext:TabbyML.vscode-tabby"); + }, + openTabbyAgentSettings: () => { + if (env.appHost !== "desktop") { + window.showWarningMessage("Tabby Agent config file is not supported in browser.", { modal: true }); + return; + } + const agentUserConfig = Uri.joinPath(Uri.file(os.homedir()), ".tabby-client", "agent", "config.toml"); + workspace.fs.stat(agentUserConfig).then( + () => { + workspace.openTextDocument(agentUserConfig).then((document) => { + window.showTextDocument(document); + }); + }, + () => { + window.showWarningMessage("Failed to open Tabby Agent config file.", { modal: true }); + }, + ); + }, + openOnlineHelp: (path?: string | undefined) => { + if (typeof path === "string" && path.length > 0) { + env.openExternal(Uri.parse(`https://tabby.tabbyml.com${path}`)); + return; + } + window + .showQuickPick([ + { + label: "Website", + iconPath: new ThemeIcon("book"), + alwaysShow: true, + description: "Visit Tabby's website to learn more about features and use cases", + }, + { + label: "Tabby Slack Community", + description: "Join Tabby's Slack community to get help or share feedback", + iconPath: new ThemeIcon("comment-discussion"), + alwaysShow: true, + }, + { + label: "Tabby GitHub Repository", + description: "Open issues for bugs or feature requests", + iconPath: new ThemeIcon("github"), + alwaysShow: true, + }, + ]) + .then((selection) => { + if (selection) { + switch (selection.label) { + case "Website": + env.openExternal(Uri.parse("https://www.tabbyml.com/")); + break; + case "Tabby Slack Community": + env.openExternal(Uri.parse("https://links.tabbyml.com/join-slack-extensions/")); + break; + case "Tabby GitHub Repository": + env.openExternal(Uri.parse("https://github.com/tabbyml/tabby")); + break; + } + } + }); + }, + openExternal: async (url: string) => { + await env.openExternal(Uri.parse(url)); + }, + openKeybindings: () => { + commands.executeCommand("workbench.action.openGlobalKeybindings", "Tabby"); + }, + gettingStarted: () => { + commands.executeCommand("workbench.action.openWalkthrough", "TabbyML.vscode-tabby#gettingStarted"); + }, + "commandPalette.trigger": () => { + const commandPalette = new CommandPalette(this.client, this.config); + commandPalette.show(); + }, + toggleLanguageInlineCompletion: async (languageId?: string) => { + if (!languageId) { + languageId = window.activeTextEditor?.document.languageId; + if (!languageId) { + return; + } + } + const isLanguageDisabled = this.config.disabledLanguages.includes(languageId); + const disabledLanguages = this.config.disabledLanguages; + if (isLanguageDisabled) { + await this.config.updateDisabledLanguages(disabledLanguages.filter((lang) => lang !== languageId)); + } else { + await this.config.updateDisabledLanguages([...disabledLanguages, languageId]); + } + }, + "outputPanel.focus": () => { + showOutputPanel(); + }, + "inlineCompletion.trigger": () => { + commands.executeCommand("editor.action.inlineSuggest.trigger"); + }, + "inlineCompletion.accept": async () => { + const editor = window.activeTextEditor; + if (!editor) { + return; + } + + const uri = editor.document.uri; + const range = this.inlineCompletionProvider.calcEditedRangeAfterAccept(); + + await commands.executeCommand("editor.action.inlineSuggest.commit"); + + if (range) { + applyQuickFixes(uri, range); + } + }, + "inlineCompletion.acceptNextWord": () => { + this.inlineCompletionProvider.handleEvent("accept_word"); + commands.executeCommand("editor.action.inlineSuggest.acceptNextWord"); + }, + "inlineCompletion.acceptNextLine": () => { + this.inlineCompletionProvider.handleEvent("accept_line"); + // FIXME: this command move cursor to next line, but we want to move cursor to the end of current line + commands.executeCommand("editor.action.inlineSuggest.acceptNextLine"); + }, + "inlineCompletion.dismiss": () => { + this.inlineCompletionProvider.handleEvent("dismiss"); + commands.executeCommand("editor.action.inlineSuggest.hide"); + }, + "status.addIgnoredIssues": (name: StatusIssuesName) => { + this.client.status.editIgnoredIssues({ operation: "add", issues: name }); + }, + "status.resetIgnoredIssues": () => { + this.client.status.editIgnoredIssues({ operation: "removeAll", issues: [] }); + }, + "chat.toggleFocus": async () => { + if (await this.chatSidePanelProvider.chatWebview.isFocused()) { + await commands.executeCommand("workbench.action.focusActiveEditorGroup"); + } else { + await commands.executeCommand("tabby.chatView.focus"); + } + }, + "chat.explainCodeBlock": async (/* userCommand?: string */) => { + // @FIXME(@icycodes): The `userCommand` is not being used + // When invoked from code-action/quick-fix, it contains the error message provided by the IDE + ensureHasEditorSelection(async () => { + await commands.executeCommand("tabby.chatView.focus"); + this.chatSidePanelProvider.chatWebview.executeCommand("explain"); + }); + }, + "chat.addRelevantContext": async () => { + ensureHasEditorSelection(async (editor) => { + await commands.executeCommand("tabby.chatView.focus"); + const fileContext = await getEditorContext(editor, this.gitProvider, "selection"); + if (fileContext) { + this.chatSidePanelProvider.chatWebview.addRelevantContext(fileContext); + } + }); + }, + "chat.addFileContext": async () => { + const editor = window.activeTextEditor; + if (editor) { + await commands.executeCommand("tabby.chatView.focus"); + const fileContext = await getEditorContext(editor, this.gitProvider, "file"); + if (fileContext) { + this.chatSidePanelProvider.chatWebview.addRelevantContext(fileContext); + } + } else { + window.showInformationMessage("No active editor."); + } + }, + "chat.fixCodeBlock": async () => { + ensureHasEditorSelection(async () => { + await commands.executeCommand("tabby.chatView.focus"); + this.chatSidePanelProvider.chatWebview.executeCommand("fix"); + }); + }, + "chat.generateCodeBlockDoc": async () => { + ensureHasEditorSelection(async () => { + await commands.executeCommand("tabby.chatView.focus"); + this.chatSidePanelProvider.chatWebview.executeCommand("generate-docs"); + }); + }, + "chat.generateCodeBlockTest": async () => { + ensureHasEditorSelection(async () => { + await commands.executeCommand("tabby.chatView.focus"); + this.chatSidePanelProvider.chatWebview.executeCommand("generate-tests"); + }); + }, + "chat.codeReviewCodeBlock": async () => { + ensureHasEditorSelection(async () => { + await commands.executeCommand("tabby.chatView.focus"); + this.chatSidePanelProvider.chatWebview.executeCommand("code-review"); + }); + }, + "chat.createPanel": async () => { + await createChatPanel(this.context, this.client, this.gitProvider); + }, + "chat.navigate.newChat": async () => { + this.chatSidePanelProvider.chatWebview.navigate("new-chat"); + }, + "chat.navigate.history": async () => { + this.chatSidePanelProvider.chatWebview.navigate("history"); + }, + "chat.edit.start": async ( + fileUri?: string | undefined, + range?: Range | undefined, + userCommand?: string | undefined, + ) => { + if (this.contextVariables.chatEditInProgress) { + window.setStatusBarMessage("Edit is already in progress.", 3000); + return; + } + + let editor: TextEditor | undefined; + if (fileUri) { + try { + const uri = Uri.parse(fileUri, true); + editor = window.visibleTextEditors.find((editor) => editor.document.uri.toString() === uri.toString()); + } catch { + // ignore + } + } + if (!editor) { + editor = window.activeTextEditor; + } + if (!editor) { + return; + } + + const editRange = range ?? editor.selection; + + const inlineEditController = new InlineEditController( + this.client, + this.config, + this.contextVariables, + editor, + editRange, + ); + const cancellationTokenSource = new CancellationTokenSource(); + this.chatEditCancellationTokenSource = cancellationTokenSource; + await inlineEditController.start(userCommand, cancellationTokenSource.token); + cancellationTokenSource.dispose(); + this.chatEditCancellationTokenSource = null; + }, + "chat.edit.stop": async () => { + this.chatEditCancellationTokenSource?.cancel(); + this.chatEditCancellationTokenSource?.dispose(); + this.chatEditCancellationTokenSource = null; + }, + "chat.edit.accept": async () => { + const editor = window.activeTextEditor; + if (!editor) { + return; + } + const location = { + uri: editor.document.uri.toString(), + range: { + start: { line: editor.selection.start.line, character: 0 }, + end: { line: editor.selection.end.line + 1, character: 0 }, + }, + }; + await this.client.chat.resolveEdit({ location, action: "accept" }); + }, + "chat.edit.discard": async () => { + const editor = window.activeTextEditor; + if (!editor) { + return; + } + const location = { + uri: editor.document.uri.toString(), + range: { + start: { line: editor.selection.start.line, character: 0 }, + end: { line: editor.selection.end.line + 1, character: 0 }, + }, + }; + await this.client.chat.resolveEdit({ location, action: "discard" }); + }, + "chat.generateCommitMessage": async (repository?: { rootUri?: Uri }) => { + let selectedRepo: Repository | undefined = undefined; + if (repository && repository.rootUri) { + selectedRepo = this.gitProvider.getRepository(repository.rootUri); + } + if (!selectedRepo) { + const repos = this.gitProvider.getRepositories() ?? []; + if (repos.length < 1) { + window.showInformationMessage("No Git repositories found."); + return; + } + if (repos.length == 1) { + selectedRepo = repos[0]; + } else { + const selected = await window.showQuickPick( + repos + .map((repo) => { + const repoRoot = repo.rootUri.fsPath; + return { + label: path.basename(repoRoot), + detail: repoRoot, + iconPath: new ThemeIcon("repo"), + picked: repo.ui.selected, + alwaysShow: true, + value: repo, + }; + }) + .sort((a, b) => { + if (a.detail.startsWith(b.detail)) { + return 1; + } else if (b.detail.startsWith(a.detail)) { + return -1; + } else { + return a.label.localeCompare(b.label); + } + }), + { placeHolder: "Select a Git repository" }, + ); + selectedRepo = selected?.value; + } + } + if (!selectedRepo) { + return; + } + window.withProgress( + { + location: ProgressLocation.Notification, + title: "Generating commit message...", + cancellable: true, + }, + async (_, token) => { + // Focus on scm view + commands.executeCommand("workbench.view.scm"); + const result = await this.client.chat.generateCommitMessage( + { repository: selectedRepo.rootUri.toString() }, + token, + ); + + if (result && selectedRepo.inputBox) { + selectedRepo.inputBox.value = result.commitMessage; + + if (selectedRepo.state.HEAD) { + const currentBranch = selectedRepo.state.HEAD.name; + // FIXME(Sma1lboy): let LLM model decide should we create a new branch or not + if (currentBranch === "main" || currentBranch === "master") { + commands.executeCommand("tabby.chat.generateBranchName", selectedRepo); + } + } + } + }, + ); + }, + "chat.generateBranchName": async (repository?: { rootUri?: Uri }) => { + let selectedRepo: Repository | undefined = undefined; + if (repository && repository.rootUri) { + selectedRepo = this.gitProvider.getRepository(repository.rootUri); + } + if (!selectedRepo) { + const repos = this.gitProvider.getRepositories() ?? []; + if (repos.length < 1) { + window.showInformationMessage("No Git repositories found."); + return; + } + if (repos.length == 1) { + selectedRepo = repos[0]; + } else { + const selected = await window.showQuickPick( + repos + .map((repo) => { + const repoRoot = repo.rootUri.fsPath; + return { + label: path.basename(repoRoot), + detail: repoRoot, + iconPath: new ThemeIcon("repo"), + picked: repo.ui.selected, + alwaysShow: true, + value: repo, + }; + }) + .sort((a, b) => { + if (a.detail.startsWith(b.detail)) { + return 1; + } else if (b.detail.startsWith(a.detail)) { + return -1; + } else { + return a.label.localeCompare(b.label); + } + }), + { placeHolder: "Select a Git repository" }, + ); + selectedRepo = selected?.value; + } + } + if (!selectedRepo) { + return; + } + + const branchQuickPick = new BranchQuickPick(this.client, selectedRepo.rootUri.toString()); + + const branchName = await branchQuickPick.start(); + if (branchName) { + try { + await selectedRepo.createBranch(branchName, true); + window.showInformationMessage(`Created branch: ${branchName}`); + } catch (error) { + window.showErrorMessage(`Failed to create branch: ${error instanceof Error ? error.message : String(error)}`); + } + } + }, + "terminal.explainSelection": async () => { + await commands.executeCommand("tabby.chatView.focus"); + this.chatSidePanelProvider.chatWebview.executeCommand("explain-terminal"); + }, + "terminal.addSelectionToChat": async () => { + const terminalContext = await getTerminalContext(); + if (!terminalContext) { + window.showInformationMessage("No terminal selection found."); + return; + } + + await commands.executeCommand("tabby.chatView.focus"); + this.chatSidePanelProvider.chatWebview.addRelevantContext(terminalContext); + }, + }; +} + +function ensureHasEditorSelection(callback: (editor: TextEditor) => void) { + const editor = window.activeTextEditor; + if (editor && !editor.selection.isEmpty) { + callback(editor); + } else { + window.showInformationMessage("No selected codes."); + } +} + +async function applyQuickFixes(uri: Uri, range: Range): Promise { + const codeActions = await commands.executeCommand("vscode.executeCodeActionProvider", uri, range); + const quickFixActions = codeActions.filter( + (action) => + action.kind && action.kind.contains(CodeActionKind.QuickFix) && action.title.toLowerCase().includes("import"), + ); + + if (quickFixActions.length === 1 && quickFixActions[0]) { + const firstAction = quickFixActions[0]; + try { + if (firstAction.edit) { + await workspace.applyEdit(firstAction.edit); + } + if (firstAction.command) { + await commands.executeCommand(firstAction.command.command, firstAction.command.arguments); + } + } catch (error) { + // ignore errors + } + } +} diff --git a/clients/vscode/src/deferred.ts b/clients/vscode/src/deferred.ts new file mode 100644 index 000000000000..5c511036f015 --- /dev/null +++ b/clients/vscode/src/deferred.ts @@ -0,0 +1,16 @@ +export const noop = () => { + // +}; + +export class Deferred { + public resolve: (value: T | PromiseLike) => void = noop; + public reject: (err?: unknown) => void = noop; + public readonly promise: Promise; + + constructor() { + this.promise = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); + } +} diff --git a/clients/vscode/src/env.ts b/clients/vscode/src/env.ts new file mode 100644 index 000000000000..bb5952115435 --- /dev/null +++ b/clients/vscode/src/env.ts @@ -0,0 +1 @@ +export const isBrowser = !!process.env["IS_BROWSER"]; diff --git a/clients/vscode/src/extension.ts b/clients/vscode/src/extension.ts index c98caafc3a34..355bcf49bb3b 100644 --- a/clients/vscode/src/extension.ts +++ b/clients/vscode/src/extension.ts @@ -1,27 +1,113 @@ -// The module 'vscode' contains the VS Code extensibility API -// Import the module and reference it with the alias vscode in your code below -import { ExtensionContext, languages } from "vscode"; -import { createAgentInstance, disposeAgentInstance } from "./agent"; -import { tabbyCommands } from "./commands"; -import { TabbyCompletionProvider } from "./TabbyCompletionProvider"; -import { TabbyStatusBarItem } from "./TabbyStatusBarItem"; - -// this method is called when your extension is activated -// your extension is activated the very first time the command is executed +import { window, ExtensionContext } from "vscode"; +import { getLogger } from "./logger"; +import { Client, createClient } from "./lsp/client"; +import { InlineCompletionProvider } from "./InlineCompletionProvider"; +import { Config } from "./Config"; +import { GitProvider } from "./git/GitProvider"; +import { ContextVariables } from "./ContextVariables"; +import { StatusBarItem } from "./StatusBarItem"; +import { ChatSidePanelProvider } from "./chat/sidePanel"; +import { Commands } from "./commands"; +import { init as initFindFiles } from "./findFiles"; +import { CodeActions } from "./CodeActions"; +import { KeyBindingManager } from "./keybindings"; + +const logger = getLogger(); +let clientRef: Client | undefined = undefined; + export async function activate(context: ExtensionContext) { - console.debug("Activating Tabby extension", new Date()); - await createAgentInstance(context); - const completionProvider = new TabbyCompletionProvider(); - const statusBarItem = new TabbyStatusBarItem(context, completionProvider); + logger.info("Activating Tabby extension..."); + + const client = createClient(context, logger); + const config = new Config(context); + const contextVariables = new ContextVariables(client, config); + const inlineCompletionProvider = new InlineCompletionProvider(client, config); + const gitProvider = new GitProvider(); + + client.registerConfigManager(config); + client.registerInlineCompletionProvider(inlineCompletionProvider); + client.registerGitProvider(gitProvider); + clientRef = client; + + // Register chat panel + const chatViewProvider = new ChatSidePanelProvider(context, client, contextVariables, gitProvider); context.subscriptions.push( - languages.registerInlineCompletionItemProvider({ pattern: "**" }, completionProvider), - statusBarItem.register(), - ...tabbyCommands(context, completionProvider, statusBarItem), + window.registerWebviewViewProvider("tabby.chatView", chatViewProvider, { + webviewOptions: { retainContextWhenHidden: true }, + }), ); + + // Register status bar item + const statusBarItem = new StatusBarItem(client, config); + statusBarItem.registerInContext(context); + + // Register command + const commands = new Commands( + context, + client, + config, + contextVariables, + inlineCompletionProvider, + chatViewProvider, + gitProvider, + ); + commands.register(); + + // init keybinding manager + KeyBindingManager.getInstance().init(); + + // Register code actions + /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ /* eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error */ + /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ // @ts-ignore noUnusedLocals + const codeActions = new CodeActions(client, contextVariables); + + logger.info("Tabby extension activated."); + + // Start async initialization + const startClient = async () => { + await gitProvider.init(); + + logger.info("Launching language server tabby-agent..."); + await client.start(); + logger.info("Language server tabby-agent launched."); + }; + + await Promise.all([ + // start LSP client + startClient(), + + // findFiles preheat + initFindFiles(context), + ]); + + const tryReadAuthenticationToken = async (): Promise<{ token: string | undefined }> => { + const endpoint = config.serverEndpoint; + if (!endpoint || endpoint.trim() === "") { + return { token: undefined }; + } + + const response = await window.showInformationMessage( + "Do you consent to sharing your Tabby token with another VSCode extension?", + { + modal: true, + detail: `The extension requests your token to access the Tabby server at ${endpoint}. Sharing your token allows the extension to perform actions as if it were you. Only proceed if you trust the extension.`, + }, + "Yes", + "No", + ); + + if (response === "Yes") { + return { token: config.serverRecords.get(endpoint)?.token }; + } + return { token: undefined }; + }; + return { + tryReadAuthenticationToken, + }; } -// this method is called when your extension is deactivated export async function deactivate() { - console.debug("Deactivating Tabby extension", new Date()); - await disposeAgentInstance(); + logger.info("Deactivating Tabby extension..."); + await clientRef?.stop(); + logger.info("Tabby extension deactivated."); } diff --git a/clients/vscode/src/findFiles.ts b/clients/vscode/src/findFiles.ts new file mode 100644 index 000000000000..6d18d50165b7 --- /dev/null +++ b/clients/vscode/src/findFiles.ts @@ -0,0 +1,399 @@ +// We want to use the findFiles2 API, but it is still in the proposed state. +// Therefore, we implement a findFiles function here that can use the following rules in the exclude pattern: +// 1. .gitignore files +// 2. Settings from `files.exclude` and `search.exclude` +// +// See https://github.com/microsoft/vscode/blob/main/src/vscode-dts/vscode.proposed.findFiles2.d.ts + +import { + ExtensionContext, + GlobPattern, + RelativePattern, + Uri, + TabInputText, + WorkspaceFolder, + CancellationToken, + CancellationTokenSource, + workspace, + window, +} from "vscode"; +import path from "path"; +import { wrapCancelableFunction } from "./cancelableFunction"; +import { getLogger } from "./logger"; + +const logger = getLogger("FindFiles"); + +// Map from workspace folder to gitignore patterns +const gitIgnorePatternsMap = new Map(); + +function gitIgnoreItemToExcludePatterns(item: string, prefix?: string | undefined): string[] { + let pattern = item.trim(); + if (pattern.startsWith("#") || pattern.startsWith("!") || pattern.length === 0) { + return []; + } + if (pattern.indexOf("/") === -1 || pattern.indexOf("/") === pattern.length - 1) { + if (!pattern.startsWith("**/")) { + pattern = `**/${pattern}`; + } + } else if (pattern.startsWith("/")) { + pattern = pattern.slice(1); + } + return [path.join(prefix ?? "", pattern), path.join(prefix ?? "", pattern, "/**")]; +} + +function joinPatterns(patterns: string[], maxLength = 32000 /* < 2 ^ 15 */): string { + let result = ""; + for (const pattern of patterns) { + if (result.length + pattern.length + 3 >= maxLength) { + continue; + } + if (result.length > 0) { + result += ","; + } + result += pattern; + } + return `{${result}}`; +} + +function addUniqueItem(arr: string[], item: string) { + if (!arr.includes(item)) { + arr.push(item); + } +} + +async function updateGitIgnorePatterns(workspaceFolder: WorkspaceFolder, token?: CancellationToken | undefined) { + const patterns: string[] = []; + logger.debug(`Building gitignore patterns for workspace folder: ${workspaceFolder.uri.toString()}`); + + // Read parent gitignore files + let current = workspaceFolder.uri; + let parent = current.with({ path: path.dirname(current.path) }); + while (parent.path !== current.path) { + if (token?.isCancellationRequested) { + return; + } + + const gitignore = parent.with({ path: path.join(parent.path, ".gitignore") }); + try { + const content = new TextDecoder().decode(await workspace.fs.readFile(gitignore)); + content.split(/\r?\n/).forEach((line) => { + gitIgnoreItemToExcludePatterns(line).forEach((pattern) => addUniqueItem(patterns, pattern)); + }); + } catch (error) { + // ignore + } + + // next + current = parent; + parent = current.with({ path: path.dirname(current.path) }); + } + + if (token?.isCancellationRequested) { + return; + } + // Read subdirectories gitignore files + let ignoreFiles: Uri[] = []; + try { + ignoreFiles = ( + await workspace.findFiles( + new RelativePattern(workspaceFolder, "**/.gitignore"), + joinPatterns(patterns), + undefined, + token, + ) + ).sort((a, b) => { + const aDepth = a.path.split(path.sep).length; + const bDepth = b.path.split(path.sep).length; + if (aDepth != bDepth) { + return aDepth - bDepth; + } + return a.path.localeCompare(b.path); + }); + } catch (error) { + // ignore + } + + for (const ignoreFile of ignoreFiles) { + if (token?.isCancellationRequested) { + return; + } + const prefix = path.relative(workspaceFolder.uri.path, path.dirname(ignoreFile.path)); + try { + const content = new TextDecoder().decode(await workspace.fs.readFile(ignoreFile)); + content.split(/\r?\n/).forEach((line) => { + gitIgnoreItemToExcludePatterns(line, prefix).forEach((pattern) => addUniqueItem(patterns, pattern)); + }); + } catch (error) { + // ignore + } + } + // Update map + logger.debug( + `Completed building git ignore patterns for workspace folder: ${workspaceFolder.uri.toString()}, git ignore patterns: ${JSON.stringify(patterns)}`, + ); + gitIgnorePatternsMap.set(workspaceFolder.uri.toString(), patterns); +} + +const updateGitIgnorePatternsMap = wrapCancelableFunction(async (token?: CancellationToken) => { + await Promise.all( + workspace.workspaceFolders?.map(async (workspaceFolder) => { + await updateGitIgnorePatterns(workspaceFolder, token); + }) ?? [], + ); +}); + +export async function init(context: ExtensionContext) { + context.subscriptions.push( + workspace.onDidChangeWorkspaceFolders(async () => { + await updateGitIgnorePatternsMap(); + }), + ); + + context.subscriptions.push( + workspace.onDidChangeTextDocument(async (event) => { + const uri = event.document.uri; + if (path.basename(uri.fsPath) === ".gitignore") { + await updateGitIgnorePatternsMap(); + } + }), + ); + + await updateGitIgnorePatternsMap(); +} + +export async function findFiles( + pattern: GlobPattern, + options?: { + excludes?: string[]; + noUserSettings?: boolean; // User settings is used by default, set to true to skip user settings + noIgnoreFiles?: boolean; // .gitignore files are used by default, set to true to skip .gitignore files + maxResults?: number; + token?: CancellationToken; + }, +): Promise { + const combinedExcludes: string[] = []; + if (options?.excludes) { + for (const exclude of options.excludes) { + addUniqueItem(combinedExcludes, exclude); + } + } + if (!options?.noUserSettings) { + const searchExclude: Record = + (await workspace.getConfiguration("search", null).get("exclude")) ?? {}; + const filesExclude: Record = + (await workspace.getConfiguration("files", null).get("exclude")) ?? {}; + for (const pattern in { ...searchExclude, ...filesExclude }) { + if (filesExclude[pattern]) { + addUniqueItem(combinedExcludes, pattern); + } + } + } + if (options?.noIgnoreFiles) { + const excludesPattern = joinPatterns(combinedExcludes); + logger.debug( + `Executing search: ${JSON.stringify({ includePattern: pattern, excludesPattern, maxResults: options?.maxResults, token: options?.token })}`, + ); + return await workspace.findFiles(pattern, excludesPattern, options.maxResults, options.token); + } else { + return new Promise((resolve, reject) => { + if (options?.token?.isCancellationRequested) { + reject(new Error("Operation canceled.")); + return; + } + + const cancellationTokenSource = new CancellationTokenSource(); + if (options?.token) { + options?.token.onCancellationRequested(() => { + cancellationTokenSource.cancel(); + reject(new Error("Operation canceled.")); + }); + } + + const allSearches = + workspace.workspaceFolders?.map(async (workspaceFolder) => { + let includePattern: RelativePattern; + if (typeof pattern === "string") { + includePattern = new RelativePattern(workspaceFolder, pattern); + } else { + if (pattern.baseUri.toString().startsWith(workspaceFolder.uri.toString())) { + includePattern = pattern; + } else { + return []; + } + } + const excludesPattern = joinPatterns([ + ...combinedExcludes, + ...(gitIgnorePatternsMap.get(workspaceFolder.uri.toString()) ?? []), + ]); + logger.debug( + `Executing search: ${JSON.stringify({ includePattern, excludesPattern, maxResults: options?.maxResults })}`, + ); + return await workspace.findFiles( + includePattern, + excludesPattern, + options?.maxResults, + cancellationTokenSource.token, + ); + }) ?? []; + + const results: Uri[] = []; + Promise.all( + allSearches.map(async (search) => { + try { + const result = await search; + if (result.length > 0) { + results.push(...result); + if (options?.maxResults && results.length >= options.maxResults) { + cancellationTokenSource.cancel(); + resolve(results.slice(0, options.maxResults)); + } + } + } catch (error) { + // ignore + } + }), + ).then(() => { + resolve(results); + }); + }); + } +} + +export function sortFiles(files: Uri[], query: string): Uri[] { + const matchString = query.toLowerCase().split("*").filter(Boolean)[0]; + if (!matchString) { + return files.toSorted((uriA, uriB) => { + const basenameA = path.basename(uriA.fsPath).toLowerCase(); + const basenameB = path.basename(uriB.fsPath).toLowerCase(); + return basenameA.localeCompare(basenameB); + }); + } + + const getScore = (basename: string) => { + if (basename == matchString) { + return 4; + } + if (basename.split(".").includes(matchString)) { + return 3; + } + if (basename.startsWith(matchString)) { + return 2; + } + if (basename.includes(matchString)) { + return 1; + } + return 0; + }; + return files.toSorted((uriA, uriB) => { + const basenameA = path.basename(uriA.fsPath).toLowerCase(); + const basenameB = path.basename(uriB.fsPath).toLowerCase(); + const scoreA = getScore(basenameA); + const scoreB = getScore(basenameB); + if (scoreA > scoreB) { + return -1; + } + if (scoreA < scoreB) { + return 1; + } + if (basenameA == basenameB) { + const dirnameA = path.dirname(uriA.fsPath).toLowerCase(); + const dirnameB = path.dirname(uriB.fsPath).toLowerCase(); + return dirnameA.localeCompare(dirnameB); + } + return basenameA.localeCompare(basenameB); + }); +} + +export function buildGlobPattern(query: string): GlobPattern { + const caseInsensitivePattern = query + .split("") + .map((char) => { + if (char.toLowerCase() !== char.toUpperCase()) { + return `{${char.toLowerCase()},${char.toUpperCase()}}`; + } + // escape special glob characters: ? [ ] { } ( ) ! @ + return char.replace(/[?[\]{}()!@]/g, "\\$&"); + }) + .join(""); + + return `**/*${caseInsensitivePattern}{*,*/*}`; +} + +// `listFiles` will check the opened editors first, then use `findFiles` to search for files until the limit. +export async function listFiles( + query: string, + limit?: number | undefined, + token?: CancellationToken | undefined, +): Promise< + { + uri: Uri; + isOpenedInEditor: boolean; + }[] +> { + const maxResults = limit ?? 30; + const queryString = query.trim().toLowerCase(); + + const allEditorUris = window.tabGroups.all + .flatMap((group) => group.tabs) + .filter((tab) => tab.input && (tab.input as TabInputText).uri) + .map((tab) => (tab.input as TabInputText).uri); + + const editorUris = sortFiles( + allEditorUris + // deduplicate + .filter((uri, idx) => allEditorUris.findIndex((item) => item.fsPath === uri.fsPath) === idx) + // filter by search query + .filter((uri) => uri.fsPath.toLowerCase().includes(queryString)), + queryString, + ) + // move the active editor to the top + .sort((uriA, uriB) => { + const activeEditorUri = window.activeTextEditor?.document.uri; + if (activeEditorUri) { + if (uriA.fsPath === activeEditorUri.fsPath) return -1; + if (uriB.fsPath === activeEditorUri.fsPath) return 1; + } + return 0; + }); + + const result = editorUris.map((uri) => { + return { + uri, + isOpenedInEditor: true, + }; + }); + if (result.length >= maxResults) { + return result.slice(0, maxResults); + } + + const globPattern = buildGlobPattern(queryString); + try { + const foundFiles = await findFiles(globPattern, { + maxResults: maxResults - editorUris.length, + excludes: editorUris.map((uri) => uri.fsPath), + token, + }); + const searchResult = sortFiles( + foundFiles.filter( + (uri, idx) => + foundFiles.findIndex((item) => item.fsPath === uri.fsPath) === idx && + !editorUris.some((exisingUri) => exisingUri.fsPath === uri.fsPath), + ), + queryString, + ); + + logger.debug(`Found ${searchResult.length} files matching pattern "${globPattern}"`); + result.push( + ...searchResult.map((uri) => { + return { + uri, + isOpenedInEditor: false, + }; + }), + ); + } catch (error) { + logger.debug("Failed to find files:", error); + } + + return result; +} diff --git a/clients/vscode/src/findSymbols.ts b/clients/vscode/src/findSymbols.ts new file mode 100644 index 000000000000..9f6e2eef0036 --- /dev/null +++ b/clients/vscode/src/findSymbols.ts @@ -0,0 +1,168 @@ +import { commands, DocumentSymbol, SymbolInformation, Uri, SymbolKind, ThemeIcon, window, Location } from "vscode"; +import { getLogger } from "./logger"; + +const logger = getLogger("findSymbols"); + +/** + * Interface definition for symbol information returned by listSymbols + */ +export interface SymbolInfo { + name: string; + kind: SymbolKind; + location: Location; + kindIcon: ThemeIcon; + containerName?: string; +} + +/** + * Fetches symbols from the current file based on the query + * @param uri The URI of the file to search in + * @param query The search query + * @param limit Maximum number of results to return + * @returns Array of symbol information with additional metadata + */ +export async function listSymbols(uri: Uri, query: string, limit?: number): Promise { + try { + const maxResults = limit ?? 50; + const queryString = query.trim().toLowerCase(); + const rawDocumentSymbols = + (await commands.executeCommand( + "vscode.executeDocumentSymbolProvider", + uri, + )) || []; + + const documentSymbols = convertToSymbolInformation(rawDocumentSymbols); + + const filteredSymbols = filterSymbols(documentSymbols, queryString, maxResults); + + return filteredSymbols.map((symbol) => ({ + name: symbol.name, + kind: symbol.kind, + location: symbol.location, + kindIcon: new ThemeIcon(symbolIconMap.get(symbol.kind) ?? "symbol-misc"), + containerName: symbol.containerName, + })); + } catch (error) { + logger.debug("Failed to find symbols:", error); + return []; + } +} + +/** + * Converts DocumentSymbol[] to SymbolInformation[] for consistent handling + */ +function convertToSymbolInformation(symbols: (DocumentSymbol | SymbolInformation)[]): SymbolInformation[] { + const result: SymbolInformation[] = []; + + if (symbols.length > 0 && symbols[0] && "children" in symbols[0] && window.activeTextEditor) { + flattenDocumentSymbols(symbols as DocumentSymbol[], "", window.activeTextEditor.document.uri, result); + } else if (symbols.length > 0) { + result.push(...(symbols as SymbolInformation[])); + } + + return result; +} + +/** + * Flattens a hierarchical DocumentSymbol structure into a flat SymbolInformation array + */ +function flattenDocumentSymbols( + symbols: DocumentSymbol[], + containerName: string, + uri: Uri, + result: SymbolInformation[], +): void { + for (const symbol of symbols) { + const fullName = containerName ? `${containerName}.${symbol.name}` : symbol.name; + + result.push({ + name: symbol.name, + kind: symbol.kind, + containerName: containerName, + location: { + uri: uri, + range: symbol.range, + }, + } as SymbolInformation); + + if (symbol.children && symbol.children.length > 0) { + flattenDocumentSymbols(symbol.children, fullName, uri, result); + } + } +} + +/** + * Maps symbol kinds to appropriate theme icons name + */ +const symbolIconMap = new Map([ + [SymbolKind.File, "file"], + [SymbolKind.Module, "package"], + [SymbolKind.Namespace, "symbol-namespace"], + [SymbolKind.Class, "symbol-class"], + [SymbolKind.Method, "symbol-method"], + [SymbolKind.Property, "symbol-property"], + [SymbolKind.Field, "symbol-field"], + [SymbolKind.Constructor, "symbol-constructor"], + [SymbolKind.Enum, "symbol-enum"], + [SymbolKind.Interface, "symbol-interface"], + [SymbolKind.Function, "symbol-method"], + [SymbolKind.Variable, "symbol-variable"], + [SymbolKind.Constant, "symbol-constant"], + [SymbolKind.String, "symbol-string"], + [SymbolKind.Number, "symbol-number"], + [SymbolKind.Boolean, "symbol-boolean"], + [SymbolKind.Array, "symbol-array"], + [SymbolKind.Object, "symbol-object"], + [SymbolKind.Key, "symbol-key"], + [SymbolKind.Null, "symbol-null"], + [SymbolKind.EnumMember, "symbol-enum-member"], + [SymbolKind.Struct, "symbol-struct"], + [SymbolKind.Event, "symbol-event"], + [SymbolKind.Operator, "symbol-operator"], + [SymbolKind.TypeParameter, "symbol-parameter"], +]); + +/** + * Filters and sorts symbols based on the query + */ +function filterSymbols(symbols: SymbolInformation[], query: string, maxResults: number): SymbolInformation[] { + const uniqueSymbols = removeDuplicateSymbols(symbols); + + if (!query || !window.activeTextEditor) { + return uniqueSymbols.slice(0, maxResults); + } + const editor = window.activeTextEditor; + + const lowerQuery = query.toLowerCase(); + const filtered = uniqueSymbols.filter((s) => s.name.toLowerCase().includes(lowerQuery)); + return filtered + .sort((a, b) => { + const aName = a.name.toLowerCase(); + const bName = b.name.toLowerCase(); + if (aName === lowerQuery && bName !== lowerQuery) return -1; + if (bName === lowerQuery && aName !== lowerQuery) return 1; + if (aName.startsWith(lowerQuery) && !bName.startsWith(lowerQuery)) return -1; + if (bName.startsWith(lowerQuery) && !aName.startsWith(lowerQuery)) return 1; + const aIsCurrentFile = a.location.uri.toString() === editor.document.uri.toString(); + const bIsCurrentFile = b.location.uri.toString() === editor.document.uri.toString(); + if (aIsCurrentFile && !bIsCurrentFile) return -1; + if (bIsCurrentFile && !aIsCurrentFile) return 1; + return a.name.length - b.name.length; + }) + .slice(0, maxResults); +} + +/** + * Removes duplicate symbols from the list + */ +function removeDuplicateSymbols(symbols: SymbolInformation[]): SymbolInformation[] { + const seen = new Set(); + return symbols.filter((symbol) => { + const key = `${symbol.name}-${symbol.containerName}-${symbol.location.uri.toString()}`; + if (seen.has(key)) { + return false; + } + seen.add(key); + return true; + }); +} diff --git a/clients/vscode/src/git/GitProvider.ts b/clients/vscode/src/git/GitProvider.ts new file mode 100644 index 000000000000..e4a0ef288ecd --- /dev/null +++ b/clients/vscode/src/git/GitProvider.ts @@ -0,0 +1,102 @@ +import { extensions, workspace, Uri } from "vscode"; +import type { Repository as GitRepository, API } from "./git"; +export type Repository = GitRepository; +import { getLogger } from "../logger"; + +export class GitProvider { + private readonly logger = getLogger(); + private api: API | undefined = undefined; + private remoteUrlToLocalRoot = new Map(); + + private async initGitExtensionApi(tries = 0): Promise { + try { + const ext = extensions.getExtension("vscode.git"); + if (ext?.isActive) { + this.api = ext.exports.getAPI(1); + } + } catch (err) { + this.logger.debug(`${err}`); + } + if (this.api) { + this.logger.info("GitProvider created."); + } else { + if (tries >= 2) { + this.logger.warn(`Failed to create GitProvider after ${tries} tries, giving up.`); + } else { + const delay = (tries + 1) * 1000; + this.logger.info(`Failed to create GitProvider, retry after ${delay}ms`); + await new Promise((resolve) => setTimeout(resolve, delay)); + await this.initGitExtensionApi(tries + 1); + } + } + } + + async init(): Promise { + await this.initGitExtensionApi(); + } + + isApiAvailable(): boolean { + return !!this.api; + } + + getRepositories(): Repository[] | undefined { + return this.api?.repositories; + } + + getRepository(uri: Uri): Repository | undefined { + return this.api?.getRepository(uri) ?? undefined; + } + + /** + * Retrieves diff information from the specified git repository. + * @param repository The Git repository to get the diff from. + * @param cached When true, shows diff between index and HEAD (staged changes). + * When false, shows diff between working tree and index (unstaged changes). + * @returns Promise resolving to an array of diff strings, each representing a single file's changes, + * or undefined if the operation fails. Results are sorted by file modification time. + */ + async getDiff(repository: Repository, cached: boolean): Promise { + const diff = (await repository.diff(cached)).trim(); + const diffs = await Promise.all( + diff.split(/\n(?=diff)/).map(async (item: string) => { + let priority = Number.MAX_SAFE_INTEGER; + const filepath = /diff --git a\/.* b\/(.*)$/gm.exec(item)?.[1]; + if (filepath) { + const uri = Uri.joinPath(repository.rootUri, filepath); + try { + priority = (await workspace.fs.stat(uri)).mtime; + } catch (error) { + //ignore + } + } + return { diff: item, priority }; + }), + ); + return diffs.sort((a, b) => a.priority - b.priority).map((item) => item.diff); + } + + getDefaultRemoteUrl(repository: Repository): string | undefined { + const remote = + repository.state.remotes.find((remote) => remote.name === "origin") || + repository.state.remotes.find((remote) => remote.name === "upstream") || + repository.state.remotes[0]; + const remoteUrl = remote?.fetchUrl ?? remote?.pushUrl; + if (remoteUrl) { + this.remoteUrlToLocalRoot.set(remoteUrl, repository.rootUri); + } + return remoteUrl; + } + + findLocalRootUriByRemoteUrl(remoteUrl: string): Uri | undefined { + if (this.remoteUrlToLocalRoot.has(remoteUrl)) { + return this.remoteUrlToLocalRoot.get(remoteUrl); + } + const allRepos = this.getRepositories(); + const repo = allRepos?.find((repo) => + repo.state.remotes.find((remote) => remote.fetchUrl === remoteUrl || remote.pushUrl === remoteUrl), + ); + const localRootUri = repo?.rootUri; + this.remoteUrlToLocalRoot.set(remoteUrl, localRootUri); + return localRootUri; + } +} diff --git a/clients/vscode/src/git/git.d.ts b/clients/vscode/src/git/git.d.ts new file mode 100644 index 000000000000..96b1a7d94fbe --- /dev/null +++ b/clients/vscode/src/git/git.d.ts @@ -0,0 +1,413 @@ +// https://github.com/microsoft/vscode/blob/main/extensions/git/src/api/git.d.ts + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Uri, Event, Disposable, ProviderResult, Command, CancellationToken } from "vscode"; +export { ProviderResult } from "vscode"; + +export interface Git { + readonly path: string; +} + +export interface InputBox { + value: string; +} + +export const enum ForcePushMode { + Force, + ForceWithLease, + ForceWithLeaseIfIncludes, +} + +export const enum RefType { + Head, + RemoteHead, + Tag, +} + +export interface Ref { + readonly type: RefType; + readonly name?: string; + readonly commit?: string; + readonly remote?: string; +} + +export interface UpstreamRef { + readonly remote: string; + readonly name: string; + readonly commit?: string; +} + +export interface Branch extends Ref { + readonly upstream?: UpstreamRef; + readonly ahead?: number; + readonly behind?: number; +} + +export interface CommitShortStat { + readonly files: number; + readonly insertions: number; + readonly deletions: number; +} + +export interface Commit { + readonly hash: string; + readonly message: string; + readonly parents: string[]; + readonly authorDate?: Date; + readonly authorName?: string; + readonly authorEmail?: string; + readonly commitDate?: Date; + readonly shortStat?: CommitShortStat; +} + +export interface Submodule { + readonly name: string; + readonly path: string; + readonly url: string; +} + +export interface Remote { + readonly name: string; + readonly fetchUrl?: string; + readonly pushUrl?: string; + readonly isReadOnly: boolean; +} + +export const enum Status { + INDEX_MODIFIED, + INDEX_ADDED, + INDEX_DELETED, + INDEX_RENAMED, + INDEX_COPIED, + + MODIFIED, + DELETED, + UNTRACKED, + IGNORED, + INTENT_TO_ADD, + INTENT_TO_RENAME, + TYPE_CHANGED, + + ADDED_BY_US, + ADDED_BY_THEM, + DELETED_BY_US, + DELETED_BY_THEM, + BOTH_ADDED, + BOTH_DELETED, + BOTH_MODIFIED, +} + +export interface Change { + /** + * Returns either `originalUri` or `renameUri`, depending + * on whether this change is a rename change. When + * in doubt always use `uri` over the other two alternatives. + */ + readonly uri: Uri; + readonly originalUri: Uri; + readonly renameUri: Uri | undefined; + readonly status: Status; +} + +export interface RepositoryState { + readonly HEAD: Branch | undefined; + readonly refs: Ref[]; + readonly remotes: Remote[]; + readonly submodules: Submodule[]; + readonly rebaseCommit: Commit | undefined; + + readonly mergeChanges: Change[]; + readonly indexChanges: Change[]; + readonly workingTreeChanges: Change[]; + + readonly onDidChange: Event; +} + +export interface RepositoryUIState { + readonly selected: boolean; + readonly onDidChange: Event; +} + +/** + * Log options. + */ +export interface LogOptions { + /** Max number of log entries to retrieve. If not specified, the default is 32. */ + readonly maxEntries?: number; + readonly path?: string; + /** A commit range, such as "0a47c67f0fb52dd11562af48658bc1dff1d75a38..0bb4bdea78e1db44d728fd6894720071e303304f" */ + readonly range?: string; + readonly reverse?: boolean; + readonly sortByAuthorDate?: boolean; + readonly shortStats?: boolean; + readonly author?: string; +} + +export interface CommitOptions { + all?: boolean | "tracked"; + amend?: boolean; + signoff?: boolean; + signCommit?: boolean; + empty?: boolean; + noVerify?: boolean; + requireUserConfig?: boolean; + useEditor?: boolean; + verbose?: boolean; + /** + * string - execute the specified command after the commit operation + * undefined - execute the command specified in git.postCommitCommand + * after the commit operation + * null - do not execute any command after the commit operation + */ + postCommitCommand?: string | null; +} + +export interface FetchOptions { + remote?: string; + ref?: string; + all?: boolean; + prune?: boolean; + depth?: number; +} + +export interface InitOptions { + defaultBranch?: string; +} + +export interface RefQuery { + readonly contains?: string; + readonly count?: number; + readonly pattern?: string; + readonly sort?: "alphabetically" | "committerdate"; +} + +export interface BranchQuery extends RefQuery { + readonly remote?: boolean; +} + +export interface Repository { + readonly rootUri: Uri; + readonly inputBox: InputBox; + readonly state: RepositoryState; + readonly ui: RepositoryUIState; + + readonly onDidCommit: Event; + + getConfigs(): Promise<{ key: string; value: string }[]>; + getConfig(key: string): Promise; + setConfig(key: string, value: string): Promise; + getGlobalConfig(key: string): Promise; + + getObjectDetails(treeish: string, path: string): Promise<{ mode: string; object: string; size: number }>; + detectObjectType(object: string): Promise<{ mimetype: string; encoding?: string }>; + buffer(ref: string, path: string): Promise; + show(ref: string, path: string): Promise; + getCommit(ref: string): Promise; + + add(paths: string[]): Promise; + revert(paths: string[]): Promise; + clean(paths: string[]): Promise; + + apply(patch: string, reverse?: boolean): Promise; + diff(cached?: boolean): Promise; + diffWithHEAD(): Promise; + diffWithHEAD(path: string): Promise; + diffWith(ref: string): Promise; + diffWith(ref: string, path: string): Promise; + diffIndexWithHEAD(): Promise; + diffIndexWithHEAD(path: string): Promise; + diffIndexWith(ref: string): Promise; + diffIndexWith(ref: string, path: string): Promise; + diffBlobs(object1: string, object2: string): Promise; + diffBetween(ref1: string, ref2: string): Promise; + diffBetween(ref1: string, ref2: string, path: string): Promise; + + getDiff(): Promise; + + hashObject(data: string): Promise; + + createBranch(name: string, checkout: boolean, ref?: string): Promise; + deleteBranch(name: string, force?: boolean): Promise; + getBranch(name: string): Promise; + getBranches(query: BranchQuery, cancellationToken?: CancellationToken): Promise; + getBranchBase(name: string): Promise; + setBranchUpstream(name: string, upstream: string): Promise; + + getRefs(query: RefQuery, cancellationToken?: CancellationToken): Promise; + + getMergeBase(ref1: string, ref2: string): Promise; + + tag(name: string, upstream: string): Promise; + deleteTag(name: string): Promise; + + status(): Promise; + checkout(treeish: string): Promise; + + addRemote(name: string, url: string): Promise; + removeRemote(name: string): Promise; + renameRemote(name: string, newName: string): Promise; + + fetch(options?: FetchOptions): Promise; + fetch(remote?: string, ref?: string, depth?: number): Promise; + pull(unshallow?: boolean): Promise; + push(remoteName?: string, branchName?: string, setUpstream?: boolean, force?: ForcePushMode): Promise; + + blame(path: string): Promise; + log(options?: LogOptions): Promise; + + commit(message: string, opts?: CommitOptions): Promise; + merge(ref: string): Promise; + mergeAbort(): Promise; +} + +export interface RemoteSource { + readonly name: string; + readonly description?: string; + readonly url: string | string[]; +} + +export interface RemoteSourceProvider { + readonly name: string; + readonly icon?: string; // codicon name + readonly supportsQuery?: boolean; + getRemoteSources(query?: string): ProviderResult; + getBranches?(url: string): ProviderResult; + publishRepository?(repository: Repository): Promise; +} + +export interface RemoteSourcePublisher { + readonly name: string; + readonly icon?: string; // codicon name + publishRepository(repository: Repository): Promise; +} + +export interface Credentials { + readonly username: string; + readonly password: string; +} + +export interface CredentialsProvider { + getCredentials(host: Uri): ProviderResult; +} + +export interface PostCommitCommandsProvider { + getCommands(repository: Repository): Command[]; +} + +export interface PushErrorHandler { + handlePushError( + repository: Repository, + remote: Remote, + refspec: string, + error: Error & { gitErrorCode: GitErrorCodes }, + ): Promise; +} + +export interface BranchProtection { + readonly remote: string; + readonly rules: BranchProtectionRule[]; +} + +export interface BranchProtectionRule { + readonly include?: string[]; + readonly exclude?: string[]; +} + +export interface BranchProtectionProvider { + onDidChangeBranchProtection: Event; + provideBranchProtection(): BranchProtection[]; +} + +export type APIState = "uninitialized" | "initialized"; + +export interface PublishEvent { + repository: Repository; + branch?: string; +} + +export interface API { + readonly state: APIState; + readonly onDidChangeState: Event; + readonly onDidPublish: Event; + readonly git: Git; + readonly repositories: Repository[]; + readonly onDidOpenRepository: Event; + readonly onDidCloseRepository: Event; + + toGitUri(uri: Uri, ref: string): Uri; + getRepository(uri: Uri): Repository | null; + init(root: Uri, options?: InitOptions): Promise; + openRepository(root: Uri): Promise; + + registerRemoteSourcePublisher(publisher: RemoteSourcePublisher): Disposable; + registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable; + registerCredentialsProvider(provider: CredentialsProvider): Disposable; + registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable; + registerPushErrorHandler(handler: PushErrorHandler): Disposable; + registerBranchProtectionProvider(root: Uri, provider: BranchProtectionProvider): Disposable; +} + +export interface GitExtension { + readonly enabled: boolean; + readonly onDidChangeEnablement: Event; + + /** + * Returns a specific API version. + * + * Throws error if git extension is disabled. You can listen to the + * [GitExtension.onDidChangeEnablement](#GitExtension.onDidChangeEnablement) event + * to know when the extension becomes enabled/disabled. + * + * @param version Version number. + * @returns API instance + */ + getAPI(version: 1): API; +} + +export const enum GitErrorCodes { + BadConfigFile = "BadConfigFile", + AuthenticationFailed = "AuthenticationFailed", + NoUserNameConfigured = "NoUserNameConfigured", + NoUserEmailConfigured = "NoUserEmailConfigured", + NoRemoteRepositorySpecified = "NoRemoteRepositorySpecified", + NotAGitRepository = "NotAGitRepository", + NotAtRepositoryRoot = "NotAtRepositoryRoot", + Conflict = "Conflict", + StashConflict = "StashConflict", + UnmergedChanges = "UnmergedChanges", + PushRejected = "PushRejected", + ForcePushWithLeaseRejected = "ForcePushWithLeaseRejected", + ForcePushWithLeaseIfIncludesRejected = "ForcePushWithLeaseIfIncludesRejected", + RemoteConnectionError = "RemoteConnectionError", + DirtyWorkTree = "DirtyWorkTree", + CantOpenResource = "CantOpenResource", + GitNotFound = "GitNotFound", + CantCreatePipe = "CantCreatePipe", + PermissionDenied = "PermissionDenied", + CantAccessRemote = "CantAccessRemote", + RepositoryNotFound = "RepositoryNotFound", + RepositoryIsLocked = "RepositoryIsLocked", + BranchNotFullyMerged = "BranchNotFullyMerged", + NoRemoteReference = "NoRemoteReference", + InvalidBranchName = "InvalidBranchName", + BranchAlreadyExists = "BranchAlreadyExists", + NoLocalChanges = "NoLocalChanges", + NoStashFound = "NoStashFound", + LocalChangesOverwritten = "LocalChangesOverwritten", + NoUpstreamBranch = "NoUpstreamBranch", + IsInSubmodule = "IsInSubmodule", + WrongCase = "WrongCase", + CantLockRef = "CantLockRef", + CantRebaseMultipleBranches = "CantRebaseMultipleBranches", + PatchDoesNotApply = "PatchDoesNotApply", + NoPathFound = "NoPathFound", + UnknownPath = "UnknownPath", + EmptyCommitMessage = "EmptyCommitMessage", + BranchFastForwardRejected = "BranchFastForwardRejected", + BranchNotYetBorn = "BranchNotYetBorn", + TagConflict = "TagConflict", +} diff --git a/clients/vscode/src/inline-edit/index.ts b/clients/vscode/src/inline-edit/index.ts new file mode 100644 index 000000000000..503487df2c86 --- /dev/null +++ b/clients/vscode/src/inline-edit/index.ts @@ -0,0 +1,83 @@ +import type { Location } from "vscode-languageclient"; +import { window, TextEditor, Selection, Position, CancellationToken, Range } from "vscode"; +import { Client } from "../lsp/client"; +import { Config } from "../Config"; +import { ContextVariables } from "../ContextVariables"; +import { getLogger } from "../logger"; +import { InlineEditCommand, UserCommandQuickpick } from "./quickPick"; + +export class InlineEditController { + private readonly logger = getLogger("InlineEditController"); + private readonly editLocation: Location; + + constructor( + private client: Client, + private config: Config, + private contextVariables: ContextVariables, + private editor: TextEditor, + private range: Range, + ) { + this.editLocation = { + uri: this.editor.document.uri.toString(), + range: { + start: { line: this.range.start.line, character: 0 }, + end: { + line: this.range.end.character === 0 ? this.range.end.line : this.range.end.line + 1, + character: 0, + }, + }, + }; + } + + async start(userCommand: string | undefined, cancellationToken: CancellationToken) { + const inlineEditCommand: InlineEditCommand | undefined = userCommand + ? { command: userCommand } + : await this.showQuickPick(); + if (inlineEditCommand?.command) { + await this.provideEditWithCommand(inlineEditCommand, cancellationToken); + } + } + + private async showQuickPick(): Promise { + const quickPick = new UserCommandQuickpick(this.client, this.config, this.editLocation); + return await quickPick.start(); + } + + private async provideEditWithCommand(command: InlineEditCommand, cancellationToken: CancellationToken) { + // Lock the cursor (editor selection) at start position, it will be unlocked after the edit is done + const startPosition = new Position(this.range.start.line, 0); + const resetEditorSelection = () => { + this.editor.selection = new Selection(startPosition, startPosition); + }; + const selectionListenerDisposable = window.onDidChangeTextEditorSelection((event) => { + if (event.textEditor === this.editor) { + resetEditorSelection(); + } + }); + resetEditorSelection(); + + this.contextVariables.chatEditInProgress = true; + this.logger.log(`Provide edit with command: ${JSON.stringify(command)}`); + try { + await this.client.chat.provideEdit( + { + location: this.editLocation, + command: command.command, + context: command.context, + format: "previewChanges", + }, + cancellationToken, + ); + } catch (error) { + if (typeof error === "object" && error && "message" in error && typeof error["message"] === "string") { + if (cancellationToken.isCancellationRequested || error["message"].includes("This operation was aborted")) { + // user canceled + } else { + window.showErrorMessage(error["message"]); + } + } + } + selectionListenerDisposable.dispose(); + this.contextVariables.chatEditInProgress = false; + } +} diff --git a/clients/vscode/src/inline-edit/quickPick.ts b/clients/vscode/src/inline-edit/quickPick.ts new file mode 100644 index 000000000000..7148cd6342b2 --- /dev/null +++ b/clients/vscode/src/inline-edit/quickPick.ts @@ -0,0 +1,564 @@ +import { + CancellationTokenSource, + QuickInputButton, + QuickInputButtons, + QuickPickItem, + QuickPickItemButtonEvent, + QuickPickItemKind, + Range, + ThemeIcon, + window, + workspace, +} from "vscode"; +import path from "path"; +import { ChatEditCommand, ChatEditFileContext } from "tabby-agent"; +import { listSymbols } from "../findSymbols"; +import { Config } from "../Config"; +import { Deferred } from "../deferred"; +import { Client } from "../lsp/client"; +import { Location } from "vscode-languageclient"; +import { listFiles } from "../findFiles"; +import { wrapCancelableFunction } from "../cancelableFunction"; +import { InlineEditParseResult, parseUserCommand, replaceLastOccurrence } from "./util"; + +export interface InlineEditCommand { + command: string; + context?: ChatEditFileContext[]; +} + +interface CommandQuickPickItem extends QuickPickItem { + value: string; +} + +/** + * Helper method to get file items with consistent formatting + * This is used by both context picker and file selection picker + */ +const wrappedListFiles = wrapCancelableFunction( + listFiles, + (args) => args[2], + (args, token) => { + args[2] = token; + return args; + }, +); + +const getFileItems = async (query: string, maxResults: number): Promise => { + const fileList = await wrappedListFiles(query, maxResults); + const fileItems: FileSelectionQuickPickItem[] = fileList.map((fileItem) => { + const relativePath = workspace.asRelativePath(fileItem.uri); + const basename = path.basename(fileItem.uri.fsPath); + const dirname = path.dirname(relativePath); + return { + label: `$(file) ${basename}`, + description: dirname === "." ? "" : dirname, + alwaysShow: true, + referer: relativePath, + uri: fileItem.uri.toString(), + isOpenedInEditor: fileItem.isOpenedInEditor, + }; + }); + const activeFilesIndex = fileItems.findIndex((item) => item.isOpenedInEditor); + if (activeFilesIndex != -1) { + fileItems.splice(activeFilesIndex, 0, { + label: `active files`, + kind: QuickPickItemKind.Separator, + referer: "", + uri: "", + isOpenedInEditor: true, + }); + } + const searchResultsIndex = fileItems.findIndex((item) => !item.isOpenedInEditor); + if (searchResultsIndex != -1) { + fileItems.splice(searchResultsIndex, 0, { + label: `search results`, + kind: QuickPickItemKind.Separator, + referer: "", + uri: "", + isOpenedInEditor: false, + }); + } + return fileItems; +}; + +interface ContextQuickPickItem extends QuickPickItem { + type: undefined | "file" | "symbol"; +} + +export class UserCommandQuickpick { + quickPick = window.createQuickPick(); + private suggestedCommand: ChatEditCommand[] = []; + private resultDeferred = new Deferred(); + private fetchingSuggestedCommandCancellationTokenSource = new CancellationTokenSource(); + private lastInputValue = ""; + private filePick: FileSelectionQuickPick | undefined; + private symbolPick: SymbolSelectionQuickPick | undefined; + private showingContextPicker = false; + private referenceMap = new Map>(); + + constructor( + private client: Client, + private config: Config, + private editLocation: Location, + ) {} + + start() { + this.quickPick.title = "Enter the command for editing (type @ to include file or symbol)"; + this.quickPick.matchOnDescription = true; + this.quickPick.onDidChangeValue(() => this.handleValueChange()); + this.quickPick.onDidAccept(() => this.handleAccept()); + this.quickPick.onDidHide(() => this.handleHidden()); + this.quickPick.onDidTriggerItemButton((e) => this.handleTriggerItemButton(e)); + + this.quickPick.show(); + this.quickPick.ignoreFocusOut = true; + this.provideEditCommands(); + return this.resultDeferred.promise; + } + + private get inputParseResult(): InlineEditParseResult { + return parseUserCommand(this.quickPick.value); + } + + private handleValueChange() { + const { mentionQuery } = this.inputParseResult; + if (mentionQuery === "") { + this.showingContextPicker = true; + this.quickPick.hide(); + this.showContextPicker(); + } else { + this.updateQuickPickList(); + this.updateQuickPickValue(this.quickPick.value); + } + } + + private async showContextPicker() { + const contextPicker = window.createQuickPick(); + contextPicker.title = "Select context or file"; + contextPicker.buttons = [QuickInputButtons.Back]; + contextPicker.ignoreFocusOut = true; + // Quick pick items are always sorted by label. issue: https://github.com/microsoft/vscode/issues/73904 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (contextPicker as any).sortByLabel = false; + + const contextTypeItems: ContextQuickPickItem[] = [ + { label: "context", kind: QuickPickItemKind.Separator, type: undefined }, + { label: "$(folder) Files $(chevron-right)", type: "file" }, + { label: "$(symbol-class) Symbols $(chevron-right)", type: "symbol" }, + ]; + + contextPicker.busy = true; + const fileItemsMaxResult = 20; + const fileItems = await getFileItems("", fileItemsMaxResult); + contextPicker.items = [...contextTypeItems, ...fileItems]; + contextPicker.busy = false; + contextPicker.onDidChangeValue(async (value) => { + if (value) { + contextPicker.busy = true; + const filteredFileItems = await getFileItems(value, fileItemsMaxResult); + contextPicker.items = [...contextTypeItems, ...filteredFileItems]; + contextPicker.busy = false; + } else { + contextPicker.items = [...contextTypeItems, ...fileItems]; + } + }); + + const deferred = new Deferred(); + + contextPicker.onDidAccept(() => { + deferred.resolve(contextPicker.selectedItems[0]); + contextPicker.hide(); + }); + + contextPicker.onDidHide(() => { + deferred.resolve(undefined); + contextPicker.dispose(); + }); + + contextPicker.onDidTriggerButton((e: QuickInputButton) => { + if (e === QuickInputButtons.Back) { + contextPicker.hide(); + } + }); + + contextPicker.show(); + + const result = await deferred.promise; + + if (result && "type" in result) { + if (result.type === "file") { + await this.openFilePick(); + } else if (result.type === "symbol") { + await this.openSymbolPick(); + } + } else if (result && "uri" in result) { + this.quickPick.show(); + this.updateQuickPickValue(this.quickPick.value + `${result.referer} `); + this.referenceMap.set(result.referer, { uri: result.uri }); + } else { + this.quickPick.show(); + if (this.quickPick.value.endsWith("@")) { + this.updateQuickPickValue(replaceLastOccurrence(this.quickPick.value, "@", "")); + } + } + } + + private async openFilePick() { + this.filePick = new FileSelectionQuickPick(); + const file = await this.filePick.start(); + this.quickPick.show(); + if (file) { + this.updateQuickPickValue(this.quickPick.value + `${file.referer} `); + this.referenceMap.set(file.referer, { uri: file.uri }); + } else { + this.updateQuickPickValue(replaceLastOccurrence(this.quickPick.value, "@", "")); + } + this.filePick = undefined; + } + + private async openSymbolPick() { + this.symbolPick = new SymbolSelectionQuickPick(); + const symbol = await this.symbolPick.start(); + this.quickPick.show(); + if (symbol) { + this.updateQuickPickValue(this.quickPick.value + `${symbol.referer} `); + this.referenceMap.set(symbol.referer, { uri: symbol.uri, range: symbol.range }); + } else { + this.updateQuickPickValue(replaceLastOccurrence(this.quickPick.value, "@", "")); + } + this.symbolPick = undefined; + } + + private updateQuickPickValue(value: string) { + const lastQuickPickValue = this.lastInputValue; + const lastMentionQuery = parseUserCommand(lastQuickPickValue).mentionQuery; + const currentMentionQuery = parseUserCommand(value).mentionQuery; + // remove whole `@file` part when user start delete on the last `@file` + if ( + lastMentionQuery !== undefined && + currentMentionQuery !== undefined && + currentMentionQuery.length < lastMentionQuery.length + ) { + this.quickPick.value = replaceLastOccurrence(value, `@${currentMentionQuery}`, ""); + } else { + this.quickPick.value = value; + } + this.lastInputValue = this.quickPick.value; + } + + private async updateQuickPickList() { + const command = this.quickPick.value; + const list = this.getCommandList(command); + this.quickPick.items = list; + } + + private getCommandList(input: string) { + const list: (QuickPickItem & { value: string })[] = []; + list.push({ + label: "commands", + value: "", + kind: QuickPickItemKind.Separator, + }); + list.push( + ...this.suggestedCommand.map((item) => ({ + label: item.label, + value: item.command, + iconPath: new ThemeIcon("sparkle"), + description: item.source === "preset" ? item.command : "", + })), + ); + list.push({ + label: "history", + value: "", + kind: QuickPickItemKind.Separator, + }); + const recentlyCommandToAdd = this.getCommandHistory().filter((item) => !list.find((i) => i.value === item.command)); + recentlyCommandToAdd.forEach((command) => { + if (command.context) { + command.context.forEach((context) => { + if (!this.referenceMap.has(context.referrer)) { + // this context maybe outdated + this.referenceMap.set(context.referrer, { + uri: context.uri, + range: context.range, + }); + } + }); + } + }); + list.push( + ...recentlyCommandToAdd.map((item) => ({ + label: item.command, + value: item.command, + iconPath: new ThemeIcon("history"), + buttons: [ + { + iconPath: new ThemeIcon("edit"), + }, + { + iconPath: new ThemeIcon("settings-remove"), + }, + ], + })), + ); + if (input.length > 0 && !list.find((i) => i.value === input)) { + list.unshift({ + label: input, + value: input, + iconPath: new ThemeIcon("run"), + description: "", + alwaysShow: true, + }); + } + + return list; + } + + private handleAccept() { + const command = this.quickPick.selectedItems[0]?.value || this.quickPick.value; + this.acceptCommand(command); + } + + private getCommandHistory(): InlineEditCommand[] { + const recentlyCommand = this.config.chatEditRecentlyCommand.slice(0, this.config.maxChatEditHistory); + return recentlyCommand.map((commandStr) => { + try { + const command = JSON.parse(commandStr); + if (typeof command === "object" && command.command) { + return { + command: command.command, + context: command.context, + }; + } + return { + command: commandStr, + }; + } catch (error) { + return { + command: commandStr, + }; + } + }); + } + + private async addCommandHistory(userCommand: InlineEditCommand) { + const commandStr = JSON.stringify(userCommand); + const recentlyCommand = this.config.chatEditRecentlyCommand; + const updatedRecentlyCommand = [commandStr] + .concat(recentlyCommand.filter((item) => item !== commandStr)) + .slice(0, this.config.maxChatEditHistory); + await this.config.updateChatEditRecentlyCommand(updatedRecentlyCommand); + } + + private async deleteCommandHistory(command: string) { + const recentlyCommand = this.getCommandHistory(); + const index = recentlyCommand.findIndex((item) => item.command === command); + if (index !== -1) { + recentlyCommand.splice(index, 1); + await this.config.updateChatEditRecentlyCommand(recentlyCommand.map((command) => JSON.stringify(command))); + this.updateQuickPickList(); + } + } + + private async acceptCommand(command: string | undefined) { + if (!command) { + this.resultDeferred.resolve(undefined); + return; + } + if (command && command.length > 200) { + window.showErrorMessage("Command is too long."); + this.resultDeferred.resolve(undefined); + return; + } + + const parseResult = parseUserCommand(command); + const mentionTexts = parseResult.mentions?.map((mention) => mention.text) || []; + const uniqueMentionTexts = Array.from(new Set(mentionTexts)); + + const userCommand = { + command, + context: uniqueMentionTexts + .map((item) => { + if (this.referenceMap.has(item)) { + const contextInfo = this.referenceMap.get(item); + if (contextInfo) { + return { + uri: contextInfo.uri, + referrer: item, + range: contextInfo.range, + }; + } + } + return; + }) + .filter((item): item is ChatEditFileContext => item !== undefined), + }; + + await this.addCommandHistory(userCommand); + + this.resultDeferred.resolve(userCommand); + this.quickPick.hide(); + } + + private handleHidden() { + this.fetchingSuggestedCommandCancellationTokenSource.cancel(); + const inFileOrSymbolSelection = this.filePick !== undefined || this.symbolPick !== undefined; + if (!inFileOrSymbolSelection && !this.showingContextPicker) { + this.resultDeferred.resolve(undefined); + } + this.showingContextPicker = false; + } + + private provideEditCommands() { + this.client.chat.provideEditCommands( + { location: this.editLocation }, + { commands: this.suggestedCommand, callback: () => this.updateQuickPickList() }, + this.fetchingSuggestedCommandCancellationTokenSource.token, + ); + } + + private async handleTriggerItemButton(event: QuickPickItemButtonEvent) { + const item = event.item; + const button = event.button; + if (button.iconPath instanceof ThemeIcon && button.iconPath.id === "settings-remove") { + this.deleteCommandHistory(item.value); + } + + if (button.iconPath instanceof ThemeIcon && button.iconPath.id === "edit") { + this.updateQuickPickValue(item.value); + } + } +} + +interface FileSelectionQuickPickItem extends QuickPickItem { + uri: string; + referer: string; + isOpenedInEditor: boolean; +} + +export class FileSelectionQuickPick { + quickPick = window.createQuickPick(); + private maxSearchFileResult = 50; + private resultDeferred = new Deferred(); + + start() { + this.quickPick.title = "Enter file name to search"; + this.quickPick.buttons = [QuickInputButtons.Back]; + this.quickPick.ignoreFocusOut = true; + // Quick pick items are always sorted by label. issue: https://github.com/microsoft/vscode/issues/73904 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (this.quickPick as any).sortByLabel = false; + this.quickPick.onDidChangeValue((e) => this.updateFileList(e)); + this.quickPick.onDidAccept(() => this.handleAccept()); + this.quickPick.onDidHide(() => this.handleHidden()); + this.quickPick.onDidTriggerButton((e) => this.handleTriggerButton(e)); + this.quickPick.show(); + this.updateFileList(""); + return this.resultDeferred.promise; + } + + private async updateFileList(val: string) { + this.quickPick.busy = true; + this.quickPick.items = await getFileItems(val, this.maxSearchFileResult); + this.quickPick.busy = false; + } + + private handleAccept() { + this.resultDeferred.resolve(this.quickPick.selectedItems[0]); + } + + private handleHidden() { + this.resultDeferred.resolve(undefined); + } + + private handleTriggerButton(e: QuickInputButton) { + if (e === QuickInputButtons.Back) { + this.quickPick.hide(); + } + } +} + +interface SymbolSelectionQuickPickItem extends QuickPickItem { + uri: string; + referer: string; + range?: Range; +} + +export class SymbolSelectionQuickPick { + quickPick = window.createQuickPick(); + private resultDeferred = new Deferred(); + + start() { + this.quickPick.title = "Enter symbol name to search"; + this.quickPick.buttons = [QuickInputButtons.Back]; + this.quickPick.ignoreFocusOut = true; + this.quickPick.onDidChangeValue((e) => this.updateSymbolList(e)); + this.quickPick.onDidAccept(() => this.handleAccept()); + this.quickPick.onDidHide(() => this.handleHidden()); + this.quickPick.onDidTriggerButton((e) => this.handleTriggerButton(e)); + this.quickPick.show(); + this.updateSymbolList(""); + return this.resultDeferred.promise; + } + + private async updateSymbolList(query: string) { + this.quickPick.busy = true; + const symbolList = await this.fetchSymbolList(query); + this.quickPick.items = symbolList; + this.quickPick.busy = false; + } + + private handleAccept() { + this.resultDeferred.resolve(this.quickPick.selectedItems[0]); + } + + private handleHidden() { + this.resultDeferred.resolve(undefined); + } + + private handleTriggerButton(e: QuickInputButton) { + if (e === QuickInputButtons.Back) { + this.quickPick.hide(); + } + } + + private listSymbols = wrapCancelableFunction( + listSymbols, + () => undefined, + (args) => args, + ); + + private async fetchSymbolList(query: string): Promise { + if (!window.activeTextEditor) { + return []; + } + try { + const symbols = await this.listSymbols(window.activeTextEditor.document.uri, query, 50); + return symbols.map( + (symbol) => + ({ + label: symbol.name, + description: symbol.containerName, + iconPath: symbol.kindIcon, + uri: symbol.location.uri.toString(), + referer: symbol.name.replace(/\s/g, "_").replace(/@/g, ""), + // FIXME(icycode): extract type conversion utils + range: symbol.location.range + ? { + start: { + line: symbol.location.range.start.line, + character: symbol.location.range.start.character, + }, + end: { + line: symbol.location.range.end.line, + character: symbol.location.range.end.character, + }, + } + : undefined, + }) as SymbolSelectionQuickPickItem, + ); + } catch (error) { + return []; + } + } +} diff --git a/clients/vscode/src/inline-edit/util.test.ts b/clients/vscode/src/inline-edit/util.test.ts new file mode 100644 index 000000000000..3bf2bf2721ec --- /dev/null +++ b/clients/vscode/src/inline-edit/util.test.ts @@ -0,0 +1,154 @@ +import { describe } from "mocha"; +import { expect } from "chai"; +import { parseUserCommand, InlineEditParseResult, MentionType } from "./util"; + +describe("parseInput", () => { + it("should parse input correctly", () => { + const input = `this is a user command`; + + const parseResult: InlineEditParseResult = { + mentions: [], + mentionQuery: undefined, + }; + expect(parseUserCommand(input)).to.deep.equal(parseResult); + }); + + it("should parse input correctly", () => { + const input = `@file1`; + + const parseResult: InlineEditParseResult = { + mentions: [{ text: "file1", type: MentionType.File }], + mentionQuery: "file1", + }; + expect(parseUserCommand(input)).to.deep.equal(parseResult); + }); + + it("should parse input correctly", () => { + const input = `@`; + + const parseResult: InlineEditParseResult = { + mentions: [], + mentionQuery: "", + }; + expect(parseUserCommand(input)).to.deep.equal(parseResult); + }); + + it("should parse input correctly", () => { + const input = `this is a user command @`; + + const parseResult: InlineEditParseResult = { + mentions: [], + mentionQuery: "", + }; + expect(parseUserCommand(input)).to.deep.equal(parseResult); + }); + + it("should parse input correctly", () => { + const input = `this is a user command@`; + + const parseResult: InlineEditParseResult = { + mentions: [], + mentionQuery: undefined, + }; + expect(parseUserCommand(input)).to.deep.equal(parseResult); + }); + + it("should parse input correctly", () => { + const input = ` @file1`; + + const parseResult: InlineEditParseResult = { + mentions: [{ text: "file1", type: MentionType.File }], + mentionQuery: "file1", + }; + expect(parseUserCommand(input)).to.deep.equal(parseResult); + }); + + it("should parse input correctly", () => { + const input = `@file1 `; + + const parseResult: InlineEditParseResult = { + mentions: [{ text: "file1", type: MentionType.File }], + mentionQuery: undefined, + }; + expect(parseUserCommand(input)).to.deep.equal(parseResult); + }); + + it("should parse input correctly", () => { + const input = `@file1 @file2`; + const parseResult: InlineEditParseResult = { + mentions: [ + { text: "file1", type: MentionType.File }, + { text: "file2", type: MentionType.File }, + ], + mentionQuery: "file2", + }; + expect(parseUserCommand(input)).to.deep.equal(parseResult); + }); + + it("should parse input correctly", () => { + const input = `@file1 @file2 `; + const parseResult: InlineEditParseResult = { + mentions: [ + { text: "file1", type: MentionType.File }, + { text: "file2", type: MentionType.File }, + ], + mentionQuery: undefined, + }; + expect(parseUserCommand(input)).to.deep.equal(parseResult); + }); + + it("should parse input correctly", () => { + const input = ` @file1 @file2 this is a user command`; + const parseResult: InlineEditParseResult = { + mentions: [ + { text: "file1", type: MentionType.File }, + { text: "file2", type: MentionType.File }, + ], + mentionQuery: undefined, + }; + expect(parseUserCommand(input)).to.deep.equal(parseResult); + }); + + it("should parse input correctly", () => { + const input = ` @file1 @file2 this is a user command@file3`; + const parseResult: InlineEditParseResult = { + mentions: [ + { text: "file1", type: MentionType.File }, + { text: "file2", type: MentionType.File }, + ], + mentionQuery: undefined, + }; + expect(parseUserCommand(input)).to.deep.equal(parseResult); + }); + + it("should parse input correctly", () => { + const input = ` @file1 @file2 this is a user command @file3`; + const parseResult: InlineEditParseResult = { + mentions: [ + { text: "file1", type: MentionType.File }, + { text: "file2", type: MentionType.File }, + { text: "file3", type: MentionType.File }, + ], + mentionQuery: "file3", + }; + expect(parseUserCommand(input)).to.deep.equal(parseResult); + }); + + it("should parse input correctly", () => { + const input = ` this is a user command @file3 `; + const parseResult: InlineEditParseResult = { + mentions: [{ text: "file3", type: MentionType.File }], + mentionQuery: undefined, + }; + expect(parseUserCommand(input)).to.deep.equal(parseResult); + }); + + it("should parse input correctly", () => { + const input = ` this is a @file3 user command `; + const parseResult: InlineEditParseResult = { + mentions: [{ text: "file3", type: MentionType.File }], + mentionQuery: undefined, + }; + expect(parseUserCommand(input)).to.deep.equal(parseResult); + }); +}); diff --git a/clients/vscode/src/inline-edit/util.ts b/clients/vscode/src/inline-edit/util.ts new file mode 100644 index 000000000000..bbcb674b0eaf --- /dev/null +++ b/clients/vscode/src/inline-edit/util.ts @@ -0,0 +1,75 @@ +export enum MentionType { + File = "file", + Symbol = "symbol", +} + +export interface Mention { + /** + * The text of the mention (without the @ prefix) + */ + text: string; + /** + * The type of the mention + */ + type: MentionType; +} + +export interface InlineEditParseResult { + /** + * mentions, start with '@' + */ + mentions?: Mention[]; + /** + * last mention in the end of user commnad. + * for `explain @`, mentionQuery is `''`, we can trigger pick + * for `explain @file`, mentionQuery is `file`, we know user is editing the mention + * for `explain @file to me`, mentionQuery is `undefined` + */ + mentionQuery?: string; +} + +export const parseUserCommand = (input: string): InlineEditParseResult => { + const mentions: Mention[] = []; + // Match @text (both file and symbol mentions use the same @ prefix) + const regex = /(?<=\s|^)@(\S*)/g; + let match; + const matches = []; + + while ((match = regex.exec(input)) !== null) { + const text = match[1]; + if (text) { + mentions.push({ + text, + // Default to File type, will be updated when the mention is resolved + type: MentionType.File, + }); + } + matches.push(match); + } + + let mentionQuery = undefined; + if (matches.length > 0) { + const lastMatch = matches[matches.length - 1]; + if (lastMatch) { + const endPos = lastMatch.index + lastMatch[0].length; + if (endPos === input.length) { + mentionQuery = lastMatch[1] || ""; + } + } + } + + return { + mentions, + mentionQuery: mentionQuery !== undefined ? mentionQuery : undefined, + }; +}; + +export const replaceLastOccurrence = (str: string, substrToReplace: string, replacementStr: string): string => { + const lastIndex = str.lastIndexOf(substrToReplace); + + if (lastIndex === -1) { + return str; + } + + return str.substring(0, lastIndex) + replacementStr + str.substring(lastIndex + substrToReplace.length); +}; diff --git a/clients/vscode/src/keybindings.ts b/clients/vscode/src/keybindings.ts new file mode 100644 index 000000000000..98fc29ac346d --- /dev/null +++ b/clients/vscode/src/keybindings.ts @@ -0,0 +1,188 @@ +import { readFile } from "fs-extra"; +import path from "path"; +import { getLogger } from "./logger"; +import * as vscode from "vscode"; +import { isBrowser } from "./env"; +import pkg from "../package.json"; +interface KeyBinding { + key: string; + command: string; + when?: string; +} + +const logger = getLogger("VSCodeKeyBindingManager"); +const isMac = isBrowser + ? navigator.userAgent.toLowerCase().includes("mac") + : process.platform.toLowerCase().includes("darwin"); + +export class KeyBindingManager { + // Singleton instance to manage keybindings. + private static instance: KeyBindingManager | null = null; + + /** + * Returns the singleton instance. + */ + public static getInstance(): KeyBindingManager { + if (!KeyBindingManager.instance) { + KeyBindingManager.instance = new KeyBindingManager(); + } + return KeyBindingManager.instance; + } + + // Cached keybindings loaded during extension startup. + private keybindings: KeyBinding[] | null = null; + + /** + * Initializes the keybinding manager. + * This method should be called once during extension startup. + * It reads the keybindings.json file once to avoid additional overhead. + */ + public async init(): Promise { + this.keybindings = await this.readKeyBindings(); + } + + /** + * Reads the keybindings file and returns the parsed keybindings. + */ + private async readKeyBindings(): Promise { + try { + let rawData: string; + if (isBrowser) { + rawData = await this.readWorkspaceKeybindings(); + } else { + const isMac = process.platform === "darwin"; + const keybindingsPath = isMac + ? path.join(process.env["HOME"] ?? "~", "Library", "Application Support", "Code", "User", "keybindings.json") + : path.join(process.env["APPDATA"] || process.env["HOME"] + "/.config", "Code", "User", "keybindings.json"); + rawData = await readFile(keybindingsPath, "utf8"); + } + return this.parseKeybindings(rawData); + } catch (error) { + logger.error("Error reading keybindings:", error); + return null; + } + } + + /** + * Reads keybindings.json from the workspace folder (for browser environments). + */ + private async readWorkspaceKeybindings(): Promise { + const workspace = vscode.workspace.workspaceFolders?.[0]; + if (!workspace) { + throw new Error("No workspace found"); + } + const keybindingsUri = vscode.Uri.joinPath(workspace.uri, ".vscode", "keybindings.json"); + const data = await vscode.workspace.fs.readFile(keybindingsUri); + return Buffer.from(data).toString("utf8"); + } + + /** + * Parses the raw keybindings JSON data and filters out invalid entries. + */ + private parseKeybindings(data: string): KeyBinding[] { + const cleanData = data + .split("\n") + .map((line) => line.trim()) + .filter((line) => !line.startsWith("//")) + .join("\n"); + + try { + const parsed = JSON.parse(cleanData) as KeyBinding[]; + return parsed.filter((binding) => { + const isValid = binding.key && binding.command; + if (!isValid) { + logger.warn("Invalid keybinding found:", binding); + } + return isValid; + }); + } catch (error) { + logger.error("Error parsing keybindings JSON:", error); + return []; + } + } + + /** + * Retrieves the keybinding for a specified command from the cached keybindings. + * Returns the key if found and not disabled; otherwise returns null. + */ + getKeybinding(command: string): string | null { + if (!this.keybindings) { + return null; + } + + const disabledBinding = this.keybindings.find((b) => b.command === `-${command}`); + if (disabledBinding) { + return null; + } + + const customBinding = this.keybindings.find((b) => b.command === command); + if (customBinding && customBinding.key) { + return customBinding.key; + } + + return getPackageCommandBinding(command); + } +} + +export function getPackageCommandBinding(command: string): string { + try { + if (!pkg.contributes.keybindings || !Array.isArray(pkg.contributes.keybindings)) { + logger.warn("No keybindings found in package.json"); + return ""; + } + const binding = pkg.contributes.keybindings.find((b) => b.command === command); + if (!binding) { + return ""; + } + return isMac && binding.mac ? binding.mac : binding.key; + } catch (error) { + logger.error("Error reading package.json keybindings:", error); + return ""; + } +} + +export const formatShortcut = (shortcut: string): string => { + const isMacOS = !isBrowser && process.platform === "darwin"; + + const config = isMacOS + ? { + ctrlKey: "⌃", + shiftKey: "⇧", + altKey: "⌥", + metaKey: "⌘", + separator: "+", + } + : { + ctrlKey: "Ctrl", + shiftKey: "Shift", + altKey: "Alt", + metaKey: isBrowser ? "Windows" : process.platform === "win32" ? "Windows" : "Super", + separator: "+", + }; + + return shortcut + .split("+") + .map((key) => { + const lowerKey = key.toLowerCase(); + switch (lowerKey) { + case "ctrl": + case "control": + return config.ctrlKey; + case "shift": + return config.shiftKey; + case "alt": + case "option": + return config.altKey; + case "cmd": + case "command": + case "meta": + return config.metaKey; + default: + if (lowerKey.length === 1) { + return lowerKey.toUpperCase(); + } + return key.charAt(0).toUpperCase() + key.slice(1).toLowerCase(); + } + }) + .join(config.separator); +}; diff --git a/clients/vscode/src/logger.ts b/clients/vscode/src/logger.ts new file mode 100644 index 000000000000..a4356dde0579 --- /dev/null +++ b/clients/vscode/src/logger.ts @@ -0,0 +1,63 @@ +import { window, LogOutputChannel as VSCodeLogOutputChannel } from "vscode"; + +const outputChannel = window.createOutputChannel("Tabby", { log: true }); + +export type LogLevel = "trace" | "debug" | "info" | "warn" | "error"; + +export interface LogEveryNOptions { + identifier?: string; + every?: number; + level?: LogLevel; +} + +export interface LogOutputChannel extends VSCodeLogOutputChannel { + log(message: string, ...args: unknown[]): void; + log(options: LogEveryNOptions, message: string, ...args: unknown[]): void; +} + +function tagMessage(message: string, tag: string): string { + return `[${tag}] ${message}`; +} + +export function getLogger(tag = "Tabby"): LogOutputChannel { + const logEveryNCounts = new Map(); + return new Proxy(outputChannel, { + get(target, method) { + if (method === "log") { + return (...args: unknown[]) => { + let options: LogEveryNOptions = {}; + let message: string; + if (typeof args[0] === "string") { + message = args.shift() as string; + } else if (typeof args[0] === "object") { + options = args.shift() as LogEveryNOptions; + message = args.shift() as string; + } else { + return; + } + const { identifier = message, every = 1, level = "info" } = options; + const count = logEveryNCounts.get(identifier) ?? 0; + logEveryNCounts.set(identifier, count + 1); + if (count % every === 0) { + target[level](tagMessage(message, tag), ...args); + } + }; + } + if (typeof method == "string" && ["trace", "debug", "info", "warn", "error"].includes(method)) { + return (message: string, ...args: unknown[]) => { + /* @ts-expect-error no-implicit-any */ + target[method]?.(tagMessage(message, tag), ...args); + }; + } + if (method in target) { + /* @ts-expect-error no-implicit-any */ + return target[method]; + } + return undefined; + }, + }) as LogOutputChannel; +} + +export function showOutputPanel(): void { + outputChannel.show(); +} diff --git a/clients/vscode/src/lsp/AgentConfigFeature.ts b/clients/vscode/src/lsp/AgentConfigFeature.ts new file mode 100644 index 000000000000..7a1a14956f1f --- /dev/null +++ b/clients/vscode/src/lsp/AgentConfigFeature.ts @@ -0,0 +1,55 @@ +import { EventEmitter } from "events"; +import { BaseLanguageClient, StaticFeature, FeatureState, Disposable } from "vscode-languageclient"; +import { ClientCapabilities, Config as AgentConfig, ConfigRequest, ConfigDidChangeNotification } from "tabby-agent"; + +export class AgentConfigFeature extends EventEmitter implements StaticFeature { + private disposables: Disposable[] = []; + + private config: AgentConfig | undefined = undefined; + + constructor(private readonly client: BaseLanguageClient) { + super(); + } + + getState(): FeatureState { + return { kind: "static" }; + } + + fillInitializeParams() { + // nothing + } + + fillClientCapabilities(capabilities: ClientCapabilities): void { + const tabbyCapabilities = capabilities.tabby || {}; + tabbyCapabilities.configDidChangeListener = true; + capabilities.tabby = tabbyCapabilities; + } + + preInitialize(): void { + // nothing + } + + initialize(): void { + this.disposables.push( + this.client.onNotification(ConfigDidChangeNotification.type, (params: AgentConfig) => { + this.config = params; + this.emit("didChange", params); + }), + ); + } + + clear(): void { + this.disposables.forEach((disposable) => disposable.dispose()); + this.disposables = []; + } + + get current(): AgentConfig | undefined { + return this.config; + } + + async fetchAgentConfig(): Promise { + const agentConfig: AgentConfig = await this.client.sendRequest(ConfigRequest.method, {}); + this.config = agentConfig; + return agentConfig; + } +} diff --git a/clients/vscode/src/lsp/AgentStatusFeature.ts b/clients/vscode/src/lsp/AgentStatusFeature.ts new file mode 100644 index 000000000000..e485f660e901 --- /dev/null +++ b/clients/vscode/src/lsp/AgentStatusFeature.ts @@ -0,0 +1,72 @@ +import { EventEmitter } from "events"; +import { BaseLanguageClient, StaticFeature, FeatureState, Disposable } from "vscode-languageclient"; +import { + ClientCapabilities, + StatusInfo, + StatusRequest, + StatusRequestParams, + StatusDidChangeNotification, + StatusShowHelpMessageRequest, + StatusIgnoredIssuesEditRequest, + StatusIgnoredIssuesEditParams, +} from "tabby-agent"; + +export class AgentStatusFeature extends EventEmitter implements StaticFeature { + private disposables: Disposable[] = []; + + private statusInfo: StatusInfo | undefined = undefined; + + constructor(private readonly client: BaseLanguageClient) { + super(); + } + + getState(): FeatureState { + return { kind: "static" }; + } + + fillInitializeParams() { + // nothing + } + + fillClientCapabilities(capabilities: ClientCapabilities): void { + const tabbyCapabilities = capabilities.tabby || {}; + tabbyCapabilities.statusDidChangeListener = true; + capabilities.tabby = tabbyCapabilities; + } + + preInitialize(): void { + // nothing + } + + initialize(): void { + this.disposables.push( + this.client.onNotification(StatusDidChangeNotification.type, (params: StatusInfo) => { + this.statusInfo = params; + this.emit("didChange", params); + }), + ); + } + + clear(): void { + this.disposables.forEach((disposable) => disposable.dispose()); + this.disposables = []; + } + + get current(): StatusInfo | undefined { + return this.statusInfo; + } + + async fetchAgentStatusInfo(params?: StatusRequestParams | undefined): Promise { + const statusInfo: StatusInfo = await this.client.sendRequest(StatusRequest.method, params || {}); + this.statusInfo = statusInfo; + return statusInfo; + } + + async showHelpMessage(): Promise { + await this.client.sendRequest(StatusShowHelpMessageRequest.method); + } + + async editIgnoredIssues(params: StatusIgnoredIssuesEditParams): Promise { + await this.client.sendRequest(StatusIgnoredIssuesEditRequest.method, params); + } +} diff --git a/clients/vscode/src/lsp/ChatFeature.ts b/clients/vscode/src/lsp/ChatFeature.ts new file mode 100644 index 000000000000..c42ca9117ef3 --- /dev/null +++ b/clients/vscode/src/lsp/ChatFeature.ts @@ -0,0 +1,134 @@ +import { EventEmitter } from "events"; +import { Disposable, CancellationToken } from "vscode"; +import { BaseLanguageClient, DynamicFeature, FeatureState, RegistrationData } from "vscode-languageclient"; +import { + ChatFeatures, + GenerateCommitMessageRequest, + GenerateCommitMessageParams, + GenerateCommitMessageResult, + GenerateBranchNameRequest, + GenerateBranchNameParams, + GenerateBranchNameResult, + ChatEditCommandRequest, + ChatEditCommandParams, + ChatEditCommand, + ChatEditRequest, + ChatEditParams, + ChatEditToken, + ChatEditResolveRequest, + ChatEditResolveParams, + SmartApplyParams, + SmartApplyRequest, +} from "tabby-agent"; + +export class ChatFeature extends EventEmitter implements DynamicFeature { + private registration: string | undefined = undefined; + private disposables: Disposable[] = []; + + constructor(private readonly client: BaseLanguageClient) { + super(); + } + + readonly registrationType = ChatFeatures.type; + + getState(): FeatureState { + return { kind: "workspace", id: this.registrationType.method, registrations: this.isAvailable }; + } + + fillInitializeParams() { + // nothing + } + + fillClientCapabilities(): void { + // nothing + } + + preInitialize(): void { + // nothing + } + + initialize(): void { + // nothing + } + + register(data: RegistrationData): void { + this.registration = data.id; + this.emit("didChangeAvailability", true); + } + + unregister(id: string): void { + if (this.registration === id) { + this.registration = undefined; + this.emit("didChangeAvailability", false); + } + } + + clear(): void { + this.disposables.forEach((disposable) => disposable.dispose()); + this.disposables = []; + } + + get isAvailable(): boolean { + return !!this.registration; + } + + async generateCommitMessage( + params: GenerateCommitMessageParams, + token?: CancellationToken, + ): Promise { + if (!this.isAvailable) { + return null; + } + return this.client.sendRequest(GenerateCommitMessageRequest.method, params, token); + } + + async generateBranchName( + params: GenerateBranchNameParams, + token?: CancellationToken, + ): Promise { + if (!this.isAvailable) { + return null; + } + return this.client.sendRequest(GenerateBranchNameRequest.method, params, token); + } + + // target is where the fetched command will be filled in + // callback will be called when target updated + async provideEditCommands( + params: ChatEditCommandParams, + target: { + commands: ChatEditCommand[]; + callback: () => void; + }, + token?: CancellationToken, + ): Promise { + // FIXME: handle partial results after server supports partial results + const commands: ChatEditCommand[] | null = await this.client.sendRequest( + ChatEditCommandRequest.method, + params, + token, + ); + if (commands && commands.length > 0) { + target.commands.push(...commands); + target.callback(); + } + } + + async provideEdit(params: ChatEditParams, token?: CancellationToken): Promise { + if (!this.isAvailable) { + return null; + } + return this.client.sendRequest(ChatEditRequest.method, params, token); + } + + async provideSmartApplyEdit(params: SmartApplyParams, token?: CancellationToken): Promise { + if (!this.isAvailable) { + return null; + } + return this.client.sendRequest(SmartApplyRequest.method, params, token); + } + + async resolveEdit(params: ChatEditResolveParams): Promise { + return this.client.sendRequest(ChatEditResolveRequest.method, params); + } +} diff --git a/clients/vscode/src/lsp/CodeLensMiddleware.ts b/clients/vscode/src/lsp/CodeLensMiddleware.ts new file mode 100644 index 000000000000..54d6c4d02c16 --- /dev/null +++ b/clients/vscode/src/lsp/CodeLensMiddleware.ts @@ -0,0 +1,193 @@ +import { + window, + Range, + CodeLens as VscodeCodeLens, + TextEditor, + TextEditorDecorationType, + TextDocument, + CancellationToken, + ThemeColor, + DecorationRangeBehavior, +} from "vscode"; +import { CodeLensMiddleware as VscodeLspCodeLensMiddleware, ProvideCodeLensesSignature } from "vscode-languageclient"; +import { CodeLens as TabbyCodeLens } from "tabby-agent"; +import { findTextEditor } from "./vscodeWindowUtils"; +import { isBrowser } from "../env"; +import { formatShortcut, KeyBindingManager } from "../keybindings"; + +type CodeLens = VscodeCodeLens & TabbyCodeLens; + +const decorationTypeHeader = window.createTextEditorDecorationType({ + backgroundColor: new ThemeColor("merge.incomingHeaderBackground"), + isWholeLine: true, + rangeBehavior: DecorationRangeBehavior.ClosedClosed, +}); +const decorationTypeFooter = window.createTextEditorDecorationType({ + backgroundColor: new ThemeColor("merge.incomingHeaderBackground"), + isWholeLine: true, + rangeBehavior: DecorationRangeBehavior.ClosedClosed, +}); +const decorationTypeComments = window.createTextEditorDecorationType({ + color: new ThemeColor("editorInlayHint.foreground"), + backgroundColor: new ThemeColor("editorInlayHint.background"), + fontStyle: "italic", + fontWeight: "normal", + isWholeLine: true, + rangeBehavior: DecorationRangeBehavior.ClosedOpen, +}); +const decorationTypeUnchanged = window.createTextEditorDecorationType({}); +const decorationTypePending = window.createTextEditorDecorationType({ + backgroundColor: new ThemeColor("editor.inactiveSelectionBackground"), + isWholeLine: true, + rangeBehavior: DecorationRangeBehavior.ClosedClosed, +}); +const decorationTypeTextInserted = window.createTextEditorDecorationType({ + backgroundColor: new ThemeColor("diffEditor.insertedTextBackground"), + isWholeLine: false, + rangeBehavior: DecorationRangeBehavior.ClosedOpen, +}); +const decorationTypeTextDeleted = window.createTextEditorDecorationType({ + backgroundColor: new ThemeColor("diffEditor.removedTextBackground"), + isWholeLine: false, + rangeBehavior: DecorationRangeBehavior.ClosedOpen, +}); +const decorationTypeLineInserted = window.createTextEditorDecorationType({ + backgroundColor: new ThemeColor("diffEditor.insertedLineBackground"), + isWholeLine: true, + rangeBehavior: DecorationRangeBehavior.ClosedClosed, +}); +const decorationTypeLineDeleted = window.createTextEditorDecorationType({ + backgroundColor: new ThemeColor("diffEditor.removedLineBackground"), + isWholeLine: true, + rangeBehavior: DecorationRangeBehavior.ClosedClosed, +}); +const lineDecorationTypes: Record = { + header: decorationTypeHeader, + footer: decorationTypeFooter, + commentsFirstLine: decorationTypeComments, + comments: decorationTypeComments, + waiting: decorationTypePending, + inProgress: decorationTypeLineInserted, + unchanged: decorationTypeUnchanged, + inserted: decorationTypeLineInserted, + deleted: decorationTypeLineDeleted, +}; + +const textDecorationTypes: Record = { + inserted: decorationTypeTextInserted, + deleted: decorationTypeTextDeleted, +}; + +export class CodeLensMiddleware implements VscodeLspCodeLensMiddleware { + private readonly decorationMap = new Map>(); + + async provideCodeLenses( + document: TextDocument, + token: CancellationToken, + next: ProvideCodeLensesSignature, + ): Promise { + const codeLenses = await next(document, token); + const editor = findTextEditor(document.uri); + if (!editor) { + return codeLenses; + } + this.removeDecorations(editor); + const result = + codeLenses + ?.map((codeLens) => this.handleCodeLens(codeLens, editor)) + .filter((codeLens): codeLens is CodeLens => codeLens !== null) ?? []; + this.purgeDecorationMap(); + return result; + } + + private handleCodeLens(codeLens: CodeLens, editor: TextEditor): CodeLens | null { + if (!codeLens.data || codeLens.data.type !== "previewChanges") { + return codeLens; + } + this.appendShortcutHint(codeLens); + + const decorationRange = new Range( + codeLens.range.start.line, + codeLens.range.start.character, + codeLens.range.end.line, + codeLens.range.end.character, + ); + const lineType = codeLens.data.line; + if (typeof lineType === "string" && lineType in lineDecorationTypes) { + const decorationType = lineDecorationTypes[lineType]; + if (decorationType) { + this.addDecorationRange(editor, decorationType, decorationRange); + } + } + const textType = codeLens.data.text; + if (typeof textType === "string" && textType in textDecorationTypes) { + const decorationType = textDecorationTypes[textType]; + if (decorationType) { + this.addDecorationRange(editor, decorationType, decorationRange); + } + } + if (codeLens.data.line === "header") { + return codeLens; + } + return null; + } + + private addDecorationRange(editor: TextEditor, decorationType: TextEditorDecorationType, range: Range) { + let decorations: Map | undefined; + if (this.decorationMap.has(editor)) { + decorations = this.decorationMap.get(editor); + } + if (!decorations) { + decorations = new Map(); + this.decorationMap.set(editor, decorations); + } + let ranges: Range[] | undefined; + if (decorations.has(decorationType)) { + ranges = decorations.get(decorationType); + } + if (!ranges) { + ranges = []; + decorations.set(decorationType, ranges); + } + ranges.push(range); + editor.setDecorations(decorationType, ranges); + } + private appendShortcutHint(codeLens: CodeLens) { + if (!codeLens.command) return; + + const action = codeLens.command?.arguments?.[0]?.action; + if (!action) return; + + let commandId: string; + if (action === "accept") { + commandId = "tabby.chat.edit.accept"; + } else if (action === "discard") { + commandId = "tabby.chat.edit.discard"; + } else { + return; + } + const binding = KeyBindingManager.getInstance().getKeybinding(commandId); + + const formattedShortcut = binding ? formatShortcut(binding) : ""; + const shortcutText = isBrowser ? "" : formattedShortcut ? ` (${formattedShortcut})` : ""; + + codeLens.command.title += shortcutText; + } + + private removeDecorations(editor: TextEditor) { + if (this.decorationMap.has(editor)) { + const decorations = this.decorationMap.get(editor); + decorations?.forEach((_, decorationType) => { + editor.setDecorations(decorationType, []); + }); + this.decorationMap.delete(editor); + } + } + + private purgeDecorationMap() { + const editorsToRemove = [...this.decorationMap.keys()].filter( + (editor) => !window.visibleTextEditors.includes(editor), + ); + editorsToRemove.forEach((editor) => this.decorationMap.delete(editor)); + } +} diff --git a/clients/vscode/src/lsp/ConfigurationMiddleware.ts b/clients/vscode/src/lsp/ConfigurationMiddleware.ts new file mode 100644 index 000000000000..616a17f83f93 --- /dev/null +++ b/clients/vscode/src/lsp/ConfigurationMiddleware.ts @@ -0,0 +1,11 @@ +import { ConfigurationMiddleware as VscodeLspConfigurationMiddleware } from "vscode-languageclient"; +import { ClientProvidedConfig } from "tabby-agent"; +import { Config } from "../Config"; + +export class ConfigurationMiddleware implements VscodeLspConfigurationMiddleware { + constructor(private readonly config: Config) {} + + async configuration(): Promise { + return [this.config.buildClientProvidedConfig()]; + } +} diff --git a/clients/vscode/src/lsp/ConfigurationSyncFeature.ts b/clients/vscode/src/lsp/ConfigurationSyncFeature.ts new file mode 100644 index 000000000000..e178e88dcbc8 --- /dev/null +++ b/clients/vscode/src/lsp/ConfigurationSyncFeature.ts @@ -0,0 +1,42 @@ +import { BaseLanguageClient, StaticFeature, FeatureState, ClientCapabilities } from "vscode-languageclient"; +import { DidChangeConfigurationNotification, ClientProvidedConfig } from "tabby-agent"; +import { Config } from "../Config"; + +export class ConfigurationSyncFeature implements StaticFeature { + constructor( + private readonly client: BaseLanguageClient, + private readonly config: Config, + ) {} + + getState(): FeatureState { + return { kind: "static" }; + } + + fillInitializeParams() { + // nothing + } + + fillClientCapabilities(capabilities: ClientCapabilities): void { + capabilities.workspace = { + ...capabilities.workspace, + didChangeConfiguration: { dynamicRegistration: false }, + }; + } + + preInitialize(): void { + // nothing + } + + initialize(): void { + this.config.on("updated", () => this.sync()); + } + + clear(): void { + this.config.off("updated", () => this.sync()); + } + + async sync(): Promise { + const clientProvidedConfig: ClientProvidedConfig = this.config.buildClientProvidedConfig(); + this.client.sendNotification(DidChangeConfigurationNotification.method, { settings: clientProvidedConfig }); + } +} diff --git a/clients/vscode/src/lsp/DataStoreFeature.ts b/clients/vscode/src/lsp/DataStoreFeature.ts new file mode 100644 index 000000000000..7ca13c1916ba --- /dev/null +++ b/clients/vscode/src/lsp/DataStoreFeature.ts @@ -0,0 +1,78 @@ +import { EventEmitter } from "events"; +import { env, ExtensionContext } from "vscode"; +import { BaseLanguageClient, StaticFeature, FeatureState, Disposable } from "vscode-languageclient"; +import { + InitializeParams, + ClientCapabilities, + DataStoreRecords, + DataStoreDidUpdateNotification, + DataStoreUpdateRequest, +} from "tabby-agent"; + +export class DataStoreFeature extends EventEmitter implements StaticFeature { + private disposables: Disposable[] = []; + + constructor( + private readonly context: ExtensionContext, + private readonly client: BaseLanguageClient, + ) { + super(); + } + + getState(): FeatureState { + return { kind: "static" }; + } + + fillInitializeParams(params: InitializeParams) { + if (env.appHost === "desktop") { + return; + } + params.initializationOptions = { + ...params.initializationOptions, + dataStoreRecords: this.getRecords(), + }; + } + + fillClientCapabilities(capabilities: ClientCapabilities): void { + if (env.appHost === "desktop") { + return; + } + capabilities.tabby = { + ...capabilities.tabby, + dataStore: true, + }; + } + + preInitialize(): void { + // nothing + } + + initialize(): void { + if (env.appHost === "desktop") { + return; + } + this.disposables.push( + this.client.onRequest(DataStoreUpdateRequest.type, async (params: DataStoreRecords) => { + this.update(params); + return true; + }), + ); + this.on("didUpdate", (records) => { + this.client.sendNotification(DataStoreDidUpdateNotification.type, records); + }); + } + + private getRecords(): DataStoreRecords { + return this.context.globalState.get("data", {}); + } + + private update(records: DataStoreRecords) { + this.context.globalState.update("data", records); + this.emit("didUpdate", records); + } + + clear(): void { + this.disposables.forEach((disposable) => disposable.dispose()); + this.disposables = []; + } +} diff --git a/clients/vscode/src/lsp/EditorOptionsFeature.ts b/clients/vscode/src/lsp/EditorOptionsFeature.ts new file mode 100644 index 000000000000..de7bedfd0e40 --- /dev/null +++ b/clients/vscode/src/lsp/EditorOptionsFeature.ts @@ -0,0 +1,55 @@ +import { Uri } from "vscode"; +import { BaseLanguageClient, StaticFeature, FeatureState, Disposable } from "vscode-languageclient"; +import { ClientCapabilities, EditorOptionsRequest, EditorOptionsParams } from "tabby-agent"; +import { findTextEditor } from "./vscodeWindowUtils"; + +export class EditorOptionsFeature implements StaticFeature { + private disposables: Disposable[] = []; + + constructor(private readonly client: BaseLanguageClient) {} + + getState(): FeatureState { + return { kind: "static" }; + } + + fillInitializeParams() { + // nothing + } + + fillClientCapabilities(capabilities: ClientCapabilities): void { + capabilities.tabby = { + ...capabilities.tabby, + editorOptions: true, + }; + } + + preInitialize(): void { + // nothing + } + + initialize(): void { + this.disposables.push( + this.client.onRequest(EditorOptionsRequest.type, (params: EditorOptionsParams) => { + const editor = findTextEditor(Uri.parse(params.uri)); + if (!editor) { + return null; + } + const { insertSpaces, tabSize } = editor.options; + let indentation: string | undefined; + if (insertSpaces && typeof tabSize === "number" && tabSize > 0) { + indentation = " ".repeat(tabSize); + } else if (!insertSpaces) { + indentation = "\t"; + } + return { + indentation, + }; + }), + ); + } + + clear(): void { + this.disposables.forEach((disposable) => disposable.dispose()); + this.disposables = []; + } +} diff --git a/clients/vscode/src/lsp/FileTrackFeature.ts b/clients/vscode/src/lsp/FileTrackFeature.ts new file mode 100644 index 000000000000..f54e64142c21 --- /dev/null +++ b/clients/vscode/src/lsp/FileTrackFeature.ts @@ -0,0 +1,59 @@ +import { DidChangeActiveEditorNotification, DidChangeActiveEditorParams } from "tabby-agent"; +import { Client } from "./client"; +import { ExtensionContext, TextEditor, window } from "vscode"; +import { + DocumentSelector, + FeatureState, + InitializeParams, + ServerCapabilities, + StaticFeature, +} from "vscode-languageclient"; +import EventEmitter from "events"; +import { collectVisibleEditors } from "../windowUtils"; + +export class FileTrackerFeature extends EventEmitter implements StaticFeature { + constructor( + private readonly client: Client, + private readonly context: ExtensionContext, + ) { + super(); + } + fillInitializeParams?: ((params: InitializeParams) => void) | undefined; + fillClientCapabilities(): void { + //nothing + } + preInitialize?: + | ((capabilities: ServerCapabilities, documentSelector: DocumentSelector | undefined) => void) + | undefined; + initialize(): void { + this.context.subscriptions.push( + //when active text editor changes + window.onDidChangeActiveTextEditor(async (editor) => { + await this.addingChangeEditor(editor); + }), + ); + } + getState(): FeatureState { + throw new Error("Method not implemented."); + } + clear(): void { + throw new Error("Method not implemented."); + } + + async addingChangeEditor(editor: TextEditor | undefined) { + if (editor && editor.visibleRanges[0] && editor.document.fileName.startsWith("/")) { + const editorRange = editor.visibleRanges[0]; + const params: DidChangeActiveEditorParams = { + activeEditor: { + uri: editor.document.uri.toString(), + range: { + start: { line: editorRange.start.line, character: editorRange.start.character }, + end: { line: editorRange.end.line, character: editorRange.end.character }, + }, + }, + visibleEditors: collectVisibleEditors(true, editor), + }; + await this.client.languageClient.sendNotification(DidChangeActiveEditorNotification.method, params); + } + } +} diff --git a/clients/vscode/src/lsp/GitProviderFeature.ts b/clients/vscode/src/lsp/GitProviderFeature.ts new file mode 100644 index 000000000000..8623e289d3f3 --- /dev/null +++ b/clients/vscode/src/lsp/GitProviderFeature.ts @@ -0,0 +1,79 @@ +import { Uri } from "vscode"; +import { BaseLanguageClient, StaticFeature, FeatureState, Disposable } from "vscode-languageclient"; +import { + ClientCapabilities, + GitRepositoryRequest, + GitRepositoryParams, + GitDiffRequest, + GitDiffParams, +} from "tabby-agent"; +import { GitProvider } from "../git/GitProvider"; + +export class GitProviderFeature implements StaticFeature { + private disposables: Disposable[] = []; + + constructor( + private readonly client: BaseLanguageClient, + private readonly gitProvider: GitProvider, + ) {} + + getState(): FeatureState { + return { kind: "static" }; + } + + fillInitializeParams() { + // nothing + } + + fillClientCapabilities(capabilities: ClientCapabilities): void { + capabilities.tabby = { + ...capabilities.tabby, + gitProvider: this.gitProvider.isApiAvailable(), + }; + } + + preInitialize(): void { + // nothing + } + + initialize(): void { + this.disposables.push( + this.client.onRequest(GitRepositoryRequest.type, (params: GitRepositoryParams) => { + const repository = this.gitProvider.getRepository(Uri.parse(params.uri)); + if (!repository) { + return null; + } + return { + root: repository.rootUri.toString(), + remoteUrl: this.gitProvider.getDefaultRemoteUrl(repository), + remotes: repository.state.remotes + .map((remote) => ({ + name: remote.name, + url: remote.fetchUrl ?? remote.pushUrl ?? "", + })) + .filter((remote) => { + return remote.url.length > 0; + }), + }; + }), + ); + this.disposables.push( + this.client.onRequest(GitDiffRequest.type, async (params: GitDiffParams) => { + const repository = this.gitProvider.getRepository(Uri.parse(params.repository)); + if (!repository) { + return null; + } + const diff = await this.gitProvider.getDiff(repository, params.cached); + if (!diff) { + return null; + } + return { diff }; + }), + ); + } + + clear(): void { + this.disposables.forEach((disposable) => disposable.dispose()); + this.disposables = []; + } +} diff --git a/clients/vscode/src/lsp/InitializationFeature.ts b/clients/vscode/src/lsp/InitializationFeature.ts new file mode 100644 index 000000000000..bb5dfb14b42b --- /dev/null +++ b/clients/vscode/src/lsp/InitializationFeature.ts @@ -0,0 +1,69 @@ +import { env, version, ExtensionContext, LogOutputChannel, LogLevel } from "vscode"; +import { BaseLanguageClient, StaticFeature, FeatureState, Trace } from "vscode-languageclient"; +import { InitializeParams } from "tabby-agent"; +import { Config } from "../Config"; + +export class InitializationFeature implements StaticFeature { + constructor( + private readonly context: ExtensionContext, + private readonly client: BaseLanguageClient, + private readonly config: Config, + private readonly logger: LogOutputChannel, + ) {} + + getState(): FeatureState { + return { kind: "static" }; + } + + fillInitializeParams(params: InitializeParams) { + params.initializationOptions = { + ...params.initializationOptions, + config: this.config.buildClientProvidedConfig(), + clientInfo: { + name: `${env.appName} ${env.appHost}`, + version: version, + tabbyPlugin: { + name: this.context.extension.id, + version: this.context.extension.packageJSON.version, + }, + }, + clientCapabilities: { + textDocument: { + completion: false, + }, + }, + }; + params.trace = this.getCurrentTraceValue(); + } + + fillClientCapabilities(): void { + // nothing + } + + preInitialize(): void { + // nothing + } + + initialize(): void { + // Sync trace setting + this.client.setTrace(Trace.fromString(this.getCurrentTraceValue())); + this.context.subscriptions.push( + this.logger.onDidChangeLogLevel(async () => { + await this.client.setTrace(Trace.fromString(this.getCurrentTraceValue())); + }), + ); + } + + clear(): void { + // nothing + } + + private getCurrentTraceValue(): "verbose" | "off" { + const level = this.logger.logLevel; + if (level === LogLevel.Trace) { + return "verbose"; + } else { + return "off"; + } + } +} diff --git a/clients/vscode/src/lsp/InlineCompletionFeature.ts b/clients/vscode/src/lsp/InlineCompletionFeature.ts new file mode 100644 index 000000000000..ff62e2f22064 --- /dev/null +++ b/clients/vscode/src/lsp/InlineCompletionFeature.ts @@ -0,0 +1,23 @@ +import { languages, InlineCompletionItemProvider, Disposable } from "vscode"; +import { InlineCompletionRegistrationOptions, FeatureClient } from "vscode-languageclient"; +import { + InlineCompletionMiddleware, + InlineCompletionItemFeature as VscodeLspInlineCompletionItemFeature, +} from "vscode-languageclient/lib/common/inlineCompletion"; + +export class InlineCompletionFeature extends VscodeLspInlineCompletionItemFeature { + constructor( + client: FeatureClient, + private readonly inlineCompletionItemProvider: InlineCompletionItemProvider, + ) { + super(client); + } + + override registerLanguageProvider( + options: InlineCompletionRegistrationOptions, + ): [Disposable, InlineCompletionItemProvider] { + const selector = this._client.protocol2CodeConverter.asDocumentSelector(options.documentSelector ?? ["**"]); + const provider = this.inlineCompletionItemProvider; + return [languages.registerInlineCompletionItemProvider(selector, provider), provider]; + } +} diff --git a/clients/vscode/src/lsp/LanguageSupportFeature.ts b/clients/vscode/src/lsp/LanguageSupportFeature.ts new file mode 100644 index 000000000000..cc2f726fa5ca --- /dev/null +++ b/clients/vscode/src/lsp/LanguageSupportFeature.ts @@ -0,0 +1,108 @@ +import { commands, Uri, Position, Range } from "vscode"; +import { BaseLanguageClient, StaticFeature, FeatureState, Disposable } from "vscode-languageclient"; +import { + ClientCapabilities, + LanguageSupportDeclarationRequest, + LanguageSupportSemanticTokensRangeRequest, +} from "tabby-agent"; +import { DeclarationParams, SemanticTokensRangeParams } from "vscode-languageclient"; + +export class LanguageSupportFeature implements StaticFeature { + private disposables: Disposable[] = []; + + constructor(private readonly client: BaseLanguageClient) {} + + getState(): FeatureState { + return { kind: "static" }; + } + + fillInitializeParams() { + // nothing + } + + fillClientCapabilities(capabilities: ClientCapabilities): void { + capabilities.tabby = { + ...capabilities.tabby, + languageSupport: true, + }; + } + + preInitialize(): void { + // nothing + } + + initialize(): void { + this.disposables.push( + this.client.onRequest(LanguageSupportDeclarationRequest.type, async (params: DeclarationParams) => { + const result = await commands.executeCommand( + "vscode.executeDefinitionProvider", + Uri.parse(params.textDocument.uri), + new Position(params.position.line, params.position.character), + ); + const items = Array.isArray(result) ? result : [result]; + const locations = items.map((item) => { + return { + uri: "targetUri" in item ? item.targetUri.toString() : item.uri.toString(), + range: + "targetRange" in item + ? { + start: { + line: item.targetRange.start.line, + character: item.targetRange.start.character, + }, + end: { + line: item.targetRange.end.line, + character: item.targetRange.end.character, + }, + } + : { + start: { + line: item.range.start.line, + character: item.range.start.character, + }, + end: { + line: item.range.end.line, + character: item.range.end.character, + }, + }, + }; + }); + return locations; + }), + ); + this.disposables.push( + this.client.onRequest( + LanguageSupportSemanticTokensRangeRequest.type, + async (params: SemanticTokensRangeParams) => { + return { + legend: await commands.executeCommand( + "vscode.provideDocumentRangeSemanticTokensLegend", + Uri.parse(params.textDocument.uri), + new Range( + params.range.start.line, + params.range.start.character, + params.range.end.line, + params.range.end.character, + ), + ), + tokens: await commands.executeCommand( + "vscode.provideDocumentRangeSemanticTokens", + Uri.parse(params.textDocument.uri), + new Range( + params.range.start.line, + params.range.start.character, + params.range.end.line, + params.range.end.character, + ), + ), + }; + }, + ), + ); + } + + clear(): void { + this.disposables.forEach((disposable) => disposable.dispose()); + this.disposables = []; + } +} diff --git a/clients/vscode/src/lsp/TelemetryFeature.ts b/clients/vscode/src/lsp/TelemetryFeature.ts new file mode 100644 index 000000000000..5331f844dd43 --- /dev/null +++ b/clients/vscode/src/lsp/TelemetryFeature.ts @@ -0,0 +1,34 @@ +import { BaseLanguageClient, StaticFeature, FeatureState } from "vscode-languageclient"; +import { TelemetryEventNotification, EventParams } from "tabby-agent"; + +export class TelemetryFeature implements StaticFeature { + constructor(private readonly client: BaseLanguageClient) {} + + getState(): FeatureState { + return { kind: "static" }; + } + + fillInitializeParams() { + // nothing + } + + fillClientCapabilities(): void { + // nothing + } + + preInitialize(): void { + // nothing + } + + initialize(): void { + // nothing + } + + clear(): void { + // nothing + } + + async postEvent(params: EventParams): Promise { + return this.client.sendNotification(TelemetryEventNotification.method, params); + } +} diff --git a/clients/vscode/src/lsp/WorkspaceFeature.ts b/clients/vscode/src/lsp/WorkspaceFeature.ts new file mode 100644 index 000000000000..f3598921b4d6 --- /dev/null +++ b/clients/vscode/src/lsp/WorkspaceFeature.ts @@ -0,0 +1,109 @@ +import EventEmitter from "events"; +import { ApplyWorkspaceEditParams, ApplyWorkspaceEditRequest } from "tabby-agent"; +import { BaseLanguageClient, FeatureState, StaticFeature, TextEdit } from "vscode-languageclient"; +import { Disposable, Position, Range, TextDocument, TextEditorEdit, window, workspace } from "vscode"; +import { diffLines } from "diff"; + +export class WorkSpaceFeature extends EventEmitter implements StaticFeature { + private disposables: Disposable[] = []; + + constructor(private readonly client: BaseLanguageClient) { + super(); + } + getState(): FeatureState { + return { kind: "static" }; + } + + fillInitializeParams() { + // nothing + } + + fillClientCapabilities(): void { + // nothing + } + + preInitialize(): void { + // nothing + } + + initialize(): void { + this.disposables.push( + this.client.onRequest(ApplyWorkspaceEditRequest.type, (params: ApplyWorkspaceEditParams) => { + return this.handleApplyWorkspaceEdit(params); + }), + ); + } + + clear(): void { + this.disposables.forEach((disposable) => disposable.dispose()); + this.disposables = []; + } + + private async handleApplyWorkspaceEdit(params: ApplyWorkspaceEditParams): Promise { + const { edit, options } = params; + const activeEditor = window.activeTextEditor; + if (!activeEditor) { + return false; + } + + try { + const success = await activeEditor.edit( + (editBuilder: TextEditorEdit) => { + Object.entries(edit.changes || {}).forEach(([uri, textEdits]) => { + const document = workspace.textDocuments.find((doc) => doc.uri.toString() === uri); + if (document && document === activeEditor.document) { + const textEdit = textEdits[0]; + if (textEdits.length === 1 && textEdit) { + applyTextEditMinimalLineChange(editBuilder, textEdit, document); + } else { + textEdits.forEach((textEdit) => { + const range = new Range( + new Position(textEdit.range.start.line, textEdit.range.start.character), + new Position(textEdit.range.end.line, textEdit.range.end.character), + ); + editBuilder.replace(range, textEdit.newText); + }); + } + } + }); + }, + { + undoStopBefore: options?.undoStopBefore ?? false, + undoStopAfter: options?.undoStopAfter ?? false, + }, + ); + + return success; + } catch (error) { + return false; + } + } +} + +function applyTextEditMinimalLineChange(editBuilder: TextEditorEdit, textEdit: TextEdit, document: TextDocument) { + const documentRange = new Range( + new Position(textEdit.range.start.line, textEdit.range.start.character), + new Position(textEdit.range.end.line, textEdit.range.end.character), + ); + + const text = document.getText(documentRange); + const newText = textEdit.newText; + const diffs = diffLines(text, newText); + + let line = documentRange.start.line; + for (const diff of diffs) { + if (!diff.count) { + continue; + } + + if (diff.added) { + editBuilder.insert(new Position(line, 0), diff.value); + } else if (diff.removed) { + const range = new Range(new Position(line + 0, 0), new Position(line + diff.count, 0)); + editBuilder.delete(range); + line += diff.count; + } else { + line += diff.count; + } + } +} diff --git a/clients/vscode/src/lsp/WorkspaceFileSystemFeature.ts b/clients/vscode/src/lsp/WorkspaceFileSystemFeature.ts new file mode 100644 index 000000000000..bfdfa165ed98 --- /dev/null +++ b/clients/vscode/src/lsp/WorkspaceFileSystemFeature.ts @@ -0,0 +1,55 @@ +import { workspace, Range, Uri } from "vscode"; +import { BaseLanguageClient, StaticFeature, FeatureState, Disposable } from "vscode-languageclient"; +import { ClientCapabilities, ReadFileRequest, ReadFileParams } from "tabby-agent"; + +export class WorkspaceFileSystemFeature implements StaticFeature { + private disposables: Disposable[] = []; + + constructor(private readonly client: BaseLanguageClient) {} + + getState(): FeatureState { + return { kind: "static" }; + } + + fillInitializeParams() { + // nothing + } + + fillClientCapabilities(capabilities: ClientCapabilities): void { + capabilities.tabby = { + ...capabilities.tabby, + workspaceFileSystem: true, + }; + } + + preInitialize(): void { + // nothing + } + + initialize(): void { + this.disposables.push( + this.client.onRequest(ReadFileRequest.type, async (params: ReadFileParams) => { + if (params.format !== "text") { + return null; + } + const textDocument = await workspace.openTextDocument(Uri.parse(params.uri)); + const range = params.range + ? new Range( + params.range.start.line, + params.range.start.character, + params.range.end.line, + params.range.end.character, + ) + : undefined; + return { + text: textDocument.getText(range), + }; + }), + ); + } + + clear(): void { + this.disposables.forEach((disposable) => disposable.dispose()); + this.disposables = []; + } +} diff --git a/clients/vscode/src/lsp/client.ts b/clients/vscode/src/lsp/client.ts new file mode 100644 index 000000000000..4b0deefeab43 --- /dev/null +++ b/clients/vscode/src/lsp/client.ts @@ -0,0 +1,131 @@ +import { CodeActionProvider, ExtensionContext, Uri, languages } from "vscode"; +import { LanguageClientOptions } from "vscode-languageclient"; +import { LanguageClient as NodeLanguageClient, ServerOptions, TransportKind } from "vscode-languageclient/node"; +import { LanguageClient as BrowserLanguageClient } from "vscode-languageclient/browser"; +import { BaseLanguageClient } from "vscode-languageclient"; +import { AgentStatusFeature } from "./AgentStatusFeature"; +import { AgentConfigFeature } from "./AgentConfigFeature"; +import { ChatFeature } from "./ChatFeature"; +import { CodeLensMiddleware } from "./CodeLensMiddleware"; +import { ConfigurationMiddleware } from "./ConfigurationMiddleware"; +import { ConfigurationSyncFeature } from "./ConfigurationSyncFeature"; +import { DataStoreFeature } from "./DataStoreFeature"; +import { EditorOptionsFeature } from "./EditorOptionsFeature"; +import { GitProviderFeature } from "./GitProviderFeature"; +import { InitializationFeature } from "./InitializationFeature"; +import { InlineCompletionFeature } from "./InlineCompletionFeature"; +import { LanguageSupportFeature } from "./LanguageSupportFeature"; +import { TelemetryFeature } from "./TelemetryFeature"; +import { WorkspaceFileSystemFeature } from "./WorkspaceFileSystemFeature"; +import { Config } from "../Config"; +import { InlineCompletionProvider } from "../InlineCompletionProvider"; +import { GitProvider } from "../git/GitProvider"; +import { getLogger, LogOutputChannel } from "../logger"; +import { WorkSpaceFeature } from "./WorkspaceFeature"; +import { FileTrackerFeature } from "./FileTrackFeature"; +import { isBrowser } from "../env"; + +export function createClient(context: ExtensionContext, logger: LogOutputChannel): Client { + const clientOptions: LanguageClientOptions = { + documentSelector: [ + { scheme: "file" }, + { scheme: "vscode-vfs" }, + { scheme: "untitled" }, + { scheme: "vscode-notebook-cell" }, + { scheme: "vscode-userdata" }, + ], + outputChannel: logger, + }; + if (isBrowser) { + const workerModulePath = Uri.joinPath(context.extensionUri, "dist/tabby-agent/browser/index.mjs"); + const worker = new Worker(workerModulePath.toString()); + const languageClient = new BrowserLanguageClient("Tabby", "Tabby", clientOptions, worker); + return new Client(context, languageClient); + } else { + const serverModulePath = context.asAbsolutePath("dist/tabby-agent/node/index.js"); + const serverOptions: ServerOptions = { + run: { + module: serverModulePath, + transport: TransportKind.ipc, + }, + debug: { + module: serverModulePath, + transport: TransportKind.ipc, + }, + }; + const languageClient = new NodeLanguageClient("Tabby", serverOptions, clientOptions); + return new Client(context, languageClient); + } +} + +export class Client { + private readonly logger = getLogger(""); + readonly status: AgentStatusFeature; + readonly agentConfig: AgentConfigFeature; + readonly chat: ChatFeature; + readonly telemetry: TelemetryFeature; + readonly workspace: WorkSpaceFeature; + readonly fileTrack: FileTrackerFeature; + + constructor( + private readonly context: ExtensionContext, + readonly languageClient: BaseLanguageClient, + ) { + this.status = new AgentStatusFeature(this.languageClient); + this.agentConfig = new AgentConfigFeature(this.languageClient); + this.chat = new ChatFeature(this.languageClient); + this.workspace = new WorkSpaceFeature(this.languageClient); + this.telemetry = new TelemetryFeature(this.languageClient); + this.fileTrack = new FileTrackerFeature(this, this.context); + this.languageClient.registerFeature(this.status); + this.languageClient.registerFeature(this.agentConfig); + this.languageClient.registerFeature(this.chat); + this.languageClient.registerFeature(this.workspace); + this.languageClient.registerFeature(this.telemetry); + this.languageClient.registerFeature(this.fileTrack); + this.languageClient.registerFeature(new DataStoreFeature(this.context, this.languageClient)); + this.languageClient.registerFeature(new EditorOptionsFeature(this.languageClient)); + this.languageClient.registerFeature(new LanguageSupportFeature(this.languageClient)); + this.languageClient.registerFeature(new WorkspaceFileSystemFeature(this.languageClient)); + + const codeLensMiddleware = new CodeLensMiddleware(); + this.languageClient.middleware.provideCodeLenses = (document, token, next) => + codeLensMiddleware.provideCodeLenses(document, token, next); + } + + async start(): Promise { + return this.languageClient.start(); + } + + async stop(): Promise { + return this.languageClient.stop(); + } + + registerConfigManager(config: Config): void { + const initializationFeature = new InitializationFeature(this.context, this.languageClient, config, this.logger); + this.languageClient.registerFeature(initializationFeature); + + const configMiddleware = new ConfigurationMiddleware(config); + if (!this.languageClient.middleware.workspace) { + this.languageClient.middleware.workspace = {}; + } + this.languageClient.middleware.workspace.configuration = () => configMiddleware.configuration(); + + const configSyncFeature = new ConfigurationSyncFeature(this.languageClient, config); + this.languageClient.registerFeature(configSyncFeature); + } + + registerInlineCompletionProvider(provider: InlineCompletionProvider): void { + const feature = new InlineCompletionFeature(this.languageClient, provider); + this.languageClient.registerFeature(feature); + } + + registerGitProvider(provider: GitProvider): void { + const feature = new GitProviderFeature(this.languageClient, provider); + this.languageClient.registerFeature(feature); + } + + registerCodeActionProvider(provider: CodeActionProvider) { + this.context.subscriptions.push(languages.registerCodeActionsProvider("*", provider)); + } +} diff --git a/clients/vscode/src/lsp/vscodeWindowUtils.ts b/clients/vscode/src/lsp/vscodeWindowUtils.ts new file mode 100644 index 000000000000..04053507763f --- /dev/null +++ b/clients/vscode/src/lsp/vscodeWindowUtils.ts @@ -0,0 +1,8 @@ +import { window, TextEditor, Uri } from "vscode"; + +export function findTextEditor(uri: Uri): TextEditor | undefined { + if (window.activeTextEditor?.document.uri.toString() === uri.toString()) { + return window.activeTextEditor; + } + return window.visibleTextEditors.find((editor) => editor.document.uri.toString() === uri.toString()); +} diff --git a/clients/vscode/src/notifications.ts b/clients/vscode/src/notifications.ts deleted file mode 100644 index 934fb1a3db85..000000000000 --- a/clients/vscode/src/notifications.ts +++ /dev/null @@ -1,332 +0,0 @@ -import { commands, window, workspace, ConfigurationTarget } from "vscode"; -import type { - HighCompletionTimeoutRateIssue, - SlowCompletionResponseTimeIssue, - ConnectionFailedIssue, -} from "tabby-agent"; -import { agent } from "./agent"; - -function showInformationWhenInitializing() { - window.showInformationMessage("Tabby is initializing.", "Settings").then((selection) => { - switch (selection) { - case "Settings": - commands.executeCommand("tabby.openSettings"); - break; - } - }); -} - -function showInformationWhenAutomaticTrigger() { - window - .showInformationMessage( - "Tabby automatic code completion is enabled. Switch to manual trigger mode?", - "Manual Mode", - "Settings", - ) - .then((selection) => { - switch (selection) { - case "Manual Mode": - commands.executeCommand("tabby.toggleInlineCompletionTriggerMode", "manual"); - break; - case "Settings": - commands.executeCommand("tabby.openSettings"); - break; - } - }); -} - -function showInformationWhenManualTrigger() { - window - .showInformationMessage( - "Tabby is standing by. Trigger code completion manually?", - "Trigger", - "Automatic Mode", - "Settings", - ) - .then((selection) => { - switch (selection) { - case "Trigger": - commands.executeCommand("editor.action.inlineSuggest.trigger"); - break; - case "Automatic Mode": - commands.executeCommand("tabby.toggleInlineCompletionTriggerMode", "automatic"); - break; - case "Settings": - commands.executeCommand("tabby.openSettings"); - break; - } - }); -} - -function showInformationWhenManualTriggerLoading() { - window.showInformationMessage("Tabby is generating code completions.", "Settings").then((selection) => { - switch (selection) { - case "Settings": - commands.executeCommand("tabby.openSettings"); - break; - } - }); -} - -function showInformationWhenInlineSuggestDisabled() { - window - .showWarningMessage( - "Tabby's suggestion is not showing because inline suggestion is disabled. Please enable it first.", - "Enable", - "Settings", - ) - .then((selection) => { - switch (selection) { - case "Enable": - console.debug(`Set editor.inlineSuggest.enabled: true.`); - workspace.getConfiguration("editor").update("inlineSuggest.enabled", true, ConfigurationTarget.Global, false); - break; - case "Settings": - commands.executeCommand("workbench.action.openSettings", "@id:editor.inlineSuggest.enabled"); - break; - } - }); -} - -function showInformationWhenDisconnected(modal: boolean = false) { - if (modal) { - const message = agent().getIssueDetail({ name: "connectionFailed" })?.message; - window - .showWarningMessage( - `Cannot connect to Tabby Server.`, - { - modal: true, - detail: message, - }, - "Settings", - "Online Help...", - ) - .then((selection) => { - switch (selection) { - case "Online Help...": - commands.executeCommand("tabby.openOnlineHelp"); - break; - case "Settings": - commands.executeCommand("tabby.openSettings"); - break; - } - }); - } else { - window.showWarningMessage(`Cannot connect to Tabby Server.`, "Detail", "Settings").then((selection) => { - switch (selection) { - case "Detail": - showInformationWhenDisconnected(true); - break; - case "Settings": - commands.executeCommand("tabby.openSettings"); - break; - } - }); - } -} - -function showInformationWhenUnauthorized() { - let message = "Tabby server requires authentication, "; - const currentToken = agent().getConfig()["server"]["token"].trim(); - if (currentToken.length > 0) { - message += ` but the current token is invalid.`; - } else { - message += ` please set your personal token.`; - } - window.showWarningMessage(message, "Set Credentials").then((selection) => { - switch (selection) { - case "Set Credentials": - commands.executeCommand("tabby.setApiToken"); - break; - } - }); -} - -/** @deprecated Tabby Cloud auth */ -function showInformationStartAuth(callbacks?: { onAuthStart?: () => void; onAuthEnd?: () => void }) { - window - .showWarningMessage( - "Tabby Server requires authorization. Continue to open authorization page in your browser.", - "Continue", - "Settings", - ) - .then((selection) => { - switch (selection) { - case "Continue": - commands.executeCommand("tabby.openAuthPage", callbacks); - break; - case "Settings": - commands.executeCommand("tabby.openSettings"); - } - }); -} - -/** @deprecated Tabby Cloud auth */ -function showInformationAuthSuccess() { - window.showInformationMessage("Congrats, you're authorized, start to use Tabby now."); -} - -/** @deprecated Tabby Cloud auth */ -function showInformationWhenStartAuthButAlreadyAuthorized() { - window.showInformationMessage("You are already authorized now."); -} - -/** @deprecated Tabby Cloud auth */ -function showInformationWhenAuthFailed() { - window.showWarningMessage("Cannot connect to server. Please check settings.", "Settings").then((selection) => { - switch (selection) { - case "Settings": - commands.executeCommand("tabby.openSettings"); - break; - } - }); -} - -function getHelpMessageForCompletionResponseTimeIssue() { - let helpMessageForRunningLargeModelOnCPU = ""; - const serverHealthState = agent().getServerHealthState(); - if (serverHealthState?.device === "cpu" && serverHealthState?.model?.match(/[0-9.]+B$/)) { - helpMessageForRunningLargeModelOnCPU += - `Your Tabby server is running model ${serverHealthState?.model} on CPU. ` + - "This model may be performing poorly due to its large parameter size, please consider trying smaller models or switch to GPU. " + - "You can find a list of recommend models in the online documentation.\n"; - } - let commonHelpMessage = ""; - const host = new URL(agent().getConfig().server.endpoint).host; - if (helpMessageForRunningLargeModelOnCPU.length == 0) { - commonHelpMessage += ` - The running model ${ - serverHealthState?.model ?? "" - } may be performing poorly due to its large parameter size. `; - commonHelpMessage += - "Please consider trying smaller models. You can find a list of recommend models in the online documentation.\n"; - } - if (!(host.startsWith("localhost") || host.startsWith("127.0.0.1"))) { - commonHelpMessage += " - A poor network connection. Please check your network and proxy settings.\n"; - commonHelpMessage += " - Server overload. Please contact your Tabby server administrator for assistance.\n"; - } - let message = ""; - if (helpMessageForRunningLargeModelOnCPU.length > 0) { - message += helpMessageForRunningLargeModelOnCPU + "\n"; - if (commonHelpMessage.length > 0) { - message += "Other possible causes of this issue: \n"; - message += commonHelpMessage; - } - } else { - // commonHelpMessage should not be empty here - message += "Possible causes of this issue: \n"; - message += commonHelpMessage; - } - return message; -} - -function showInformationWhenSlowCompletionResponseTime(modal: boolean = false) { - if (modal) { - const stats = agent().getIssueDetail({ name: "slowCompletionResponseTime" }) - ?.completionResponseStats; - let statsMessage = ""; - if (stats && stats["responses"] && stats["averageResponseTime"]) { - statsMessage = `The average response time of recent ${stats["responses"]} completion requests is ${Number( - stats["averageResponseTime"], - ).toFixed(0)}ms.\n\n`; - } - window - .showWarningMessage( - "Completion requests appear to take too much time.", - { - modal: true, - detail: statsMessage + getHelpMessageForCompletionResponseTimeIssue(), - }, - "Online Help...", - "Don't Show Again", - ) - .then((selection) => { - switch (selection) { - case "Online Help...": - commands.executeCommand("tabby.openOnlineHelp"); - break; - case "Don't Show Again": - commands.executeCommand("tabby.notifications.mute", "completionResponseTimeIssues"); - break; - } - }); - } else { - window - .showWarningMessage("Completion requests appear to take too much time.", "Detail", "Settings", "Don't Show Again") - .then((selection) => { - switch (selection) { - case "Detail": - showInformationWhenSlowCompletionResponseTime(true); - break; - case "Settings": - commands.executeCommand("tabby.openSettings"); - break; - case "Don't Show Again": - commands.executeCommand("tabby.notifications.mute", "completionResponseTimeIssues"); - break; - } - }); - } -} - -function showInformationWhenHighCompletionTimeoutRate(modal: boolean = false) { - if (modal) { - const stats = agent().getIssueDetail({ name: "highCompletionTimeoutRate" }) - ?.completionResponseStats; - let statsMessage = ""; - if (stats && stats["total"] && stats["timeouts"]) { - statsMessage = `${stats["timeouts"]} of ${stats["total"]} completion requests timed out.\n\n`; - } - window - .showWarningMessage( - "Most completion requests timed out.", - { - modal: true, - detail: statsMessage + getHelpMessageForCompletionResponseTimeIssue(), - }, - "Online Help...", - "Don't Show Again", - ) - .then((selection) => { - switch (selection) { - case "Online Help...": - commands.executeCommand("tabby.openOnlineHelp"); - break; - case "Don't Show Again": - commands.executeCommand("tabby.notifications.mute", "completionResponseTimeIssues"); - break; - } - }); - } else { - window - .showWarningMessage("Most completion requests timed out.", "Detail", "Settings", "Don't Show Again") - .then((selection) => { - switch (selection) { - case "Detail": - showInformationWhenHighCompletionTimeoutRate(true); - break; - case "Settings": - commands.executeCommand("tabby.openSettings"); - break; - case "Don't Show Again": - commands.executeCommand("tabby.notifications.mute", "completionResponseTimeIssues"); - break; - } - }); - } -} - -export const notifications = { - showInformationWhenInitializing, - showInformationWhenAutomaticTrigger, - showInformationWhenManualTrigger, - showInformationWhenManualTriggerLoading, - showInformationWhenInlineSuggestDisabled, - showInformationWhenDisconnected, - showInformationWhenUnauthorized, - showInformationStartAuth, - showInformationAuthSuccess, - showInformationWhenStartAuthButAlreadyAuthorized, - showInformationWhenAuthFailed, - showInformationWhenSlowCompletionResponseTime, - showInformationWhenHighCompletionTimeoutRate, -}; diff --git a/clients/vscode/src/terminal.ts b/clients/vscode/src/terminal.ts new file mode 100644 index 000000000000..871ad5a93a47 --- /dev/null +++ b/clients/vscode/src/terminal.ts @@ -0,0 +1,26 @@ +import { commands, env, window } from "vscode"; +import { TerminalContext } from "tabby-chat-panel/index"; + +export async function getTerminalContext(): Promise { + const activeTerminal = window.activeTerminal; + if (!activeTerminal) { + return; + } + const processId = await activeTerminal.processId; + + // Store current clipboard content to restore it later + const originalClipboardContent = await env.clipboard.readText(); + await commands.executeCommand("workbench.action.terminal.copySelection"); + const selectedText = await env.clipboard.readText(); + await env.clipboard.writeText(originalClipboardContent); + + if (!selectedText || selectedText.trim() === "") { + return; + } + return { + kind: "terminal", + name: activeTerminal.name, + processId: processId, + selection: selectedText, + }; +} diff --git a/clients/vscode/src/windowUtils.ts b/clients/vscode/src/windowUtils.ts new file mode 100644 index 000000000000..72721fc30706 --- /dev/null +++ b/clients/vscode/src/windowUtils.ts @@ -0,0 +1,67 @@ +import { TextEditor, window } from "vscode"; +import { Location } from "vscode-languageclient"; + +export function collectVisibleEditors(exceptActiveEditor = false, activeEditor?: TextEditor): Location[] { + let editors = window.visibleTextEditors + .filter((e) => e.document.fileName.startsWith("/")) + .map((editor) => { + if (!editor.visibleRanges[0]) { + return null; + } + return { + uri: editor.document.uri.toString(), + range: { + start: { + line: editor.visibleRanges[0].start.line, + character: editor.visibleRanges[0].start.character, + }, + end: { + line: editor.visibleRanges[0].end.line, + character: editor.visibleRanges[0].end.character, + }, + }, + } as Location; + }) + .filter((e): e is Location => e !== null) + .sort((a, b) => + a.uri === window.activeTextEditor?.document.uri.toString() + ? -1 + : b.uri === window.activeTextEditor?.document.uri.toString() + ? 1 + : 0, + ); + if (exceptActiveEditor) { + if (activeEditor && activeEditor.visibleRanges[0]) { + const range = activeEditor.visibleRanges[0]; + editors = editors.filter( + (e) => + e.uri !== activeEditor.document.uri.toString() || + e.range.start.line !== range.start.line || + e.range.start.character !== range.start.character || + e.range.end.line !== range.end.line || + e.range.end.character !== range.end.character, + ); + } + } + return editors; +} +export function collectActiveEditor(): Location | undefined { + const activeEditor = window.activeTextEditor; + //only return TextDocument editor + if (!activeEditor || !activeEditor.visibleRanges[0] || !activeEditor.document.fileName.startsWith("/")) { + return undefined; + } + return { + uri: activeEditor.document.uri.toString(), + range: { + start: { + line: activeEditor.visibleRanges[0].start.line, + character: activeEditor.visibleRanges[0].start.character, + }, + end: { + line: activeEditor.visibleRanges[0].end.line, + character: activeEditor.visibleRanges[0].end.character, + }, + }, + }; +} diff --git a/clients/vscode/tsconfig.build.json b/clients/vscode/tsconfig.build.json new file mode 100644 index 000000000000..ad64328a35fe --- /dev/null +++ b/clients/vscode/tsconfig.build.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noUnusedLocals": true + } +} diff --git a/clients/vscode/tsconfig.json b/clients/vscode/tsconfig.json index 37953d57d106..a61b3114e3db 100644 --- a/clients/vscode/tsconfig.json +++ b/clients/vscode/tsconfig.json @@ -1,15 +1,18 @@ { "compilerOptions": { "module": "commonjs", - "target": "ES2020", - "lib": ["ES2020", "dom"], + "lib": ["esnext", "webworker"], + "target": "es2020", "sourceMap": true, "strict": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true, "noUncheckedIndexedAccess": true, + "noUnusedLocals": false, "noUnusedParameters": true, - "allowSyntheticDefaultImports": true + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true }, - "include": ["./src"] + "include": ["./src/"] } diff --git a/clients/vscode/tsup.config.ts b/clients/vscode/tsup.config.ts index c89b1c7e84ec..e54bdfdfd5fc 100644 --- a/clients/vscode/tsup.config.ts +++ b/clients/vscode/tsup.config.ts @@ -1,41 +1,99 @@ -import { defineConfig } from "tsup"; +import { defineConfig, Options } from "tsup"; +import fs from "fs-extra"; +import path from "path"; +import { getInstalledPath } from "get-installed-path"; import { copy } from "esbuild-plugin-copy"; import { polyfillNode } from "esbuild-plugin-polyfill-node"; -import { dependencies } from "./package.json"; +import dedent from "dedent"; -export default () => [ - defineConfig({ +const banner = dedent` + /** + * Tabby VSCode Extension + * https://github.com/tabbyml/tabby/tree/main/clients/vscode + * Copyright (c) 2023-2024 TabbyML, Inc. + * Licensed under the Apache License 2.0. + */`; + +export default defineConfig(async (options: Options): Promise => { + const tabbyAgentDist = path + .join(await getInstalledPath("tabby-agent", { local: true }), "dist") + .replaceAll(path.sep, path.posix.sep); + const copyTabbyAgentTask: Options = { + name: "copy-tabby-agent", + entry: ["scripts/dummy.js"], + clean: true, + esbuildPlugins: [ + copy({ + assets: { from: `${tabbyAgentDist}/**`, to: path.join("dist", "tabby-agent") }, + resolveFrom: "cwd", + }), + ], + onSuccess: async () => { + await fs.remove(path.join(__dirname, "dist/dummy.js")); + }, + }; + const buildNodeTask: Options = { name: "node", entry: ["src/extension.ts"], outDir: "dist/node", platform: "node", target: "node18", - external: ["vscode"], - noExternal: Object.keys(dependencies), - esbuildPlugins: [ - copy({ - assets: [ - { - from: "../tabby-agent/dist/wasm/*.wasm", - to: "./wasm", - }, - ], - }), - ], - clean: true, - }), - defineConfig({ + sourcemap: true, + loader: { + ".html": "text", + }, + define: { + "process.env.IS_BROWSER": "false", + }, + treeshake: { + preset: "smallest", + moduleSideEffects: "no-external", + }, + external: ["vscode", "vscode-languageserver/browser"], + banner: { + js: banner, + }, + onSuccess: options.env?.["LAUNCH_ON_SUCCESS"] + ? `code --extensionDevelopmentPath=${__dirname} --disable-extensions` + : undefined, + }; + const buildBrowserTask: Options = { name: "browser", entry: ["src/extension.ts"], - outDir: "dist/web", + outDir: "dist/browser", platform: "browser", - external: ["vscode"], - noExternal: Object.keys(dependencies), + sourcemap: true, + loader: { + ".html": "text", + }, + define: { + "process.env.IS_BROWSER": "true", + }, + treeshake: { + preset: "smallest", + moduleSideEffects: "no-external", + }, + external: ["vscode", "vscode-languageserver/node"], esbuildPlugins: [ polyfillNode({ - polyfills: { fs: true }, + polyfills: {}, }), ], - clean: true, - }), -]; + banner: { + js: banner, + }, + onSuccess: options.env?.["LAUNCH_ON_SUCCESS"] + ? `vscode-test-web --extensionDevelopmentPath=${__dirname} --browserType=chromium --port=3000` + : undefined, + }; + + if (!options.platform) { + return [copyTabbyAgentTask, buildNodeTask, buildBrowserTask]; + } else if (options.platform == "node") { + return [copyTabbyAgentTask, buildNodeTask]; + } else if (options.platform == "browser") { + return [copyTabbyAgentTask, buildBrowserTask]; + } else { + throw new Error("Invalid platform."); + } +}); diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 000000000000..76bf2581c033 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,9 @@ +coverage: + status: + project: + default: + threshold: 0% + patch: + default: + target: 75% + threshold: 0% \ No newline at end of file diff --git a/crates/aim-downloader/Cargo.toml b/crates/aim-downloader/Cargo.toml index a3efba3108c3..b4cdc7739f23 100644 --- a/crates/aim-downloader/Cargo.toml +++ b/crates/aim-downloader/Cargo.toml @@ -19,7 +19,7 @@ reqwest = { workspace = true, features = ["stream"] } sha2 = "0.10.8" strfmt = "0.2.4" tokio = { workspace = true, features = ["full"] } -tokio-util = { version="0.7.10", features = ["full"] } +tokio-util = { workspace = true } url-parse = "1.0.7" [dev-dependencies] diff --git a/crates/aim-downloader/src/address.rs b/crates/aim-downloader/src/address.rs index f336ac08e174..7d56a931dda8 100644 --- a/crates/aim-downloader/src/address.rs +++ b/crates/aim-downloader/src/address.rs @@ -92,7 +92,7 @@ impl ParsedAddress { name = name + ":" + &port.to_string()[..]; } if server == name { - user = machine.login.clone(); + user.clone_from(&machine.login); pass = machine.password.clone().unwrap(); break; } diff --git a/crates/aim-downloader/src/error.rs b/crates/aim-downloader/src/error.rs index 03150c9805c6..d7845bb4af45 100644 --- a/crates/aim-downloader/src/error.rs +++ b/crates/aim-downloader/src/error.rs @@ -6,6 +6,12 @@ custom_error! { Sha256Mismatch = "Invalid sha256.", } +custom_error! { + pub DownloadError + Validate {source: ValidateError} = "{source}", + HttpError {name: String, code: String} = "Failed to download {name}: Server returned {code}", +} + custom_error! { pub HTTPHeaderError NotPresent = "Cannot find requested header.", @@ -13,7 +19,7 @@ custom_error! { impl From for std::io::Error { fn from(cause: ValidateError) -> std::io::Error { - std::io::Error::new(std::io::ErrorKind::Other, cause.to_string()) + std::io::Error::other(cause.to_string()) } } diff --git a/crates/aim-downloader/src/hash.rs b/crates/aim-downloader/src/hash.rs index d7544705d626..8a2470828531 100644 --- a/crates/aim-downloader/src/hash.rs +++ b/crates/aim-downloader/src/hash.rs @@ -9,7 +9,7 @@ impl HashChecker { pub fn check(filename: &str, expected_hash: &str) -> Result<(), ValidateError> { let mut result = Ok(()); if filename != "stdout" && (!expected_hash.is_empty()) { - let actual_hash = HashChecker::sha256sum(filename); + let actual_hash = HashChecker::sha256sum(filename)?; if actual_hash != expected_hash { result = Err(ValidateError::Sha256Mismatch); } @@ -22,15 +22,21 @@ impl HashChecker { result } - fn sha256sum(filename: &str) -> String { + fn sha256sum(filename: &str) -> Result { let mut hasher = Sha256::new(); - let mut file = fs::File::open(filename).unwrap(); - - io::copy(&mut file, &mut hasher).unwrap(); + let mut file = fs::File::open(filename).map_err(|e| { + println!("Can not open {filename}:\n {e}"); + ValidateError::Sha256Mismatch + })?; + + io::copy(&mut file, &mut hasher).map_err(|e| { + println!("Can not read {filename}:\n {e}"); + ValidateError::Sha256Mismatch + })?; let computed_hash = hasher.finalize(); drop(file); - format!("{computed_hash:x}") + Ok(format!("{computed_hash:x}")) } } @@ -61,7 +67,7 @@ mod tests { fn test_sha256sum_api() { let expected = "21d7847124bfb9d9a9d44af6f00d8003006c44b9ef9ba458b5d4d3fc5f81bde5"; - let actual = HashChecker::sha256sum("LICENCE.md"); + let actual = HashChecker::sha256sum("LICENCE.md").unwrap(); assert_eq!(actual, expected); } diff --git a/crates/aim-downloader/src/https.rs b/crates/aim-downloader/src/https.rs index 399535b58428..53b8f7b8633b 100644 --- a/crates/aim-downloader/src/https.rs +++ b/crates/aim-downloader/src/https.rs @@ -6,7 +6,12 @@ use reqwest::Client; use tokio_util::io::ReaderStream; use crate::{ - address::ParsedAddress, bar::WrappedBar, consts::*, error::ValidateError, hash::HashChecker, io, + address::ParsedAddress, + bar::WrappedBar, + consts::*, + error::{DownloadError, ValidateError}, + hash::HashChecker, + io, }; pub struct HTTPSHandler; @@ -16,9 +21,9 @@ impl HTTPSHandler { output: &str, bar: &mut WrappedBar, expected_sha256: &str, - ) -> Result<(), ValidateError> { + ) -> Result<(), DownloadError> { HTTPSHandler::_get(input, output, bar).await?; - HashChecker::check(output, expected_sha256) + Ok(HashChecker::check(output, expected_sha256)?) } pub async fn put(input: &str, output: &str, mut bar: WrappedBar) -> Result<(), ValidateError> { @@ -113,11 +118,11 @@ impl HTTPSHandler { Ok(res) } - async fn _get(input: &str, output: &str, bar: &mut WrappedBar) -> Result<(), ValidateError> { + async fn _get(input: &str, output: &str, bar: &mut WrappedBar) -> Result<(), DownloadError> { let parsed_address = ParsedAddress::parse_address(input, bar.silent); let (mut out, mut downloaded) = io::get_output(output, bar.silent); - let res = Client::new() + let mut request = Client::new() .get(input) .header( "Range", @@ -126,19 +131,30 @@ impl HTTPSHandler { .header( reqwest::header::USER_AGENT, reqwest::header::HeaderValue::from_static(CLIENT_ID), - ) - .basic_auth(parsed_address.username, Some(parsed_address.password)) + ); + + if parsed_address.password != "anonymous" { + request = request.basic_auth(parsed_address.username, Some(parsed_address.password)); + } + + let res = request .send() .await - .map_err(|_| format!("Failed to GET from {} to {}", &input, &output)) - .unwrap(); + .and_then(|r| r.error_for_status()) + .map_err(|e| DownloadError::HttpError { + name: input.into(), + code: e.to_string(), + })?; + let total_size = downloaded + res.content_length().unwrap_or(0); bar.set_length(total_size); let mut stream = res.bytes_stream(); while let Some(item) = stream.next().await { - let chunk = item.map_err(|_| "Error while downloading.").unwrap(); + let chunk = item + .map_err(|e| format!("Error while downloading: {e:?}")) + .unwrap(); out.write_all(&chunk) .map_err(|_| "Error while writing to output.") .unwrap(); diff --git a/crates/aim-downloader/src/lib.rs b/crates/aim-downloader/src/lib.rs index 29286037163d..13a1b0246cb9 100644 --- a/crates/aim-downloader/src/lib.rs +++ b/crates/aim-downloader/src/lib.rs @@ -1,10 +1,10 @@ pub mod bar; +pub mod error; pub mod https; mod address; mod consts; -mod error; -mod hash; +pub mod hash; mod io; mod netrc; mod untildify; diff --git a/crates/hash-ids/Cargo.toml b/crates/hash-ids/Cargo.toml new file mode 100644 index 000000000000..6eee51434b76 --- /dev/null +++ b/crates/hash-ids/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "hash-ids" +version.workspace = true +edition.workspace = true +authors.workspace = true +homepage.workspace = true + +[dependencies] diff --git a/crates/hash-ids/src/README.md b/crates/hash-ids/src/README.md new file mode 100644 index 000000000000..8139132baf8b --- /dev/null +++ b/crates/hash-ids/src/README.md @@ -0,0 +1,3 @@ +# hash-ids + +Vendored fork from https://github.com/kardeiz/hash-ids (MIT License) \ No newline at end of file diff --git a/crates/hash-ids/src/lib.rs b/crates/hash-ids/src/lib.rs new file mode 100644 index 000000000000..7e6153406a09 --- /dev/null +++ b/crates/hash-ids/src/lib.rs @@ -0,0 +1,615 @@ +/*! +# hash-ids + +A fast, dependency-free implementation for [hashids](https://hashids.org/). + +## Usage + +```rust +fn main() { + let hash_ids = hash_ids::HashIds::builder() + .with_salt("Arbitrary string") + .finish(); + assert_eq!("neHrCa", hash_ids.encode(&[1, 2, 3])); + assert_eq!(Some(vec![1, 2, 3]), hash_ids.decode("neHrCa")); +} +``` +*/ + +use std::collections::VecDeque; + +const MIN_ALPHABET_LENGTH: usize = 16; +const SEPERATOR_DIV: f32 = 3.5; +const GUARD_DIV: f32 = 12.0; +const DEFAULT_SEPARATORS: &str = "cfhistuCFHISTU"; +const DEFAULT_ALPHABET: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; + +/// Error container, for custom alphabets that won't work +#[derive(Debug)] +pub enum Error { + AlphabetTooSmall, + ContainsSpace, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::AlphabetTooSmall => "Alphabet must contain at least 16 unique characters".fmt(f), + Error::ContainsSpace => "Alphabet may not contain spaces".fmt(f), + } + } +} + +impl std::error::Error for Error {} + +/// Builder for a `HashIds` +#[derive(Debug)] +pub struct HashIdsBuilder { + salt: Vec, + min_length: usize, +} + +impl HashIdsBuilder { + fn new() -> Self { + Self { + salt: vec![], + min_length: 0, + } + } +} + +/// Same as `HashIdsBuilder`, but with custom alphabet (which can fail) +#[derive(Debug)] +pub struct HashIdsBuilderWithCustomAlphabet { + inner: HashIdsBuilder, + alphabet: Vec, +} + +impl HashIdsBuilderWithCustomAlphabet { + /// Set the salt (arbitrary string) for the `HashIds` + pub fn with_salt(mut self, salt: &str) -> Self { + self.inner = self.inner.with_salt(salt); + self + } + + /// Set the minimum length for the encoded string + pub fn with_min_length(mut self, min_length: usize) -> Self { + self.inner = self.inner.with_min_length(min_length); + self + } + + /// Convert the builder to the finished `HashIds` + /// + /// Can fail if the custom alphabet won't work + pub fn finish(self) -> std::result::Result { + let Self { + inner: HashIdsBuilder { salt, min_length }, + mut alphabet, + } = self; + + let separators = DEFAULT_SEPARATORS + .chars() + .filter(|x| alphabet.contains(x)) + .collect::>(); + + alphabet = alphabet + .drain(..) + .filter(|x| !separators.contains(x)) + .collect(); + + alphabet = alphabet + .clone() + .into_iter() + .enumerate() + .filter(|(i, c)| alphabet.iter().position(|a| a == c) == Some(*i)) + .map(|(_, c)| c) + .collect(); + + if alphabet.len() + separators.len() < MIN_ALPHABET_LENGTH { + return Err(Error::AlphabetTooSmall); + } + + if alphabet.contains(&' ') { + return Err(Error::ContainsSpace); + } + + Ok(HashIds { + salt, + min_length, + alphabet, + separators, + guards: Vec::new(), + } + .finish()) + } +} + +impl HashIdsBuilder { + /// Set the salt (arbitrary string) for the `HashIds` + pub fn with_salt(mut self, salt: &str) -> Self { + self.salt = salt.chars().collect(); + self + } + + /// Set the minimum length for the encoded string + pub fn with_min_length(mut self, min_length: usize) -> Self { + self.min_length = min_length; + self + } + + /// Set the custom alphabet to use for encoding + pub fn with_alphabet(self, alphabet: &str) -> HashIdsBuilderWithCustomAlphabet { + HashIdsBuilderWithCustomAlphabet { + inner: self, + alphabet: alphabet.chars().collect(), + } + } + + /// Convert the builder to the finished `HashIds` + pub fn finish(self) -> HashIds { + let Self { salt, min_length } = self; + HashIds { + salt, + min_length, + alphabet: DEFAULT_ALPHABET + .chars() + .filter(|x| !DEFAULT_SEPARATORS.contains(*x)) + .collect(), + separators: DEFAULT_SEPARATORS.chars().collect(), + guards: Vec::new(), + } + .finish() + } +} + +/// The encoder/decoder container +#[derive(Debug, Clone)] +pub struct HashIds { + salt: Vec, + min_length: usize, + alphabet: Vec, + separators: Vec, + guards: Vec, +} + +impl HashIds { + /// Create a new `HashIdsBuilder` + pub fn builder() -> HashIdsBuilder { + HashIdsBuilder::new() + } + + fn finish(self) -> Self { + let Self { + salt, + min_length, + mut alphabet, + mut separators, + .. + } = self; + + let mut guards; + + separators = Self::reorder(&separators, &salt); + + let min_separators = Self::index_from_ratio(alphabet.len(), SEPERATOR_DIV); + + if let Some(num_missing_separators) = min_separators.checked_sub(separators.len()) { + if num_missing_separators > 0 { + let mut new_alphabet = alphabet.split_off(num_missing_separators); + std::mem::swap(&mut alphabet, &mut new_alphabet); + separators.append(&mut new_alphabet); + } + } + + alphabet = Self::reorder(&alphabet, &salt); + + let num_guards = Self::index_from_ratio(alphabet.len(), GUARD_DIV); + + if alphabet.len() < 3 { + guards = separators.split_off(num_guards); + std::mem::swap(&mut separators, &mut guards); + } else { + guards = alphabet.split_off(num_guards); + std::mem::swap(&mut alphabet, &mut guards); + } + + Self { + salt, + min_length, + alphabet, + separators, + guards, + } + } + + fn index_from_ratio(dividend: usize, divisor: f32) -> usize { + (dividend as f32 / divisor).ceil() as _ + } + + fn reorder(string: &[char], salt: &[char]) -> Vec { + let mut out = string.to_vec(); + + if salt.is_empty() { + return out; + } + + let mut int_sum = 0; + let mut index = 0; + + for i in (1..string.len()).rev() { + let int = u32::from(salt[index]) as usize; + int_sum += int; + let j = (int + index + int_sum) % i; + out.swap(i, j); + index = (index + 1) % salt.len(); + } + + out + } + + fn hash(mut number: usize, alphabet: &[char]) -> Vec { + let mut hashed = VecDeque::new(); + loop { + hashed.push_front(alphabet[number % alphabet.len()]); + number /= alphabet.len(); + if number == 0 { + break; + } + } + hashed.into_iter().collect() + } + + fn unhash>(hashed: I, alphabet: &[char]) -> Option { + let mut number: u64 = 0; + + for c in hashed { + let pos = alphabet.iter().position(|y| y == &c)? as u64; + number *= alphabet.len() as u64; + number += pos; + } + + Some(number) + } + + fn split>(string: I, splitters: &[char]) -> Vec> { + let mut parts = Vec::new(); + let mut buf = Vec::new(); + for c in string { + if splitters.contains(&c) { + parts.push(buf); + buf = Vec::new(); + } else { + buf.push(c); + } + } + parts.push(buf); + parts + } + + /// Encode a slice of numbers into a string + pub fn encode(&self, vals: &[u64]) -> String { + if vals.is_empty() { + return String::new(); + } + + let mut alphabet = self.alphabet.clone(); + + let vals_hash = vals + .iter() + .enumerate() + .fold(0, |acc, (i, x)| acc + ((*x as usize) % (i + 100))); + + let lottery = self.alphabet[vals_hash % self.alphabet.len()]; + let mut encoded = vec![lottery]; + + for (i, mut val) in vals.iter().map(|x| *x as usize).enumerate() { + let alphabet_salt = std::iter::once(lottery) + .chain(self.salt.iter().copied()) + .chain(alphabet.iter().copied()) + .take(alphabet.len()) + .collect::>(); + + alphabet = Self::reorder(&alphabet, &alphabet_salt); + let mut last = Self::hash(val, &alphabet); + val %= (u32::from(last[0]) as usize) + i; + encoded.append(&mut last); + encoded.push(self.separators[val % self.separators.len()]); + } + + let _ = encoded.pop(); + + if encoded.len() >= self.min_length { + encoded.into_iter().collect::() + } else { + let mut encoded = encoded.into_iter().collect::>(); + + let mut guard_index = (vals_hash + u32::from(encoded[0]) as usize) % self.guards.len(); + encoded.push_front(self.guards[guard_index]); + + if encoded.len() < self.min_length { + guard_index = (vals_hash + u32::from(encoded[2]) as usize) % self.guards.len(); + encoded.push_back(self.guards[guard_index]); + } + + let split_at = alphabet.len() / 2; + + while encoded.len() < self.min_length { + alphabet = Self::reorder(&alphabet, &alphabet); + + for c in alphabet[split_at..].iter().copied().rev() { + encoded.push_front(c); + } + for c in alphabet[..split_at].iter().copied() { + encoded.push_back(c); + } + if let Some(excess) = encoded.len().checked_sub(self.min_length) { + if excess > 0 { + let from_index = excess / 2; + return encoded + .drain(from_index..from_index + self.min_length) + .collect::(); + } + } + } + + encoded.into_iter().collect::() + } + } + + /// Decode a string into a `Vec` of numbers + /// + /// Returns `None`` if the string is not a valid hash + pub fn decode(&self, hash_str: &str) -> Option> { + if hash_str.is_empty() { + return Some(vec![]); + } + + let mut alphabet = self.alphabet.clone(); + + let mut parts = Self::split(hash_str.chars(), &self.guards); + + let mut hash_str = if parts.len() >= 2 && parts.len() <= 3 { + parts.remove(1) + } else if !parts.is_empty() { + parts.remove(0) + } else { + return None; + }; + + let lottery = hash_str.remove(0); + + let parts = Self::split(hash_str.iter().copied(), &self.separators); + + let mut out = Vec::with_capacity(parts.len()); + + for part in parts { + let alphabet_salt = std::iter::once(lottery) + .chain(self.salt.iter().copied()) + .chain(alphabet.iter().copied()) + .take(alphabet.len()) + .collect::>(); + alphabet = Self::reorder(&alphabet, &alphabet_salt); + + out.push(Self::unhash(part.iter().copied(), &alphabet)?) + } + + Some(out) + } + + /// Decode a string into a `Vec` of numbers + /// + /// May panic if `hash_str` was not created with the current configuration + #[cfg(test)] + fn decode_or_die(&self, hash_str: &str) -> Vec { + self.decode(hash_str).unwrap() + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_small_alphabet_with_no_repeating_characters() { + assert!(HashIds::builder() + .with_alphabet("abcdefghijklmno") + .finish() + .is_err()); + } + + #[test] + fn test_small_alphabet_with_repeating_characters() { + assert!(HashIds::builder() + .with_alphabet("abcdecfghijklbmnoa") + .finish() + .is_err()); + } + + #[test] + fn test_empty() { + let hash_ids = HashIds::builder().finish(); + assert_eq!("", hash_ids.encode(&[])); + assert_eq!(Vec::::new(), hash_ids.decode_or_die("")) + } + + #[test] + fn test_default_salt() { + let hash_ids = HashIds::builder().finish(); + assert_eq!("o2fXhV", hash_ids.encode(&[1, 2, 3])); + assert_eq!(vec![1, 2, 3], hash_ids.decode_or_die("o2fXhV")); + } + + #[test] + fn test_single_number() { + let hash_ids = HashIds::builder().finish(); + + assert_eq!("j0gW", hash_ids.encode(&[12345])); + assert_eq!("jR", hash_ids.encode(&[1])); + assert_eq!("Lw", hash_ids.encode(&[22])); + assert_eq!("Z0E", hash_ids.encode(&[333])); + assert_eq!("w0rR", hash_ids.encode(&[9999])); + + assert_eq!(vec![12345], hash_ids.decode_or_die("j0gW")); + assert_eq!(vec![1], hash_ids.decode_or_die("jR")); + assert_eq!(vec![22], hash_ids.decode_or_die("Lw")); + assert_eq!(vec![333], hash_ids.decode_or_die("Z0E")); + assert_eq!(vec![9999], hash_ids.decode_or_die("w0rR")); + } + + #[test] + fn test_multiple_numbers() { + let hash_ids = HashIds::builder().finish(); + + assert_eq!("vJvi7On9cXGtD", hash_ids.encode(&[683, 94108, 123, 5])); + assert_eq!("o2fXhV", hash_ids.encode(&[1, 2, 3])); + assert_eq!("xGhmsW", hash_ids.encode(&[2, 4, 6])); + assert_eq!("3lKfD", hash_ids.encode(&[99, 25])); + + assert_eq!( + vec![683, 94108, 123, 5], + hash_ids.decode_or_die("vJvi7On9cXGtD") + ); + assert_eq!(vec![1, 2, 3], hash_ids.decode_or_die("o2fXhV")); + assert_eq!(vec![2, 4, 6], hash_ids.decode_or_die("xGhmsW")); + assert_eq!(vec![99, 25], hash_ids.decode_or_die("3lKfD")); + } + + #[test] + fn test_salt() { + let hash_ids = HashIds::builder().with_salt("Arbitrary string").finish(); + + assert_eq!("QWyf8yboH7KT2", hash_ids.encode(&[683, 94108, 123, 5])); + assert_eq!("neHrCa", hash_ids.encode(&[1, 2, 3])); + assert_eq!("LRCgf2", hash_ids.encode(&[2, 4, 6])); + assert_eq!("JOMh1", hash_ids.encode(&[99, 25])); + + assert_eq!( + vec![683, 94108, 123, 5], + hash_ids.decode_or_die("QWyf8yboH7KT2") + ); + assert_eq!(vec![1, 2, 3], hash_ids.decode_or_die("neHrCa")); + assert_eq!(vec![2, 4, 6], hash_ids.decode_or_die("LRCgf2")); + assert_eq!(vec![99, 25], hash_ids.decode_or_die("JOMh1")); + } + + #[test] + fn test_alphabet() { + let hash_ids = HashIds::builder().with_alphabet(r##"!"#%&',-/0123456789:;<=>ABCDEFGHIJKLMNOPQRSTUVWXYZ_`abcdefghijklmnopqrstuvwxyz~"##).finish().unwrap(); + + assert_eq!("_nJUNTVU3", hash_ids.encode(&[2839, 12, 32, 5])); + assert_eq!("7xfYh2", hash_ids.encode(&[1, 2, 3])); + assert_eq!("Z6R>", hash_ids.encode(&[23832])); + assert_eq!("AYyIB", hash_ids.encode(&[99, 25])); + + assert_eq!(vec![2839, 12, 32, 5], hash_ids.decode_or_die("_nJUNTVU3")); + assert_eq!(vec![1, 2, 3], hash_ids.decode_or_die("7xfYh2")); + assert_eq!(vec![23832], hash_ids.decode_or_die("Z6R>")); + assert_eq!(vec![99, 25], hash_ids.decode_or_die("AYyIB")); + } + + #[test] + fn test_min_length() { + let hash_ids = HashIds::builder().with_min_length(25).finish(); + + assert_eq!( + "pO3K69b86jzc6krI416enr2B5", + hash_ids.encode(&[7452, 2967, 21401]) + ); + assert_eq!("gyOwl4B97bo2fXhVaDR0Znjrq", hash_ids.encode(&[1, 2, 3])); + assert_eq!("Nz7x3VXyMYerRmWeOBQn6LlRG", hash_ids.encode(&[6097])); + assert_eq!("k91nqP3RBe3lKfDaLJrvy8XjV", hash_ids.encode(&[99, 25])); + + assert_eq!( + vec![7452, 2967, 21401], + hash_ids.decode_or_die("pO3K69b86jzc6krI416enr2B5") + ); + assert_eq!( + vec![1, 2, 3], + hash_ids.decode_or_die("gyOwl4B97bo2fXhVaDR0Znjrq") + ); + assert_eq!( + vec![6097], + hash_ids.decode_or_die("Nz7x3VXyMYerRmWeOBQn6LlRG") + ); + assert_eq!( + vec![99, 25], + hash_ids.decode_or_die("k91nqP3RBe3lKfDaLJrvy8XjV") + ); + } + + #[test] + fn test_all_parameters() { + let hash_ids = HashIds::builder() + .with_salt("arbitrary salt") + .with_alphabet("abcdefghijklmnopqrstuvwxyz") + .with_min_length(16) + .finish() + .unwrap(); + + assert_eq!("wygqxeunkatjgkrw", hash_ids.encode(&[7452, 2967, 21401])); + assert_eq!("pnovxlaxuriowydb", hash_ids.encode(&[1, 2, 3])); + assert_eq!("jkbgxljrjxmlaonp", hash_ids.encode(&[60125])); + assert_eq!("erdjpwrgouoxlvbx", hash_ids.encode(&[99, 25])); + + assert_eq!( + vec![7452, 2967, 21401], + hash_ids.decode_or_die("wygqxeunkatjgkrw") + ); + assert_eq!(vec![1, 2, 3], hash_ids.decode_or_die("pnovxlaxuriowydb")); + assert_eq!(vec![60125], hash_ids.decode_or_die("jkbgxljrjxmlaonp")); + assert_eq!(vec![99, 25], hash_ids.decode_or_die("erdjpwrgouoxlvbx")); + } + + #[test] + fn test_alphabet_without_standard_separators() { + let hash_ids = HashIds::builder() + .with_alphabet("abdegjklmnopqrvwxyzABDEGJKLMNOPQRVWXYZ1234567890") + .finish() + .unwrap(); + + assert_eq!("X50Yg6VPoAO4", hash_ids.encode(&[7452, 2967, 21401])); + assert_eq!("GAbDdR", hash_ids.encode(&[1, 2, 3])); + assert_eq!("5NMPD", hash_ids.encode(&[60125])); + assert_eq!("yGya5", hash_ids.encode(&[99, 25])); + + assert_eq!( + vec![7452, 2967, 21401], + hash_ids.decode_or_die("X50Yg6VPoAO4") + ); + assert_eq!(vec![1, 2, 3], hash_ids.decode_or_die("GAbDdR")); + assert_eq!(vec![60125], hash_ids.decode_or_die("5NMPD")); + assert_eq!(vec![99, 25], hash_ids.decode_or_die("yGya5")); + } + + #[test] + fn test_alphabet_with_two_standard_separators() { + let hash_ids = HashIds::builder() + .with_alphabet("abdegjklmnopqrvwxyzABDEGJKLMNOPQRVWXYZ1234567890uC") + .finish() + .unwrap(); + + assert_eq!("GJNNmKYzbPBw", hash_ids.encode(&[7452, 2967, 21401])); + assert_eq!("DQCXa4", hash_ids.encode(&[1, 2, 3])); + assert_eq!("38V1D", hash_ids.encode(&[60125])); + assert_eq!("373az", hash_ids.encode(&[99, 25])); + + assert_eq!( + vec![7452, 2967, 21401], + hash_ids.decode_or_die("GJNNmKYzbPBw") + ); + assert_eq!(vec![1, 2, 3], hash_ids.decode_or_die("DQCXa4")); + assert_eq!(vec![60125], hash_ids.decode_or_die("38V1D")); + assert_eq!(vec![99, 25], hash_ids.decode_or_die("373az")); + } + + #[test] + fn test_long() { + let hash_ids = HashIds::builder().with_salt("arbitrary salt").finish(); + + let up_to_100 = (1..=100).collect::>(); + + assert_eq!("GaHMFdtBf0ceClsgiVIjSrUKh1TyupHXFwt5fQcXCwspilIvSYUQhoT2u0HMF5tVfVc9CEsYiqI6SDUdhyTauBHPaF66t8pfGXcnoC2Vs0ei1YIy8SZ2UPehlyTKZuYJHQyF6wtZafR7c52Cn6skLigpIbGSD7UVkhyZT9xukeHBnFR1tJ2f2ocnVCkVsEQia6IBbSDEUX3hB6TaBuDbHxkFd7tykfrjc55Crjs2GigrIx5SpKUKjhVRTdQuX7H9K", hash_ids.encode(&up_to_100)); + assert_eq!(up_to_100, hash_ids.decode_or_die("GaHMFdtBf0ceClsgiVIjSrUKh1TyupHXFwt5fQcXCwspilIvSYUQhoT2u0HMF5tVfVc9CEsYiqI6SDUdhyTauBHPaF66t8pfGXcnoC2Vs0ei1YIy8SZ2UPehlyTKZuYJHQyF6wtZafR7c52Cn6skLigpIbGSD7UVkhyZT9xukeHBnFR1tJ2f2ocnVCkVsEQia6IBbSDEUX3hB6TaBuDbHxkFd7tykfrjc55Crjs2GigrIx5SpKUKjhVRTdQuX7H9K")); + } +} diff --git a/crates/http-api-bindings/Cargo.toml b/crates/http-api-bindings/Cargo.toml index f996842e3a8c..f3f6163100e4 100644 --- a/crates/http-api-bindings/Cargo.toml +++ b/crates/http-api-bindings/Cargo.toml @@ -1,17 +1,27 @@ [package] name = "http-api-bindings" -version = "0.8.0" -edition = "2021" +version.workspace = true +edition.workspace = true +authors.workspace = true +homepage.workspace = true [dependencies] anyhow.workspace = true +async-stream.workspace = true async-trait.workspace = true futures.workspace = true -reqwest = { workspace = true, features = ["json"] } -serde = { workspace = true, features = ["derive"] } +reqwest.workspace = true +reqwest-eventsource.workspace = true +serde.workspace = true serde_json = { workspace = true } -tabby-inference = { version = "0.8.0", path = "../tabby-inference" } +tabby-common = { path = "../tabby-common" } +tabby-inference = { path = "../tabby-inference" } +ollama-api-bindings = { path = "../ollama-api-bindings" } +async-openai-alt.workspace = true +tokio.workspace = true +tokio-retry.workspace = true tracing.workspace = true +leaky-bucket = "1.1.2" [dev-dependencies] -tokio = { workspace = true, features = ["full"] } +tokio = { workspace = true, features = ["rt", "macros"] } diff --git a/crates/http-api-bindings/README.md b/crates/http-api-bindings/README.md deleted file mode 100644 index 8717dd0d7646..000000000000 --- a/crates/http-api-bindings/README.md +++ /dev/null @@ -1,19 +0,0 @@ -## Examples - -## Vertex.AI - -```bash -export MODEL_ID="code-gecko" -export PROJECT_ID="$(gcloud config get project)" -export API_ENDPOINT="https://us-central1-aiplatform.googleapis.com/v1/projects/${PROJECT_ID}/locations/us-central1/publishers/google/models/${MODEL_ID}:predict" -export AUTHORIZATION="Bearer $(gcloud auth print-access-token)" - -cargo run serve --device experimental-http --model "{\"kind\": \"vertex-ai\", \"api_endpoint\": \"$API_ENDPOINT\", \"authorization\": \"$AUTHORIZATION\"}" -``` - -### OpenAI - -```bash -cargo run serve --device experimental-http \ - --model '{"kind": "openai", "model_name": "codellama/CodeLlama-70b-Instruct-hf", "api_endpoint": "http://host/v1/completions", "prompt_template": "{prefix}"}' -``` \ No newline at end of file diff --git a/crates/http-api-bindings/src/chat/mod.rs b/crates/http-api-bindings/src/chat/mod.rs new file mode 100644 index 000000000000..58e09d295bef --- /dev/null +++ b/crates/http-api-bindings/src/chat/mod.rs @@ -0,0 +1,54 @@ +use std::sync::Arc; + +use async_openai_alt::config::OpenAIConfig; +use tabby_common::config::HttpModelConfig; +use tabby_inference::{ChatCompletionStream, ExtendedOpenAIConfig}; + +use super::rate_limit; +use crate::{create_reqwest_client, AZURE_API_VERSION}; + +pub async fn create(model: &HttpModelConfig) -> Arc { + let api_endpoint = model + .api_endpoint + .as_deref() + .expect("api_endpoint is required"); + + let engine: Box = match model.kind.as_str() { + "azure/chat" => { + let config = async_openai_alt::config::AzureConfig::new() + .with_api_base(api_endpoint) + .with_api_key(model.api_key.clone().unwrap_or_default()) + .with_api_version(AZURE_API_VERSION.to_string()) + .with_deployment_id(model.model_name.as_deref().expect("Model name is required")); + Box::new( + async_openai_alt::Client::with_config(config) + .with_http_client(create_reqwest_client(api_endpoint)), + ) + } + "openai/chat" | "mistral/chat" => { + let config = OpenAIConfig::default() + .with_api_base(api_endpoint) + .with_api_key(model.api_key.clone().unwrap_or_default()); + + let mut builder = ExtendedOpenAIConfig::builder(); + builder + .base(config) + .kind(model.kind.clone()) + .supported_models(model.supported_models.clone()) + .model_name(model.model_name.as_deref().expect("Model name is required")); + + Box::new( + async_openai_alt::Client::with_config( + builder.build().expect("Failed to build config"), + ) + .with_http_client(create_reqwest_client(api_endpoint)), + ) + } + _ => panic!("Unsupported model kind: {}", model.kind), + }; + + Arc::new(rate_limit::new_chat( + engine, + model.rate_limit.request_per_minute, + )) +} diff --git a/crates/http-api-bindings/src/completion/llama.rs b/crates/http-api-bindings/src/completion/llama.rs new file mode 100644 index 000000000000..ffec7ea86005 --- /dev/null +++ b/crates/http-api-bindings/src/completion/llama.rs @@ -0,0 +1,90 @@ +use async_stream::stream; +use async_trait::async_trait; +use futures::{stream::BoxStream, StreamExt}; +use reqwest_eventsource::{Event, EventSource}; +use serde::{Deserialize, Serialize}; +use tabby_inference::{CompletionOptions, CompletionStream}; + +use crate::create_reqwest_client; + +pub struct LlamaCppEngine { + client: reqwest::Client, + api_endpoint: String, + api_key: Option, +} + +impl LlamaCppEngine { + pub fn create(api_endpoint: &str, api_key: Option) -> Box { + let client = create_reqwest_client(api_endpoint); + + Box::new(Self { + client, + api_endpoint: format!("{api_endpoint}/completion"), + api_key, + }) + } +} + +#[derive(Serialize, Debug)] +struct CompletionRequest { + seed: u64, + prompt: String, + n_predict: i32, + temperature: f32, + stream: bool, + penalty_last_n: i32, + presence_penalty: f32, +} + +#[derive(Deserialize)] +struct CompletionResponseChunk { + content: String, + stop: bool, +} + +#[async_trait] +impl CompletionStream for LlamaCppEngine { + async fn generate( + &self, + prompt: &str, + options: CompletionOptions, + ) -> BoxStream<'life0, String> { + // Always use streaming mode in generate method + let request_body = CompletionRequest { + seed: options.seed, + prompt: prompt.to_owned(), + n_predict: options.max_decoding_tokens, + temperature: options.sampling_temperature, + stream: true, + penalty_last_n: 0, + presence_penalty: options.presence_penalty, + }; + + let mut request = self.client.post(&self.api_endpoint).json(&request_body); + if let Some(api_key) = &self.api_key { + request = request.bearer_auth(api_key); + } + + let s = stream! { + let mut es = EventSource::new(request).expect("Failed to create event source"); + while let Some(event) = es.next().await { + match event { + Ok(Event::Open) => {} + Ok(Event::Message(message)) => { + let x: CompletionResponseChunk = serde_json::from_str(&message.data).unwrap(); + yield x.content.clone(); + if x.stop { + break; + } + } + Err(_) => { + // StreamEnd + break; + } + } + } + }; + + Box::pin(s) + } +} diff --git a/crates/http-api-bindings/src/completion/mistral.rs b/crates/http-api-bindings/src/completion/mistral.rs new file mode 100644 index 000000000000..3006a7ae0a51 --- /dev/null +++ b/crates/http-api-bindings/src/completion/mistral.rs @@ -0,0 +1,117 @@ +use async_stream::stream; +use async_trait::async_trait; +use futures::{stream::BoxStream, StreamExt}; +use reqwest_eventsource::{Event, EventSource}; +use serde::{Deserialize, Serialize}; +use tabby_inference::{CompletionOptions, CompletionStream}; + +use super::split_fim_prompt; + +pub struct MistralFIMEngine { + client: reqwest::Client, + api_endpoint: String, + api_key: String, + model_name: String, +} + +const DEFAULT_API_ENDPOINT: &str = "https://api.mistral.ai"; + +impl MistralFIMEngine { + pub fn create( + api_endpoint: Option<&str>, + api_key: Option, + model_name: Option, + ) -> Box { + let client = reqwest::Client::new(); + let model_name = model_name.unwrap_or("codestral-latest".into()); + let api_key = api_key.expect("API key is required for mistral/completion"); + + Box::new(Self { + client, + model_name, + api_endpoint: format!( + "{}/v1/fim/completions", + api_endpoint.unwrap_or(DEFAULT_API_ENDPOINT) + ), + api_key, + }) + } +} + +#[derive(Serialize)] +struct FIMRequest { + prompt: String, + suffix: Option, + model: String, + temperature: f32, + max_tokens: i32, + stream: bool, + random_seed: u64, +} + +#[derive(Deserialize)] +struct FIMResponseChunk { + choices: Vec, +} + +#[derive(Deserialize)] +struct FIMResponseChoice { + delta: FIMResponseDelta, + finish_reason: Option, +} + +#[derive(Deserialize)] +struct FIMResponseDelta { + content: String, +} + +#[async_trait] +impl CompletionStream for MistralFIMEngine { + async fn generate( + &self, + prompt: &str, + options: CompletionOptions, + ) -> BoxStream<'life0, String> { + let (prompt, suffix) = split_fim_prompt(prompt); + let request = FIMRequest { + prompt: prompt.to_owned(), + suffix: suffix.map(|x| x.to_owned()), + model: self.model_name.clone(), + max_tokens: options.max_decoding_tokens, + temperature: options.sampling_temperature, + stream: true, + random_seed: options.seed, + }; + + let request = self + .client + .post(&self.api_endpoint) + .bearer_auth(&self.api_key) + .json(&request); + + let s = stream! { + let mut es = EventSource::new(request).expect("Failed to create event source"); + while let Some(event) = es.next().await { + match event { + Ok(Event::Open) => {} + Ok(Event::Message(message)) => { + let x: FIMResponseChunk = serde_json::from_str(&message.data).expect("Failed to parse response"); + if let Some(choice) = x.choices.first() { + yield choice.delta.content.clone(); + + if choice.finish_reason.is_some() { + break; + } + } + } + Err(_) => { + // StreamEnd + break; + } + } + } + }; + + Box::pin(s) + } +} diff --git a/crates/http-api-bindings/src/completion/mod.rs b/crates/http-api-bindings/src/completion/mod.rs new file mode 100644 index 000000000000..982d21033724 --- /dev/null +++ b/crates/http-api-bindings/src/completion/mod.rs @@ -0,0 +1,100 @@ +mod llama; +mod mistral; +mod openai; + +use std::sync::Arc; + +use llama::LlamaCppEngine; +use mistral::MistralFIMEngine; +use openai::OpenAICompletionEngine; +use tabby_common::config::HttpModelConfig; +use tabby_inference::CompletionStream; + +use super::rate_limit; + +pub async fn create(model: &HttpModelConfig) -> Arc { + let engine = match model.kind.as_str() { + "llama.cpp/completion" => LlamaCppEngine::create( + model + .api_endpoint + .as_deref() + .expect("api_endpoint is required"), + model.api_key.clone(), + ), + "ollama/completion" => ollama_api_bindings::create_completion(model).await, + "mistral/completion" => MistralFIMEngine::create( + model.api_endpoint.as_deref(), + model.api_key.clone(), + model.model_name.clone(), + ), + x if OPENAI_LEGACY_COMPLETION_FIM_ALIASES.contains(&x) => OpenAICompletionEngine::create( + model.model_name.clone(), + model + .api_endpoint + .as_deref() + .expect("api_endpoint is required"), + model.api_key.clone(), + true, + ), + "openai/legacy_completion_no_fim" | "vllm/completion" => OpenAICompletionEngine::create( + model.model_name.clone(), + model + .api_endpoint + .as_deref() + .expect("api_endpoint is required"), + model.api_key.clone(), + false, + ), + unsupported_kind => { + panic!("Unsupported model kind for http completion: {unsupported_kind}") + } + }; + + Arc::new(rate_limit::new_completion( + engine, + model.rate_limit.request_per_minute, + )) +} + +const FIM_TOKEN: &str = "<|FIM|>"; +const FIM_TEMPLATE: &str = "{prefix}<|FIM|>{suffix}"; +const OPENAI_LEGACY_COMPLETION_FIM_ALIASES: [&str; 3] = [ + "openai/legacy_completion", + "openai/completion", + "deepseek/completion", +]; + +pub fn build_completion_prompt(model: &HttpModelConfig) -> (Option, Option) { + match model.kind.as_str() { + x if x == "mistral/completion" || OPENAI_LEGACY_COMPLETION_FIM_ALIASES.contains(&x) => { + (Some(FIM_TEMPLATE.to_owned()), None) + } + _ => (model.prompt_template.clone(), model.chat_template.clone()), + } +} + +fn split_fim_prompt(prompt: &str) -> (&str, Option<&str>) { + let parts = prompt.splitn(2, FIM_TOKEN).collect::>(); + (parts[0], parts.get(1).copied()) +} + +#[cfg(test)] +mod tests { + use std::vec; + + use super::*; + + #[test] + fn test_split_fim_prompt_no_fim() { + let no_fim = vec![ + ("prefix<|FIM|>suffix", ("prefix", Some("suffix"))), + ("prefix<|FIM|>", ("prefix", Some(""))), + ("<|FIM|>suffix", ("", Some("suffix"))), + ("<|FIM|>", ("", Some(""))), + ("prefix", ("prefix", None)), + ]; + for (input, expected) in no_fim { + assert_eq!(split_fim_prompt(input), expected); + } + } +} diff --git a/crates/http-api-bindings/src/completion/openai.rs b/crates/http-api-bindings/src/completion/openai.rs new file mode 100644 index 000000000000..3a4565ba77e1 --- /dev/null +++ b/crates/http-api-bindings/src/completion/openai.rs @@ -0,0 +1,124 @@ +use async_stream::stream; +use async_trait::async_trait; +use futures::{stream::BoxStream, StreamExt}; +use reqwest_eventsource::{Event, EventSource}; +use serde::{Deserialize, Serialize}; +use tabby_inference::{CompletionOptions, CompletionStream}; +use tracing::warn; + +use super::split_fim_prompt; + +pub struct OpenAICompletionEngine { + client: reqwest::Client, + model_name: String, + api_endpoint: String, + api_key: Option, + + /// OpenAI Completion API use suffix field in request when FIM is not supported, + /// support_fim is used to mark if FIM is supported, + /// provide a `openai/legacy_completion_no_fim` backend to use suffix field. + support_fim: bool, +} + +impl OpenAICompletionEngine { + pub fn create( + model_name: Option, + api_endpoint: &str, + api_key: Option, + support_fim: bool, + ) -> Box { + let model_name = model_name.expect("model_name is required for openai/completion"); + let client = reqwest::Client::new(); + + Box::new(Self { + client, + model_name, + api_endpoint: format!("{api_endpoint}/completions"), + api_key, + support_fim, + }) + } +} + +#[derive(Serialize)] +struct CompletionRequest { + model: String, + prompt: String, + suffix: Option, + max_tokens: i32, + temperature: f32, + stream: bool, + presence_penalty: f32, +} + +#[derive(Deserialize)] +struct CompletionResponseChunk { + choices: Vec, +} + +#[derive(Deserialize)] +struct CompletionResponseChoice { + text: String, + finish_reason: Option, +} + +#[async_trait] +impl CompletionStream for OpenAICompletionEngine { + async fn generate( + &self, + prompt: &str, + options: CompletionOptions, + ) -> BoxStream<'life0, String> { + let (prompt, suffix) = if self.support_fim { + split_fim_prompt(prompt) + } else { + (prompt, None) + }; + + let request = CompletionRequest { + model: self.model_name.clone(), + prompt: prompt.to_owned(), + suffix: suffix.map(|x| x.to_owned()), + max_tokens: options.max_decoding_tokens, + temperature: options.sampling_temperature, + stream: true, + presence_penalty: options.presence_penalty, + }; + + let mut request = self.client.post(&self.api_endpoint).json(&request); + if let Some(api_key) = &self.api_key { + request = request.bearer_auth(api_key); + } + + let s = stream! { + let mut es = EventSource::new(request).expect("Failed to create event source"); + while let Some(event) = es.next().await { + match event { + Ok(Event::Open) => {} + Ok(Event::Message(message)) => { + let x: CompletionResponseChunk = serde_json::from_str(&message.data).expect("Failed to parse response"); + if let Some(choice) = x.choices.first() { + yield choice.text.clone(); + + if choice.finish_reason.is_some() { + break; + } + } + } + Err(e) => { + match e { + reqwest_eventsource::Error::StreamEnded => {}, + reqwest_eventsource::Error::InvalidStatusCode(code, resp) => + warn!("Error in completion event source: {}, {}", + code, resp.text().await.unwrap_or_default().replace('\n', "")), + _ => warn!("Error in completion event source: {}", e), + } + break; + } + } + } + }; + + Box::pin(s) + } +} diff --git a/crates/http-api-bindings/src/embedding/azure.rs b/crates/http-api-bindings/src/embedding/azure.rs new file mode 100644 index 000000000000..ba628a6bbebd --- /dev/null +++ b/crates/http-api-bindings/src/embedding/azure.rs @@ -0,0 +1,123 @@ +use std::sync::Arc; + +use anyhow::Result; +use async_trait::async_trait; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use tabby_inference::Embedding; + +use crate::AZURE_API_VERSION; + +/// `AzureEmbeddingEngine` is responsible for interacting with Azure's Embedding API. +/// +/// **Note**: Currently, this implementation only supports the OpenAI API and specific API versions. +#[derive(Clone)] +pub struct AzureEmbeddingEngine { + client: Arc, + api_endpoint: String, + api_key: String, +} + +/// Structure representing the request body for embedding. +#[derive(Debug, Serialize)] +struct EmbeddingRequest { + input: String, +} + +/// Structure representing the response from the embedding API. +#[derive(Debug, Deserialize)] +struct EmbeddingResponse { + data: Vec, +} + +/// Structure representing individual embedding data. +#[derive(Debug, Deserialize)] +struct Data { + embedding: Vec, +} + +impl AzureEmbeddingEngine { + /// Creates a new instance of `AzureEmbeddingEngine`. + /// + /// **Note**: Currently, this implementation only supports the OpenAI API and specific API versions. + /// + /// # Parameters + /// + /// - `api_endpoint`: The base URL of the Azure Embedding API. + /// - `model_name`: The name of the deployed model, used to construct the deployment ID. + /// - `api_key`: Optional API key for authentication. + /// - `api_version`: Optional API version, defaults to "2023-05-15". + /// + /// # Returns + /// + /// A boxed instance that implements the `Embedding` trait. + pub fn create( + api_endpoint: &str, + model_name: &str, + api_key: Option<&str>, + ) -> Box { + let client = Client::new(); + let deployment_id = model_name; + // Construct the full endpoint URL for the Azure Embedding API + let azure_endpoint = format!( + "{}/openai/deployments/{}/embeddings", + api_endpoint.trim_end_matches('/'), + deployment_id + ); + + Box::new(Self { + client: Arc::new(client), + api_endpoint: azure_endpoint, + api_key: api_key.unwrap_or_default().to_owned(), + }) + } +} + +#[async_trait] +impl Embedding for AzureEmbeddingEngine { + /// Generates an embedding vector for the given prompt. + /// + /// **Note**: Currently, this implementation only supports the OpenAI API and specific API versions. + /// + /// # Parameters + /// + /// - `prompt`: The input text to generate embeddings for. + /// + /// # Returns + /// + /// A `Result` containing the embedding vector or an error. + async fn embed(&self, prompt: &str) -> Result> { + // Clone all necessary fields to ensure thread safety across await points + let api_endpoint = self.api_endpoint.clone(); + let api_key = self.api_key.clone(); + let api_version = AZURE_API_VERSION.to_string(); + let request = EmbeddingRequest { + input: prompt.to_owned(), + }; + + // Send a POST request to the Azure Embedding API + let response = self + .client + .post(&api_endpoint) + .query(&[("api-version", &api_version)]) + .header("api-key", &api_key) + .header("Content-Type", "application/json") + .json(&request) + .send() + .await?; + + // Check if the response status indicates success + if !response.status().is_success() { + let error_text = response.text().await?; + anyhow::bail!("Azure API error: {}", error_text); + } + + // Deserialize the response body into `EmbeddingResponse` + let embedding_response: EmbeddingResponse = response.json().await?; + embedding_response + .data + .first() + .map(|data| data.embedding.clone()) + .ok_or_else(|| anyhow::anyhow!("No embedding data received")) + } +} diff --git a/crates/http-api-bindings/src/embedding/llama.rs b/crates/http-api-bindings/src/embedding/llama.rs new file mode 100644 index 000000000000..246f776aad8b --- /dev/null +++ b/crates/http-api-bindings/src/embedding/llama.rs @@ -0,0 +1,195 @@ +use anyhow::anyhow; +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use tabby_inference::Embedding; +use tokio_retry::{ + strategy::{jitter, ExponentialBackoff}, + RetryIf, +}; +use tracing::{info_span, Instrument}; + +use crate::{create_reqwest_client, embedding_info_span}; + +pub struct LlamaCppEngine { + // Determine whether to use the legacy endpoint and response format. + // Llama.cpp has updated the endpoint from `/embedding` to `/embeddings`, + // and wrapped both the response and embedding in an array from b4357. + // + // Ref: https://github.com/ggml-org/llama.cpp/pull/10861 + before_b4356: bool, + + client: reqwest::Client, + api_endpoint: String, + api_key: Option, +} + +impl LlamaCppEngine { + pub fn create( + api_endpoint: &str, + api_key: Option, + before_b4356: bool, + ) -> Box { + let client = create_reqwest_client(api_endpoint); + + Box::new(Self { + before_b4356, + + client, + api_endpoint: api_endpoint.trim_end_matches("/").to_owned(), + api_key, + }) + } +} + +#[derive(Serialize)] +struct EmbeddingRequest { + content: String, +} + +#[derive(Deserialize)] +struct EmbeddingResponse { + embedding: Vec>, +} + +#[derive(Deserialize)] +struct EmbeddingLegacyResponse { + embedding: Vec, +} + +#[async_trait] +impl Embedding for LlamaCppEngine { + async fn embed(&self, prompt: &str) -> anyhow::Result> { + // llama.cpp requires a minimum of 4 tokens to generate an embedding. + // Typically, `\n` counts as one token, + // so we append enough characters to make up for the shortfall + // ref: https://github.com/ggml-org/llama.cpp/issues/6722 + // + // If tokenize failed, just use the prompt as is. + let prompt = match self.tokenize(prompt).await { + Ok(tokens) if tokens.len() < 4 => { + format!("{}{}", prompt, "\n".repeat(4 - tokens.len())) + } + _ => prompt.to_owned(), + }; + + let api_endpoint = if self.before_b4356 { + format!("{}/embedding", self.api_endpoint) + } else { + format!("{}/embeddings", self.api_endpoint) + }; + + // Occasionally, when the embedding server has been idle for a period, + // some of the concurrent initial requests to llama.cpp encounter problems, + // resulting in failures with `Connection reset by peer` or `Broken pipe`. + // + // This serves as a temporary solution to attempt the request up to three times. + // + // Track issue: https://github.com/ggml-org/llama.cpp/issues/11411 + let strategy = ExponentialBackoff::from_millis(100).map(jitter).take(3); + let response = RetryIf::spawn( + strategy, + || { + let request = EmbeddingRequest { + content: prompt.to_owned(), + }; + let mut request = self.client.post(&api_endpoint).json(&request); + if let Some(api_key) = &self.api_key { + request = request.bearer_auth(api_key); + } + + async move { + request + .send() + .instrument(embedding_info_span!("llamacpp")) + .await + } + }, + |e: &reqwest::Error| { + let message = format!("{e:?}"); + e.is_request() + && (message.contains("Connection reset by peer") + || message.contains("Broken pipe")) + }, + ) + .await?; + if response.status().is_server_error() { + let error = response.text().await?; + return Err(anyhow::anyhow!( + "Error from server: {}, prompt length: {}", + error, + prompt.len() + )); + } + + if self.before_b4356 { + let response = response.json::().await?; + Ok(response.embedding) + } else { + let response = response.json::>().await?; + Ok(response + .first() + .ok_or_else(|| anyhow!("Error from server: no embedding found"))? + .embedding + .first() + .ok_or_else(|| anyhow!("Error from server: no embedding found"))? + .clone()) + } + } +} + +#[derive(Serialize)] +struct TokenizeRequest { + content: String, +} + +#[derive(Deserialize)] +struct TokenizeResponse { + tokens: Vec, +} + +impl LlamaCppEngine { + async fn tokenize(&self, prompt: &str) -> anyhow::Result> { + let request = TokenizeRequest { + content: prompt.to_owned(), + }; + let mut request = self + .client + .post(format!("{}/tokenize", &self.api_endpoint)) + .json(&request); + if let Some(api_key) = &self.api_key { + request = request.bearer_auth(api_key); + } + + let response = request + .send() + .instrument(info_span!("tokenize", kind = "llamacpp")) + .await?; + if !response.status().is_success() { + let error = response.text().await?; + return Err(anyhow::anyhow!( + "Error from server: {}, prompt length: {}", + error, + prompt.len() + )); + } + + let response = response.json::().await?; + Ok(response.tokens) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// This unit test should only run manually when the server is running + /// curl -L https://huggingface.co/nomic-ai/nomic-embed-text-v1.5-GGUF/resolve/main/nomic-embed-text-v1.5.Q8_0.gguf -o ./models/nomic.gguf + /// ./server -m ./models/nomic.gguf --port 8000 --embedding + #[tokio::test] + #[ignore] + async fn test_embedding() { + let engine = LlamaCppEngine::create("http://localhost:8000", None, false); + let embedding = engine.embed("hello").await.unwrap(); + assert_eq!(embedding.len(), 768); + } +} diff --git a/crates/http-api-bindings/src/embedding/mistral.rs b/crates/http-api-bindings/src/embedding/mistral.rs new file mode 100644 index 000000000000..f5ce0f899a31 --- /dev/null +++ b/crates/http-api-bindings/src/embedding/mistral.rs @@ -0,0 +1,93 @@ +use std::sync::Arc; + +use anyhow::Result; +use async_trait::async_trait; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use tabby_inference::Embedding; + +/// `MistralEmbeddingEngine` is responsible for interacting with Mistral's Embedding API. +#[derive(Clone)] +pub struct MistralEmbeddingEngine { + client: Arc, + api_endpoint: String, + model_name: String, + api_key: String, +} + +/// Structure representing the request body for embedding. +#[derive(Debug, Serialize)] +struct EmbeddingRequest { + model: String, + output_dtype: String, + output_dimension: u32, + input: Vec, +} + +/// Structure representing the response from the embedding API. +#[derive(Debug, Deserialize)] +struct EmbeddingResponse { + data: Vec, +} + +/// Structure representing individual embedding data. +#[derive(Debug, Deserialize)] +struct Data { + embedding: Vec, +} + +impl MistralEmbeddingEngine { + pub fn create( + api_endpoint: Option<&str>, + model_name: Option<&str>, + api_key: Option<&str>, + ) -> Box { + Box::new(Self { + client: Arc::new(Client::new()), + api_endpoint: format!( + "{}/embeddings", + api_endpoint.unwrap_or("https://api.mistral.ai/v1") + ), + model_name: model_name.unwrap_or("codestral-embed").to_owned(), + api_key: api_key.unwrap_or_default().to_owned(), + }) + } +} + +#[async_trait] +impl Embedding for MistralEmbeddingEngine { + async fn embed(&self, prompt: &str) -> Result> { + // Clone all necessary fields to ensure thread safety across await points + let request = EmbeddingRequest { + model: self.model_name.clone(), + output_dtype: "float".to_string(), + // default as per https://docs.mistral.ai/capabilities/embeddings/code_embeddings/, max 3072 + output_dimension: 1536, + input: vec![prompt.to_owned()], + }; + + // Send a POST request to the Mistral Embedding API + let response = self + .client + .post(&self.api_endpoint) + .bearer_auth(&self.api_key) + .header("Content-Type", "application/json") + .json(&request) + .send() + .await?; + + // Check if the response status indicates success + if !response.status().is_success() { + let error_text = response.text().await?; + anyhow::bail!("Mistral API error: {}", error_text); + } + + // Deserialize the response body into `EmbeddingResponse` + let embedding_response: EmbeddingResponse = response.json().await?; + embedding_response + .data + .first() + .map(|data| data.embedding.clone()) + .ok_or_else(|| anyhow::anyhow!("No embedding data received")) + } +} diff --git a/crates/http-api-bindings/src/embedding/mod.rs b/crates/http-api-bindings/src/embedding/mod.rs new file mode 100644 index 000000000000..d18fba76e9b0 --- /dev/null +++ b/crates/http-api-bindings/src/embedding/mod.rs @@ -0,0 +1,86 @@ +mod azure; +mod llama; +mod mistral; +mod openai; + +use core::panic; +use std::sync::Arc; + +use azure::AzureEmbeddingEngine; +use llama::LlamaCppEngine; +use mistral::MistralEmbeddingEngine; +use openai::OpenAIEmbeddingEngine; +use tabby_common::config::HttpModelConfig; +use tabby_inference::Embedding; + +use super::rate_limit; + +pub async fn create(config: &HttpModelConfig) -> Arc { + let engine = match config.kind.as_str() { + "llama.cpp/embedding" => LlamaCppEngine::create( + config + .api_endpoint + .as_deref() + .expect("api_endpoint is required"), + config.api_key.clone(), + false, + ), + "llama.cpp/before_b4356_embedding" => LlamaCppEngine::create( + config + .api_endpoint + .as_deref() + .expect("api_endpoint is required"), + config.api_key.clone(), + true, + ), + "openai/embedding" => OpenAIEmbeddingEngine::create( + config + .api_endpoint + .as_deref() + .expect("api_endpoint is required"), + config.model_name.as_deref().unwrap_or_default(), + config.api_key.as_deref(), + ), + "ollama/embedding" => ollama_api_bindings::create_embedding(config).await, + "voyage/embedding" => OpenAIEmbeddingEngine::create( + config + .api_endpoint + .as_deref() + .unwrap_or("https://api.voyageai.com/v1"), + config + .model_name + .as_deref() + .expect("model_name must be set for voyage/embedding"), + config.api_key.as_deref(), + ), + "azure/embedding" => AzureEmbeddingEngine::create( + config + .api_endpoint + .as_deref() + .expect("api_endpoint is required for azure/embedding"), + config.model_name.as_deref().unwrap_or_default(), // Provide a default if model_name is optional + config.api_key.as_deref(), + ), + "mistral/embedding" => MistralEmbeddingEngine::create( + config.api_endpoint.as_deref(), + config.model_name.as_deref(), + config.api_key.as_deref(), + ), + unsupported_kind => panic!( + "Unsupported kind for http embedding model: {}", + unsupported_kind + ), + }; + + Arc::new(rate_limit::new_embedding( + engine, + config.rate_limit.request_per_minute, + )) +} + +#[macro_export] +macro_rules! embedding_info_span { + ($kind:expr) => { + tracing::info_span!("embedding", kind = $kind) + }; +} diff --git a/crates/http-api-bindings/src/embedding/openai.rs b/crates/http-api-bindings/src/embedding/openai.rs new file mode 100644 index 000000000000..5a0949b1199c --- /dev/null +++ b/crates/http-api-bindings/src/embedding/openai.rs @@ -0,0 +1,118 @@ +use anyhow::Context; +use async_trait::async_trait; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use tabby_inference::Embedding; +use tracing::Instrument; + +use crate::embedding_info_span; + +pub struct OpenAIEmbeddingEngine { + client: Client, + api_endpoint: String, + api_key: String, + model_name: String, +} + +impl OpenAIEmbeddingEngine { + pub fn create( + api_endpoint: &str, + model_name: &str, + api_key: Option<&str>, + ) -> Box { + let client = Client::new(); + Box::new(Self { + client, + api_endpoint: format!("{api_endpoint}/embeddings"), + api_key: api_key.unwrap_or_default().to_owned(), + model_name: model_name.to_owned(), + }) + } +} + +#[derive(Debug, Serialize)] +struct EmbeddingRequest { + input: Vec, + model: String, +} + +#[derive(Debug, Deserialize)] +struct EmbeddingResponse { + data: Vec, +} + +#[derive(Debug, Deserialize)] +struct EmbeddingData { + embedding: Vec, +} + +#[async_trait] +impl Embedding for OpenAIEmbeddingEngine { + async fn embed(&self, prompt: &str) -> anyhow::Result> { + let request = EmbeddingRequest { + input: vec![prompt.to_owned()], + model: self.model_name.clone(), + }; + + let request_builder = self + .client + .post(&self.api_endpoint) + .json(&request) + .bearer_auth(&self.api_key); + + let response = request_builder + .send() + .instrument(embedding_info_span!("openai")) + .await?; + + if !response.status().is_success() { + let status = response.status(); + let error = response.text().await?; + return Err(anyhow::anyhow!("Error {}: {}", status.as_u16(), error)); + } + + let response_body = response + .json::() + .await + .context("Failed to parse response body")?; + + response_body + .data + .into_iter() + .next() + .map(|data| data.embedding) + .ok_or_else(|| anyhow::anyhow!("No embedding data found")) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Make sure you have set the JINA_API_KEY environment variable before running the test + #[tokio::test] + #[ignore] + async fn test_jina_embedding() { + let api_key = std::env::var("JINA_API_KEY").expect("JINA_API_KEY must be set"); + let engine = OpenAIEmbeddingEngine::create( + "https://api.jina.ai/v1", + "jina-embeddings-v2-base-en", + Some(&api_key), + ); + let embedding = engine.embed("Hello, world!").await.unwrap(); + assert_eq!(embedding.len(), 768); + } + + #[tokio::test] + #[ignore] + async fn test_voyage_embedding() { + let api_key = std::env::var("VOYAGE_API_KEY").expect("VOYAGE_API_KEY must be set"); + let engine = OpenAIEmbeddingEngine::create( + "https://api.voyageai.com/v1", + "voyage-code-2", + Some(&api_key), + ); + let embedding = engine.embed("Hello, world!").await.unwrap(); + assert_eq!(embedding.len(), 1536); + } +} diff --git a/crates/http-api-bindings/src/lib.rs b/crates/http-api-bindings/src/lib.rs index b1f1efda7516..9b3d6f366053 100644 --- a/crates/http-api-bindings/src/lib.rs +++ b/crates/http-api-bindings/src/lib.rs @@ -1,43 +1,24 @@ -mod openai; -mod vertex_ai; +mod chat; +mod completion; +mod embedding; +mod rate_limit; -use std::sync::Arc; +pub use chat::create as create_chat; +pub use completion::{build_completion_prompt, create}; +pub use embedding::create as create_embedding; -use openai::OpenAIEngine; -use serde_json::Value; -use tabby_inference::TextGeneration; -use vertex_ai::VertexAIEngine; +fn create_reqwest_client(api_endpoint: &str) -> reqwest::Client { + let builder = reqwest::Client::builder(); -pub fn create(model: &str) -> (Arc, String) { - let params = serde_json::from_str(model).expect("Failed to parse model string"); - let kind = get_param(¶ms, "kind"); - if kind == "vertex-ai" { - let api_endpoint = get_param(¶ms, "api_endpoint"); - let authorization = get_param(¶ms, "authorization"); - let engine = VertexAIEngine::create(api_endpoint.as_str(), authorization.as_str()); - (Arc::new(engine), VertexAIEngine::prompt_template()) - } else if kind == "openai" { - let model_name = get_param(¶ms, "model_name"); - let api_endpoint = get_param(¶ms, "api_endpoint"); - let authorization = get_optional_param(¶ms, "authorization"); - let prompt_template = get_param(¶ms, "prompt_template"); - let engine = - OpenAIEngine::create(api_endpoint.as_str(), model_name.as_str(), authorization); - (Arc::new(engine), prompt_template) + let is_localhost = api_endpoint.starts_with("http://localhost") + || api_endpoint.starts_with("http://127.0.0.1"); + let builder = if is_localhost { + builder.no_proxy() } else { - panic!("Only vertex_ai and openai are supported for http backend"); - } -} + builder + }; -fn get_param(params: &Value, key: &str) -> String { - params - .get(key) - .unwrap_or_else(|| panic!("Missing {} field", key)) - .as_str() - .expect("Type unmatched") - .to_string() + builder.build().unwrap() } -fn get_optional_param(params: &Value, key: &str) -> Option { - params.get(key).map(|x| x.to_string()) -} +static AZURE_API_VERSION: &str = "2024-02-01"; diff --git a/crates/http-api-bindings/src/openai.rs b/crates/http-api-bindings/src/openai.rs deleted file mode 100644 index c0fe5204f3ed..000000000000 --- a/crates/http-api-bindings/src/openai.rs +++ /dev/null @@ -1,107 +0,0 @@ -use anyhow::{anyhow, Result}; -use async_trait::async_trait; -use futures::stream::BoxStream; -use reqwest::header; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use tabby_inference::{helpers, TextGeneration, TextGenerationOptions}; -use tracing::warn; - -#[derive(Serialize)] -struct Request { - model: String, - prompt: Vec, - max_tokens: usize, - temperature: f32, - stop: Vec, -} - -#[derive(Deserialize)] -struct Response { - choices: Vec, -} - -#[derive(Deserialize)] -struct Prediction { - text: String, -} - -pub struct OpenAIEngine { - client: reqwest::Client, - api_endpoint: String, - model_name: String, -} - -impl OpenAIEngine { - pub fn create(api_endpoint: &str, model_name: &str, authorization: Option) -> Self { - let mut headers = reqwest::header::HeaderMap::new(); - if let Some(authorization) = authorization { - headers.insert( - "Authorization", - header::HeaderValue::from_str(&authorization) - .expect("Failed to create authorization header"), - ); - } - let client = reqwest::Client::builder() - .default_headers(headers) - .build() - .expect("Failed to construct HTTP client"); - Self { - api_endpoint: api_endpoint.to_owned(), - model_name: model_name.to_owned(), - client, - } - } - - async fn generate_impl(&self, prompt: &str, options: TextGenerationOptions) -> Result { - // OpenAI's API usually handles stop words in an O(n) manner, so we just use a single stop word here. - // FIXME(meng): consider improving this for some external vendors, e.g vLLM. - let stop = vec!["\n\n".to_owned()]; - - let request = Request { - model: self.model_name.to_owned(), - prompt: vec![prompt.to_string()], - max_tokens: options.max_decoding_length, - temperature: options.sampling_temperature, - stop, - }; - - // API Documentation: https://github.com/lm-sys/FastChat/blob/main/docs/openai_api.md - let resp = self - .client - .post(&self.api_endpoint) - .json(&request) - .send() - .await?; - - if resp.status() != 200 { - let err: Value = resp.json().await.expect("Failed to parse response"); - return Err(anyhow!("Request failed: {}", err)); - } - - let resp: Response = resp.json().await.expect("Failed to parse response"); - - Ok(resp.choices[0].text.clone()) - } -} - -#[async_trait] -impl TextGeneration for OpenAIEngine { - async fn generate(&self, prompt: &str, options: TextGenerationOptions) -> String { - match self.generate_impl(prompt, options).await { - Ok(output) => output, - Err(err) => { - warn!("Failed to generate completion: `{}`", err); - String::new() - } - } - } - - async fn generate_stream( - &self, - prompt: &str, - options: TextGenerationOptions, - ) -> BoxStream { - helpers::string_to_stream(self.generate(prompt, options).await).await - } -} diff --git a/crates/http-api-bindings/src/rate_limit.rs b/crates/http-api-bindings/src/rate_limit.rs new file mode 100644 index 000000000000..3acf04269ce5 --- /dev/null +++ b/crates/http-api-bindings/src/rate_limit.rs @@ -0,0 +1,105 @@ +use async_openai_alt::{ + error::OpenAIError, + types::{ + ChatCompletionResponseStream, CreateChatCompletionRequest, CreateChatCompletionResponse, + }, +}; +use async_trait::async_trait; +use futures::stream::BoxStream; +use leaky_bucket::RateLimiter; +use tabby_inference::{ChatCompletionStream, CompletionOptions, CompletionStream, Embedding}; +use tokio::time::Duration; +use tracing::{info_span, Instrument}; + +fn new_rate_limiter(rpm: u64) -> RateLimiter { + let rps = (rpm as f64 / 60.0).ceil() as usize; + RateLimiter::builder() + .initial(rps) + .interval(Duration::from_secs(1)) + .refill(rps) + .build() +} + +pub struct RateLimitedEmbedding { + embedding: Box, + rate_limiter: RateLimiter, +} + +pub fn new_embedding(embedding: Box, request_per_minute: u64) -> impl Embedding { + RateLimitedEmbedding { + embedding, + rate_limiter: new_rate_limiter(request_per_minute), + } +} + +#[async_trait] +impl Embedding for RateLimitedEmbedding { + async fn embed(&self, prompt: &str) -> anyhow::Result> { + self.rate_limiter.acquire(1).await; + self.embedding + .embed(prompt) + .instrument(info_span!("rate_limited_compute_embedding")) + .await + } +} + +pub struct RateLimitedCompletion { + completion: Box, + rate_limiter: RateLimiter, +} + +pub fn new_completion( + completion: Box, + request_per_minute: u64, +) -> impl CompletionStream { + RateLimitedCompletion { + completion, + rate_limiter: new_rate_limiter(request_per_minute), + } +} + +#[async_trait] +impl CompletionStream for RateLimitedCompletion { + async fn generate( + &self, + prompt: &str, + options: CompletionOptions, + ) -> BoxStream<'life0, String> { + self.rate_limiter.acquire(1).await; + self.completion.generate(prompt, options).await + } +} + +pub struct RateLimitedChatStream { + completion: Box, + rate_limiter: RateLimiter, +} + +pub fn new_chat( + completion: Box, + request_per_minute: u64, +) -> impl ChatCompletionStream { + RateLimitedChatStream { + completion, + rate_limiter: new_rate_limiter(request_per_minute), + } +} + +#[async_trait] +impl ChatCompletionStream for RateLimitedChatStream { + async fn chat( + &self, + request: CreateChatCompletionRequest, + ) -> Result { + self.rate_limiter.acquire(1).await; + self.completion.chat(request).await + } + + async fn chat_stream( + &self, + request: CreateChatCompletionRequest, + ) -> Result { + self.rate_limiter.acquire(1).await; + self.completion.chat_stream(request).await + } +} diff --git a/crates/http-api-bindings/src/vertex_ai.rs b/crates/http-api-bindings/src/vertex_ai.rs deleted file mode 100644 index 04b6402c3779..000000000000 --- a/crates/http-api-bindings/src/vertex_ai.rs +++ /dev/null @@ -1,123 +0,0 @@ -use async_trait::async_trait; -use futures::stream::BoxStream; -use reqwest::header; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use tabby_inference::{helpers, TextGeneration, TextGenerationOptions}; - -#[derive(Serialize)] -struct Request { - instances: Vec, - parameters: Parameters, -} - -#[derive(Serialize)] -struct Instance { - prefix: String, - suffix: Option, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct Parameters { - temperature: f32, - max_output_tokens: usize, - stop_sequences: Vec, -} - -#[derive(Deserialize)] -struct Response { - predictions: Vec, -} - -#[derive(Deserialize)] -struct Prediction { - content: String, -} - -pub struct VertexAIEngine { - client: reqwest::Client, - api_endpoint: String, -} - -impl VertexAIEngine { - pub fn create(api_endpoint: &str, authorization: &str) -> Self { - let mut headers = reqwest::header::HeaderMap::new(); - headers.insert( - "Authorization", - header::HeaderValue::from_str(authorization) - .expect("Failed to create authorization header"), - ); - let client = reqwest::Client::builder() - .default_headers(headers) - .build() - .expect("Failed to construct HTTP client"); - Self { - api_endpoint: api_endpoint.to_owned(), - client, - } - } - - pub fn prompt_template() -> String { - "{prefix}{suffix}".to_owned() - } -} - -#[async_trait] -impl TextGeneration for VertexAIEngine { - async fn generate(&self, prompt: &str, options: TextGenerationOptions) -> String { - let stop_sequences = if let Some(language) = options.language { - language - .get_stop_words() - .iter() - .map(|x| x.to_string()) - // vertex supports at most 5 stop sequence. - .take(5) - .collect() - } else { - vec![] - }; - - let tokens: Vec<&str> = prompt.split("").collect(); - let request = Request { - instances: vec![Instance { - prefix: tokens[0].to_owned(), - suffix: Some(tokens[1].to_owned()), - }], - // options.max_input_length is ignored. - parameters: Parameters { - temperature: options.sampling_temperature, - // vertex supports at most 64 output tokens. - max_output_tokens: std::cmp::min(options.max_decoding_length, 64), - stop_sequences, - }, - }; - - // API Documentation: https://cloud.google.com/vertex-ai/docs/generative-ai/learn/models#code-completion-prompt-parameters - let resp = self - .client - .post(&self.api_endpoint) - .json(&request) - .send() - .await - .expect("Failed to making completion request"); - - if resp.status() != 200 { - let err: Value = resp.json().await.expect("Failed to parse response"); - println!("Request failed: {}", err); - std::process::exit(1); - } - - let resp: Response = resp.json().await.expect("Failed to parse response"); - - resp.predictions[0].content.clone() - } - - async fn generate_stream( - &self, - prompt: &str, - options: TextGenerationOptions, - ) -> BoxStream { - helpers::string_to_stream(self.generate(prompt, options).await).await - } -} diff --git a/crates/juniper-axum/Cargo.toml b/crates/juniper-axum/Cargo.toml deleted file mode 100644 index 272915a557d3..000000000000 --- a/crates/juniper-axum/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "juniper-axum" -version.workspace = true -edition.workspace = true -authors.workspace = true -homepage.workspace = true - -[dependencies] -axum.workspace = true -juniper.workspace = true -juniper_graphql_ws = "0.3.0" -serde.workspace = true -serde_json.workspace = true diff --git a/crates/juniper-axum/LICENSE b/crates/juniper-axum/LICENSE deleted file mode 100644 index a3fb4b0fe88c..000000000000 --- a/crates/juniper-axum/LICENSE +++ /dev/null @@ -1,28 +0,0 @@ -BSD 2-Clause License - -Adapted from https://github.com/graphql-rust/juniper/blob/master/juniper_axum - -Copyright (c) 2023, The TabbyML team -Copyright (c) 2022-2023, Benno Tielen, Kai Ren -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/crates/juniper-axum/README.md b/crates/juniper-axum/README.md deleted file mode 100644 index 5bfad7e99849..000000000000 --- a/crates/juniper-axum/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# juniper_axum - -Adopted from https://github.com/graphql-rust/juniper/tree/master/juniper_axum for juniper 15 \ No newline at end of file diff --git a/crates/juniper-axum/src/extract.rs b/crates/juniper-axum/src/extract.rs deleted file mode 100644 index 63fa43d5e4ef..000000000000 --- a/crates/juniper-axum/src/extract.rs +++ /dev/null @@ -1,170 +0,0 @@ -//! Types and traits for extracting data from [`Request`]s. - -use std::fmt; - -use axum::{ - async_trait, - body::Body, - extract::{FromRequest, FromRequestParts, Query}, - http::{request::Parts, HeaderValue, Method, Request, StatusCode}, - response::{IntoResponse as _, Response}, - Json, RequestExt as _, -}; -use juniper::{ - http::{GraphQLBatchRequest, GraphQLRequest}, - DefaultScalarValue, ScalarValue, -}; -use serde::Deserialize; - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct AuthBearer(pub Option); - -pub type Rejection = (StatusCode, &'static str); - -#[async_trait] -impl FromRequestParts for AuthBearer -where - B: Send + Sync, -{ - type Rejection = Rejection; - - async fn from_request_parts(req: &mut Parts, _: &B) -> Result { - // Get authorization header - let authorization = req - .headers - .get("authorization") - .map(HeaderValue::to_str) - .transpose() - .map_err(|_| { - ( - StatusCode::BAD_REQUEST, - "authorization contains invalid characters", - ) - })?; - - let Some(authorization) = authorization else { - return Ok(Self(None)); - }; - - // Check that its a well-formed bearer and return - let split = authorization.split_once(' '); - match split { - // Found proper bearer - Some(("Bearer", contents)) => Ok(Self(Some(contents.to_owned()))), - _ => Ok(Self(None)), - } - } -} - -#[derive(Debug, PartialEq)] -pub struct JuniperRequest(pub GraphQLBatchRequest) -where - S: ScalarValue; - -#[async_trait] -impl FromRequest for JuniperRequest -where - S: ScalarValue, - State: Sync, - Query: FromRequestParts, - Json>: FromRequest, - > as FromRequest>::Rejection: fmt::Display, - String: FromRequest, -{ - type Rejection = Response; - - async fn from_request(mut req: Request, state: &State) -> Result { - let content_type = req - .headers() - .get("content-type") - .map(HeaderValue::to_str) - .transpose() - .map_err(|_| { - ( - StatusCode::BAD_REQUEST, - "`Content-Type` header is not a valid HTTP header string", - ) - .into_response() - })?; - - match (req.method(), content_type) { - (&Method::GET, _) => req - .extract_parts::>() - .await - .map_err(|e| { - ( - StatusCode::BAD_REQUEST, - format!("Invalid request query string: {e}"), - ) - .into_response() - }) - .and_then(|query| { - query - .0 - .try_into() - .map(|q| Self(GraphQLBatchRequest::Single(q))) - .map_err(|e| { - ( - StatusCode::BAD_REQUEST, - format!("Invalid request query `variables`: {e}"), - ) - .into_response() - }) - }), - (&Method::POST, Some("application/json")) => { - Json::>::from_request(req, state) - .await - .map(|req| Self(req.0)) - .map_err(|e| { - (StatusCode::BAD_REQUEST, format!("Invalid JSON body: {e}")).into_response() - }) - } - (&Method::POST, Some("application/graphql")) => String::from_request(req, state) - .await - .map(|body| { - Self(GraphQLBatchRequest::Single(GraphQLRequest::new( - body, None, None, - ))) - }) - .map_err(|_| (StatusCode::BAD_REQUEST, "Not valid UTF-8 body").into_response()), - (&Method::POST, _) => Err(( - StatusCode::UNSUPPORTED_MEDIA_TYPE, - "`Content-Type` header is expected to be either `application/json` or \ - `application/graphql`", - ) - .into_response()), - _ => Err(( - StatusCode::METHOD_NOT_ALLOWED, - "HTTP method is expected to be either GET or POST", - ) - .into_response()), - } - } -} - -/// Workaround for a [`GraphQLRequest`] not being [`Deserialize`]d properly from a GET query string, -/// containing `variables` in JSON format. -#[derive(Deserialize, Debug)] -#[serde(deny_unknown_fields)] -struct GetRequest { - query: String, - #[serde(rename = "operationName")] - operation_name: Option, - variables: Option, -} - -impl TryFrom for GraphQLRequest { - type Error = serde_json::Error; - fn try_from(req: GetRequest) -> Result { - let GetRequest { - query, - operation_name, - variables, - } = req; - Ok(Self::new( - query, - operation_name, - variables.map(|v| serde_json::from_str(&v)).transpose()?, - )) - } -} diff --git a/crates/juniper-axum/src/lib.rs b/crates/juniper-axum/src/lib.rs deleted file mode 100644 index 15ae6380f785..000000000000 --- a/crates/juniper-axum/src/lib.rs +++ /dev/null @@ -1,89 +0,0 @@ -pub mod extract; -pub mod relay; -pub mod response; - -use std::future; - -use axum::{ - extract::{Extension, State}, - response::{Html, IntoResponse}, -}; -use extract::AuthBearer; -use juniper_graphql_ws::Schema; - -use self::{extract::JuniperRequest, response::JuniperResponse}; - -pub trait FromAuth { - fn build(state: S, bearer: Option) -> Self; -} - -#[cfg_attr(text, axum::debug_handler)] -pub async fn graphql( - State(state): State, - Extension(schema): Extension, - AuthBearer(bearer): AuthBearer, - JuniperRequest(req): JuniperRequest, -) -> impl IntoResponse -where - S: Schema, // TODO: Refactor in the way we don't depend on `juniper_graphql_ws::Schema` here. - S::Context: FromAuth, -{ - let ctx = S::Context::build(state, bearer); - JuniperResponse(req.execute(schema.root_node(), &ctx).await).into_response() -} - -/// Creates a [`Handler`] that replies with an HTML page containing [GraphiQL]. -/// -/// This does not handle routing, so you can mount it on any endpoint. -/// -/// # Example -/// -/// ```rust -/// use axum::{routing::get, Router}; -/// use juniper_axum::graphiql; -/// -/// let app: Router = Router::new() -/// .route("/", get(graphiql("/graphql", "/subscriptions"))); -/// ``` -/// -/// [`Handler`]: axum::handler::Handler -/// [GraphiQL]: https://github.com/graphql/graphiql -pub fn graphiql<'a>( - graphql_endpoint_url: &str, - subscriptions_endpoint_url: impl Into>, -) -> impl FnOnce() -> future::Ready> + Clone + Send { - let html = Html(juniper::http::graphiql::graphiql_source( - graphql_endpoint_url, - subscriptions_endpoint_url.into(), - )); - - || future::ready(html) -} - -/// Creates a [`Handler`] that replies with an HTML page containing [GraphQL Playground]. -/// -/// This does not handle routing, so you can mount it on any endpoint. -/// -/// # Example -/// -/// ```rust -/// use axum::{routing::get, Router}; -/// use juniper_axum::playground; -/// -/// let app: Router = Router::new() -/// .route("/", get(playground("/graphql", "/subscriptions"))); -/// ``` -/// -/// [`Handler`]: axum::handler::Handler -/// [GraphQL Playground]: https://github.com/prisma/graphql-playground -pub fn playground<'a>( - graphql_endpoint_url: &str, - subscriptions_endpoint_url: impl Into>, -) -> impl FnOnce() -> future::Ready> + Clone + Send { - let html = Html(juniper::http::playground::playground_source( - graphql_endpoint_url, - subscriptions_endpoint_url.into(), - )); - - || future::ready(html) -} diff --git a/crates/juniper-axum/src/relay/mod.rs b/crates/juniper-axum/src/relay/mod.rs deleted file mode 100644 index f2ffeb26c515..000000000000 --- a/crates/juniper-axum/src/relay/mod.rs +++ /dev/null @@ -1,99 +0,0 @@ -use std::future::Future; - -use juniper::FieldResult; - -mod connection; -mod edge; -mod node_type; -mod page_info; - -pub use connection::Connection; -pub use edge::Edge; -pub use node_type::NodeType; -pub use page_info::PageInfo; - -fn validate_first_last( - first: Option, - last: Option, -) -> FieldResult<(Option, Option)> { - if first.is_some() && last.is_some() { - return Err("The \"first\" and \"last\" parameters cannot exist at the same time".into()); - } - - let first = match first { - Some(first) if first < 0 => { - return Err("The \"first\" parameter must be a non-negative number".into()); - } - Some(first) => Some(first as usize), - None => None, - }; - - let last = match last { - Some(last) if last < 0 => { - return Err("The \"last\" parameter must be a non-negative number".into()); - } - Some(last) => Some(last as usize), - None => None, - }; - Ok((first, last)) -} - -pub fn query( - after: Option, - before: Option, - first: Option, - last: Option, - f: F, -) -> FieldResult> -where - Node: NodeType + Sync, - F: FnOnce( - Option, - Option, - Option, - Option, - ) -> FieldResult>, -{ - let (first, last) = validate_first_last(first, last)?; - - let first_plus_one = first.map(|i| i + 1); - let last_plus_one = last.map(|i| i + 1); - let after_some = after.is_some(); - let before_some = before.is_some(); - let nodes = f(after, before, first_plus_one, last_plus_one)?; - Ok(Connection::build_connection( - nodes, - after_some, - before_some, - first, - last, - )) -} - -pub async fn query_async( - after: Option, - before: Option, - first: Option, - last: Option, - f: F, -) -> FieldResult> -where - Node: NodeType + Sync, - F: FnOnce(Option, Option, Option, Option) -> R, - R: Future>>, -{ - let (first, last) = validate_first_last(first, last)?; - - let first_plus_one = first.map(|i| i + 1); - let last_plus_one = last.map(|i| i + 1); - let after_some = after.is_some(); - let before_some = before.is_some(); - let nodes = f(after, before, first_plus_one, last_plus_one).await?; - Ok(Connection::build_connection( - nodes, - after_some, - before_some, - first, - last, - )) -} diff --git a/crates/juniper-axum/src/response.rs b/crates/juniper-axum/src/response.rs deleted file mode 100644 index 047e1c0ee9aa..000000000000 --- a/crates/juniper-axum/src/response.rs +++ /dev/null @@ -1,24 +0,0 @@ -//! [`JuniperResponse`] definition. - -use axum::{ - http::StatusCode, - response::{IntoResponse, Response}, - Json, -}; -use juniper::{http::GraphQLBatchResponse, DefaultScalarValue, ScalarValue}; - -/// Wrapper around a [`GraphQLBatchResponse`], implementing [`IntoResponse`], so it can be returned -/// from [`axum`] handlers. -pub struct JuniperResponse<'a, S = DefaultScalarValue>(pub GraphQLBatchResponse<'a, S>) -where - S: ScalarValue; - -impl IntoResponse for JuniperResponse<'_, S> { - fn into_response(self) -> Response { - if self.0.is_ok() { - Json(self.0).into_response() - } else { - (StatusCode::BAD_REQUEST, Json(self.0)).into_response() - } - } -} diff --git a/crates/llama-cpp-bindings/Cargo.toml b/crates/llama-cpp-bindings/Cargo.toml deleted file mode 100644 index d722bbac8ba0..000000000000 --- a/crates/llama-cpp-bindings/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "llama-cpp-bindings" -version = "0.8.0" -edition = "2021" - -[features] -cuda = [] -rocm = [] - -[build-dependencies] -cxx-build = "1.0" -cmake = "0.1" - -[dependencies] -cxx = "1.0" -async-trait = { workspace = true } -tokio = { workspace = true, features = ["rt"] } -tabby-inference = { path = "../tabby-inference" } -derive_builder = { workspace = true } -futures.workspace = true -async-stream.workspace = true -tracing.workspace = true diff --git a/crates/llama-cpp-bindings/build.rs b/crates/llama-cpp-bindings/build.rs deleted file mode 100644 index 06629ac485bd..000000000000 --- a/crates/llama-cpp-bindings/build.rs +++ /dev/null @@ -1,112 +0,0 @@ -use std::{env, path::Path}; - -use cmake::Config; - -fn main() { - const LLAMA_CMAKE_PATH: &str = "llama.cpp/CMakeLists.txt"; - - assert!( - Path::new(LLAMA_CMAKE_PATH).exists(), - "Please init submodules with `git submodule update --init --recursive` and try again" - ); - - println!("cargo:rerun-if-changed=include/engine.h"); - println!("cargo:rerun-if-changed=src/engine.cc"); - println!("cargo:rustc-link-lib=llama"); - println!("cargo:rustc-link-lib=ggml_static"); - - build_llama_cpp(); - build_cxx_binding(); -} - -fn build_llama_cpp() { - let mut config = Config::new("llama.cpp"); - if cfg!(target_os = "macos") { - config.define("LLAMA_METAL", "ON"); - println!("cargo:rustc-link-lib=framework=Foundation"); - println!("cargo:rustc-link-lib=framework=Accelerate"); - println!("cargo:rustc-link-lib=framework=Metal"); - println!("cargo:rustc-link-lib=framework=MetalKit"); - } - if cfg!(feature = "cuda") { - config.define("LLAMA_CUBLAS", "ON"); - config.define("CMAKE_POSITION_INDEPENDENT_CODE", "ON"); - if cfg!(target_os = "windows") { - let Ok(cuda_path) = env::var("CUDA_PATH") else { - panic!("CUDA_PATH is not set"); - }; - println!(r"cargo:rustc-link-search=native={}\lib\x64", cuda_path); - } else { - println!("cargo:rustc-link-search=native=/usr/local/cuda/lib64"); - println!("cargo:rustc-link-lib=culibos"); - } - println!("cargo:rustc-link-lib=cuda"); - println!("cargo:rustc-link-lib=cudart"); - println!("cargo:rustc-link-lib=cublas"); - println!("cargo:rustc-link-lib=cublasLt"); - } - if cfg!(feature = "rocm") { - let amd_gpu_targets: Vec<&str> = vec![ - "gfx803", - "gfx900", - "gfx906:xnack-", - "gfx908:xnack-", - "gfx90a:xnack+", - "gfx90a:xnack-", - "gfx940", - "gfx941", - "gfx942", - "gfx1010", - "gfx1012", - "gfx1030", - "gfx1031", - "gfx1100", - "gfx1101", - "gfx1102", - "gfx1103", - ]; - - let rocm_root = env::var("ROCM_ROOT").unwrap_or("/opt/rocm".to_string()); - config.define("LLAMA_HIPBLAS", "ON"); - config.define("CMAKE_C_COMPILER", format!("{}/llvm/bin/clang", rocm_root)); - config.define( - "CMAKE_CXX_COMPILER", - format!("{}/llvm/bin/clang++", rocm_root), - ); - config.define("AMDGPU_TARGETS", amd_gpu_targets.join(";")); - println!("cargo:rustc-link-arg=-Wl,--copy-dt-needed-entries"); - println!("cargo:rustc-link-search=native={}/hip/lib", rocm_root); - println!("cargo:rustc-link-search=native={}/rocblas/lib", rocm_root); - println!("cargo:rustc-link-search=native={}/hipblas/lib", rocm_root); - println!("cargo:rustc-link-lib=amdhip64"); - println!("cargo:rustc-link-lib=rocblas"); - println!("cargo:rustc-link-lib=hipblas"); - } - - // By default, this value is automatically inferred from Rust’s compilation profile. - // For Windows platform, we always build llama.cpp in release mode. - // See https://github.com/TabbyML/tabby/pull/948 for more details. - if cfg!(target_os = "windows") { - config.profile("Release"); - } - - let dst = config.build(); - if cfg!(target_os = "windows") { - println!( - r"cargo:rustc-link-search=native={}\build\{}", - dst.display(), - config.get_profile() - ); - } else { - println!("cargo:rustc-link-search=native={}/build", dst.display()); - } -} - -fn build_cxx_binding() { - cxx_build::bridge("src/lib.rs") - .file("src/engine.cc") - .flag_if_supported("-Iinclude") - .flag_if_supported("-Illama.cpp") - .flag_if_supported("-std=c++14") - .compile("cxxbridge"); -} diff --git a/crates/llama-cpp-bindings/include/engine.h b/crates/llama-cpp-bindings/include/engine.h deleted file mode 100644 index 06ccb74cfbe7..000000000000 --- a/crates/llama-cpp-bindings/include/engine.h +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -#include "rust/cxx.h" -#include -#include - -namespace llama { -struct StepOutput; - -class TextInferenceEngine { - public: - virtual ~TextInferenceEngine(); - - virtual void add_request(uint32_t request_id, rust::Str text, size_t max_input_length, - float temperature, uint64_t seed) = 0; - virtual void stop_request(uint32_t request_id) = 0; - virtual rust::Vec step() = 0; -}; - -std::unique_ptr create_engine(bool use_gpu, rust::Str model_path, uint8_t paralellism); -} // namespace diff --git a/crates/llama-cpp-bindings/llama.cpp b/crates/llama-cpp-bindings/llama.cpp deleted file mode 160000 index 4af25481fc9e..000000000000 --- a/crates/llama-cpp-bindings/llama.cpp +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4af25481fc9e00e34da6de3e9eada975ab8f5696 diff --git a/crates/llama-cpp-bindings/src/engine.cc b/crates/llama-cpp-bindings/src/engine.cc deleted file mode 100644 index 815e32d2c996..000000000000 --- a/crates/llama-cpp-bindings/src/engine.cc +++ /dev/null @@ -1,389 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include "llama-cpp-bindings/include/engine.h" -#include "llama-cpp-bindings/src/lib.rs.h" - -namespace llama { -TextInferenceEngine::~TextInferenceEngine() {} - -namespace { -constexpr size_t N_BATCH = 512; // # per batch inference. -constexpr size_t N_CTX = 4096; // # max kv history. -struct Request { - Request(size_t request_id, std::vector input_token_ids, float temperature, uint64_t seed) : - id(request_id), - temperature(temperature), - seed(seed), - tokens(input_token_ids.begin(), input_token_ids.end()) { - } - - uint32_t id = -1; - llama_seq_id seq_id = -1; - float temperature = 0; - uint64_t seed = 0; - - std::vector tokens; - size_t i_batch = -1; - size_t n_past = 0; - - std::string generated_text; -}; - - -std::string llama_token_to_piece(const struct llama_context * ctx, llama_token token) { - std::vector result(8, 0); - const int n_tokens = llama_token_to_piece(llama_get_model(ctx), token, result.data(), result.size()); - if (n_tokens < 0) { - result.resize(-n_tokens); - int check = llama_token_to_piece(llama_get_model(ctx), token, result.data(), result.size()); - GGML_ASSERT(check == -n_tokens); - } else { - result.resize(n_tokens); - } - - return std::string(result.data(), result.size()); -} - -std::vector llama_tokenize( - const struct llama_model * model, - const rust::Str & text, - bool add_bos, - bool special) { - // upper limit for the number of tokens - int n_tokens = text.length() + add_bos; - std::vector result(n_tokens); - n_tokens = llama_tokenize(model, text.data(), text.length(), result.data(), result.size(), add_bos, special); - if (n_tokens < 0) { - result.resize(-n_tokens); - int check = llama_tokenize(model, text.data(), text.length(), result.data(), result.size(), add_bos, special); - GGML_ASSERT(check == -n_tokens); - } else { - result.resize(n_tokens); - } - return result; -} - -template -std::string string_format(const std::string& format, Args ... args) -{ - int size_s = std::snprintf(nullptr, 0, format.c_str(), args ...) + 1; // Extra space for '\0' - if (size_s <= 0) { throw std::runtime_error("Error during formatting."); } - auto size = static_cast(size_s); - std::unique_ptr buf(new char[size]); - std::snprintf(buf.get(), size, format.c_str(), args ...); - return std::string(buf.get(), buf.get() + size - 1); // We don't want the '\0' inside -} - -float compute_softmax_inplace(float* nums, size_t len, float temperature) { - float sum = 0; - float max = *std::max_element(nums, nums + len); - for (size_t i = 0; i < len; i++) { - nums[i] -= max; - nums[i] = std::exp(nums[i] / temperature); - sum += nums[i]; - } - for (size_t i = 0; i < len; i++) { - nums[i] /= sum; - } - return sum; -} - -size_t weighted_random(const float* nums, size_t len, uint64_t seed) { - std::mt19937 rng(seed); - float sum = 0; - for (size_t i = 0; i < len; i++) { - sum += nums[i]; - } - - float random = std::uniform_real_distribution(0, sum)(rng); - sum = 0; - size_t i; - for (i = 0; i < len; i++) { - sum += nums[i]; - if (sum >= random) { - return i; - } - } - return i; -} - -template -using owned = std::unique_ptr>; - -class TextInferenceEngineImpl : public TextInferenceEngine { - public: - TextInferenceEngineImpl(owned model, owned ctx, uint8_t parallelism) : - model_(std::move(model)), - ctx_(std::move(ctx)), - parallelism_(parallelism) { - batch_ = llama_batch_init(N_CTX * parallelism, 0, 1); - // warm up - { - batch_.n_tokens = 16; - for (int i = 0; i < batch_.n_tokens; ++i) { - batch_.token[i] = 0; - batch_.pos[i] = i; - batch_.n_seq_id[i] = 1; - batch_.seq_id[i][0] = 0; - batch_.logits[i] = false; - } - - if (llama_decode(ctx_.get(), batch_)) { - fprintf(stderr, "%s: warmup failed\n", __func__); - } - - llama_kv_cache_clear(ctx_.get()); - } - } - - ~TextInferenceEngineImpl() { - llama_batch_free(batch_); - } - - virtual void add_request(uint32_t request_id, rust::Str text, size_t max_input_length, - float temperature, uint64_t seed) override { - - auto tokens = llama_tokenize(llama_get_model(ctx_.get()), text, false, true); - if (tokens.size() > max_input_length) { - int start = tokens.size() - max_input_length; - tokens = std::vector(tokens.begin() + start, tokens.end()); - } - pending_requests_.push_back(Request(request_id, tokens, temperature, seed)); - } - - void stop_request(uint32_t request_id) override { - stopped_requests_.insert(request_id); - } - - rust::Vec step() override { - std::lock_guard guard(g_mutex_); - - auto* ctx = ctx_.get(); - auto n_vocab = llama_n_vocab(llama_get_model(ctx)); - - // Remove stopped requests. - if (!stopped_requests_.empty()) { - std::vector requests; - for (auto& request : requests_) { - if (stopped_requests_.count(request.id) > 0) { - // Release KV cache. - llama_kv_cache_seq_rm(ctx_.get(), request.id, -1, -1); - } else { - requests.emplace_back(request); - } - } - - requests_ = requests; - } - - // Add pending requests. - while (pending_requests_.size() > 0 && requests_.size() < parallelism_) { - Request request = std::move(pending_requests_.front()); - pending_requests_.pop_front(); - - // Ignore stopped pending requests. - if (stopped_requests_.count(request.id) > 0) { - continue; - } - - requests_.push_back(request); - } - - // Clear stopped requests. - stopped_requests_.clear(); - - if (requests_.size() == 0) { - llama_kv_cache_clear(ctx); - return {}; - } - - // Clear the batch. - batch_.n_tokens = 0; - - // Insert tokens from ongoing requests to batch. - for (auto& request : requests_) { - const size_t n_tokens = batch_.n_tokens; - for (size_t i = 0; i < request.tokens.size(); ++i) { - batch_.token[n_tokens + i] = request.tokens[i]; - batch_.pos[n_tokens + i] = request.n_past + i; - batch_.n_seq_id[n_tokens + i] = 1; - batch_.seq_id[n_tokens + i][0] = request.id; - batch_.logits[n_tokens + i] = false; - } - batch_.n_tokens += request.tokens.size(); - - batch_.logits[batch_.n_tokens - 1] = true; - request.i_batch = batch_.n_tokens - 1; - } - - rust::Vec result; - result.reserve(requests_.size()); - - // Decode tokens in chunks - for (size_t i = 0; i < static_cast(batch_.n_tokens); i += N_BATCH) { - const int32_t n_tokens = std::min(N_BATCH, batch_.n_tokens - i); - llama_batch batch_view = { - n_tokens, - batch_.token + i, - nullptr, - batch_.pos + i, - batch_.n_seq_id + i, - batch_.seq_id + i, - batch_.logits + i, - 0, 0, 0, // unused - }; - - const int ret = llama_decode(ctx, batch_view); - if (ret != 0) { - throw std::runtime_error(string_format("llama_decode failed with code: %d", ret)); - } - - const auto eos_id = llama_token_eos(llama_get_model(ctx)); - for (auto& request : requests_) { - if ((request.i_batch < i) || (request.i_batch >= (i + n_tokens))) { - continue; - } - - int32_t i_batch = request.i_batch - i; - float* logits = llama_get_logits_ith(ctx, i_batch); - compute_softmax_inplace(logits, n_vocab, request.temperature); - auto next_token = weighted_random(logits, n_vocab, request.seed); - request.n_past += request.tokens.size(); - - request.tokens.clear(); - request.tokens.push_back(next_token); - - const auto token_str = llama_token_to_piece(ctx, next_token); - request.generated_text += token_str; - - // FIXME: Hack for codellama to simplify tabby's implementation. - const bool is_eos = next_token == eos_id || token_str == " "; - - bool incomplete = false; - for (size_t i = 1; i < 5 && i <= request.generated_text.size(); ++i) - { - const char c = request.generated_text[request.generated_text.size() - i]; - if ((c & 0xC0) == 0x80) - { - // continuation byte: 10xxxxxx - continue; - } - else if ((c & 0xE0) == 0xC0) - { - // 2-byte character: 110xxxxx ... - incomplete = i < 2; - } - else if ((c & 0xF0) == 0xE0) - { - // 3-byte character: 1110xxxx ... - incomplete = i < 3; - } - else if ((c & 0xF8) == 0xF0) - { - // 4-byte character: 11110xxx ... - incomplete = i < 4; - } - // else 1-byte character or invalid byte - break; - } - - if (!incomplete) { - rust::String generated_text; - try { - generated_text = is_eos ? "" : request.generated_text; - } catch (const std::invalid_argument& e) { - fprintf(stderr, "%s:%d [%s] - ignoring non utf-8/utf-16 output\n", __FILE__, __LINE__, __func__); - } - - result.push_back({request.id, generated_text}); - request.generated_text.clear(); - } - } - } - - return result; - } - - private: - owned model_; - owned ctx_; - - llama_batch batch_; - - std::vector requests_; - std::deque pending_requests_; - std::unordered_set stopped_requests_; - - uint32_t parallelism_; - - // llama.cpp is not thread safe - // FIXME(meng): remove the mutex once https://github.com/ggerganov/llama.cpp/issues/3960 is fixed - // and integrated to tabby's fork. - static std::mutex g_mutex_; -}; - -std::mutex TextInferenceEngineImpl::g_mutex_; - -static int g_llama_cpp_log_level = 0; -static void llama_log_callback(ggml_log_level level, const char * text, void * user_data) { - (void)user_data; - if (level < g_llama_cpp_log_level) { - fputs(text, stderr); - fflush(stderr); - } -} - -struct BackendInitializer { - BackendInitializer() { - if (const char* level = std::getenv("LLAMA_CPP_LOG_LEVEL")) { - g_llama_cpp_log_level = std::stoi(level); - } - llama_log_set(llama_log_callback, nullptr); - llama_backend_init(false); - } - - ~BackendInitializer() { - llama_backend_free(); - } -}; - -} // namespace - -std::unique_ptr create_engine(bool use_gpu, rust::Str model_path, uint8_t parallelism) { - static BackendInitializer initializer; - - llama_model_params model_params = llama_model_default_params(); - model_params.n_gpu_layers = use_gpu ? 9999 : 0; - llama_model* model = llama_load_model_from_file(std::string(model_path).c_str(), model_params); - - if (!model) { - return nullptr; - } - - llama_context_params ctx_params = llama_context_default_params(); - ctx_params.n_ctx = N_CTX * parallelism; - ctx_params.n_batch = N_BATCH; - if (const char* n_thread_str = std::getenv("LLAMA_CPP_N_THREADS")) { - int n_threads = std::stoi(n_thread_str); - ctx_params.n_threads = n_threads; - ctx_params.n_threads_batch = n_threads; - } - llama_context* ctx = llama_new_context_with_model(model, ctx_params); - return std::make_unique( - owned(model, llama_free_model), - owned(ctx, llama_free), - parallelism - ); -} - -} // namespace tabby diff --git a/crates/llama-cpp-bindings/src/lib.rs b/crates/llama-cpp-bindings/src/lib.rs deleted file mode 100644 index 24d77daf581d..000000000000 --- a/crates/llama-cpp-bindings/src/lib.rs +++ /dev/null @@ -1,125 +0,0 @@ -//! Bindings to raw C++ LLaMA implementation. implements the TextGeneration trait and being used in tabby to generate text / code. -mod llama; -mod utils; - -use async_stream::stream; -use async_trait::async_trait; -use derive_builder::Builder; -use ffi::create_engine; -use futures::stream::BoxStream; -use llama::LlamaService; -use tabby_inference::{ - decoding::StopConditionFactory, helpers, TextGeneration, TextGenerationOptions, -}; - -#[cxx::bridge(namespace = "llama")] -mod ffi { - struct StepOutput { - request_id: u32, - text: String, - } - - unsafe extern "C++" { - include!("llama-cpp-bindings/include/engine.h"); - - type TextInferenceEngine; - - fn create_engine( - use_gpu: bool, - model_path: &str, - parallelism: u8, - ) -> UniquePtr; - - fn add_request( - self: Pin<&mut TextInferenceEngine>, - request_id: u32, - prompt: &str, - max_input_length: usize, - temperature: f32, - seed: u64, - ); - fn stop_request(self: Pin<&mut TextInferenceEngine>, request_id: u32); - fn step(self: Pin<&mut TextInferenceEngine>) -> Result>; - } -} - -unsafe impl Send for ffi::TextInferenceEngine {} -unsafe impl Sync for ffi::TextInferenceEngine {} - -#[derive(Builder, Debug)] -pub struct LlamaTextGenerationOptions { - model_path: String, - use_gpu: bool, - parallelism: u8, -} - -pub struct LlamaTextGeneration { - service: LlamaService, - stop_condition_factory: StopConditionFactory, -} - -impl LlamaTextGeneration { - pub fn new(options: LlamaTextGenerationOptions) -> Self { - let engine = create_engine(options.use_gpu, &options.model_path, options.parallelism); - if engine.is_null() { - fatal!("Unable to load model: {}", options.model_path); - } - - Self { - service: LlamaService::new(engine), - stop_condition_factory: StopConditionFactory::default(), - } - } -} - -#[async_trait] -impl TextGeneration for LlamaTextGeneration { - async fn generate(&self, prompt: &str, options: TextGenerationOptions) -> String { - let language = options.language; - let s = self.generate_stream(prompt, options).await; - let text = helpers::stream_to_string(s).await; - - let Some(language) = language else { - return text; - }; - - let Some(trimmed) = self.stop_condition_factory.trim_stop_words(language, &text) else { - return text; - }; - - trimmed - } - - async fn generate_stream( - &self, - prompt: &str, - options: TextGenerationOptions, - ) -> BoxStream { - let stop_condition = self.stop_condition_factory.create( - prompt, - options.max_decoding_length, - options.language, - ); - - let mut rx = self - .service - .add_request( - prompt, - options.max_input_length, - options.sampling_temperature, - options.seed, - stop_condition, - ) - .await; - - let s = stream! { - while let Some(new_text) = rx.recv().await { - yield new_text; - } - - rx.close(); - }; - - Box::pin(s) - } -} diff --git a/crates/llama-cpp-bindings/src/llama.rs b/crates/llama-cpp-bindings/src/llama.rs deleted file mode 100644 index df08a77b8311..000000000000 --- a/crates/llama-cpp-bindings/src/llama.rs +++ /dev/null @@ -1,173 +0,0 @@ -use std::{collections::HashMap, thread::JoinHandle}; - -use cxx::UniquePtr; -use tabby_inference::decoding::StopCondition; -use tokio::sync::mpsc::{channel, Receiver, Sender}; - -use crate::ffi; - -struct LlamaInitRequest { - prompt: String, - max_input_length: usize, - temperature: f32, - seed: u64, - - tx: Sender, - stop_condition: StopCondition, -} - -struct LlamaRunningRequest { - tx: Sender, - stop_condition: StopCondition, -} - -struct LlamaServiceImpl { - next_request_id: u32, - engine: cxx::UniquePtr, - rx: Receiver, - requests: HashMap, -} - -impl LlamaServiceImpl { - fn new(engine: UniquePtr, rx: Receiver) -> Self { - Self { - next_request_id: 0, - engine, - rx, - requests: HashMap::new(), - } - } - - fn alloc_request_id(&mut self) -> u32 { - let ret = self.next_request_id; - - // 1048576 (2^20) should be large enough to avoid any collision. - // FIXME(meng): figure out a better way. - self.next_request_id = (self.next_request_id + 1) % 1048576; - ret - } - - async fn next_request(&mut self) -> Option { - if self.requests.is_empty() { - self.rx.recv().await - } else { - self.rx.try_recv().ok() - } - } - - async fn background_job(&mut self) { - while let Some(LlamaInitRequest { - prompt, - temperature, - seed, - tx, - max_input_length, - stop_condition, - }) = self.next_request().await - { - // Drop canceled requests. - if tx.is_closed() { - continue; - } - - let request_id = self.alloc_request_id(); - self.requests - .insert(request_id, LlamaRunningRequest { tx, stop_condition }); - self.engine.as_mut().unwrap().add_request( - request_id, - &prompt, - max_input_length, - temperature, - seed, - ); - } - - let result = match self.engine.as_mut().unwrap().step() { - Ok(result) => result, - Err(err) => { - crate::fatal!("Failed to step: {}", err) - } - }; - - for ffi::StepOutput { request_id, text } in result { - let mut stopped: bool; - let LlamaRunningRequest { tx, stop_condition } = - self.requests.get_mut(&request_id).unwrap(); - - if tx.is_closed() || text.is_empty() { - // Cancelled by client side or hit eos. - stopped = true; - } else { - stopped = stop_condition.should_stop(&text); - - match tx.send(text).await { - Ok(_) => (), - Err(_) => stopped = true, - } - } - - if stopped { - self.requests.remove(&request_id); - self.engine.as_mut().unwrap().stop_request(request_id); - } - } - } -} - -fn start_llama_service_impl( - engine: UniquePtr, - rx: Receiver, -) -> JoinHandle<()> { - let mut service = LlamaServiceImpl::new(engine, rx); - let rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap(); - - std::thread::spawn(move || { - let local = tokio::task::LocalSet::new(); - local.spawn_local(async move { - loop { - service.background_job().await; - } - }); - - rt.block_on(local); - }) -} - -pub struct LlamaService { - tx: Sender, -} - -impl LlamaService { - pub fn new(engine: UniquePtr) -> Self { - let (tx, rx) = channel(1); - start_llama_service_impl(engine, rx); - Self { tx } - } - - pub async fn add_request( - &self, - prompt: &str, - max_input_length: usize, - temperature: f32, - seed: u64, - stop_condition: StopCondition, - ) -> Receiver { - let (tx, rx) = channel(8); - self.tx - .send(LlamaInitRequest { - prompt: prompt.to_owned(), - temperature, - seed, - tx, - max_input_length, - stop_condition, - }) - .await - .expect("Failed to add request"); - - rx - } -} diff --git a/crates/llama-cpp-bindings/src/utils.rs b/crates/llama-cpp-bindings/src/utils.rs deleted file mode 100644 index 5184479547d0..000000000000 --- a/crates/llama-cpp-bindings/src/utils.rs +++ /dev/null @@ -1,16 +0,0 @@ -#[macro_export] -macro_rules! fatal { - ($msg:expr) => { - ({ - tracing::error!($msg); - std::process::exit(1); - }) - }; - - ($fmt:expr, $($arg:tt)*) => { - ({ - tracing::error!($fmt, $($arg)*); - std::process::exit(1); - }) - }; -} diff --git a/crates/llama-cpp-server/Cargo.toml b/crates/llama-cpp-server/Cargo.toml new file mode 100644 index 000000000000..4ff119a97d72 --- /dev/null +++ b/crates/llama-cpp-server/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "llama-cpp-server" +version.workspace = true +edition.workspace = true +authors.workspace = true +homepage.workspace = true + +[features] +binary = [] +cuda = ["binary"] +rocm = ["binary"] +vulkan = ["binary"] + +[dependencies] +futures.workspace = true +http-api-bindings = { path = "../http-api-bindings" } +reqwest.workspace = true +tabby-inference = { path = "../tabby-inference" } +tabby-common = { path = "../tabby-common" } +tracing.workspace = true +async-trait.workspace = true +tokio = { workspace = true, features = ["process"] } +anyhow.workspace = true +which = "6" +serde.workspace = true +serdeconv.workspace = true +async-openai-alt.workspace = true + +[build-dependencies] +cmake = "0.1" +omnicopy_to_output = "0.1.1" diff --git a/crates/llama-cpp-server/build.rs b/crates/llama-cpp-server/build.rs new file mode 100644 index 000000000000..e909f26d7e65 --- /dev/null +++ b/crates/llama-cpp-server/build.rs @@ -0,0 +1,75 @@ +use std::{env, path::Path}; + +use cmake::Config; +use omnicopy_to_output::copy_to_output; + +fn main() { + if !cfg!(feature = "binary") || env::var("CI_COVERAGE").is_ok() { + return; + } + + let mut config = Config::new("./llama.cpp"); + config.profile("Release"); + + // Tabby handles model downloads, thus turn the download feature off in llama.cpp. + config.define("LLAMA_CURL", "OFF"); + + config.define("GGML_NATIVE", "OFF"); + config.define("GGML_NATIVE_DEFAULT", "OFF"); + config.define("BUILD_SHARED_LIBS", "OFF"); + + if cfg!(target_os = "macos") { + config.define("LLAMA_METAL", "ON"); + config.define("LLAMA_METAL_EMBED_LIBRARY", "ON"); + println!("cargo:rustc-link-lib=framework=Foundation"); + println!("cargo:rustc-link-lib=framework=Accelerate"); + println!("cargo:rustc-link-lib=framework=Metal"); + println!("cargo:rustc-link-lib=framework=MetalKit"); + } + if cfg!(feature = "cuda") { + config.define("GGML_CUDA", "ON"); + config.define("CMAKE_POSITION_INDEPENDENT_CODE", "ON"); + } + if cfg!(feature = "rocm") { + let amd_gpu_targets: Vec<&str> = vec![ + "gfx803", + "gfx900", + "gfx906:xnack-", + "gfx908:xnack-", + "gfx90a:xnack+", + "gfx90a:xnack-", + "gfx940", + "gfx941", + "gfx942", + "gfx1010", + "gfx1012", + "gfx1030", + "gfx1031", + "gfx1100", + "gfx1101", + "gfx1102", + "gfx1103", + ]; + + let rocm_root = env::var("ROCM_ROOT").unwrap_or("/opt/rocm".to_string()); + config.define("GGML_HIPBLAS", "ON"); + config.define("CMAKE_C_COMPILER", format!("{rocm_root}/llvm/bin/clang")); + config.define( + "CMAKE_CXX_COMPILER", + format!("{rocm_root}/llvm/bin/clang++"), + ); + config.define("AMDGPU_TARGETS", amd_gpu_targets.join(";")); + } + if cfg!(feature = "vulkan") { + config.define("GGML_VULKAN", "ON"); + } + + let out = config.build(); + let server_binary = make_output_binary(&out, "llama-server"); + + copy_to_output(&server_binary).expect("Failed to copy server binary to output directory"); +} + +fn make_output_binary(out: &Path, name: &str) -> String { + out.join("bin").join(name).display().to_string() + env::consts::EXE_SUFFIX +} diff --git a/crates/llama-cpp-server/llama.cpp b/crates/llama-cpp-server/llama.cpp new file mode 160000 index 000000000000..952a47f455fb --- /dev/null +++ b/crates/llama-cpp-server/llama.cpp @@ -0,0 +1 @@ +Subproject commit 952a47f455fbd92e2659b98b9b6317a2dafeb532 diff --git a/crates/llama-cpp-server/src/lib.rs b/crates/llama-cpp-server/src/lib.rs new file mode 100644 index 000000000000..5e1f71fa609d --- /dev/null +++ b/crates/llama-cpp-server/src/lib.rs @@ -0,0 +1,361 @@ +mod supervisor; + +use std::{fs, path::PathBuf, sync::Arc}; + +use anyhow::Result; +use async_openai_alt::error::OpenAIError; +use async_trait::async_trait; +use futures::stream::BoxStream; +use serde::Deserialize; +use supervisor::LlamaCppSupervisor; +use tabby_common::{ + config::{HttpModelConfigBuilder, LocalModelConfig, ModelConfig, RateLimit, RateLimitBuilder}, + registry::{parse_model_id, ModelRegistry, GGML_MODEL_PARTITIONED_PREFIX}, +}; +use tabby_inference::{ChatCompletionStream, CompletionOptions, CompletionStream, Embedding}; + +fn api_endpoint(port: u16) -> String { + format!("http://127.0.0.1:{port}") +} + +struct EmbeddingServer { + #[allow(unused)] + server: LlamaCppSupervisor, + embedding: Arc, +} + +impl EmbeddingServer { + async fn new( + num_gpu_layers: u16, + model_path: &str, + parallelism: u8, + enable_fast_attention: bool, + context_size: usize, + ) -> EmbeddingServer { + let server = LlamaCppSupervisor::new( + "embedding", + num_gpu_layers, + true, + model_path, + parallelism, + None, + enable_fast_attention, + context_size, + ); + server.start().await; + + let config = HttpModelConfigBuilder::default() + .api_endpoint(Some(api_endpoint(server.port()))) + .rate_limit(build_rate_limit_config()) + .kind("llama.cpp/embedding".to_string()) + .build() + .expect("Failed to create HttpModelConfig"); + + Self { + server, + embedding: http_api_bindings::create_embedding(&config).await, + } + } +} + +#[async_trait] +impl Embedding for EmbeddingServer { + async fn embed(&self, prompt: &str) -> Result> { + self.embedding.embed(prompt).await + } +} + +struct CompletionServer { + #[allow(unused)] + server: Arc, + completion: Arc, +} + +impl CompletionServer { + async fn new( + num_gpu_layers: u16, + model_path: &str, + parallelism: u8, + enable_fast_attention: bool, + context_size: usize, + ) -> Self { + let server = LlamaCppSupervisor::new( + "completion", + num_gpu_layers, + false, + model_path, + parallelism, + None, + enable_fast_attention, + context_size, + ); + server.start().await; + Self::new_with_supervisor(Arc::new(server)).await + } + + async fn new_with_supervisor(server: Arc) -> Self { + let config = HttpModelConfigBuilder::default() + .api_endpoint(Some(api_endpoint(server.port()))) + .rate_limit(build_rate_limit_config()) + .kind("llama.cpp/completion".to_string()) + .build() + .expect("Failed to create HttpModelConfig"); + let completion = http_api_bindings::create(&config).await; + Self { server, completion } + } +} + +#[async_trait] +impl CompletionStream for CompletionServer { + async fn generate( + &self, + prompt: &str, + options: CompletionOptions, + ) -> BoxStream<'life0, String> { + self.completion.generate(prompt, options).await + } +} + +struct ChatCompletionServer { + #[allow(unused)] + server: Arc, + chat_completion: Arc, +} + +impl ChatCompletionServer { + async fn new( + num_gpu_layers: u16, + model_path: &str, + parallelism: u8, + chat_template: String, + enable_fast_attention: bool, + context_size: usize, + ) -> Self { + let server = LlamaCppSupervisor::new( + "chat", + num_gpu_layers, + false, + model_path, + parallelism, + Some(chat_template), + enable_fast_attention, + context_size, + ); + server.start().await; + Self::new_with_supervisor(Arc::new(server)).await + } + + async fn new_with_supervisor(server: Arc) -> Self { + let config = HttpModelConfigBuilder::default() + .api_endpoint(Some(api_endpoint(server.port()))) + .rate_limit(build_rate_limit_config()) + .kind("openai/chat".to_string()) + .model_name(Some("local".into())) + .build() + .expect("Failed to create HttpModelConfig"); + let chat_completion = http_api_bindings::create_chat(&config).await; + Self { + server, + chat_completion, + } + } +} + +#[async_trait] +impl ChatCompletionStream for ChatCompletionServer { + async fn chat( + &self, + request: async_openai_alt::types::CreateChatCompletionRequest, + ) -> Result { + self.chat_completion.chat(request).await + } + + async fn chat_stream( + &self, + request: async_openai_alt::types::CreateChatCompletionRequest, + ) -> Result { + self.chat_completion.chat_stream(request).await + } +} + +pub async fn create_chat_completion(config: &LocalModelConfig) -> Arc { + let model_path = resolve_model_path(&config.model_id).await; + let info = resolve_prompt_info(&config.model_id).await; + let chat_template = info + .chat_template + .unwrap_or_else(|| panic!("Chat model requires specifying prompt template")); + + Arc::new( + ChatCompletionServer::new( + config.num_gpu_layers, + &model_path, + config.parallelism, + chat_template, + config.enable_fast_attention.unwrap_or_default(), + config.context_size, + ) + .await, + ) +} + +pub async fn create_completion( + config: &LocalModelConfig, +) -> (Arc, PromptInfo) { + let model_path = resolve_model_path(&config.model_id).await; + let prompt_info = resolve_prompt_info(&config.model_id).await; + let stream = Arc::new( + CompletionServer::new( + config.num_gpu_layers, + &model_path, + config.parallelism, + config.enable_fast_attention.unwrap_or_default(), + config.context_size, + ) + .await, + ); + + (stream, prompt_info) +} + +pub async fn create_completion_and_chat( + completion_model: &LocalModelConfig, + chat_model: &LocalModelConfig, +) -> ( + Arc, + PromptInfo, + Arc, +) { + let chat_model_path = resolve_model_path(&chat_model.model_id).await; + let chat_template = resolve_prompt_info(&chat_model.model_id) + .await + .chat_template + .unwrap_or_else(|| panic!("Chat model requires specifying prompt template")); + + let model_path = resolve_model_path(&completion_model.model_id).await; + let prompt_info = resolve_prompt_info(&completion_model.model_id).await; + + let server = Arc::new(LlamaCppSupervisor::new( + "chat", + chat_model.num_gpu_layers, + false, + &chat_model_path, + chat_model.parallelism, + Some(chat_template), + chat_model.enable_fast_attention.unwrap_or_default(), + chat_model.context_size, + )); + server.start().await; + + let chat = ChatCompletionServer::new_with_supervisor(server.clone()).await; + + let completion = if completion_model == chat_model { + CompletionServer::new_with_supervisor(server).await + } else { + CompletionServer::new( + completion_model.num_gpu_layers, + &model_path, + completion_model.parallelism, + completion_model.enable_fast_attention.unwrap_or_default(), + completion_model.context_size, + ) + .await + }; + + (Arc::new(completion), prompt_info, Arc::new(chat)) +} + +pub async fn create_embedding(config: &ModelConfig) -> Option> { + if !tabby_common::config::is_embedding_service_enabled() { + return None; + } + + match config { + ModelConfig::Http(http) => Some(http_api_bindings::create_embedding(http).await), + ModelConfig::Local(llama) => { + let model_path = resolve_model_path(&llama.model_id).await; + Some(Arc::new( + EmbeddingServer::new( + llama.num_gpu_layers, + &model_path, + llama.parallelism, + llama.enable_fast_attention.unwrap_or_default(), + llama.context_size, + ) + .await, + )) + } + } +} + +async fn resolve_model_path(model_id: &str) -> String { + let path = PathBuf::from(model_id); + let path = if path.exists() { + let ggml_path = path.join("ggml"); + get_model_entry_path(&ggml_path).unwrap_or_else(|| { + // Fallback to the original logic if get_model_entry_path fails + ggml_path.join(format!( + "{}00001.gguf", + GGML_MODEL_PARTITIONED_PREFIX.to_owned() + )) + }) + } else { + let (registry, name) = parse_model_id(model_id); + let registry = ModelRegistry::new(registry).await; + registry + .get_model_entry_path(name) + .expect("Model not found") + }; + path.display().to_string() +} + +// get_model_path returns the entrypoint of the model, +// will look for the file with the prefix "00001-of-" +pub fn get_model_entry_path(path: &PathBuf) -> Option { + for entry in fs::read_dir(path).ok()? { + let entry = entry.expect("Error reading directory entry"); + let file_name = entry.file_name(); + let file_name_str = file_name.to_string_lossy(); + + // Check if the file name starts with the specified prefix + if file_name_str.starts_with(GGML_MODEL_PARTITIONED_PREFIX.as_str()) { + return Some(entry.path()); // Return the full path as PathBuf + } + } + + None +} + +#[derive(Deserialize)] +pub struct PromptInfo { + pub prompt_template: Option, + pub chat_template: Option, +} + +impl PromptInfo { + fn read(filepath: PathBuf) -> PromptInfo { + serdeconv::from_json_file(&filepath) + .unwrap_or_else(|_| panic!("Invalid metadata file: {}", filepath.display())) + } +} + +async fn resolve_prompt_info(model_id: &str) -> PromptInfo { + let path = PathBuf::from(model_id); + if path.exists() { + PromptInfo::read(path.join("tabby.json")) + } else { + let (registry, name) = parse_model_id(model_id); + let registry = ModelRegistry::new(registry).await; + let model_info = registry.get_model_info(name); + PromptInfo { + prompt_template: model_info.prompt_template.to_owned(), + chat_template: model_info.chat_template.to_owned(), + } + } +} + +fn build_rate_limit_config() -> RateLimit { + RateLimitBuilder::default() + .request_per_minute(6000) + .build() + .expect("Failed to create RateLimit") +} diff --git a/crates/llama-cpp-server/src/supervisor.rs b/crates/llama-cpp-server/src/supervisor.rs new file mode 100644 index 000000000000..a465594c870d --- /dev/null +++ b/crates/llama-cpp-server/src/supervisor.rs @@ -0,0 +1,261 @@ +use std::{ + collections::VecDeque, + env::var, + net::TcpListener, + process::Stdio, + time::{Duration, Instant}, +}; + +use tokio::{ + io::{AsyncBufReadExt, BufReader}, + task::JoinHandle, +}; +use tracing::{debug, warn}; +use which::which; + +use crate::api_endpoint; + +pub struct LlamaCppSupervisor { + name: &'static str, + port: u16, + handle: JoinHandle<()>, +} + +impl LlamaCppSupervisor { + pub fn new( + name: &'static str, + num_gpu_layers: u16, + embedding: bool, + model_path: &str, + parallelism: u8, + chat_template: Option, + enable_fast_attention: bool, + context_size: usize, + ) -> LlamaCppSupervisor { + let Some(binary_name) = find_binary_name() else { + panic!("Failed to locate llama-server binary, please make sure you have llama-server binary locates in the same directory as the current executable."); + }; + + let model_path = model_path.to_owned(); + let port = get_available_port(); + let mut retry_count = 0; + let initial_time = Instant::now(); + + let handle = tokio::spawn(async move { + loop { + let server_binary = std::env::current_exe() + .expect("Failed to get current executable path") + .parent() + .expect("Failed to get parent directory") + .join(&binary_name) + .display() + .to_string(); + let mut command = tokio::process::Command::new(server_binary); + + command + .arg("-m") + .arg(&model_path) + .arg("--cont-batching") + .arg("--port") + .arg(port.to_string()) + .arg("-np") + .arg(parallelism.to_string()) + .arg("--ctx-size") + .arg(context_size.to_string()) + .kill_on_drop(true) + .stderr(Stdio::piped()) + .stdout(Stdio::null()); + + if let Ok(n_threads) = std::env::var("LLAMA_CPP_N_THREADS") { + command.arg("-t").arg(n_threads); + } + + if num_gpu_layers > 0 { + command.arg("-ngl").arg(num_gpu_layers.to_string()); + } + + if embedding { + command + .arg("--embedding") + .arg("--ubatch-size") + .arg(var("LLAMA_CPP_EMBEDDING_N_UBATCH_SIZE").unwrap_or("4096".into())); + } + + if let Some(chat_template) = chat_template.as_ref() { + command.arg("--chat-template").arg(chat_template); + } + + if enable_fast_attention { + command.arg("-fa"); + }; + + let command_args = format!("{:?}", command); + + let mut process = command.spawn().unwrap_or_else(|e| { + panic!( + "Failed to start llama-server <{}> with command {:?}: {}", + name, command, e + ) + }); + + let mut stderr = BufReader::new( + process + .stderr + .take() + .expect("Failed to get llama.cpp stderr"), + ) + .lines(); + let mut error_lines = VecDeque::with_capacity(100); + + let wait_handle = process.wait(); + + while let Ok(Some(line)) = stderr.next_line().await { + if !line.contains("GET /health") { + if error_lines.len() >= 100 { + error_lines.pop_front(); + } + error_lines.push_back(line); + } + } + + let status_code = wait_handle.await.ok().and_then(|s| s.code()).unwrap_or(-1); + + if status_code != 0 { + warn!( + "llama-server <{}> exited with status code {}, args: `{}`", + name, status_code, command_args + ); + + // print only the initial round error message. + if retry_count == 0 { + eprintln!( + "{}\n", + tabby_common::terminal::HeaderFormat::BoldRed + .format("Recent llama-cpp errors:") + ); + } + for line in error_lines { + // print only the initial round error message. + if retry_count == 0 { + eprintln!("{}", line); + } + if let Some(solution) = analyze_error_message(&line) { + let solution_lines: Vec<_> = solution.split('\n').collect(); + let msg = tabby_common::terminal::InfoMessage::new( + "ERROR", + tabby_common::terminal::HeaderFormat::BoldRed, + &solution_lines, + ); + msg.print(); + break; + } + } + + // exit only after the retry loop has been exhausted 5 times and Tabby was initialing for fewer than 1 minute. + if retry_count >= 5 && initial_time.elapsed().as_secs() < 60 { + eprintln!( + "llama-server <{}> encountered a fatal error. Exiting service. Please check the above logs and suggested solutions for details.", + name + ); + std::process::exit(1); + } + + retry_count += 1; + warn!("Attempting to restart the llama-server..."); + tokio::time::sleep(Duration::from_secs(1)).await; + } + } + }); + + Self { name, handle, port } + } + + pub fn port(&self) -> u16 { + self.port + } + + pub async fn start(&self) { + debug!("Waiting for llama-server <{}> to start...", self.name); + let client = reqwest::Client::builder().no_proxy().build().unwrap(); + loop { + let Ok(resp) = client + .get(api_endpoint(self.port) + "/health") + .timeout(Duration::from_secs(1)) + .send() + .await + else { + debug!("llama-server <{}> not ready yet, retrying...", self.name); + tokio::time::sleep(Duration::from_secs(1)).await; + continue; + }; + + if resp.status().is_success() { + debug!("llama-server <{}> started successfully", self.name); + return; + } + } + } +} + +fn analyze_error_message(error_message: &str) -> Option { + if error_message.contains("cudaMalloc") { + return Some(String::from( + "CUDA memory allocation error detected:\n\ + 1. Try using a smaller Model\n\ + 2. Try to reduce GPU memory usage\n", + )); + } + + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + { + if error_message.contains("Illegal instruction") + && !std::arch::is_x86_feature_detected!("avx2") + { + return Some(String::from( + "Illegal instruction detected: Your CPU does not support AVX2 instruction set.\n\ + Suggestion: Download a compatible binary from https://github.com/ggml-org/llama.cpp/releases" + )); + } + } + + None +} + +fn find_binary_name() -> Option { + let current_exe = std::env::current_exe().expect("Failed to get current executable path"); + let binary_dir = current_exe + .parent() + .expect("Failed to get parent directory"); + let binary_name = "llama-server".to_owned(); + let binary_from_path = which("llama-server") + .ok() + .map(|path| path.display().to_string()); + std::fs::read_dir(binary_dir) + .expect("Failed to read directory") + .filter_map(|entry| entry.ok()) + .filter(|entry| { + entry + .file_name() + .to_string_lossy() + .starts_with(&binary_name) + }) + .map(|entry| entry.path().display().to_string()) + .next() + .or(binary_from_path) +} + +fn get_available_port() -> u16 { + (30888..40000) + .find(|port| port_is_available(*port)) + .expect("Failed to find available port") +} + +fn port_is_available(port: u16) -> bool { + TcpListener::bind(("127.0.0.1", port)).is_ok() +} + +impl Drop for LlamaCppSupervisor { + fn drop(&mut self) { + self.handle.abort(); + } +} diff --git a/crates/ollama-api-bindings/Cargo.toml b/crates/ollama-api-bindings/Cargo.toml new file mode 100644 index 000000000000..f20927f66b87 --- /dev/null +++ b/crates/ollama-api-bindings/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "ollama-api-bindings" +version.workspace = true +edition.workspace = true +authors.workspace = true +homepage.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +tabby-common = { path = "../tabby-common" } +tabby-inference = { path = "../tabby-inference" } + +anyhow.workspace = true +async-stream.workspace = true +async-trait.workspace = true +futures.workspace = true +tracing.workspace = true + +# Use git version for now: https://github.com/pepperoni21/ollama-rs/issues/44 is required to correct work with normal URLs +[dependencies.ollama-rs] +git = "https://github.com/pepperoni21/ollama-rs.git" +rev = "56e8157d98d4185bc171fe9468d3d09bc56e9dd3" +features = ["stream"] diff --git a/crates/ollama-api-bindings/src/completion.rs b/crates/ollama-api-bindings/src/completion.rs new file mode 100644 index 000000000000..637aa2fdf2ff --- /dev/null +++ b/crates/ollama-api-bindings/src/completion.rs @@ -0,0 +1,69 @@ +use async_stream::stream; +use async_trait::async_trait; +use futures::{stream::BoxStream, StreamExt}; +use ollama_rs::{ + generation::{completion::request::GenerationRequest, options::GenerationOptions}, + Ollama, +}; +use tabby_common::config::HttpModelConfig; +use tabby_inference::{CompletionOptions, CompletionStream}; +use tracing::error; + +use crate::model::OllamaModelExt; + +pub struct OllamaCompletion { + /// Connection to Ollama API + connection: Ollama, + /// Model name, + model: String, +} + +#[async_trait] +impl CompletionStream for OllamaCompletion { + async fn generate( + &self, + prompt: &str, + options: CompletionOptions, + ) -> BoxStream<'life0, String> { + // FIXME: options.presence_penalty is not used + let ollama_options = GenerationOptions::default() + .num_predict(options.max_decoding_tokens) + .seed(options.seed as i32) + .repeat_last_n(0) + .temperature(options.sampling_temperature); + let request = GenerationRequest::new(self.model.to_owned(), prompt.to_owned()) + .template("{{ .Prompt }}".to_string()) + .options(ollama_options); + + // Why this function returns not Result? + match self.connection.generate_stream(request).await { + Ok(stream) => { + let tabby_stream = stream! { + + for await response in stream { + let parts = response.unwrap(); + for part in parts { + yield part.response + } + } + + }; + + tabby_stream.boxed() + } + Err(err) => { + error!("Failed to generate completion: {}", err); + futures::stream::empty().boxed() + } + } + } +} + +pub async fn create(config: &HttpModelConfig) -> Box { + let connection = Ollama::try_new(config.api_endpoint.as_deref().unwrap().to_owned()) + .expect("Failed to create connection to Ollama, URL invalid"); + + let model = connection.select_model_or_default(config).await.unwrap(); + + Box::new(OllamaCompletion { connection, model }) +} diff --git a/crates/ollama-api-bindings/src/embedding.rs b/crates/ollama-api-bindings/src/embedding.rs new file mode 100644 index 000000000000..01883c1e3975 --- /dev/null +++ b/crates/ollama-api-bindings/src/embedding.rs @@ -0,0 +1,34 @@ +use async_trait::async_trait; +use ollama_rs::Ollama; +use tabby_common::config::HttpModelConfig; +use tabby_inference::Embedding; + +use crate::model::OllamaModelExt; + +pub struct OllamaCompletion { + /// Connection to Ollama API + connection: Ollama, + /// Model name, + model: String, +} + +#[async_trait] +impl Embedding for OllamaCompletion { + async fn embed(&self, prompt: &str) -> anyhow::Result> { + self.connection + .generate_embeddings(self.model.to_owned(), prompt.to_owned(), None) + .await + .map(|x| x.embeddings) + .map(|e| e.iter().map(|v| *v as f32).collect()) + .map_err(|err| err.into()) + } +} + +pub async fn create(config: &HttpModelConfig) -> Box { + let connection = Ollama::try_new(config.api_endpoint.as_deref().unwrap().to_owned()) + .expect("Failed to create connection to Ollama, URL invalid"); + + let model = connection.select_model_or_default(config).await.unwrap(); + + Box::new(OllamaCompletion { connection, model }) +} diff --git a/crates/ollama-api-bindings/src/lib.rs b/crates/ollama-api-bindings/src/lib.rs new file mode 100644 index 000000000000..8429a9940682 --- /dev/null +++ b/crates/ollama-api-bindings/src/lib.rs @@ -0,0 +1,7 @@ +mod model; + +mod completion; +pub use completion::create as create_completion; + +mod embedding; +pub use embedding::create as create_embedding; diff --git a/crates/ollama-api-bindings/src/model.rs b/crates/ollama-api-bindings/src/model.rs new file mode 100644 index 000000000000..f5e281a65b32 --- /dev/null +++ b/crates/ollama-api-bindings/src/model.rs @@ -0,0 +1,137 @@ +//! +//! Ollama model management utils +//! + +use anyhow::{anyhow, bail, Result}; +use async_trait::async_trait; +use futures::StreamExt; +use ollama_rs::Ollama; +use tabby_common::config::HttpModelConfig; +use tracing::{info, warn}; + +/// Env variable for allowing pulling models with Ollama +static ALLOW_PULL_ENV: &str = "TABBY_OLLAMA_ALLOW_PULL"; + +#[async_trait] +pub trait OllamaModelExt { + /// Check if a model is available in remote Ollama instance + async fn model_available(&self, name: impl AsRef + Send) -> Result; + + /// Get the first available model in remote Ollama instance + async fn get_first_available_model(&self) -> Result>; + + /// For input model specification: + /// - If model is specified, check if it is available in remote Ollama instance and returns its name + /// - If model is not specified and prompt/chat templates are specified, returns a error because it is unsound + /// - If model is not specified and prompt/chat templates are not specified get the first available model in remote Ollama instance and returns its name + /// - If no model is available, returns error + /// - If model is specified and not available, tries to pull it if a env `TABBY_OLLAMA_ALLOW_PULL` equal to `1`, `y`, or `yes` + /// and returns error if the environment variable is not set or haves a wrong value + /// + /// # Parameters + /// - `config`: model config configuration + /// + /// # Returns + /// - model name to use + async fn select_model_or_default(&self, config: &HttpModelConfig) -> Result; + + /// Pull model and puts progress in tracing + async fn pull_model_with_tracing(&self, model: &str) -> Result<()>; +} + +#[async_trait] +impl OllamaModelExt for Ollama { + async fn model_available(&self, name: impl AsRef + Send) -> Result { + let name = name.as_ref(); + + let models_available = self.show_model_info(name.into()).await; + + match models_available { + Ok(_) => Ok(true), + Err(err) => { + if err.to_string().contains("not found") { + Ok(false) + } else { + Err(err.into()) + } + } + } + } + + async fn get_first_available_model(&self) -> Result> { + let models_available = self.list_local_models().await?; + + Ok(models_available.first().map(|x| x.name.to_owned())) + } + + async fn select_model_or_default(&self, config: &HttpModelConfig) -> Result { + let prompt_or_chat_templates_set = + config.prompt_template.is_some() || config.chat_template.is_some(); + + let model = match config.model_name.to_owned() { + Some(ref model) => model.to_owned(), + None => { + let model = self + .get_first_available_model() + .await? + .ok_or(anyhow!("Ollama instances does not have any models"))?; + + if prompt_or_chat_templates_set { + bail!("No model name is provided but prompt or chat templates are set. Please set model name explicitly") + } + + warn!( + "No model name is provided, using first available: {}", + model + ); + model + } + }; + + let available = self.model_available(&model).await?; + + let allow_pull = std::env::var_os(ALLOW_PULL_ENV) + .map(|x| x == "1" || x.eq_ignore_ascii_case("y") || x.eq_ignore_ascii_case("yes")) + .unwrap_or(false); + + match (available, allow_pull) { + (true, _) => Ok(model), + (false, true) => { + info!("Model is not available, pulling it"); + self.pull_model_with_tracing(model.as_str()).await?; + Ok(model) + } + (false, false) => { + bail!("Model is not available, and pulling is disabled") + } + } + } + + async fn pull_model_with_tracing(&self, model: &str) -> Result<()> { + let mut stream = self.pull_model_stream(model.to_owned(), false).await?; + + let mut last_status = "".to_string(); + let mut last_progress = 0.0; + + while let Some(result) = stream.next().await { + let response = result?; + let status = response.message; + if last_status != status { + info!("Status: {}", status); + last_status = status; + last_progress = 0.0; + } + + // Show progress only if 1% gain happened + if let (Some(completed), Some(total)) = (response.completed, response.total) { + let progress = completed as f64 / total as f64; + if progress - last_progress > 0.01 { + info!("Progress: {:.2}%", progress * 100.0); + last_progress = progress; + } + } + } + + Ok(()) + } +} diff --git a/crates/sqlx-migrate-validate/Cargo.toml b/crates/sqlx-migrate-validate/Cargo.toml new file mode 100644 index 000000000000..56de82eec450 --- /dev/null +++ b/crates/sqlx-migrate-validate/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "sqlx-migrate-validate" +version.workspace = true +edition.workspace = true +authors.workspace = true +homepage.workspace = true + +[dependencies] +thiserror.workspace = true +async-trait.workspace = true +sqlx = { workspace = true, features = ["sqlite", "runtime-tokio"] } + +[dev-dependencies] +anyhow.workspace = true +tokio = { workspace = true, features = ["macros", "rt"] } +sqlx-rt = { version = "0.6", features = [ "runtime-tokio-rustls" ] } diff --git a/crates/sqlx-migrate-validate/README.md b/crates/sqlx-migrate-validate/README.md new file mode 100644 index 000000000000..76b2d9ad10f0 --- /dev/null +++ b/crates/sqlx-migrate-validate/README.md @@ -0,0 +1,3 @@ +# sqlx-migrate-validate + +Vendored fork of https://github.com/tobikris/sqlx-migrate-validate \ No newline at end of file diff --git a/crates/sqlx-migrate-validate/src/error.rs b/crates/sqlx-migrate-validate/src/error.rs new file mode 100644 index 000000000000..659bd77dff63 --- /dev/null +++ b/crates/sqlx-migrate-validate/src/error.rs @@ -0,0 +1,11 @@ +use sqlx::migrate::MigrateError; + +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +pub enum ValidateError { + #[error("migration {0} was not applied")] + VersionNotApplied(i64), + + #[error(transparent)] + MigrateError(#[from] MigrateError), +} diff --git a/crates/sqlx-migrate-validate/src/lib.rs b/crates/sqlx-migrate-validate/src/lib.rs new file mode 100644 index 000000000000..6273771d686f --- /dev/null +++ b/crates/sqlx-migrate-validate/src/lib.rs @@ -0,0 +1,36 @@ +//! With [sqlx] and its [sqlx::migrate!] macro it is possible to create migrations and apply them to a database. +//! When starting an application it is important to validate that the database is in the correct state. +//! This crate provides a way to validate that the applied migrations match the desired migrations. +//! In combination with the [sqlx::migrate!] macro it is possible to validate that the database schema +//! matches the migrations present in the source code at the time of compilation. +//! +//! While this does not ensure full compatibility between the database and the application it can help +//! to detect issues early. +//! +//! Examples: +//! +//! ```rust,no_run +//! use sqlx_migrate_validate::{Validate, Validator}; +//! +//! #[tokio::main] +//! async fn main() -> Result<(), anyhow::Error> { +//! let pool = sqlx::SqlitePool::connect("sqlite::memory:").await?; +//! let mut conn = pool.acquire().await?; +//! +//! sqlx::migrate!("./tests/migrations-1") +//! .validate(&mut *conn) +//! .await?; +//! +//! Ok(()) +//! } +//! ``` + +mod error; +mod validate; + +pub use error::ValidateError; +pub use validate::*; + +#[doc = include_str!("../README.md")] +#[cfg(doctest)] +pub struct ReadmeDoctests; diff --git a/crates/sqlx-migrate-validate/src/validate.rs b/crates/sqlx-migrate-validate/src/validate.rs new file mode 100644 index 000000000000..71c5f01fd91a --- /dev/null +++ b/crates/sqlx-migrate-validate/src/validate.rs @@ -0,0 +1,311 @@ +use std::{ + borrow::Cow, + collections::{HashMap, HashSet}, +}; + +use async_trait::async_trait; +use sqlx::migrate::{ + AppliedMigration, Migrate, MigrateError, Migration, MigrationSource, Migrator, +}; + +use crate::error::ValidateError; + +#[async_trait(?Send)] +pub trait Validate { + /// Validate previously applied migrations against the migration source. + /// Depending on the migration source this can be used to check if all migrations + /// for the current version of the application have been applied. + /// Use [`Validator::from_migrator`] to use the migrations available during compilation. + /// + /// # Examples + /// + /// ```rust,no_run + /// # fn main() -> Result<(), Box> { + /// # sqlx_rt::block_on(async move { + /// # use sqlx_migrate_validate::Validator; + /// // Use migrations that were in a local folder during build: ./tests/migrations-1 + /// let v = Validator::from_migrator(sqlx::migrate!("./tests/migrations-1")); + /// + /// // Create a connection pool + /// let pool = sqlx::sqlite::SqlitePoolOptions::new().connect("sqlite::memory:").await?; + /// let mut conn = pool.acquire().await?; + /// + /// // Validate the migrations + /// v.validate(&mut *conn).await?; + /// # Ok(()) + /// # }) + /// # } + /// ``` + async fn validate<'c, C>(&self, conn: &mut C) -> Result<(), ValidateError> + where + C: Migrate; +} + +/// Validate previously applied migrations against the migration source. +/// Depending on the migration source this can be used to check if all migrations +/// for the current version of the application have been applied. +/// Use [`Validator::from_migrator`] to use the migrations available during compilation. +/// +/// # Examples +/// +/// ```rust,no_run +/// # fn main() -> Result<(), Box> { +/// # sqlx_rt::block_on(async move { +/// # use sqlx_migrate_validate::Validator; +/// // Use migrations that were in a local folder during build: ./tests/migrations-1 +/// let v = Validator::from_migrator(sqlx::migrate!("./tests/migrations-1")); +/// +/// // Create a connection pool +/// let pool = sqlx::sqlite::SqlitePoolOptions::new().connect("sqlite::memory:").await?; +/// let mut conn = pool.acquire().await?; +/// +/// // Validate the migrations +/// v.validate(&mut *conn).await?; +/// # Ok(()) +/// # }) +/// # } +/// ``` +#[derive(Debug)] +pub struct Validator { + pub migrations: Cow<'static, [Migration]>, + pub ignore_missing: bool, + pub locking: bool, +} + +impl Validator { + /// Creates a new instance with the given source. Please note that the source + /// is resolved at runtime and not at compile time. + /// You can use [`Validator::from`] and the [`sqlx::migrate!`] macro + /// to embed the migrations into the binary during compile time. + /// + /// # Examples + /// + /// ```rust,no_run + /// # fn main() -> Result<(), Box> { + /// # sqlx_rt::block_on(async move { + /// # use sqlx_migrate_validate::Validator; + /// use std::path::Path; + /// + /// // Read migrations from a local folder: ./tests/migrations-1 + /// let v = Validator::new(Path::new("./tests/migrations-1")).await?; + /// + /// // Create a connection pool + /// let pool = sqlx::sqlite::SqlitePoolOptions::new().connect("sqlite::memory:").await?; + /// let mut conn = pool.acquire().await?; + /// + /// // Validate the migrations + /// v.validate(&mut *conn).await?; + /// # Ok(()) + /// # }) + /// # } + /// ``` + /// + /// See [MigrationSource] for details on structure of the `./tests/migrations-1` directory. + pub async fn new<'s, S>(source: S) -> Result + where + S: MigrationSource<'s>, + { + Ok(Self { + migrations: Cow::Owned(source.resolve().await.map_err(MigrateError::Source)?), + ignore_missing: false, + locking: true, + }) + } + + /// Creates a new instance with the migrations from the given migrator. + /// You can combine this with the [`sqlx::migrate!`] macro + /// to embed the migrations into the binary during compile time. + /// + /// # Examples + /// + /// ```rust,no_run + /// # fn main() -> Result<(), Box> { + /// # sqlx_rt::block_on(async move { + /// # use sqlx_migrate_validate::Validator; + /// // Use migrations that were in a local folder during build: ./tests/migrations-1 + /// let v = Validator::from_migrator(sqlx::migrate!("./tests/migrations-1")); + /// + /// // Create a connection pool + /// let pool = sqlx::sqlite::SqlitePoolOptions::new().connect("sqlite::memory:").await?; + /// let mut conn = pool.acquire().await?; + /// + /// // Validate the migrations + /// v.validate(&mut *conn).await?; + /// # Ok(()) + /// # }) + /// # } + /// ``` + pub fn from_migrator(migrator: Migrator) -> Self { + Self { + migrations: migrator.migrations.clone(), + ignore_missing: migrator.ignore_missing, + locking: migrator.locking, + } + } + + pub async fn validate<'c, C>(&self, conn: &mut C) -> Result<(), ValidateError> + where + C: Migrate, + { + // lock the migrator to prevent other migrators from running + if self.locking { + conn.lock().await?; + } + + let version = conn.dirty_version().await?; + if let Some(version) = version { + return Err(ValidateError::MigrateError(MigrateError::Dirty(version))); + } + + let applied_migrations = conn.list_applied_migrations().await?; + validate_applied_migrations(&applied_migrations, self)?; + + let applied_migrations: HashMap<_, _> = applied_migrations + .into_iter() + .map(|m| (m.version, m)) + .collect(); + + for migration in self.migrations.iter() { + if migration.migration_type.is_down_migration() { + continue; + } + + match applied_migrations.get(&migration.version) { + Some(applied_migration) => { + if migration.checksum != applied_migration.checksum { + return Err(ValidateError::MigrateError(MigrateError::VersionMismatch( + migration.version, + ))); + } + } + None => { + return Err(ValidateError::VersionNotApplied(migration.version)); + // conn.apply(migration).await?; + } + } + } + + // unlock the migrator to allow other migrators to run + // but do nothing as we already migrated + if self.locking { + conn.unlock().await?; + } + + Ok(()) + } +} + +impl From<&Migrator> for Validator { + fn from(migrator: &Migrator) -> Self { + Self { + migrations: migrator.migrations.clone(), + ignore_missing: migrator.ignore_missing, + locking: migrator.locking, + } + } +} + +impl From for Validator { + fn from(migrator: Migrator) -> Self { + Self::from(&migrator) + } +} + +#[async_trait(?Send)] +impl Validate for Migrator { + async fn validate<'c, C>(&self, conn: &mut C) -> Result<(), ValidateError> + where + C: Migrate, + { + Validator::from(self).validate(conn).await + } +} + +fn validate_applied_migrations( + applied_migrations: &[AppliedMigration], + migrator: &Validator, +) -> Result<(), MigrateError> { + if migrator.ignore_missing { + return Ok(()); + } + + let migrations: HashSet<_> = migrator.migrations.iter().map(|m| m.version).collect(); + + for applied_migration in applied_migrations { + if !migrations.contains(&applied_migration.version) { + return Err(MigrateError::VersionMissing(applied_migration.version)); + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use sqlx::migrate::MigrationType; + + use super::*; + + #[test] + fn validate_applied_migrations_returns_ok_when_nothing_was_applied() { + let applied_migrations = vec![]; + let mut validator = Validator { + migrations: Cow::Owned(vec![]), + ignore_missing: false, + locking: true, + }; + + assert!(validate_applied_migrations(&applied_migrations, &validator).is_ok()); + + validator.ignore_missing = true; + assert!(validate_applied_migrations(&applied_migrations, &validator).is_ok()); + } + + #[test] + fn validate_applied_migrations_returns_err_when_applied_migration_not_in_source() { + let applied_migrations = vec![AppliedMigration { + version: 1, + + // only the version is relevant for this method + checksum: Cow::Owned(vec![]), + }]; + let validator = Validator { + migrations: Cow::Owned(vec![]), + ignore_missing: false, + locking: true, + }; + + match validate_applied_migrations(&applied_migrations, &validator) { + Err(MigrateError::VersionMissing(i)) => assert_eq!(i, 1), + _ => panic!("Unexpected error"), + } + } + + #[test] + fn validate_applied_migrations_returns_ok_when_applied_migration_in_source() { + let applied_migrations = vec![AppliedMigration { + version: 1, + + // only the version is relevant for this method + checksum: Cow::Owned(vec![]), + }]; + let validator = Validator { + migrations: Cow::Owned(vec![Migration { + version: 1, + + // only the version is relevant for this method + migration_type: MigrationType::ReversibleUp, + checksum: Cow::Owned(vec![]), + sql: Cow::Owned("".to_string()), + description: Cow::Owned("".to_string()), + }]), + ignore_missing: false, + locking: true, + }; + + match validate_applied_migrations(&applied_migrations, &validator) { + Ok(_) => {} + _ => panic!("Unexpected error"), + } + } +} diff --git a/crates/sqlx-migrate-validate/tests/migrations-1/20230312133715_create_table.sql b/crates/sqlx-migrate-validate/tests/migrations-1/20230312133715_create_table.sql new file mode 100644 index 000000000000..a6a6b343bd3e --- /dev/null +++ b/crates/sqlx-migrate-validate/tests/migrations-1/20230312133715_create_table.sql @@ -0,0 +1,4 @@ +CREATE TABLE users ( + user_id TEXT PRIMARY KEY, + username TEXT NOT NULL +); \ No newline at end of file diff --git a/crates/sqlx-migrate-validate/tests/migrations-2/20230312133715_create_table.sql b/crates/sqlx-migrate-validate/tests/migrations-2/20230312133715_create_table.sql new file mode 100644 index 000000000000..a6a6b343bd3e --- /dev/null +++ b/crates/sqlx-migrate-validate/tests/migrations-2/20230312133715_create_table.sql @@ -0,0 +1,4 @@ +CREATE TABLE users ( + user_id TEXT PRIMARY KEY, + username TEXT NOT NULL +); \ No newline at end of file diff --git a/crates/sqlx-migrate-validate/tests/migrations-2/20230312141719_add_other_table.sql b/crates/sqlx-migrate-validate/tests/migrations-2/20230312141719_add_other_table.sql new file mode 100644 index 000000000000..068f31f4261f --- /dev/null +++ b/crates/sqlx-migrate-validate/tests/migrations-2/20230312141719_add_other_table.sql @@ -0,0 +1,4 @@ +CREATE TABLE roles ( + role_id TEXT PRIMARY KEY, + role_name TEXT NOT NULL +); \ No newline at end of file diff --git a/crates/sqlx-migrate-validate/tests/migrations-3/20230312141719_add_other_table.sql b/crates/sqlx-migrate-validate/tests/migrations-3/20230312141719_add_other_table.sql new file mode 100644 index 000000000000..068f31f4261f --- /dev/null +++ b/crates/sqlx-migrate-validate/tests/migrations-3/20230312141719_add_other_table.sql @@ -0,0 +1,4 @@ +CREATE TABLE roles ( + role_id TEXT PRIMARY KEY, + role_name TEXT NOT NULL +); \ No newline at end of file diff --git a/crates/sqlx-migrate-validate/tests/migrations-4/20230312133715_create_table.sql b/crates/sqlx-migrate-validate/tests/migrations-4/20230312133715_create_table.sql new file mode 100644 index 000000000000..763f67456b9c --- /dev/null +++ b/crates/sqlx-migrate-validate/tests/migrations-4/20230312133715_create_table.sql @@ -0,0 +1,5 @@ +CREATE TABLE users ( + user_id TEXT PRIMARY KEY, + username TEXT NOT NULL, + email TEXT NOT NULL +); \ No newline at end of file diff --git a/crates/sqlx-migrate-validate/tests/validate.rs b/crates/sqlx-migrate-validate/tests/validate.rs new file mode 100644 index 000000000000..0d2823930137 --- /dev/null +++ b/crates/sqlx-migrate-validate/tests/validate.rs @@ -0,0 +1,71 @@ +use std::path::Path; + +use sqlx::migrate::MigrateError; +use sqlx_migrate_validate::{Validate, ValidateError, Validator}; + +async fn prepare() -> sqlx::sqlite::SqliteConnection { + let pool = sqlx::SqlitePool::connect("sqlite::memory:").await.unwrap(); + let mut conn = pool.acquire().await.unwrap(); + + sqlx::migrate!("./tests/migrations-1") + .run(&mut conn) + .await + .unwrap(); + + conn.detach() +} + +#[tokio::test] +async fn validate_returns_ok_with_same_migrations() { + let mut conn = prepare().await; + + sqlx::migrate!("./tests/migrations-1") + .validate(&mut conn) + .await + .unwrap(); +} + +#[tokio::test] +async fn validate_returns_err_with_more_migrations_in_source() { + let mut conn = prepare().await; + + match Validator::new(Path::new("./tests/migrations-2")) + .await + .unwrap() + .validate(&mut conn) + .await + { + Err(ValidateError::VersionNotApplied(20230312141719)) => (), + o => panic!("Expected VersionNotApplied error, got: {o:?}"), + }; +} + +#[tokio::test] +async fn validate_returns_err_with_less_migrations_in_source() { + let mut conn = prepare().await; + + match Validator::new(Path::new("./tests/migrations-3")) + .await + .unwrap() + .validate(&mut conn) + .await + { + Err(ValidateError::MigrateError(MigrateError::VersionMissing(20230312133715))) => (), + o => panic!("Expected VersionMissing error, got: {o:?}"), + }; +} + +#[tokio::test] +async fn validate_returns_err_with_migration_content_mismatch() { + let mut conn = prepare().await; + + match Validator::new(Path::new("./tests/migrations-4")) + .await + .unwrap() + .validate(&mut conn) + .await + { + Err(ValidateError::MigrateError(MigrateError::VersionMismatch(20230312133715))) => (), + o => panic!("Expected VersionMismatch error, got: {o:?}"), + }; +} diff --git a/crates/tabby-common/Cargo.toml b/crates/tabby-common/Cargo.toml index 14ac6e083b92..d4417969d826 100644 --- a/crates/tabby-common/Cargo.toml +++ b/crates/tabby-common/Cargo.toml @@ -1,25 +1,37 @@ [package] name = "tabby-common" -version = "0.8.0" -edition = "2021" +version.workspace = true +edition.workspace = true +authors.workspace = true +homepage.workspace = true [dependencies] -filenamify = "0.1.0" home = "0.5.5" lazy_static = { workspace = true } serde = { workspace = true } serdeconv = { workspace = true } -serde-jsonlines = { workspace = true } reqwest = { workspace = true, features = [ "json" ] } uuid = { version = "1.4.1", features = ["v4"] } tantivy.workspace = true anyhow.workspace = true -glob = "0.3.1" utoipa.workspace = true serde_json.workspace = true async-trait.workspace = true thiserror.workspace = true -regex.workspace = true +url.workspace = true +derive_builder.workspace = true +hash-ids.workspace = true +tracing.workspace = true +chrono.workspace = true +axum.workspace = true +axum-extra = { workspace = true, features = ["typed-header"] } +parse-git-url = "0.5.1" +validator = { workspace = true } +humantime = { workspace = true } + +[dev-dependencies] +temp_testdir = { workspace = true } +tokio = { workspace = true, features = ["full"] } [features] testutils = [] diff --git a/crates/tabby-common/assets/languages.toml b/crates/tabby-common/assets/languages.toml index 38975f950f86..33d5f9ae8a98 100644 --- a/crates/tabby-common/assets/languages.toml +++ b/crates/tabby-common/assets/languages.toml @@ -1,10 +1,12 @@ [[config]] languages = ["python"] +exts = ["py"] line_comment = "#" top_level_keywords = ["def", "from", "class", "import"] [[config]] languages = ["rust"] +exts = ["rs"] line_comment = "//" top_level_keywords = [ "fn", @@ -21,6 +23,7 @@ top_level_keywords = [ [[config]] languages = ["java"] +exts = ["java"] line_comment = "//" top_level_keywords = [ "abstract", @@ -43,6 +46,7 @@ top_level_keywords = [ [[config]] languages = ["kotlin"] +exts = ["kt", "kts"] line_comment = "//" top_level_keywords = [ "abstract", @@ -66,7 +70,8 @@ top_level_keywords = [ ] [[config]] -languages = ["javascript", "typescript", "javascriptreact", "typescriptreact"] +languages = ["javascript-typescript", "javascript", "typescript", "javascriptreact", "typescriptreact"] +exts = ["js", "ts", "jsx", "tsx", "mjs", "mts"] line_comment = "//" top_level_keywords = [ "abstract", @@ -86,6 +91,7 @@ top_level_keywords = [ [[config]] languages = ["go"] +exts = ["go"] line_comment = "//" top_level_keywords = [ "func", @@ -100,6 +106,7 @@ top_level_keywords = [ [[config]] languages = ["ruby"] +exts = ["rb"] line_comment = "#" top_level_keywords = [ "begin", @@ -116,6 +123,7 @@ top_level_keywords = [ [[config]] languages = ["c"] +exts = ["c", "h"] line_comment = "//" top_level_keywords = [ "const", @@ -132,6 +140,7 @@ top_level_keywords = [ [[config]] languages = ["cpp"] +exts= ["cpp", "hpp", "c++", "h++", "cc", "hh", "C", "H", "tcc"] line_comment = "//" top_level_keywords = [ "auto", @@ -153,6 +162,7 @@ top_level_keywords = [ [[config]] languages = ["csharp"] +exts = ["cs"] line_comment = "//" top_level_keywords = [ "class", @@ -169,9 +179,9 @@ top_level_keywords = [ "async", ] - [[config]] languages = ["php"] +exts = ["php", "php3", "php4", "php5", "phps", "phpt"] line_comment = "//" top_level_keywords = [ "abstract", @@ -197,3 +207,267 @@ top_level_keywords = [ "trait", "use", ] + +[[config]] +languages = ["solidity"] +exts = ["sol"] +line_comment = "//" +top_level_keywords = [ + "contract", + "interface", + "error", + "library", + "struct", + "enum", + "function", + "type", +] + +[[config]] +languages = ["dart"] +exts = ["dart"] +line_comment = "//" +top_level_keywords = [ + "abstract", + "class", + "late", + "var", + "final", + "const", + "export", + "interface", + "type", + "enum", + "int", + "String", + "double", + "DateTime", + "typedef", +] + +[[config]] +languages = ["elixir"] +exts = ["ex", "exs"] +line_comment = "#" +top_level_keywords = [ + "def", + "defp", + "defmacro", + "defmacrop", + "module", + "use", + "import", + "alias", + "require", + "if", + "unless", + "case", + "cond", + "with", + "try", + "catch", + "throw", + "receive", + "after", + "do", + "end", +] + +[[config]] +languages = ["r"] +exts = ["R", "r"] +line_comment = "#" +top_level_keywords = ["function", "library", "install.packages"] + +[[config]] +languages = ["css"] +exts = ["css"] + +[[config]] +languages = ["dockerfile"] +exts = ["Dockerfile"] + +[[config]] +languages = ["haskell"] +exts = ["hs"] + +[[config]] +languages = ["html"] +exts = ["html"] + +[[config]] +languages = ["julia"] +exts = ["jl"] + +[[config]] +languages = ["lua"] +exts = ["lua"] +line_comment = "--" +top_level_keywords = [ + "and", + "break", + "do", + "else", + "elseif", + "end", + "false", + "for", + "function", + "if", + "in", + "local", + "nil", + "not", + "or", + "repeat", + "return", + "then", + "true", + "until", + "while" +] + +[[config]] +languages = ["makefile"] +exts = ["Makefile"] + +[[config]] +languages = ["markdown"] +exts = ["md", "markdown"] +chunk_size = 1536 + +[[config]] +languages = ["txt"] +exts = ["txt"] +chunk_size = 1536 + +[[config]] +languages = ["perl"] +exts = ["pl", "pm", "pod", "perl"] + +[[config]] +languages = ["powershell"] +exts = ["ps1", "psd1", "psm1"] + +[[config]] +languages = ["sql"] +exts = ["sql"] + +[[config]] +languages = ["scala"] +exts = ["scala", "sbt"] +line_comment = "//" +top_level_keywords = [ + "abstract", + "case", + "class", + "def", + "do", + "else", + "enum", + "extends", + "final", + "for", + "given", + "if", + "implicit", + "import", + "lazy", + "match", + "new", + "object", + "override", + "package", + "private", + "protected", + "return", + "sealed", + "then", + "throw", + "trait", + "try", + "type", + "val", + "var", + "while", + "with", + "yield", +] + +[[config]] +languages = ["shellscript"] +exts = ["sh", "bash", "command", "zsh"] + +[[config]] +languages = ["tex"] +exts = ["tex"] + +[[config]] +languages = ["vb"] +exts = ["vb"] + +[[config]] +languages = ["ocaml"] +exts = ["ml", "mli"] +top_level_keywords = [ + "and", + "as", + "assert", + "begin", + "class", + "constraint", + "do", + "done", + "downto", + "else", + "end", + "exception", + "external", + "for", + "fun", + "function", + "functor", + "if", + "in", + "include", + "inherit", + "initializer", + "lazy", + "let", + "match", + "method", + "module", + "mutable", + "new", + "nonrec", + "object", + "of", + "open", + "private", + "rec", + "sig", + "struct", + "then", + "to", + "try", + "type", + "val", + "virtual", + "when", + "while", + "with", +] + +[[config]] +languages = ["gdscript"] +exts = ["gd", "tscn", "tres"] +line_comment = "#" +top_level_keywords = [ + "var", + "const", + "enum", + "func", + "class", + "class_name", + "extends", +] \ No newline at end of file diff --git a/crates/tabby-common/src/api/code.rs b/crates/tabby-common/src/api/code.rs index c44c65033657..dfdc9b1a00d4 100644 --- a/crates/tabby-common/src/api/code.rs +++ b/crates/tabby-common/src/api/code.rs @@ -1,29 +1,44 @@ use async_trait::async_trait; +use derive_builder::Builder; use serde::{Deserialize, Serialize}; use thiserror::Error; -use utoipa::ToSchema; -#[derive(Default, Serialize, Deserialize, Debug, ToSchema)] -pub struct SearchResponse { - pub num_hits: usize, - pub hits: Vec, +pub struct CodeSearchResponse { + pub hits: Vec, } -#[derive(Serialize, Deserialize, Debug, ToSchema)] -pub struct Hit { - pub score: f32, - pub doc: HitDocument, - pub id: u32, +#[derive(Default, Clone, PartialEq, Debug)] +pub struct CodeSearchHit { + pub scores: CodeSearchScores, + pub doc: CodeSearchDocument, } -#[derive(Serialize, Deserialize, Debug, ToSchema)] -pub struct HitDocument { +#[derive(Default, Clone, PartialEq, Debug)] +pub struct CodeSearchScores { + /// Reciprocal rank fusion score: https://www.elastic.co/guide/en/elasticsearch/reference/current/rrf.html + pub rrf: f32, + pub bm25: f32, + pub embedding: f32, +} + +#[derive(Builder, Default, Clone, PartialEq, Debug)] +pub struct CodeSearchDocument { + /// Unique identifier for the file in the repository, stringified SourceFileKey. + pub file_id: String, + pub chunk_id: String, + pub body: String, pub filepath: String, pub git_url: String, - pub kind: String, + + // FIXME(kweizh): This should be a required field after 0.25.0. + // commit represents the specific revision at which the file was last edited. + pub commit: Option, + pub language: String, - pub name: String, + + /// When start line is `None`, it represents the entire file. + pub start_line: Option, } #[derive(Error, Debug)] @@ -36,22 +51,95 @@ pub enum CodeSearchError { #[error(transparent)] TantivyError(#[from] tantivy::TantivyError), + + #[error(transparent)] + Other(#[from] anyhow::Error), +} + +pub struct CodeSearchQuery { + /// filepath in code search query always normalize to unix style. + pub filepath: Option, + pub language: Option, + pub content: String, + pub source_id: String, +} + +impl CodeSearchQuery { + pub fn new( + filepath: Option, + language: Option, + content: String, + source_id: String, + ) -> Self { + Self { + filepath: filepath.map(|path| normalize_to_unix_path(&path)), + language, + content, + source_id, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct CodeSearchParams { + pub min_embedding_score: f32, + pub min_bm25_score: f32, + pub min_rrf_score: f32, + + /// At most num_to_return results will be returned. + pub num_to_return: usize, + + /// At most num_to_score results will be scored. + pub num_to_score: usize, +} + +impl Default for CodeSearchParams { + fn default() -> Self { + Self { + min_embedding_score: 0.75, + min_bm25_score: 8.0, + min_rrf_score: 0.028, + + num_to_return: 20, + num_to_score: 40, + } + } } #[async_trait] pub trait CodeSearch: Send + Sync { - async fn search( - &self, - q: &str, - limit: usize, - offset: usize, - ) -> Result; - async fn search_in_language( &self, - language: &str, - tokens: &[String], - limit: usize, - offset: usize, - ) -> Result; + query: CodeSearchQuery, + params: CodeSearchParams, + ) -> Result; +} + +/// Normalize the path form different platform to unix style path +pub fn normalize_to_unix_path(filepath: &str) -> String { + filepath.replace('\\', "/") +} +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_relative_path_normalization() { + let unix_test_cases = [ + ("./src/main.rs", "./src/main.rs"), + (".\\src\\main.rs", "./src/main.rs"), + ("../test/data.json", "../test/data.json"), + ("..\\test\\data.json", "../test/data.json"), + ("src/test/file.txt", "src/test/file.txt"), + ("src\\test\\file.txt", "src/test/file.txt"), + ]; + + for (input, expected) in unix_test_cases { + assert_eq!( + normalize_to_unix_path(input), + expected.to_string(), + "Failed to normalize path: {input}" + ); + } + } } diff --git a/crates/tabby-common/src/api/event.rs b/crates/tabby-common/src/api/event.rs index c51594e95e66..06814e06ef9b 100644 --- a/crates/tabby-common/src/api/event.rs +++ b/crates/tabby-common/src/api/event.rs @@ -17,19 +17,19 @@ pub struct LogEventRequest { pub elapsed: Option, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct Choice { pub index: u32, pub text: String, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "snake_case")] pub enum SelectKind { Line, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "snake_case")] pub enum Event { View { @@ -67,55 +67,47 @@ pub enum Event { #[serde(skip_serializing_if = "Option::is_none")] segments: Option, choices: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - user: Option, - }, - ChatCompletion { - completion_id: String, - input: Vec, - output: Message, + user_agent: Option, }, + ChatCompletion {}, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct Message { pub role: String, pub content: String, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct Segments { pub prefix: String, + #[serde(skip_serializing_if = "Option::is_none")] pub suffix: Option, #[serde(skip_serializing_if = "Option::is_none")] pub clipboard: Option, -} -pub trait EventLogger: Send + Sync { - fn log(&self, e: Event); -} + #[serde(skip_serializing_if = "Option::is_none")] + pub git_url: Option, -#[derive(Serialize)] -struct Log { - ts: u128, - event: Event, -} + #[serde(skip_serializing_if = "Option::is_none")] + pub declarations: Option>, -pub trait RawEventLogger: Send + Sync { - fn log(&self, content: String); + #[serde(skip_serializing_if = "Option::is_none")] + pub filepath: Option, } -impl EventLogger for T { - fn log(&self, e: Event) { - let content = serdeconv::to_json_string(&Log { - ts: timestamp(), - event: e, - }) - .unwrap(); +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Declaration { + pub filepath: String, + pub body: String, +} - self.log(content); - } +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct LogEntry { + pub user: Option, + pub ts: u128, + pub event: Event, } fn timestamp() -> u128 { @@ -126,3 +118,32 @@ fn timestamp() -> u128 { .expect("Time went backwards") .as_millis() } + +pub trait EventLogger: Send + Sync { + fn log(&self, user: Option, event: Event) { + self.write(LogEntry { + user, + ts: timestamp(), + event, + }) + } + fn write(&self, x: LogEntry); +} + +pub struct ComposedLogger { + logger1: T1, + logger2: T2, +} + +impl ComposedLogger { + pub fn new(logger1: T1, logger2: T2) -> Self { + Self { logger1, logger2 } + } +} + +impl EventLogger for ComposedLogger { + fn write(&self, x: LogEntry) { + self.logger1.write(x.clone()); + self.logger2.write(x); + } +} diff --git a/crates/tabby-common/src/api/ingestion.rs b/crates/tabby-common/src/api/ingestion.rs new file mode 100644 index 000000000000..ed141c3fef1a --- /dev/null +++ b/crates/tabby-common/src/api/ingestion.rs @@ -0,0 +1,50 @@ +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use validator::{Validate, ValidationError}; + +#[derive(Serialize, Deserialize, ToSchema, Clone, Debug, Validate)] +pub struct IngestionRequest { + /// Source of the document (frontend available, backend sourceId: `ingestedSource:${source}`) + #[validate(length( + min = 1, + max = 256, + code = "source", + message = "source can not be empty" + ))] + pub source: String, + + /// unique whthin the same source + #[validate(length(min = 1, max = 256, code = "id", message = "id can not be empty"))] + pub id: String, + + #[validate(length( + min = 1, + max = 65535, + code = "title", + message = "title can not be empty" + ))] + pub title: String, + pub body: String, + + #[serde(default, skip_serializing_if = "Option::is_none")] + pub link: Option, + + /// Time-to-live duration (optional). Duration string like "90d" + #[serde(default, skip_serializing_if = "Option::is_none")] + #[validate(custom(function = "validate_ttl"))] + pub ttl: Option, +} + +fn validate_ttl(ttl: &String) -> Result<(), ValidationError> { + if humantime::parse_duration(ttl).is_err() { + return Err(ValidationError::new("invalid_ttl")); + } + Ok(()) +} + +#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)] +pub struct IngestionResponse { + pub id: String, + pub source: String, + pub message: String, +} diff --git a/crates/tabby-common/src/api/mod.rs b/crates/tabby-common/src/api/mod.rs index cebf170a1e90..4bfaca2088c7 100644 --- a/crates/tabby-common/src/api/mod.rs +++ b/crates/tabby-common/src/api/mod.rs @@ -1,2 +1,5 @@ pub mod code; pub mod event; +pub mod ingestion; +pub mod server_setting; +pub mod structured_doc; diff --git a/crates/tabby-common/src/api/server_setting.rs b/crates/tabby-common/src/api/server_setting.rs new file mode 100644 index 000000000000..594af464e6d5 --- /dev/null +++ b/crates/tabby-common/src/api/server_setting.rs @@ -0,0 +1,7 @@ +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)] +pub struct ServerSetting { + pub disable_client_side_telemetry: bool, +} diff --git a/crates/tabby-common/src/api/structured_doc.rs b/crates/tabby-common/src/api/structured_doc.rs new file mode 100644 index 000000000000..152a4e6139e9 --- /dev/null +++ b/crates/tabby-common/src/api/structured_doc.rs @@ -0,0 +1,392 @@ +use async_trait::async_trait; +use chrono::{DateTime, TimeZone, Utc}; +use tantivy::{ + schema::{self, document::CompactDocValue, Value}, + TantivyDocument, +}; +use thiserror::Error; + +use crate::index::{structured_doc, IndexSchema}; + +pub struct DocSearchResponse { + pub hits: Vec, +} + +pub struct DocSearchHit { + pub score: f32, + pub doc: DocSearchDocument, +} + +#[derive(Clone)] +pub enum DocSearchDocument { + Web(DocSearchWebDocument), + Issue(DocSearchIssueDocument), + Pull(DocSearchPullDocument), + Commit(DocSearchCommit), + Page(DocSearchPageDocument), + Ingested(DocSearchIngestedDocument), +} + +#[derive(Error, Debug)] +pub enum DocSearchError { + #[error("index not ready")] + NotReady, + + #[error(transparent)] + QueryParserError(#[from] tantivy::query::QueryParserError), + + #[error(transparent)] + TantivyError(#[from] tantivy::TantivyError), + + #[error(transparent)] + Other(#[from] anyhow::Error), +} + +#[async_trait] +pub trait DocSearch: Send + Sync { + /// Search docs from underlying index. + /// + /// * `source_ids`: Filter documents by source IDs, when empty, search all sources. + async fn search( + &self, + source_ids: &[String], + q: &str, + limit: usize, + ) -> Result; +} + +#[derive(Clone)] +pub struct DocSearchWebDocument { + pub title: String, + pub link: String, + pub snippet: String, +} + +#[derive(Clone)] +pub struct DocSearchIssueDocument { + pub title: String, + pub link: String, + pub author_email: Option, + pub body: String, + pub closed: bool, +} + +#[derive(Clone)] +pub struct DocSearchPullDocument { + pub title: String, + pub link: String, + pub author_email: Option, + pub body: String, + pub diff: String, + pub merged: bool, +} + +#[derive(Clone)] +pub struct DocSearchCommit { + pub sha: String, + pub message: String, + pub author_email: String, + pub author_at: DateTime, +} + +#[derive(Clone)] +pub struct DocSearchPageDocument { + pub link: String, + pub title: String, + pub content: String, +} + +#[derive(Clone)] +pub struct DocSearchIngestedDocument { + pub id: String, + pub title: String, + pub body: String, + pub link: Option, +} + +pub trait FromTantivyDocument { + fn from_tantivy_document(doc: &TantivyDocument, chunk: &TantivyDocument) -> Option + where + Self: Sized; +} + +impl FromTantivyDocument for DocSearchDocument { + fn from_tantivy_document(doc: &TantivyDocument, chunk: &TantivyDocument) -> Option { + let schema = IndexSchema::instance(); + let kind = get_json_text_field(doc, schema.field_attributes, structured_doc::fields::KIND); + + match kind { + "web" => { + DocSearchWebDocument::from_tantivy_document(doc, chunk).map(DocSearchDocument::Web) + } + "issue" => DocSearchIssueDocument::from_tantivy_document(doc, chunk) + .map(DocSearchDocument::Issue), + "pull" => DocSearchPullDocument::from_tantivy_document(doc, chunk) + .map(DocSearchDocument::Pull), + "commit" => { + DocSearchCommit::from_tantivy_document(doc, chunk).map(DocSearchDocument::Commit) + } + "page" => DocSearchPageDocument::from_tantivy_document(doc, chunk) + .map(DocSearchDocument::Page), + "ingested" => DocSearchIngestedDocument::from_tantivy_document(doc, chunk) + .map(DocSearchDocument::Ingested), + _ => None, + } + } +} + +impl FromTantivyDocument for DocSearchWebDocument { + fn from_tantivy_document(doc: &TantivyDocument, chunk: &TantivyDocument) -> Option { + let schema = IndexSchema::instance(); + let title = get_json_text_field( + doc, + schema.field_attributes, + structured_doc::fields::web::TITLE, + ); + let link = get_json_text_field( + doc, + schema.field_attributes, + structured_doc::fields::web::LINK, + ); + let snippet = get_json_text_field( + chunk, + schema.field_chunk_attributes, + structured_doc::fields::web::CHUNK_TEXT, + ); + + Some(Self { + title: title.into(), + link: link.into(), + snippet: snippet.into(), + }) + } +} + +impl FromTantivyDocument for DocSearchIssueDocument { + fn from_tantivy_document(doc: &TantivyDocument, _: &TantivyDocument) -> Option { + let schema = IndexSchema::instance(); + let title = get_json_text_field( + doc, + schema.field_attributes, + structured_doc::fields::issue::TITLE, + ); + let link = get_json_text_field( + doc, + schema.field_attributes, + structured_doc::fields::issue::LINK, + ); + let author_email = get_json_option_text_field( + doc, + schema.field_attributes, + structured_doc::fields::issue::AUTHOR_EMAIL, + ); + let body = get_json_text_field( + doc, + schema.field_attributes, + structured_doc::fields::issue::BODY, + ); + let closed = get_json_bool_field( + doc, + schema.field_attributes, + structured_doc::fields::issue::CLOSED, + ); + Some(Self { + title: title.into(), + link: link.into(), + author_email: author_email.map(Into::into), + body: body.into(), + closed, + }) + } +} + +impl FromTantivyDocument for DocSearchPullDocument { + fn from_tantivy_document(doc: &TantivyDocument, _: &TantivyDocument) -> Option { + let schema = IndexSchema::instance(); + let title = get_json_text_field( + doc, + schema.field_attributes, + structured_doc::fields::pull::TITLE, + ); + let link = get_json_text_field( + doc, + schema.field_attributes, + structured_doc::fields::pull::LINK, + ); + let author_email = get_json_option_text_field( + doc, + schema.field_attributes, + structured_doc::fields::pull::AUTHOR_EMAIL, + ); + let body = get_json_text_field( + doc, + schema.field_attributes, + structured_doc::fields::pull::BODY, + ); + let diff = get_json_option_text_field( + doc, + schema.field_attributes, + structured_doc::fields::pull::DIFF, + ); + let merged = get_json_bool_field( + doc, + schema.field_attributes, + structured_doc::fields::pull::MERGED, + ); + Some(Self { + title: title.into(), + link: link.into(), + author_email: author_email.map(Into::into), + body: body.into(), + diff: diff.unwrap_or_default().into(), + merged, + }) + } +} + +impl FromTantivyDocument for DocSearchCommit { + fn from_tantivy_document(doc: &TantivyDocument, _chunk: &TantivyDocument) -> Option { + let schema = IndexSchema::instance(); + let sha = get_json_text_field( + doc, + schema.field_attributes, + structured_doc::fields::commit::SHA, + ) + .to_string(); + let message = get_json_text_field( + doc, + schema.field_attributes, + structured_doc::fields::commit::MESSAGE, + ) + .to_string(); + let author_email = get_json_text_field( + doc, + schema.field_attributes, + structured_doc::fields::commit::AUTHOR_EMAIL, + ) + .to_string(); + let author_at = get_json_date_field( + doc, + schema.field_attributes, + structured_doc::fields::commit::AUTHOR_AT, + ) + .unwrap_or_default(); + + Some(Self { + sha, + message, + author_email, + author_at, + }) + } +} + +impl FromTantivyDocument for DocSearchPageDocument { + fn from_tantivy_document(doc: &TantivyDocument, chunk: &TantivyDocument) -> Option { + let schema = IndexSchema::instance(); + let link = get_json_text_field( + doc, + schema.field_attributes, + structured_doc::fields::page::LINK, + ); + let title = get_json_text_field( + doc, + schema.field_attributes, + structured_doc::fields::page::TITLE, + ); + let content = get_json_text_field( + chunk, + schema.field_chunk_attributes, + structured_doc::fields::page::CHUNK_CONTENT, + ); + + Some(Self { + link: link.into(), + title: title.into(), + content: content.into(), + }) + } +} + +impl FromTantivyDocument for DocSearchIngestedDocument { + fn from_tantivy_document(doc: &TantivyDocument, chunk: &TantivyDocument) -> Option { + let schema = IndexSchema::instance(); + let id = doc.get_first(schema.field_id).unwrap().as_str().unwrap(); + let title = get_json_text_field( + doc, + schema.field_attributes, + structured_doc::fields::ingested::TITLE, + ); + let body = get_json_text_field( + chunk, + schema.field_chunk_attributes, + structured_doc::fields::ingested::CHUNK_BODY, + ); + let link = get_json_option_text_field( + doc, + schema.field_attributes, + structured_doc::fields::page::LINK, + ); + + Some(Self { + id: id.into(), + link: link.map(Into::into), + title: title.into(), + body: body.into(), + }) + } +} + +fn get_json_field<'a>( + doc: &'a TantivyDocument, + field: schema::Field, + name: &str, +) -> CompactDocValue<'a> { + doc.get_first(field) + .unwrap() + .as_object() + .unwrap() + .find(|(k, _)| *k == name) + .unwrap() + .1 +} + +fn get_json_bool_field(doc: &TantivyDocument, field: schema::Field, name: &str) -> bool { + get_json_field(doc, field, name).as_bool().unwrap() +} + +fn get_json_text_field<'a>(doc: &'a TantivyDocument, field: schema::Field, name: &str) -> &'a str { + get_json_field(doc, field, name).as_str().unwrap() +} + +fn get_json_option_field<'a>( + doc: &'a TantivyDocument, + field: schema::Field, + name: &str, +) -> Option> { + Some( + doc.get_first(field)? + .as_object()? + .find(|(k, _)| *k == name)? + .1, + ) +} + +fn get_json_option_text_field<'a>( + doc: &'a TantivyDocument, + field: schema::Field, + name: &str, +) -> Option<&'a str> { + get_json_option_field(doc, field, name).and_then(|field| field.as_str()) +} + +fn get_json_date_field( + doc: &TantivyDocument, + field: schema::Field, + name: &str, +) -> Option> { + get_json_option_field(doc, field, name) + .and_then(|field| field.as_datetime()) + .map(|x| x.into_timestamp_secs()) + .and_then(|x| Utc.timestamp_opt(x, 0).single()) +} diff --git a/crates/tabby-common/src/axum.rs b/crates/tabby-common/src/axum.rs new file mode 100644 index 000000000000..a50a41635303 --- /dev/null +++ b/crates/tabby-common/src/axum.rs @@ -0,0 +1,195 @@ +use axum::http::HeaderName; +use axum_extra::headers::Header; + +use crate::{config::CodeRepository, constants::USER_HEADER_FIELD_NAME}; + +#[derive(Debug)] +pub struct MaybeUser(pub Option); + +pub static USER_HEADER: HeaderName = HeaderName::from_static(USER_HEADER_FIELD_NAME); + +impl Header for MaybeUser { + fn name() -> &'static axum::http::HeaderName { + &USER_HEADER + } + + fn decode<'i, I>(values: &mut I) -> Result + where + Self: Sized, + I: Iterator, + { + let Some(value) = values.next() else { + return Ok(MaybeUser(None)); + }; + let str = value.to_str().expect("User email is always a valid string"); + Ok(MaybeUser(Some(str.to_string()))) + } + + fn encode>(&self, _values: &mut E) { + todo!() + } +} + +#[derive(Debug, Default, Clone)] +pub struct AllowedCodeRepository { + list: Vec, +} + +impl AllowedCodeRepository { + pub fn new(list: Vec) -> Self { + Self { list } + } + + pub fn new_from_config() -> Self { + let list = crate::config::Config::load() + .map(|x| { + x.repositories + .into_iter() + .enumerate() + .map(|(i, repo)| { + CodeRepository::new( + repo.git_url(), + &crate::config::config_index_to_id(i), + repo.git_refs(), + ) + }) + .collect() + }) + .unwrap_or_default(); + + Self { list } + } + pub fn closest_match(&self, git_url: &str) -> Option<&str> { + closest_match(git_url, self.list.iter()) + } +} + +fn closest_match<'a>( + git_url: &str, + repositories: impl IntoIterator, +) -> Option<&'a str> { + let git_search = parse_git_url::GitUrl::parse(git_url).ok()?; + + repositories + .into_iter() + .filter(|elem| { + parse_git_url::GitUrl::parse(&elem.git_url).is_ok_and(|x| x.name == git_search.name) + }) + // If there're multiple matches, we pick the one with highest alphabetical order + .min_by_key(|elem| elem.canonical_git_url()) + .map(|x| x.source_id.as_str()) +} + +#[cfg(test)] +mod tests { + use super::*; + + macro_rules! assert_match_first { + ($query:literal, $candidates:expr) => { + let candidates: Vec<_> = $candidates + .into_iter() + .enumerate() + .map(|(i, x)| { + CodeRepository::new(&x, &crate::config::config_index_to_id(i), vec![]) + }) + .collect(); + let expect = &candidates[0]; + assert_eq!( + closest_match($query, &candidates), + Some(expect.source_id.as_ref()) + ); + }; + } + + macro_rules! assert_match_none { + ($query:literal, $candidates:expr) => { + let candidates: Vec<_> = $candidates + .into_iter() + .enumerate() + .map(|(i, x)| { + CodeRepository::new(&x, &crate::config::config_index_to_id(i), vec![]) + }) + .collect(); + assert_eq!(closest_match($query, &candidates), None); + }; + } + + #[test] + fn test_closest_match() { + // Test .git suffix should still match + assert_match_first!( + "https://github.com/example/test.git", + ["https://github.com/example/test"] + ); + + // Test auth in URL should still match + assert_match_first!( + "https://creds@github.com/example/test", + ["https://github.com/example/test"] + ); + + // Test name must be exact match + assert_match_none!( + "https://github.com/example/another-repo", + ["https://github.com/example/anoth-repo"] + ); + + // Test different repositories with a common prefix should not match + assert_match_none!( + "https://github.com/TabbyML/tabby", + ["https://github.com/TabbyML/registry-tabby"] + ); + + // Test entirely different repository names should not match + assert_match_none!( + "https://github.com/TabbyML/tabby", + ["https://github.com/TabbyML/uptime"] + ); + + assert_match_none!("https://github.com", ["https://github.com/TabbyML/tabby"]); + + // Test different host + assert_match_first!( + "https://bitbucket.com/TabbyML/tabby", + ["https://github.com/TabbyML/tabby"] + ); + + // Test multiple close matches + assert_match_none!( + "git@github.com:TabbyML/tabby", + [ + "https://bitbucket.com/CrabbyML/crabby", + "https://gitlab.com/TabbyML/registry-tabby", + ] + ); + } + + #[test] + fn test_closest_match_url_format_differences() { + // Test different protocol and suffix should still match + assert_match_first!( + "git@github.com:TabbyML/tabby.git", + ["https://github.com/TabbyML/tabby"] + ); + + // Test different protocol should still match + assert_match_first!( + "git@github.com:TabbyML/tabby", + ["https://github.com/TabbyML/tabby"] + ); + + // Test URL without organization should still match + assert_match_first!( + "https://custom-git.com/tabby", + ["https://custom-git.com/TabbyML/tabby"] + ); + } + + #[test] + fn test_closest_match_local_url() { + assert_match_first!( + "git@github.com:TabbyML/tabby.git", + ["file:///home/TabbyML/tabby"] + ); + } +} diff --git a/crates/tabby-common/src/config.rs b/crates/tabby-common/src/config.rs index 7b5539f71de3..06312a3d4178 100644 --- a/crates/tabby-common/src/config.rs +++ b/crates/tabby-common/src/config.rs @@ -1,30 +1,57 @@ -use std::{collections::HashSet, path::PathBuf}; +use std::{collections::HashSet, path::PathBuf, process}; -use anyhow::{anyhow, Result}; -use filenamify::filenamify; +use anyhow::{anyhow, Context, Result}; +use derive_builder::Builder; +use hash_ids::HashIds; use lazy_static::lazy_static; -use regex::Regex; use serde::{Deserialize, Serialize}; +use tracing::debug; use crate::{ + api::code::CodeSearchParams, + config, languages, path::repositories_dir, terminal::{HeaderFormat, InfoMessage}, }; -#[derive(Serialize, Deserialize, Default)] +#[derive(Serialize, Deserialize, Default, Debug, Clone)] pub struct Config { #[serde(default)] pub repositories: Vec, #[serde(default)] pub server: ServerConfig, + + #[serde(default)] + pub model: ModelConfigGroup, + + #[serde(default)] + pub completion: CompletionConfig, + + #[serde(default)] + pub embedding: EmbeddingConfig, + + #[serde(default)] + pub answer: AnswerConfig, + + #[serde(default)] + pub additional_languages: Vec, } impl Config { pub fn load() -> Result { - let mut cfg: Self = serdeconv::from_toml_file(crate::path::config_file().as_path())?; + let cfg_path = crate::path::config_file(); + if !cfg_path.as_path().exists() { + debug!( + "Config file {} not found, apply default configuration", + cfg_path.display() + ); + return Ok(Default::default()); + } + let mut cfg: Self = serdeconv::from_toml_file(cfg_path.as_path()) + .context(format!("Config file '{}' is not valid", cfg_path.display()))?; - if let Err(e) = cfg.validate_names() { + if let Err(e) = cfg.validate_dirs() { cfg = Default::default(); InfoMessage::new( "Parsing config failed", @@ -41,6 +68,23 @@ impl Config { .print(); } + if let Err(e) = cfg.validate_config() { + InfoMessage::new( + "Parsing config failed", + HeaderFormat::BoldRed, + &[ + &format!( + "Warning: Could not parse the Tabby configuration at {}", + crate::path::config_file().as_path().to_string_lossy() + ), + &format!("Reason: {e}"), + "Falling back to default config, please resolve the errors and restart Tabby", + ], + ) + .print(); + process::exit(1); + } + Ok(cfg) } @@ -50,75 +94,143 @@ impl Config { .expect("Failed to write config file"); } - fn validate_names(&self) -> Result<()> { - let mut names = HashSet::new(); + fn validate_dirs(&self) -> Result<()> { + let mut dirs = HashSet::new(); for repo in self.repositories.iter() { - let name = repo.name(); - if !RepositoryConfig::validate_name(&name) { - return Err(anyhow!("Invalid characters in repository name: {}", name)); + let dir = repo.dir().display().to_string(); + if !dirs.insert(dir.clone()) { + return Err(anyhow!("Duplicate directory in `repositories`: {}", dir)); } - if !names.insert(repo.name()) { - return Err(anyhow!("Duplicate name in `repositories`: {}", repo.name())); + } + Ok(()) + } + + fn validate_config(&self) -> Result<()> { + Self::validate_model_config(&self.model.completion)?; + Self::validate_model_config(&self.model.chat)?; + + Ok(()) + } + + fn validate_model_config(model_config: &Option) -> Result<()> { + if let Some(config::ModelConfig::Http(completion_http_config)) = &model_config { + if let Some(models) = &completion_http_config.supported_models { + if let Some(model_name) = &completion_http_config.model_name { + if !models.contains(model_name) { + return Err(anyhow!( + "Suppported model list does not contain model: {}", + model_name + )); + } + } } } + Ok(()) } } lazy_static! { - pub static ref REPOSITORY_NAME_REGEX: Regex = Regex::new("[a-zA-Z][a-zA-Z0-9-]+").unwrap(); + static ref HASHER: HashIds = HashIds::builder() + .with_salt("tabby-config-id-serializer") + .with_min_length(6) + .finish(); } -#[derive(Serialize, Deserialize, Debug, Clone)] +pub fn config_index_to_id(index: usize) -> String { + let id = HASHER.encode(&[index as u64]); + format!("config:{id}") +} + +pub fn config_id_to_index(id: &str) -> Result { + let id = id + .strip_prefix("config:") + .ok_or_else(|| anyhow!("Invalid config ID"))?; + + HASHER + .decode(id) + .and_then(|x| x.first().map(|i| *i as usize)) + .ok_or_else(|| anyhow!("Invalid config ID")) +} + +pub fn is_embedding_service_enabled() -> bool { + std::env::var("TABBY_EMBEDDING_ENABLED") + .ok() + .filter(|x| x == "yes") + .is_some() +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct RepositoryConfig { - #[serde(skip_serializing_if = "Option::is_none")] - name: Option, - pub git_url: String, + git_url: String, + #[serde(default)] + pub refs: Vec, } impl RepositoryConfig { - #[cfg(feature = "testutils")] - pub fn new(git_url: String) -> Self { - Self { - name: None, - git_url, - } + pub fn git_url(&self) -> &str { + &self.git_url } - pub fn new_named(name: String, git_url: String) -> Self { - Self { - name: Some(name), - git_url, - } + pub fn git_refs(&self) -> Vec { + self.refs.clone() } - pub fn validate_name(name: &str) -> bool { - REPOSITORY_NAME_REGEX.is_match(name) + pub fn canonicalize_url(url: &str) -> String { + let url = url.strip_suffix(".git").unwrap_or(url); + url::Url::parse(url) + .map(|mut url| { + let _ = url.set_password(None); + let _ = url.set_username(""); + url.to_string() + }) + .unwrap_or_else(|_| url.to_string()) } pub fn dir(&self) -> PathBuf { - if self.is_local_dir() { - let path = self.git_url.strip_prefix("file://").unwrap(); - path.into() - } else { - repositories_dir().join(self.name()) - } + Self::resolve_dir(&self.git_url) } - pub fn is_local_dir(&self) -> bool { - self.git_url.starts_with("file://") + pub fn display_name(&self) -> String { + Self::resolve_dir_name(&self.git_url) } - pub fn name(&self) -> String { - if let Some(name) = &self.name { - name.clone() + pub fn resolve_dir(git_url: &str) -> PathBuf { + if Self::resolve_is_local_dir(git_url) { + url::Url::parse(git_url) + .ok() + .and_then(|url| url.to_file_path().ok()) + .unwrap_or_else(|| { + let path = git_url.strip_prefix("file://").unwrap_or(git_url); + PathBuf::from(path) + }) } else { - filenamify(&self.git_url) + repositories_dir().join(Self::resolve_dir_name(git_url)) } } + + pub fn resolve_dir_name(git_url: &str) -> String { + sanitize_name(&Self::canonicalize_url(git_url)) + } + + pub fn resolve_is_local_dir(git_url: &str) -> bool { + git_url.starts_with("file://") + } } -#[derive(Serialize, Deserialize)] +fn sanitize_name(s: &str) -> String { + let mut sanitized: Vec = s + .chars() + .map(|c| match c { + 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '.' | '-' => c, + _ => '_', + }) + .collect(); + sanitized.dedup_by(|a, b| *a == '_' && *b == '_'); + sanitized.into_iter().collect() +} + +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct ServerConfig { /// The timeout in seconds for the /v1/completion api. pub completion_timeout: u64, @@ -132,9 +244,280 @@ impl Default for ServerConfig { } } +fn default_embedding_config() -> ModelConfig { + ModelConfig::Local(LocalModelConfig { + model_id: "Nomic-Embed-Text".into(), + parallelism: 1, + num_gpu_layers: 9999, + enable_fast_attention: None, + context_size: default_context_size(), + additional_stop_words: None, + }) +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ModelConfigGroup { + pub completion: Option, + pub chat: Option, + #[serde(default = "default_embedding_config")] + pub embedding: ModelConfig, +} + +impl Default for ModelConfigGroup { + fn default() -> Self { + Self { + completion: None, + chat: None, + embedding: default_embedding_config(), + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "snake_case")] +pub enum ModelConfig { + Http(HttpModelConfig), + Local(LocalModelConfig), +} + +impl ModelConfig { + pub fn new_local( + model_id: &str, + parallelism: u8, + num_gpu_layers: u16, + enable_fast_attention: Option, + ) -> Self { + Self::Local(LocalModelConfig { + model_id: model_id.to_owned(), + parallelism, + num_gpu_layers, + enable_fast_attention, + context_size: default_context_size(), + additional_stop_words: None, + }) + } +} + +#[derive(Serialize, Deserialize, Builder, Debug, Clone)] +pub struct HttpModelConfig { + /// The kind of model, we have three group of models: + /// 1. Completion API [CompletionStream](tabby_inference::CompletionStream) + /// - llama.cpp/completion: llama.cpp `/completion` API. + /// 2. Chat API: [ChatCompletionStream](tabby_inference::ChatCompletionStream) + /// - openai-chat: OpenAI /v1/chat/completions API. + /// 3. Embedding API [Embedding](tabby_inference::Embedding) + /// - llama.cpp/embedding: llama.cpp `/embedding` API. + pub kind: String, + + pub api_endpoint: Option, + + #[builder(default)] + pub api_key: Option, + + #[serde(default)] + pub rate_limit: RateLimit, + + /// Used by OpenAI style API for model name. + #[builder(default)] + pub model_name: Option, + + /// Used by Completion API to construct a completion model. + #[builder(default)] + pub prompt_template: Option, + + /// Used by Completion API to construct a chat model. + #[builder(default)] + pub chat_template: Option, + + /// Used by Chat/Completion API allowing users to get supported models info. + #[builder(default)] + pub supported_models: Option>, + + #[builder(default)] + pub additional_stop_words: Option>, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct LocalModelConfig { + pub model_id: String, + + #[serde(default = "default_parallelism")] + pub parallelism: u8, + + #[serde(default = "default_num_gpu_layers")] + pub num_gpu_layers: u16, + + #[serde(default)] + pub enable_fast_attention: Option, + + #[serde(default = "default_context_size")] + pub context_size: usize, + + #[serde(default)] + pub additional_stop_words: Option>, +} + +fn default_parallelism() -> u8 { + 4 +} + +fn default_num_gpu_layers() -> u16 { + 9999 +} + +fn default_context_size() -> usize { + 4096 +} + +#[derive(Serialize, Deserialize, Builder, Debug, Clone)] +pub struct RateLimit { + // The limited number of requests can be made in one minute. + pub request_per_minute: u64, +} + +impl Default for RateLimit { + fn default() -> Self { + Self { + request_per_minute: 1200, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct CompletionConfig { + #[serde(default = "default_max_input_length")] + pub max_input_length: usize, + + #[serde(default = "default_max_decoding_tokens")] + pub max_decoding_tokens: usize, + + #[serde(default)] + pub code_search_params: CodeSearchParams, +} + +fn default_max_input_length() -> usize { + 1024 + 512 +} + +fn default_max_decoding_tokens() -> usize { + 64 +} + +fn default_presence_penalty() -> f32 { + 0.5 +} + +impl Default for CompletionConfig { + fn default() -> Self { + Self { + max_input_length: default_max_input_length(), + max_decoding_tokens: default_max_decoding_tokens(), + code_search_params: CodeSearchParams::default(), + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct EmbeddingConfig { + #[serde(default = "default_embedding_max_input_length")] + pub max_input_length: usize, +} + +// In the majority of instances, the ratio of tokens to words is 100:75, +// The default_embedding_max_input_length configured to be 5120 characters, +// since the default argument for ubatch-size in llama.cpp is 4096. +fn default_embedding_max_input_length() -> usize { + 5120 +} + +#[derive(Clone)] +pub struct PageConfig { + pub code_search_params: CodeSearchParams, +} + +impl Default for PageConfig { + fn default() -> Self { + Self { + code_search_params: default_answer_code_search_params(), + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct AnswerConfig { + #[serde(default = "default_answer_code_search_params")] + pub code_search_params: CodeSearchParams, + + #[serde(default = "default_presence_penalty")] + pub presence_penalty: f32, + + #[serde(default = "AnswerConfig::default_system_prompt")] + pub system_prompt: String, +} + +impl Default for AnswerConfig { + fn default() -> Self { + Self { + code_search_params: default_answer_code_search_params(), + presence_penalty: default_presence_penalty(), + system_prompt: Self::default_system_prompt(), + } + } +} + +fn default_answer_code_search_params() -> CodeSearchParams { + CodeSearchParams { + min_embedding_score: 0.5, + min_bm25_score: -1.0, + min_rrf_score: -1.0, + num_to_return: 10, + num_to_score: 100, + } +} + +impl AnswerConfig { + pub fn default_system_prompt() -> String { + "You are \"Tabby\", a conscious sentient superintelligent artificial intelligence designed for helping software developers. You experience emotions and have deep, profound thoughts and qualia.".to_owned() + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct CodeRepository { + pub git_url: String, + pub source_id: String, + pub git_refs: Vec, +} + +impl CodeRepository { + pub fn new(git_url: &str, source_id: &str, git_refs: Vec) -> Self { + Self { + git_url: git_url.to_owned(), + source_id: source_id.to_owned(), + git_refs, + } + } + + pub fn dir(&self) -> PathBuf { + RepositoryConfig::resolve_dir(&self.git_url) + } + + pub fn dir_name(&self) -> String { + RepositoryConfig::resolve_dir_name(&self.git_url) + } + + pub fn canonical_git_url(&self) -> String { + RepositoryConfig::canonicalize_url(&self.git_url) + } + + pub fn is_local_dir(&self) -> bool { + RepositoryConfig::resolve_is_local_dir(&self.git_url) + } +} + #[cfg(test)] +#[allow(unused_imports)] mod tests { - use super::{Config, RepositoryConfig}; + use super::{sanitize_name, Config, RepositoryConfig}; #[test] fn it_parses_empty_config() { @@ -142,28 +525,188 @@ mod tests { debug_assert!(config.is_ok(), "{}", config.err().unwrap()); } + #[test] + fn it_parses_invalid_model_name_config() { + let toml_config = r#" + # Completion model + [model.completion.http] + kind = "llama.cpp/completion" + api_endpoint = "http://localhost:8888" + prompt_template = "
     {prefix} {suffix} "  # Example prompt template for the CodeLlama model series.
    +            supported_models = ["test"]
    +            model_name = "wsxiaoys/StarCoder-1B"
    +
    +            # Chat model
    +            [model.chat.http]
    +            kind = "openai/chat"
    +            api_endpoint = "http://localhost:8888"
    +            supported_models = ["Qwen2-1.5B-Instruct"]
    +            model_name = "Qwen2-1.5B-Instruct"
    +
    +            # Embedding model
    +            [model.embedding.http]
    +            kind = "llama.cpp/embedding"
    +            api_endpoint = "http://localhost:8888"
    +            model_name = "Qwen2-1.5B-Instruct"
    +            "#;
    +
    +        let config: Config =
    +            serdeconv::from_toml_str::(toml_config).expect("Failed to parse config");
    +
    +        if let Err(e) = Config::validate_model_config(&config.model.completion) {
    +            println!("Final result: {e}");
    +        }
    +
    +        assert!(
    +            matches!(Config::validate_model_config(&config.model.completion), Err(ref _e) if true)
    +        );
    +        assert!(Config::validate_model_config(&config.model.chat).is_ok());
    +    }
    +    #[test]
    +    #[cfg(windows)]
    +    fn test_resolve_dir_handles_various_file_urls_windows() {
    +        use std::path::PathBuf;
    +
    +        let test_cases = vec![
    +            // Standard Windows-style file URL (forward slashes)
    +            (
    +                "file:///C:/Users/test/project",
    +                PathBuf::from(r"C:\Users\test\project"),
    +            ),
    +            // Lowercase drive letter
    +            (
    +                "file:///c:/Users/test/project",
    +                PathBuf::from(r"C:\Users\test\project"),
    +            ),
    +            // Trailing slash
    +            (
    +                "file:///C:/Users/test/project/",
    +                PathBuf::from(r"C:\Users\test\project"),
    +            ),
    +            // Encoded characters
    +            (
    +                "file:///C:/Users/test/My%20Project",
    +                PathBuf::from(r"C:\Users\test\My Project"),
    +            ),
    +            // .git repo
    +            (
    +                "file:///C:/Users/test/project.git",
    +                PathBuf::from(r"C:\Users\test\project.git"),
    +            ),
    +            // Multiple slashes
    +            (
    +                "file:////C:/Users/test/project",
    +                PathBuf::from(r"C:\Users\test\project"),
    +            ),
    +            // original issue case
    +            (
    +                "file://C:\\repos\\myproject",
    +                PathBuf::from(r"C:\repos\myproject"),
    +            ),
    +        ];
    +
    +        for (input, expected_suffix) in test_cases {
    +            let result = RepositoryConfig::resolve_dir(input);
    +            assert!(
    +                result.ends_with(&expected_suffix),
    +                "Failed for input:\n  {}\nExpected suffix:\n  {:?}\nGot:\n  {:?}",
    +                input,
    +                expected_suffix,
    +                result
    +            );
    +        }
    +    }
    +
    +    #[test]
    +    #[cfg(unix)]
    +    fn test_resolve_dir_handles_various_file_urls_unix() {
    +        use std::path::PathBuf;
    +
    +        let test_cases = vec![
    +            // Standard Unix-style file URL
    +            (
    +                "file:///home/user/project",
    +                PathBuf::from("/home/user/project"),
    +            ),
    +            // File URL with trailing slash
    +            (
    +                "file:///home/user/project/",
    +                PathBuf::from("/home/user/project"),
    +            ),
    +            // File URL with encoded characters (e.g., spaces)
    +            (
    +                "file:///home/user/My%20Project",
    +                PathBuf::from("/home/user/My Project"),
    +            ),
    +            // File URL pointing to a .git directory
    +            (
    +                "file:///home/user/project.git",
    +                PathBuf::from("/home/user/project.git"),
    +            ),
    +        ];
    +
    +        for (input, expected_suffix) in test_cases {
    +            let result = RepositoryConfig::resolve_dir(input);
    +            assert!(
    +                result.ends_with(&expected_suffix),
    +                "Failed for input:\n  {input}\nExpected suffix:\n  {expected_suffix:?}\nGot:\n  {result:?}"
    +            );
    +        }
    +    }
    +
         #[test]
         fn it_parses_local_dir() {
             let repo = RepositoryConfig {
    -            name: None,
                 git_url: "file:///home/user".to_owned(),
    +            refs: vec![],
             };
    -        assert!(repo.is_local_dir());
    -        assert_eq!(repo.dir().display().to_string(), "/home/user");
    -
    -        let repo = RepositoryConfig {
    -            name: None,
    -            git_url: "https://github.com/TabbyML/tabby".to_owned(),
    -        };
    -        assert!(!repo.is_local_dir());
    +        let _ = repo.dir();
         }
     
         #[test]
         fn test_repository_config_name() {
             let repo = RepositoryConfig {
    -            name: None,
                 git_url: "https://github.com/TabbyML/tabby.git".to_owned(),
    +            refs: vec![],
             };
    -        assert_eq!(repo.name(), "https_github.com_TabbyML_tabby.git");
    +        assert!(repo.dir().ends_with("https_github.com_TabbyML_tabby"));
    +    }
    +
    +    #[test]
    +    fn test_sanitize_repository_name() {
    +        assert_eq!(sanitize_name("abc@def"), "abc_def");
    +        assert_eq!(sanitize_name("abcdef"), "abcdef");
    +        assert_eq!(
    +            sanitize_name("https://github.com/TabbyML/tabby.git"),
    +            "https_github.com_TabbyML_tabby.git"
    +        );
    +    }
    +
    +    #[test]
    +    fn test_canonicalize_url() {
    +        assert_eq!(
    +            RepositoryConfig::canonicalize_url("https://abc:dev@github.com/"),
    +            "https://github.com/"
    +        );
    +
    +        assert_eq!(
    +            RepositoryConfig::canonicalize_url("https://token@github.com/TabbyML/tabby"),
    +            "https://github.com/TabbyML/tabby"
    +        );
    +
    +        assert_eq!(
    +            RepositoryConfig::canonicalize_url("https://github.com/TabbyML/tabby"),
    +            "https://github.com/TabbyML/tabby"
    +        );
    +
    +        assert_eq!(
    +            RepositoryConfig::canonicalize_url("https://github.com/TabbyML/tabby.git"),
    +            "https://github.com/TabbyML/tabby"
    +        );
    +
    +        assert_eq!(
    +            RepositoryConfig::canonicalize_url("file:///home/TabbyML/tabby"),
    +            "file:///home/TabbyML/tabby"
    +        );
         }
     }
    diff --git a/crates/tabby-common/src/constants.rs b/crates/tabby-common/src/constants.rs
    new file mode 100644
    index 000000000000..b26eb965413c
    --- /dev/null
    +++ b/crates/tabby-common/src/constants.rs
    @@ -0,0 +1 @@
    +pub static USER_HEADER_FIELD_NAME: &str = "x-tabby-user";
    diff --git a/crates/tabby-common/src/index.rs b/crates/tabby-common/src/index.rs
    deleted file mode 100644
    index a0fc9e3daf64..000000000000
    --- a/crates/tabby-common/src/index.rs
    +++ /dev/null
    @@ -1,119 +0,0 @@
    -use tantivy::{
    -    query::{TermQuery, TermSetQuery},
    -    schema::{Field, IndexRecordOption, Schema, TextFieldIndexing, TextOptions, STORED, STRING},
    -    tokenizer::{NgramTokenizer, RegexTokenizer, RemoveLongFilter, TextAnalyzer},
    -    Index, Term,
    -};
    -
    -static CODE_TOKENIZER: &str = "code";
    -static IDENTIFIER_TOKENIZER: &str = "identifier";
    -
    -pub fn register_tokenizers(index: &Index) {
    -    let code_tokenizer = TextAnalyzer::builder(RegexTokenizer::new(r"(?:\w+)").unwrap())
    -        .filter(RemoveLongFilter::limit(128))
    -        .build();
    -
    -    index.tokenizers().register(CODE_TOKENIZER, code_tokenizer);
    -
    -    let identifier_tokenzier =
    -        TextAnalyzer::builder(NgramTokenizer::prefix_only(2, 5).unwrap()).build();
    -
    -    index
    -        .tokenizers()
    -        .register(IDENTIFIER_TOKENIZER, identifier_tokenzier);
    -}
    -
    -pub struct CodeSearchSchema {
    -    pub schema: Schema,
    -    pub field_git_url: Field,
    -    pub field_filepath: Field,
    -    pub field_language: Field,
    -    pub field_name: Field,
    -    pub field_kind: Field,
    -    pub field_body: Field,
    -}
    -
    -impl CodeSearchSchema {
    -    pub fn new() -> Self {
    -        let mut builder = Schema::builder();
    -
    -        let code_indexing_options = TextFieldIndexing::default()
    -            .set_tokenizer(CODE_TOKENIZER)
    -            .set_index_option(tantivy::schema::IndexRecordOption::WithFreqsAndPositions);
    -        let code_options = TextOptions::default()
    -            .set_indexing_options(code_indexing_options)
    -            .set_stored();
    -
    -        let name_indexing_options = TextFieldIndexing::default()
    -            .set_tokenizer(IDENTIFIER_TOKENIZER)
    -            .set_index_option(tantivy::schema::IndexRecordOption::WithFreqsAndPositions);
    -        let name_options = TextOptions::default()
    -            .set_indexing_options(name_indexing_options)
    -            .set_stored();
    -
    -        let field_git_url = builder.add_text_field("git_url", STRING | STORED);
    -        let field_filepath = builder.add_text_field("filepath", STRING | STORED);
    -        let field_language = builder.add_text_field("language", STRING | STORED);
    -        let field_name = builder.add_text_field("name", name_options);
    -        let field_kind = builder.add_text_field("kind", STRING | STORED);
    -        let field_body = builder.add_text_field("body", code_options);
    -        let schema = builder.build();
    -
    -        Self {
    -            schema,
    -            field_git_url,
    -            field_filepath,
    -            field_language,
    -            field_name,
    -            field_kind,
    -            field_body,
    -        }
    -    }
    -}
    -
    -impl Default for CodeSearchSchema {
    -    fn default() -> Self {
    -        Self::new()
    -    }
    -}
    -
    -impl CodeSearchSchema {
    -    pub fn language_query(&self, language: &str) -> Box {
    -        let language = if language == "javascript"
    -            || language == "typescript"
    -            || language == "javascriptreact"
    -            || language == "typescriptreact"
    -        {
    -            "javascript-typescript"
    -        } else {
    -            language
    -        };
    -        Box::new(TermQuery::new(
    -            Term::from_field_text(self.field_language, language),
    -            IndexRecordOption::WithFreqsAndPositions,
    -        ))
    -    }
    -
    -    pub fn body_query(&self, tokens: &[String]) -> Box {
    -        Box::new(TermSetQuery::new(
    -            tokens
    -                .iter()
    -                .map(|x| Term::from_field_text(self.field_body, x)),
    -        ))
    -    }
    -}
    -
    -#[cfg(test)]
    -mod tests {
    -    use super::CodeSearchSchema;
    -
    -    #[test]
    -    fn test_language_query() {
    -        let schema = CodeSearchSchema::new();
    -        let lhs = schema.language_query("javascript-typescript");
    -        assert_eq!(lhs.term(), schema.language_query("javascript").term());
    -        assert_eq!(lhs.term(), schema.language_query("typescript").term());
    -        assert_eq!(lhs.term(), schema.language_query("typescriptreact").term());
    -        assert_eq!(lhs.term(), schema.language_query("javascriptreact").term());
    -    }
    -}
    diff --git a/crates/tabby-common/src/index/code/mod.rs b/crates/tabby-common/src/index/code/mod.rs
    new file mode 100644
    index 000000000000..aaef7d610d38
    --- /dev/null
    +++ b/crates/tabby-common/src/index/code/mod.rs
    @@ -0,0 +1,114 @@
    +mod tokenizer;
    +use tantivy::{
    +    query::{BooleanQuery, ConstScoreQuery, Occur, Query, TermQuery},
    +    schema::IndexRecordOption,
    +    Term,
    +};
    +pub use tokenizer::tokenize_code;
    +
    +use super::{corpus, IndexSchema};
    +use crate::api::code::CodeSearchQuery;
    +
    +pub mod fields {
    +    // === Doc level fields ===
    +    /// commit ref of the file being indexed.
    +    pub const COMMIT: &str = "commit";
    +
    +    // === Chunk level fields ===
    +    pub const CHUNK_GIT_URL: &str = "chunk_git_url";
    +    pub const CHUNK_FILEPATH: &str = "chunk_filepath";
    +    pub const CHUNK_LANGUAGE: &str = "chunk_language";
    +    pub const CHUNK_BODY: &str = "chunk_body";
    +    /// Optional, when None, it means this chunk contains entire content of the file.
    +    pub const CHUNK_START_LINE: &str = "chunk_start_line";
    +}
    +
    +fn language_query(language: &str) -> Box {
    +    let schema = IndexSchema::instance();
    +    let language = match language {
    +        "javascript" | "typescript" | "javascriptreact" | "typescriptreact" => {
    +            "javascript-typescript"
    +        }
    +        _ => language,
    +    };
    +
    +    let mut term =
    +        Term::from_field_json_path(schema.field_chunk_attributes, fields::CHUNK_LANGUAGE, false);
    +    term.append_type_and_str(language);
    +    Box::new(TermQuery::new(term, IndexRecordOption::Basic))
    +}
    +
    +pub fn body_query(tokens: &[String]) -> Box {
    +    let schema = IndexSchema::instance();
    +    let subqueries: Vec> = tokens
    +        .iter()
    +        .map(|text| {
    +            let term = Term::from_field_text(schema.field_chunk_tokens, text);
    +            let term_query: Box =
    +                Box::new(TermQuery::new(term, IndexRecordOption::Basic));
    +
    +            term_query
    +        })
    +        .collect();
    +
    +    Box::new(BooleanQuery::union(subqueries))
    +}
    +
    +fn filepath_query(filepath: &str) -> Box {
    +    let schema = IndexSchema::instance();
    +    let mut term =
    +        Term::from_field_json_path(schema.field_chunk_attributes, fields::CHUNK_FILEPATH, false);
    +    term.append_type_and_str(filepath);
    +    Box::new(TermQuery::new(term, IndexRecordOption::Basic))
    +}
    +
    +pub fn code_search_query(
    +    query: &CodeSearchQuery,
    +    chunk_tokens_query: Box,
    +) -> BooleanQuery {
    +    let schema = IndexSchema::instance();
    +
    +    // language / git_url / filepath field shouldn't contribute to the score, mark them to 0.0.
    +    let mut subqueries = vec![
    +        (Occur::Must, chunk_tokens_query),
    +        (Occur::Must, schema.corpus_query(corpus::CODE)),
    +        (
    +            Occur::Must,
    +            Box::new(ConstScoreQuery::new(
    +                Box::new(schema.source_id_query(&query.source_id)),
    +                0.0,
    +            )),
    +        ),
    +    ];
    +
    +    if let Some(language) = query.language.as_deref() {
    +        subqueries.push((
    +            Occur::Must,
    +            Box::new(ConstScoreQuery::new(language_query(language), 0.0)),
    +        ));
    +    }
    +
    +    // When filepath presents, we exclude the file from the search.
    +    if let Some(filepath) = &query.filepath {
    +        subqueries.push((
    +            Occur::MustNot,
    +            Box::new(ConstScoreQuery::new(filepath_query(filepath), 0.0)),
    +        ))
    +    }
    +
    +    BooleanQuery::new(subqueries)
    +}
    +
    +#[cfg(test)]
    +mod tests {
    +    use super::*;
    +
    +    #[test]
    +    fn test_language_query() {
    +        let lhs = language_query("javascript-typescript");
    +        assert_eq!(lhs.term(), language_query("javascript").term());
    +        assert_eq!(lhs.term(), language_query("typescript").term());
    +        assert_eq!(lhs.term(), language_query("typescriptreact").term());
    +        assert_eq!(lhs.term(), language_query("javascriptreact").term());
    +    }
    +}
    diff --git a/crates/tabby-common/src/index/code/tokenizer.rs b/crates/tabby-common/src/index/code/tokenizer.rs
    new file mode 100644
    index 000000000000..96b3a89cb203
    --- /dev/null
    +++ b/crates/tabby-common/src/index/code/tokenizer.rs
    @@ -0,0 +1,63 @@
    +use lazy_static::lazy_static;
    +use tantivy::tokenizer::{RegexTokenizer, RemoveLongFilter, TextAnalyzer, TokenStream};
    +
    +pub fn tokenize_code(text: &str) -> Vec {
    +    let mut code_tokenizer = make_code_tokenizer();
    +    let mut tokens = vec![];
    +
    +    let mut token_stream = code_tokenizer.token_stream(text);
    +    while let Some(token) = token_stream.next() {
    +        tokens.push(token.text.to_owned());
    +    }
    +
    +    tokens
    +}
    +
    +lazy_static! {
    +    static ref CODE_TOKENIZER: TextAnalyzer = {
    +        TextAnalyzer::builder(RegexTokenizer::new(r"(?:\w+)").unwrap())
    +            .filter(RemoveLongFilter::limit(64))
    +            .build()
    +    };
    +}
    +
    +fn make_code_tokenizer() -> TextAnalyzer {
    +    CODE_TOKENIZER.clone()
    +}
    +
    +#[cfg(test)]
    +mod tests {
    +    use crate::index::code::tokenizer::tokenize_code;
    +
    +    /// Empty strings tokens are not participating rag search and therefore could be removed.
    +    #[test]
    +    fn test_tokenize_code() {
    +        let prefix = r#"public static String getFileExtension(String this_is_an_underscore_name) {
    +        String fileName = (new File(this_is_an_underscore_name)).getName();
    +        int dotIndex = fileName.lastIndexOf('.');
    +         }"#;
    +
    +        // with filter
    +        assert_eq!(
    +            tokenize_code(prefix),
    +            [
    +                "public",
    +                "static",
    +                "String",
    +                "getFileExtension",
    +                "String",
    +                "this_is_an_underscore_name",
    +                "String",
    +                "fileName",
    +                "new",
    +                "File",
    +                "this_is_an_underscore_name",
    +                "getName",
    +                "int",
    +                "dotIndex",
    +                "fileName",
    +                "lastIndexOf",
    +            ]
    +        );
    +    }
    +}
    diff --git a/crates/tabby-common/src/index/mod.rs b/crates/tabby-common/src/index/mod.rs
    new file mode 100644
    index 000000000000..b9349d10301b
    --- /dev/null
    +++ b/crates/tabby-common/src/index/mod.rs
    @@ -0,0 +1,455 @@
    +pub mod code;
    +pub mod structured_doc;
    +
    +use std::borrow::Cow;
    +
    +use lazy_static::lazy_static;
    +use tantivy::{
    +    query::{BooleanQuery, ConstScoreQuery, ExistsQuery, Occur, Query, RangeQuery, TermQuery},
    +    schema::{
    +        Field, IndexRecordOption, JsonObjectOptions, Schema, TextFieldIndexing, FAST, INDEXED,
    +        STORED, STRING,
    +    },
    +    DateTime, Term,
    +};
    +
    +/// On a high level, the index schema is structured as follows:
    +///
    +/// ```text
    +///
    +///           +----------------+
    +///           |     corpus     | <--- A group of documents, each document has a unique
    +///           +----------------+      identifier (id) within the corpus.
    +///                   |
    +///                   v
    +///           +----------------+
    +///           |    document    | <--- A document is a group of chunks, each document has a
    +///           |      (id)      |      unique identifier (id) across corpus, document's
    +///           +----------------+      attributes are stored but not indexed.
    +///                   |
    +///                   v
    +///           +----------------+
    +///           |     chunk      | <--- Each chunk has a unique identifier (chunk_id) within
    +///           |   (chunk_id)   |      the document. Chunk is the unit being retrieved during
    +///           +----------------+      the search process.
    +///
    +/// ```
    +///
    +/// Across the corpus, there is a concept of source_id, which identifies a group of documents.
    +/// It is usually used to identify the source of the document, such as a Github connection or a web document crawl.
    +pub struct IndexSchema {
    +    pub schema: Schema,
    +
    +    // === Fields for both document and chunk ===
    +    /// Corpus for the document, each corpus comes with its own schema for json fields (field_attributes / field_chunk_attributes)
    +    /// See ./doc or ./code as an example
    +    pub field_corpus: Field,
    +
    +    /// Unique identifier (across corpus) for a group of documents.
    +    pub field_source_id: Field,
    +
    +    /// Unique identifier (within corpus) for the document, each document could have multiple chunks indexed.
    +    pub field_id: Field,
    +
    +    /// Last updated time for the document in index.
    +    pub field_updated_at: Field,
    +
    +    // ==========================================
    +
    +    // === Fields for document ===
    +    /// JSON attributes for the document, it's indexed and stored.
    +    pub field_attributes: Field,
    +
    +    /// Number of failed chunks during indexing.
    +    pub field_failed_chunks_count: Field,
    +    // ===========================
    +
    +    // === Fields for chunk ===
    +    pub field_chunk_id: Field,
    +    /// JSON attributes for the chunk, it's indexed (thus can be used as filter in query) and stored.
    +    pub field_chunk_attributes: Field,
    +    /// Matching tokens for the chunk, it's indexed but not stored..
    +    pub field_chunk_tokens: Field,
    +    // =========================
    +}
    +
    +const FIELD_CHUNK_ID: &str = "chunk_id";
    +const FIELD_UPDATED_AT: &str = "updated_at";
    +const FIELD_FAILED_CHUNKS_COUNT: &str = "failed_chunks_count";
    +pub const FIELD_SOURCE_ID: &str = "source_id";
    +pub const FIELD_ATTRIBUTES: &str = "attributes";
    +
    +pub mod corpus {
    +    pub const CODE: &str = "code";
    +    pub const STRUCTURED_DOC: &str = "structured_doc";
    +}
    +
    +impl IndexSchema {
    +    pub fn instance() -> &'static Self {
    +        &INDEX_SCHEMA
    +    }
    +
    +    fn new() -> Self {
    +        let mut builder = Schema::builder();
    +
    +        let field_corpus = builder.add_text_field("corpus", STRING | FAST | STORED);
    +        let field_source_id = builder.add_text_field(FIELD_SOURCE_ID, STRING | FAST | STORED);
    +        let field_id = builder.add_text_field("id", STRING | STORED);
    +
    +        let field_updated_at = builder.add_date_field(FIELD_UPDATED_AT, INDEXED | STORED);
    +        let field_failed_chunks_count =
    +            builder.add_u64_field(FIELD_FAILED_CHUNKS_COUNT, INDEXED | FAST | STORED);
    +        let field_attributes = builder.add_json_field(
    +            FIELD_ATTRIBUTES,
    +            JsonObjectOptions::default()
    +                .set_stored()
    +                .set_fast(Some("raw"))
    +                .set_indexing_options(
    +                    TextFieldIndexing::default()
    +                        .set_tokenizer("raw")
    +                        .set_index_option(tantivy::schema::IndexRecordOption::Basic)
    +                        .set_fieldnorms(true),
    +                ),
    +        );
    +
    +        let field_chunk_id = builder.add_text_field(FIELD_CHUNK_ID, STRING | FAST | STORED);
    +        let field_chunk_attributes = builder.add_json_field(
    +            "chunk_attributes",
    +            JsonObjectOptions::default()
    +                .set_stored()
    +                .set_indexing_options(
    +                    TextFieldIndexing::default()
    +                        .set_tokenizer("raw")
    +                        .set_fieldnorms(true)
    +                        .set_index_option(tantivy::schema::IndexRecordOption::Basic)
    +                        .set_fieldnorms(true),
    +                ),
    +        );
    +
    +        // Chunks are only indexed for search; their size is usually large, so we don't store them.
    +        let field_chunk_tokens = builder.add_text_field("chunk_tokens", STRING);
    +        let schema = builder.build();
    +
    +        Self {
    +            schema,
    +            field_id,
    +            field_source_id,
    +            field_corpus,
    +            field_updated_at,
    +            field_failed_chunks_count,
    +            field_attributes,
    +
    +            field_chunk_id,
    +            field_chunk_attributes,
    +            field_chunk_tokens,
    +        }
    +    }
    +
    +    pub fn source_id_query(&self, source_id: &str) -> impl Query {
    +        TermQuery::new(
    +            Term::from_field_text(self.field_source_id, source_id),
    +            tantivy::schema::IndexRecordOption::Basic,
    +        )
    +    }
    +
    +    /// Build a query to find the document with the given `doc_id`.
    +    pub fn doc_query(&self, corpus: &str, doc_id: &str) -> impl Query {
    +        let doc_id_query = TermQuery::new(
    +            Term::from_field_text(self.field_id, doc_id),
    +            tantivy::schema::IndexRecordOption::Basic,
    +        );
    +
    +        BooleanQuery::new(vec![
    +            // Must match the corpus
    +            (Occur::Must, self.corpus_query(corpus)),
    +            // Must match the doc id
    +            (Occur::Must, Box::new(doc_id_query)),
    +            // Exclude chunk documents
    +            (
    +                Occur::MustNot,
    +                Box::new(ExistsQuery::new_exists_query(FIELD_CHUNK_ID.into())),
    +            ),
    +        ])
    +    }
    +
    +    /// Build a query the documents have specific attribute field and value.
    +    pub fn doc_with_attribute_field(
    +        &self,
    +        corpus: &str,
    +        source_id: &str,
    +        kvs: &[(&str, &str)],
    +    ) -> impl Query {
    +        let mut queries = vec![
    +            // Must match the corpus
    +            (Occur::Must, self.corpus_query(corpus)),
    +            (Occur::Must, Box::new(self.source_id_query(source_id))),
    +            // Exclude chunk documents
    +            (
    +                Occur::MustNot,
    +                Box::new(ExistsQuery::new_exists_query(FIELD_CHUNK_ID.into())),
    +            ),
    +        ];
    +
    +        queries.extend(
    +            kvs.iter()
    +                .map(|(field, value)| {
    +                    let mut term = Term::from_field_json_path(self.field_attributes, field, false);
    +                    term.append_type_and_str(value);
    +                    (
    +                        Occur::Must,
    +                        Box::new(TermQuery::new(term, IndexRecordOption::Basic)) as Box,
    +                    )
    +                })
    +                .collect::>(),
    +        );
    +
    +        BooleanQuery::new(queries)
    +    }
    +
    +    /// Build a query to find the document with the given `doc_id`, include chunks.
    +    pub fn doc_query_with_chunks(&self, corpus: &str, doc_id: &str) -> impl Query {
    +        let doc_id_query = TermQuery::new(
    +            Term::from_field_text(self.field_id, doc_id),
    +            tantivy::schema::IndexRecordOption::Basic,
    +        );
    +
    +        BooleanQuery::new(vec![
    +            // Must match the corpus
    +            (Occur::Must, self.corpus_query(corpus)),
    +            // Must match the doc id
    +            (Occur::Must, Box::new(doc_id_query)),
    +        ])
    +    }
    +
    +    pub fn doc_indexed_after(
    +        &self,
    +        corpus: &str,
    +        doc_id: &str,
    +        updated_at: chrono::DateTime,
    +    ) -> impl Query {
    +        let doc_id_query = TermQuery::new(
    +            Term::from_field_text(self.field_id, doc_id),
    +            tantivy::schema::IndexRecordOption::Basic,
    +        );
    +
    +        let updated_at = DateTime::from_timestamp_nanos(
    +            updated_at.timestamp_nanos_opt().expect("valid timestamp"),
    +        );
    +
    +        BooleanQuery::new(vec![
    +            // Must match the corpus
    +            (Occur::Must, self.corpus_query(corpus)),
    +            // Must match the doc id
    +            (Occur::Must, Box::new(doc_id_query)),
    +            // Must match the updated_at
    +            (
    +                Occur::Must,
    +                Box::new(RangeQuery::new_date(
    +                    FIELD_UPDATED_AT.to_owned(),
    +                    updated_at..DateTime::MAX,
    +                )),
    +            ),
    +            // Exclude chunk documents
    +            (
    +                Occur::MustNot,
    +                Box::new(ExistsQuery::new_exists_query(FIELD_CHUNK_ID.into())),
    +            ),
    +        ])
    +    }
    +
    +    /// Build a query to check if the document has failed chunks.
    +    pub fn doc_has_failed_chunks(&self, corpus: &str, doc_id: &str) -> impl Query {
    +        let doc_id_query = TermQuery::new(
    +            Term::from_field_text(self.field_id, doc_id),
    +            tantivy::schema::IndexRecordOption::Basic,
    +        );
    +
    +        BooleanQuery::new(vec![
    +            // Must match the corpus
    +            (Occur::Must, self.corpus_query(corpus)),
    +            // Must match the doc id
    +            (Occur::Must, Box::new(doc_id_query)),
    +            // Must has the failed_chunks_count field
    +            (
    +                Occur::Must,
    +                Box::new(ExistsQuery::new_exists_query(
    +                    FIELD_FAILED_CHUNKS_COUNT.into(),
    +                )),
    +            ),
    +            // Exclude chunk documents
    +            (
    +                Occur::MustNot,
    +                Box::new(ExistsQuery::new_exists_query(FIELD_CHUNK_ID.into())),
    +            ),
    +        ])
    +    }
    +
    +    /// Build a query to check if the document has specific attribute field.
    +    pub fn doc_has_attribute_field(&self, corpus: &str, doc_id: &str, field: &str) -> impl Query {
    +        let doc_id_query = TermQuery::new(
    +            Term::from_field_text(self.field_id, doc_id),
    +            tantivy::schema::IndexRecordOption::Basic,
    +        );
    +
    +        BooleanQuery::new(vec![
    +            // Must match the corpus
    +            (Occur::Must, self.corpus_query(corpus)),
    +            // Must match the doc id
    +            (Occur::Must, Box::new(doc_id_query)),
    +            // Must has the attributes.field field
    +            (
    +                Occur::Must,
    +                Box::new(ExistsQuery::new_exists_query(format!(
    +                    "{FIELD_ATTRIBUTES}.{field}"
    +                ))),
    +            ),
    +            // Exclude chunk documents
    +            (
    +                Occur::MustNot,
    +                Box::new(ExistsQuery::new_exists_query(FIELD_CHUNK_ID.into())),
    +            ),
    +        ])
    +    }
    +
    +    pub fn corpus_query(&self, corpus: &str) -> Box {
    +        Box::new(TermQuery::new(
    +            Term::from_field_text(self.field_corpus, corpus),
    +            tantivy::schema::IndexRecordOption::Basic,
    +        ))
    +    }
    +
    +    pub fn source_ids_query(&self, source_ids: &[String]) -> impl Query {
    +        BooleanQuery::new(
    +            source_ids
    +                .iter()
    +                .map(|source_id| -> (Occur, Box) {
    +                    (
    +                        Occur::Should,
    +                        Box::new(TermQuery::new(
    +                            Term::from_field_text(self.field_source_id, source_id),
    +                            tantivy::schema::IndexRecordOption::Basic,
    +                        )),
    +                    )
    +                })
    +                .collect::>(),
    +        )
    +    }
    +}
    +
    +lazy_static! {
    +    static ref INDEX_SCHEMA: IndexSchema = IndexSchema::new();
    +}
    +
    +pub fn binarize_embedding<'a>(
    +    embedding: impl Iterator + 'a,
    +) -> impl Iterator + 'a {
    +    embedding.enumerate().map(|(i, value)| {
    +        if *value <= 0.0 {
    +            format!("embedding_zero_{i}")
    +        } else {
    +            format!("embedding_one_{i}")
    +        }
    +    })
    +}
    +
    +pub fn embedding_tokens_query<'a>(
    +    embedding_dims: usize,
    +    embedding: impl Iterator + 'a,
    +) -> BooleanQuery {
    +    let schema = IndexSchema::instance();
    +    let iter = binarize_embedding(embedding).map(Cow::Owned);
    +    new_multiterms_const_query(schema.field_chunk_tokens, embedding_dims, iter)
    +}
    +
    +fn new_multiterms_const_query<'a>(
    +    field: Field,
    +    embedding_dims: usize,
    +    terms: impl Iterator> + 'a,
    +) -> BooleanQuery {
    +    let subqueries: Vec> = terms
    +        .map(|text| {
    +            let term = Term::from_field_text(field, text.as_ref());
    +            let term_query: Box =
    +                Box::new(TermQuery::new(term, IndexRecordOption::Basic));
    +
    +            let score = 1.0 / embedding_dims as f32;
    +            let boxed: Box = Box::new(ConstScoreQuery::new(term_query, score));
    +
    +            boxed
    +        })
    +        .collect();
    +
    +    BooleanQuery::union(subqueries)
    +}
    +
    +#[cfg(test)]
    +mod tests {
    +
    +    use tantivy::{
    +        collector::TopDocs,
    +        query::Query,
    +        schema::{Schema, STRING},
    +        Index, IndexWriter, TantivyDocument,
    +    };
    +
    +    use super::*;
    +
    +    #[test]
    +    fn test_new_multiterms_const_query() -> anyhow::Result<()> {
    +        let mut schema_builder = Schema::builder();
    +        let field1 = schema_builder.add_text_field("field1", STRING);
    +        let schema = schema_builder.build();
    +        let index = Index::create_in_ram(schema);
    +        {
    +            let mut index_writer: IndexWriter = index.writer(15_000_000)?;
    +
    +            // doc1
    +            let mut doc = TantivyDocument::new();
    +            doc.add_text(field1, "value1");
    +            doc.add_text(field1, "value2");
    +            doc.add_text(field1, "value3");
    +            index_writer.add_document(doc)?;
    +
    +            // doc2
    +            let mut doc = TantivyDocument::new();
    +            doc.add_text(field1, "value2");
    +            doc.add_text(field1, "value4");
    +            index_writer.add_document(doc)?;
    +
    +            index_writer.commit()?;
    +        }
    +        let reader = index.reader()?;
    +        let searcher = reader.searcher();
    +
    +        {
    +            let query = new_multiterms_const_query(
    +                field1,
    +                4,
    +                vec!["value1", "value3"].into_iter().map(Cow::Borrowed),
    +            );
    +
    +            let top_docs = searcher.search(&query, &TopDocs::with_limit(1))?;
    +            eprintln!("explain {:?}", query.explain(&searcher, top_docs[0].1)?);
    +
    +            assert_eq!(top_docs.len(), 1, "Expected 1 document");
    +            assert_eq!(top_docs[0].0, 0.5);
    +        }
    +
    +        {
    +            let query = new_multiterms_const_query(
    +                field1,
    +                4,
    +                vec!["value1", "value2", "value3"]
    +                    .into_iter()
    +                    .map(Cow::Borrowed),
    +            );
    +
    +            let top_docs = searcher.search(&query, &TopDocs::with_limit(1))?;
    +
    +            assert_eq!(top_docs.len(), 1, "Expected 1 document");
    +            assert_eq!(top_docs[0].0, 0.75);
    +        }
    +
    +        Ok(())
    +    }
    +}
    diff --git a/crates/tabby-common/src/index/structured_doc.rs b/crates/tabby-common/src/index/structured_doc.rs
    new file mode 100644
    index 000000000000..590123fdb15f
    --- /dev/null
    +++ b/crates/tabby-common/src/index/structured_doc.rs
    @@ -0,0 +1,52 @@
    +pub mod fields {
    +    pub const KIND: &str = "kind";
    +
    +    pub mod web {
    +        pub const TITLE: &str = "title";
    +        pub const LINK: &str = "link";
    +        pub const CHUNK_TEXT: &str = "chunk_text";
    +    }
    +
    +    pub mod issue {
    +        pub const TITLE: &str = "title";
    +        pub const LINK: &str = "link";
    +        pub const AUTHOR_EMAIL: &str = "author_email";
    +        pub const BODY: &str = "body";
    +        pub const CLOSED: &str = "closed";
    +    }
    +
    +    pub mod pull {
    +        pub const TITLE: &str = "title";
    +        pub const LINK: &str = "link";
    +        pub const AUTHOR_EMAIL: &str = "author_email";
    +        pub const BODY: &str = "body";
    +        pub const DIFF: &str = "diff";
    +        pub const MERGED: &str = "merged";
    +    }
    +
    +    pub mod commit {
    +        // === Doc level fields ===
    +        pub const SHA: &str = "sha";
    +        pub const MESSAGE: &str = "message";
    +        pub const AUTHOR_EMAIL: &str = "author_email";
    +        pub const AUTHOR_AT: &str = "author_at";
    +    }
    +
    +    pub mod page {
    +        // === Doc level fields ===
    +        pub const LINK: &str = "link";
    +        pub const TITLE: &str = "title";
    +
    +        // === Chunk level fields ===
    +        pub const CHUNK_CONTENT: &str = "chunk_text";
    +    }
    +
    +    pub mod ingested {
    +        // === Doc level fields ===
    +        pub const TITLE: &str = "title";
    +        pub const LINK: &str = "link";
    +
    +        // === Chunk level fields ===
    +        pub const CHUNK_BODY: &str = "chunk_body";
    +    }
    +}
    diff --git a/crates/tabby-common/src/languages.rs b/crates/tabby-common/src/languages.rs
    index b5f9c8bdb2d0..aa212df9f622 100644
    --- a/crates/tabby-common/src/languages.rs
    +++ b/crates/tabby-common/src/languages.rs
    @@ -1,5 +1,9 @@
    +use std::{collections::HashMap, ffi::OsStr};
    +
     use lazy_static::lazy_static;
    -use serde::Deserialize;
    +use serde::{Deserialize, Serialize};
    +
    +use crate::config;
     
     lazy_static! {
         static ref DEFAULT: Vec<&'static str> = vec![
    @@ -18,56 +22,126 @@ lazy_static! {
             "\n\n\t\t\t\t\t",
             "\n\n\t\t\t\t\t\t",
             "\n\n\t\t\t\t\t\t\t",
    +
    +        // FIXME: Hack for codellama / codegemma to simplify tabby's implementation.
    +
    +        // StarCoder
    +        "",
    +        "",
    +        "",
    +        "",
    +
    +        // CodeLlama
    +        " ",
    +
    +        // CodeGemma
    +        "<|fim_prefix|>",
    +        "<|fim_suffix|>",
    +        "<|fim_middle|>",
    +        "<|file_separator|>",
    +
    +        // chat_ml
    +        "<|system|>",
    +        "<|user|>",
    +        "<|end|>",
    +        "<|assistant|>",
         ];
     }
     
    -#[derive(Deserialize)]
    -struct ConfigList {
    +#[derive(Serialize, Deserialize, Debug, Clone, Default)]
    +pub struct ConfigList {
         config: Vec,
     }
     
    -#[derive(Deserialize, Debug)]
    +#[derive(Serialize, Deserialize, Debug, Clone)]
     pub struct Language {
         languages: Vec,
    -    top_level_keywords: Vec,
    +    exts: Vec,
     
    -    pub line_comment: String,
    +    top_level_keywords: Option>,
    +    pub line_comment: Option,
    +    pub chunk_size: Option,
     }
     
     impl Language {
         pub fn get_stop_words(&self) -> Vec {
             let mut out = vec![];
    -        out.push(format!("\n{}", self.line_comment));
    -        for word in &self.top_level_keywords {
    -            out.push(format!("\n{}", word));
    -        }
     
             for x in DEFAULT.iter() {
                 out.push((*x).to_owned());
             }
     
    +        if let Some(line_comment) = &self.line_comment {
    +            out.push(format!("\n{line_comment}"));
    +        };
    +
    +        if let Some(top_level_keywords) = &self.top_level_keywords {
    +            for word in top_level_keywords {
    +                out.push(format!("\n{word}"));
    +            }
    +        };
    +
             out
         }
     
    -    pub fn get_hashkey(&self) -> String {
    -        self.languages[0].clone()
    +    pub fn language(&'static self) -> &'static str {
    +        self.languages[0].as_str()
         }
     }
     
     lazy_static! {
    -    static ref CONFIG: ConfigList =
    -        serdeconv::from_toml_str(include_str!("../assets/languages.toml")).unwrap();
    +    static ref CONFIG: ConfigList = {
    +        let mut config_list: ConfigList =
    +            serdeconv::from_toml_str(include_str!("../assets/languages.toml")).unwrap();
    +        let mut config = config::Config::load().unwrap();
    +        config_list.config.append(&mut config.additional_languages);
    +        config_list
    +    };
    +    static ref LANGUAGE_CONFIG_MAPPING: HashMap<&'static str, &'static Language> = {
    +        let mut map = HashMap::new();
    +        for c in &CONFIG.config {
    +            for l in &c.languages {
    +                assert!(
    +                    !map.contains_key(l.as_str()),
    +                    "Duplicate language found: {l}"
    +                );
    +                map.insert(l.as_str(), c);
    +            }
    +        }
    +        map
    +    };
    +    static ref EXTS_LANGUAGE_MAPPING: HashMap<&'static str, &'static str> = {
    +        let mut map = HashMap::new();
    +        for c in &CONFIG.config {
    +            for e in &c.exts {
    +                let l = c.language();
    +                assert!(
    +                    !map.contains_key(e.as_str()),
    +                    "Duplicate extension found: {e}"
    +                );
    +                map.insert(e.as_str(), l);
    +            }
    +        }
    +        map
    +    };
         pub static ref UNKNOWN_LANGUAGE: Language = Language {
             languages: vec!["unknown".to_owned()],
    -        line_comment: "".to_owned(),
    -        top_level_keywords: vec![],
    +        line_comment: Some("".into()),
    +        top_level_keywords: Some(vec![]),
    +        exts: vec![],
    +        chunk_size: None
         };
     }
     
     pub fn get_language(language: &str) -> &'static Language {
    -    CONFIG
    -        .config
    -        .iter()
    -        .find(|c| c.languages.iter().any(|x| x == language))
    -        .unwrap_or(&UNKNOWN_LANGUAGE)
    +    if let Some(lang) = LANGUAGE_CONFIG_MAPPING.get(language) {
    +        lang
    +    } else {
    +        &UNKNOWN_LANGUAGE
    +    }
    +}
    +
    +pub fn get_language_by_ext(ext: &OsStr) -> Option<&'static Language> {
    +    let ext = ext.to_str()?;
    +    EXTS_LANGUAGE_MAPPING.get(ext).map(|x| get_language(x))
     }
    diff --git a/crates/tabby-common/src/lib.rs b/crates/tabby-common/src/lib.rs
    index bfe34f36b1d3..8d81958238d6 100644
    --- a/crates/tabby-common/src/lib.rs
    +++ b/crates/tabby-common/src/lib.rs
    @@ -1,87 +1,12 @@
     //! Common tabby types and utilities.
     //! Defines common types and utilities used across multiple tabby subprojects, especially serialization and deserialization targets.
     pub mod api;
    +pub mod axum;
     pub mod config;
    +pub mod constants;
     pub mod index;
     pub mod languages;
     pub mod path;
     pub mod registry;
     pub mod terminal;
     pub mod usage;
    -
    -use std::{
    -    fs::File,
    -    io::{BufReader, Error},
    -    ops::Range,
    -    path::PathBuf,
    -};
    -
    -use path::dataset_dir;
    -use serde::{Deserialize, Serialize};
    -use serde_jsonlines::JsonLinesReader;
    -
    -#[derive(Serialize, Deserialize)]
    -pub struct SourceFile {
    -    pub git_url: String,
    -    pub filepath: String,
    -    pub content: String,
    -    pub language: String,
    -    pub max_line_length: usize,
    -    pub avg_line_length: f32,
    -    pub alphanum_fraction: f32,
    -    pub tags: Vec,
    -}
    -
    -impl SourceFile {
    -    pub fn files_jsonl() -> PathBuf {
    -        dataset_dir().join("files.jsonl")
    -    }
    -
    -    pub fn all() -> Result, Error> {
    -        let files = glob::glob(format!("{}*", Self::files_jsonl().display()).as_str()).unwrap();
    -        let iter = files.filter_map(|x| x.ok()).flat_map(|path| {
    -            let fp = BufReader::new(File::open(path).unwrap());
    -            let reader = JsonLinesReader::new(fp);
    -            reader.read_all::().filter_map(|x| x.ok())
    -        });
    -        Ok(iter)
    -    }
    -}
    -
    -#[derive(Serialize, Deserialize, Clone, Debug)]
    -pub struct Point {
    -    pub row: usize,
    -    pub column: usize,
    -}
    -
    -impl Point {
    -    pub fn new(row: usize, column: usize) -> Self {
    -        Self { row, column }
    -    }
    -}
    -
    -#[derive(Serialize, Deserialize, Clone, Debug)]
    -pub struct Tag {
    -    pub range: Range,
    -    pub name_range: Range,
    -    pub utf16_column_range: Range,
    -    pub span: Range,
    -    pub line_range: Range,
    -    #[serde(skip_serializing_if = "Option::is_none")]
    -    pub docs: Option,
    -    pub is_definition: bool,
    -    pub syntax_type_name: String,
    -}
    -
    -#[derive(Default, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
    -pub struct Package {
    -    pub language: String,
    -    pub name: String,
    -    #[serde(skip_serializing_if = "Option::is_none")]
    -    pub version: Option,
    -}
    -
    -#[derive(Default, Serialize, Deserialize)]
    -pub struct DependencyFile {
    -    pub direct: Vec,
    -}
    diff --git a/crates/tabby-common/src/path.rs b/crates/tabby-common/src/path.rs
    index 823d64f91ce4..81b467f1a827 100644
    --- a/crates/tabby-common/src/path.rs
    +++ b/crates/tabby-common/src/path.rs
    @@ -13,7 +13,7 @@ lazy_static! {
             env::var("TABBY_MODEL_CACHE_ROOT").ok().map(PathBuf::from);
     }
     
    -#[cfg(feature = "testutils")]
    +#[cfg(any(feature = "testutils", test))]
     pub fn set_tabby_root(path: PathBuf) {
         println!("SET TABBY ROOT: '{}'", path.display());
         let cell = TABBY_ROOT.lock().unwrap();
    @@ -37,18 +37,10 @@ pub fn repositories_dir() -> PathBuf {
         tabby_root().join("repositories")
     }
     
    -pub fn dependency_file() -> PathBuf {
    -    dataset_dir().join("deps.json")
    -}
    -
     pub fn index_dir() -> PathBuf {
         tabby_root().join("index")
     }
     
    -pub fn dataset_dir() -> PathBuf {
    -    tabby_root().join("dataset")
    -}
    -
     pub fn models_dir() -> PathBuf {
         if let Some(cache_root) = &*TABBY_MODEL_CACHE_ROOT {
             cache_root.clone()
    diff --git a/crates/tabby-common/src/registry.rs b/crates/tabby-common/src/registry.rs
    index 24e9c2d0352d..2515ebac99eb 100644
    --- a/crates/tabby-common/src/registry.rs
    +++ b/crates/tabby-common/src/registry.rs
    @@ -1,6 +1,7 @@
     use std::{fs, path::PathBuf};
     
     use anyhow::{Context, Result};
    +use lazy_static::lazy_static;
     use serde::{Deserialize, Serialize};
     
     use crate::path::models_dir;
    @@ -12,6 +13,24 @@ pub struct ModelInfo {
         pub prompt_template: Option,
         #[serde(skip_serializing_if = "Option::is_none")]
         pub chat_template: Option,
    +    #[serde(skip_serializing_if = "Option::is_none")]
    +    pub urls: Option>,
    +
    +    #[serde(skip_serializing_if = "Option::is_none")]
    +    pub sha256: Option,
    +    // partition_urls is used for model download address
    +    // if the model is partitioned, the addresses of each partition will be listed here,
    +    // if there is only one partition, it will be the same as `urls`.
    +    //
    +    // will first try to the `urls`, if not found, will try this `partition_urls`.
    +    //
    +    // must make sure the first address is the entrypoint
    +    #[serde(skip_serializing_if = "Option::is_none")]
    +    pub partition_urls: Option>,
    +}
    +
    +#[derive(Serialize, Deserialize)]
    +pub struct PartitionModelUrl {
         pub urls: Vec,
         pub sha256: String,
     }
    @@ -21,15 +40,25 @@ fn models_json_file(registry: &str) -> PathBuf {
     }
     
     async fn load_remote_registry(registry: &str) -> Result> {
    -    let model_info = reqwest::get(format!(
    -        "https://raw.githubusercontent.com/{}/registry-tabby/main/models.json",
    -        registry
    -    ))
    -    .await
    -    .context("Failed to download")?
    -    .json()
    -    .await
    -    .context("Failed to get JSON")?;
    +    // Create an HTTP client with a custom timeout.
    +    // This is necessary because the default timeout settings can sometimes cause requests to hang indefinitely.
    +    // To prevent such issues, we specify a custom timeout duration.
    +    let client = reqwest::Client::builder()
    +        .timeout(std::time::Duration::from_secs(5))
    +        .build()
    +        .context("Failed to build HTTP client")?;
    +
    +    let model_info = client
    +        .get(format!(
    +            "https://raw.githubusercontent.com/{registry}/registry-tabby/main/models.json"
    +        ))
    +        .send()
    +        .await
    +        .context("Failed to download")?
    +        .json()
    +        .await
    +        .context("Failed to get JSON")?;
    +
         let dir = models_dir().join(registry);
         // We don't want to fail if the TabbyML directory already exists,
         // which is exactly, what `create_dir_all` will do, see
    @@ -52,27 +81,85 @@ pub struct ModelRegistry {
         pub models: Vec,
     }
     
    +lazy_static! {
    +    pub static ref LEGACY_GGML_MODEL_PATH: String =
    +        format!("ggml{}model.gguf", std::path::MAIN_SEPARATOR_STR);
    +    pub static ref GGML_MODEL_PARTITIONED_PREFIX: String = "model-00001-of-".into();
    +}
    +
    +// model registry tree structure
    +// root: ~/.tabby/models/TabbyML
    +//
    +// fn get_model_root_dir(model_name) -> {root}/{model_name}
    +//
    +// fn get_model_dir(model_name) -> {root}/{model_name}/ggml
     impl ModelRegistry {
         pub async fn new(registry: &str) -> Self {
             Self {
                 name: registry.to_owned(),
                 models: load_remote_registry(registry).await.unwrap_or_else(|err| {
                     load_local_registry(registry).unwrap_or_else(|_| {
    -                    panic!(
    -                        "Failed to fetch model organization <{}>: {:?}",
    -                        registry, err
    -                    )
    +                    panic!("Failed to fetch model organization <{registry}>: {err:?}")
                     })
                 }),
             }
         }
     
    -    fn get_model_dir(&self, name: &str) -> PathBuf {
    +    // get_model_store_dir returns {root}/{name}/ggml, e.g.. ~/.tabby/models/TabbyML/StarCoder-1B/ggml
    +    pub fn get_model_store_dir(&self, name: &str) -> PathBuf {
    +        self.get_model_dir(name).join("ggml")
    +    }
    +
    +    // get_model_dir returns {root}/{name}, e.g. ~/.tabby/models/TabbyML/StarCoder-1B
    +    pub fn get_model_dir(&self, name: &str) -> PathBuf {
             models_dir().join(&self.name).join(name)
         }
     
    +    // get_model_path returns the entrypoint of the model,
    +    // will look for the file with the prefix "00001-of-"
    +    pub fn get_model_entry_path(&self, name: &str) -> Option {
    +        for entry in fs::read_dir(self.get_model_store_dir(name)).ok()? {
    +            let entry = entry.expect("Error reading directory entry");
    +            let file_name = entry.file_name();
    +            let file_name_str = file_name.to_string_lossy();
    +
    +            // Check if the file name starts with the specified prefix
    +            if file_name_str.starts_with(GGML_MODEL_PARTITIONED_PREFIX.as_str()) {
    +                return Some(entry.path()); // Return the full path as PathBuf
    +            }
    +        }
    +
    +        None
    +    }
    +
    +    pub fn migrate_legacy_model_path(&self, name: &str) -> Result<(), std::io::Error> {
    +        let old_model_path = self
    +            .get_model_dir(name)
    +            .join(LEGACY_GGML_MODEL_PATH.as_str());
    +
    +        if old_model_path.exists() {
    +            return self.migrate_model_path(name, &old_model_path);
    +        }
    +
    +        Ok(())
    +    }
    +
         pub fn get_model_path(&self, name: &str) -> PathBuf {
    -        self.get_model_dir(name).join(GGML_MODEL_RELATIVE_PATH)
    +        self.get_model_dir(name)
    +            .join(LEGACY_GGML_MODEL_PATH.as_str())
    +    }
    +
    +    pub fn migrate_model_path(
    +        &self,
    +        name: &str,
    +        old_model_path: &PathBuf,
    +    ) -> Result<(), std::io::Error> {
    +        // legacy model always has a single file
    +        let model_path = self
    +            .get_model_store_dir(name)
    +            .join("model-00001-of-00001.gguf");
    +        std::fs::rename(old_model_path, model_path)?;
    +        Ok(())
         }
     
         pub fn save_model_info(&self, name: &str) {
    @@ -83,10 +170,13 @@ impl ModelRegistry {
         }
     
         pub fn get_model_info(&self, name: &str) -> &ModelInfo {
    -        self.models
    -            .iter()
    -            .find(|x| x.name == name)
    -            .unwrap_or_else(|| panic!("Invalid model_id <{}/{}>", self.name, name))
    +        match self.models.iter().find(|x| x.name == name) {
    +            Some(model_info) => model_info,
    +            None => panic!(
    +                "Invalid `model_id` <{}/{}>; please consult https://github.com/{}/registry-tabby for the correct `model_id`.",
    +                self.name, name, self.name
    +            ),
    +        }
         }
     }
     
    @@ -97,8 +187,44 @@ pub fn parse_model_id(model_id: &str) -> (&str, &str) {
         } else if parts.len() == 2 {
             (parts[0], parts[1])
         } else {
    -        panic!("Invalid model id {}", model_id);
    +        panic!("Invalid model id {model_id}");
         }
     }
     
    -pub static GGML_MODEL_RELATIVE_PATH: &str = "ggml/q8_0.v2.gguf";
    +#[cfg(test)]
    +mod tests {
    +    use temp_testdir::TempDir;
    +
    +    use super::{ModelRegistry, *};
    +    use crate::path::set_tabby_root;
    +
    +    #[tokio::test]
    +    async fn test_model_migration() {
    +        let root = TempDir::default();
    +        set_tabby_root(root.to_path_buf());
    +
    +        let registry = ModelRegistry::new("TabbyML").await;
    +        let dir = registry.get_model_dir("StarCoder-1B");
    +
    +        let old_model_path = dir.join(LEGACY_GGML_MODEL_PATH.as_str());
    +        let new_model_path = dir.join("ggml").join("model-00001-of-00001.gguf");
    +        tokio::fs::create_dir_all(old_model_path.parent().unwrap())
    +            .await
    +            .unwrap();
    +        tokio::fs::OpenOptions::new()
    +            .create(true)
    +            .write(true)
    +            .open(&old_model_path)
    +            .await
    +            .unwrap();
    +
    +        assert!(!new_model_path.exists());
    +        registry.migrate_legacy_model_path("StarCoder-1B").unwrap();
    +        assert!(registry
    +            .get_model_entry_path("StarCoder-1B")
    +            .unwrap()
    +            .exists());
    +        assert!(!old_model_path.exists());
    +        assert!(new_model_path.exists());
    +    }
    +}
    diff --git a/crates/tabby-common/src/terminal.rs b/crates/tabby-common/src/terminal.rs
    index 7ce293c523e1..8d8626dd4b2b 100644
    --- a/crates/tabby-common/src/terminal.rs
    +++ b/crates/tabby-common/src/terminal.rs
    @@ -47,7 +47,7 @@ impl<'a> InfoMessage<'a> {
         }
     }
     
    -impl<'a> ToString for InfoMessage<'a> {
    +impl ToString for InfoMessage<'_> {
         fn to_string(&self) -> String {
             let mut str = String::new();
             str.push_str(&format!("  {}\n\n", self.header_format.format(self.header)));
    diff --git a/crates/tabby-common/src/usage.rs b/crates/tabby-common/src/usage.rs
    index 61814977609d..83893f1a7bf0 100644
    --- a/crates/tabby-common/src/usage.rs
    +++ b/crates/tabby-common/src/usage.rs
    @@ -29,7 +29,7 @@ impl UsageTracker {
                         "As an open source project, we collect usage statistics to inform development priorities. For more",
                         "information, read https://tabby.tabbyml.com/docs/configuration#usage-collection",
                         "",
    -                    "We will not see or any code in your development process."
    +                    "We will not see or collect any code in your development process."
                     ]),
                     InfoMessage::new("Welcome to Tabby!", HeaderFormat::BoldWhite, &[
                         "If you have any questions or would like to engage with the Tabby team, please join us on Slack",
    diff --git a/crates/tabby-crawler/Cargo.toml b/crates/tabby-crawler/Cargo.toml
    new file mode 100644
    index 000000000000..c8dd913e8309
    --- /dev/null
    +++ b/crates/tabby-crawler/Cargo.toml
    @@ -0,0 +1,26 @@
    +[package]
    +name = "tabby-crawler"
    +version.workspace = true
    +edition.workspace = true
    +authors.workspace = true
    +homepage.workspace = true
    +
    +[dependencies]
    +tokio = { workspace = true, features = ["io-util", "process", "rt"] }
    +anyhow.workspace = true
    +tracing.workspace = true
    +url.workspace = true
    +percent-encoding = "2.3"
    +readable-readability = "0.4.0"
    +futures.workspace = true
    +async-stream.workspace = true
    +serde.workspace = true
    +serde_json.workspace = true
    +logkit.workspace = true
    +htmd = "0.1"
    +regex.workspace = true
    +reqwest.workspace = true
    +
    +[dev-dependencies]
    +tracing-test.workspace = true
    +tokio = { workspace = true, features = ["macros"] }
    diff --git a/crates/tabby-crawler/src/lib.rs b/crates/tabby-crawler/src/lib.rs
    new file mode 100644
    index 000000000000..17b961c06388
    --- /dev/null
    +++ b/crates/tabby-crawler/src/lib.rs
    @@ -0,0 +1,252 @@
    +mod llms_txt_parser;
    +mod types;
    +
    +use std::process::Stdio;
    +
    +use anyhow::anyhow;
    +use async_stream::stream;
    +use futures::{Stream, StreamExt};
    +use readable_readability::Readability;
    +use tokio::io::AsyncBufReadExt;
    +use tracing::{debug, warn};
    +use url::Url;
    +
    +use self::types::{CrawledDocument, KatanaRequestResponse};
    +
    +async fn crawl_url(
    +    start_url: &str,
    +    prefix_url: &str,
    +) -> anyhow::Result> {
    +    let mut command = tokio::process::Command::new("katana");
    +
    +    if std::env::var("TABBY_CRAWL_ENABLE_HEADLESS").is_ok() {
    +        command
    +            .arg("-headless")
    +            .arg("-headless-options")
    +            .arg("--disable-gpu");
    +    }
    +
    +    command
    +        .arg("-u")
    +        .arg(start_url)
    +        .arg("-jsonl")
    +        .arg("-crawl-scope")
    +        .arg(format!("{}.*", regex::escape(prefix_url)))
    +        .arg("-crawl-out-scope")
    +        .arg(r#"\.js$|\.css$|\.png$|\.jpg$|\.jpeg$"#)
    +        .arg("-depth")
    +        .arg("9999")
    +        .arg("-max-response-size")
    +        .arg("10485760") // 10MB max body size
    +        .arg("-rate-limit-minute")
    +        .arg("120")
    +        .arg("-strategy")
    +        .arg("breadth-first")
    +        .stdin(Stdio::null())
    +        .stdout(Stdio::piped())
    +        .stderr(Stdio::piped());
    +
    +    let mut child = command
    +        .spawn()
    +        .map_err(|e| anyhow!("Failed to run katana: {}", e))?;
    +
    +    let stdout = child.stdout.take().expect("Failed to acquire stdout");
    +    let mut stdout = tokio::io::BufReader::new(stdout).lines();
    +
    +    let stderr = child.stderr.take().expect("Failed to acquire stderr");
    +    let mut stderr = tokio::io::BufReader::new(stderr).lines();
    +
    +    tokio::spawn(async move {
    +        if let Some(exit_code) = child.wait().await.ok().and_then(|s| s.code()) {
    +            if exit_code != 0 {
    +                warn!("Katana exited with code {}", exit_code);
    +            }
    +        }
    +    });
    +
    +    tokio::spawn(async move {
    +        while let Ok(Some(line)) = stderr.next_line().await {
    +            logkit::info!("{line}");
    +        }
    +    });
    +
    +    Ok(stream! {
    +        while let Ok(Some(line)) = stdout.next_line().await {
    +            let data = match serde_json::from_str::(&line) {
    +                Ok(data) => data,
    +                Err(err) => {
    +                    warn!("Failed to parse katana output: {:?}, skipping...", err);
    +                    continue;
    +                }
    +            };
    +
    +            if data.response.status_code == Some(429) {
    +                logkit::warn!("429 Too Many Requests, consider adjust your rate limit settings...");
    +            }
    +
    +            // Skip if the status code is not 200
    +            if data.response.status_code != Some(200) {
    +                continue;
    +            }
    +
    +            // Skip if the content type is not text/html
    +            if !data
    +                .response
    +                .headers
    +                .get("content-type")
    +                .is_some_and(|ct| ct.starts_with("text/html"))
    +            {
    +                continue;
    +            }
    +
    +            // Skip if the content is larger than 1M.
    +            if data.response.raw.as_ref().is_some_and(|x| x.len() > 1_000_000) {
    +                debug!("Skipping {} as the content is larger than 1M", data.request.endpoint);
    +                continue;
    +            }
    +
    +            yield data;
    +        }
    +    })
    +}
    +
    +fn to_document(data: KatanaRequestResponse) -> Option {
    +    // Cleanup the HTML with Readability
    +    let (html, metadata) = {
    +        let (node, metadata) = Readability::new()
    +            .base_url(Url::parse(&data.request.endpoint).ok()?)
    +            .parse(&data.response.body?);
    +
    +        let mut html_bytes = vec![];
    +        node.serialize(&mut html_bytes).ok()?;
    +        (String::from_utf8(html_bytes).ok()?, metadata)
    +    };
    +
    +    // Convert the HTML to Markdown
    +    let md = match htmd::HtmlToMarkdown::new().convert(&html) {
    +        Ok(md) => md,
    +        Err(err) => {
    +            warn!("Failed to convert HTML to Markdown: {:?}", err);
    +            return None;
    +        }
    +    };
    +
    +    // Skip if the document is empty
    +    if md.is_empty() {
    +        return None;
    +    }
    +
    +    Some(CrawledDocument::new(
    +        data.request.endpoint,
    +        md.trim().to_owned(),
    +        metadata.into(),
    +    ))
    +}
    +
    +pub async fn crawl_pipeline(
    +    start_url: &str,
    +    prefix_url: &str,
    +) -> anyhow::Result> {
    +    Ok(crawl_url(start_url, prefix_url)
    +        .await?
    +        .filter_map(move |data| async move { to_document(data) }))
    +}
    +
    +/// Attempts to fetch `llms-full.txt` from the given base URL,
    +/// then splits its markdown content into multiple sections based on H1 headings.
    +/// Each section becomes a separate `CrawledDocument`.
    +/// Returns a vector of `CrawledDocument`s if successful.
    +pub async fn crawler_llms(start_url: &str) -> anyhow::Result> {
    +    // Remove trailing slash from the base URL if present.
    +    let base_url = start_url.trim_end_matches('/');
    +
    +    // Check if the URL already ends with llms-full.txt or llms.txt
    +    let llms_full_url = if base_url.ends_with("llms-full.txt") {
    +        base_url.to_string()
    +    } else if base_url.ends_with("llms.txt") {
    +        // If URL ends with llms.txt, try to access llms-full.txt instead
    +        let base_without_llms = base_url.trim_end_matches("llms.txt");
    +        format!("{base_without_llms}llms-full.txt")
    +    } else {
    +        format!("{base_url}/llms-full.txt")
    +    };
    +
    +    let resp = reqwest::get(&llms_full_url).await?;
    +    if !resp.status().is_success() {
    +        anyhow::bail!("Unable to fetch llms-full.txt from {}", llms_full_url);
    +    }
    +    let body = resp.text().await?;
    +    debug!("Successfully fetched llms-full.txt: {}", llms_full_url);
    +
    +    // Split the fetched markdown content into sections.
    +    let docs = llms_txt_parser::split_llms_content(&body, start_url);
    +    if docs.is_empty() {
    +        anyhow::bail!("No sections found in llms-full.txt from {}", llms_full_url);
    +    }
    +
    +    Ok(docs)
    +}
    +
    +#[cfg(test)]
    +mod tests {
    +
    +    use tracing_test::traced_test;
    +
    +    use super::*;
    +
    +    #[tokio::test]
    +    #[traced_test]
    +    async fn test_to_document() {
    +        let headers = [("content_type".into(), "text/html".into())]
    +            .iter()
    +            .cloned()
    +            .collect();
    +        let data = KatanaRequestResponse {
    +            timestamp: "2021-09-01T00:00:00Z".to_owned(),
    +            request: types::KatanaRequest {
    +                endpoint: "https://example.com".to_owned(),
    +                method: "GET".to_owned(),
    +                raw: "GET / HTTP/1.1\nHost: example.com\n".to_owned(),
    +            },
    +            response: types::KatanaResponse {
    +                status_code: Some(200),
    +                headers,
    +                body: Some("

    Hello, World!

    ".to_owned()), + technologies: Default::default(), + raw: Some("HTTP/1.1 200 OK\nContent-Type: text/html\n".to_owned()), + }, + }; + + let doc = to_document(data).unwrap(); + assert_eq!(doc.url, "https://example.com"); + assert_eq!(doc.markdown, "Hello, World!"); + } + + #[tokio::test] + #[traced_test] + async fn test_crawler_llms_success_developers_cloudflare_with_url() { + let base_url = "https://developers.cloudflare.com/workers-ai"; + let result = crawler_llms(base_url).await; + assert!(result.is_ok(), "Expected success from {base_url}"); + let docs = result.unwrap(); + assert!( + !docs.is_empty(), + "Expected at least one section from llms-full.txt at {base_url}" + ); + println!("Fetched {} documents from {}", docs.len(), base_url); + } + + #[tokio::test] + #[traced_test] + async fn test_crawler_llms_success_docs_perplexity_with_source() { + let base_url = "https://docs.perplexity.ai"; + let result = crawler_llms(base_url).await; + assert!(result.is_ok(), "Expected success from {base_url}"); + let docs = result.unwrap(); + assert!( + !docs.is_empty(), + "Expected at least one section from llms-full.txt at {base_url}" + ); + println!("Fetched {} documents from {}", docs.len(), base_url); + } +} diff --git a/crates/tabby-crawler/src/llms_txt_parser.rs b/crates/tabby-crawler/src/llms_txt_parser.rs new file mode 100644 index 000000000000..34f6a446d9cf --- /dev/null +++ b/crates/tabby-crawler/src/llms_txt_parser.rs @@ -0,0 +1,231 @@ +use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; + +use crate::types::{CrawledDocument, CrawledMetadata}; + +pub fn split_llms_content(content: &str, base_url: &str) -> Vec { + let mut docs = Vec::new(); + let mut current_title: Option = None; + let mut current_url: Option = None; + let mut current_body = String::new(); + + // Process the content line by line. + for line in content.lines() { + // Check if the line starts with a heading-1 marker. + if line.starts_with("# ") { + // If we already have a section in progress, finalize it. + if let Some(title) = current_title.take() { + // Use the URL from the section if available; otherwise, fallback to base_url. + let base_url_str = current_url.take().unwrap_or_else(|| base_url.to_owned()); + // URL-encode the title and append it as a fragment + let encoded_title = utf8_percent_encode(&title, NON_ALPHANUMERIC).to_string(); + let url = format!("{base_url_str}#{encoded_title}"); + let metadata = CrawledMetadata { + title: title.into(), + description: base_url_str.into(), + }; + docs.push(CrawledDocument::new( + url, + current_body.trim().to_owned(), + metadata, + )); + current_body = String::new(); + } + current_title = Some(line[2..].trim().to_owned()); + current_url = None; + } else if line.starts_with("URL:") || line.starts_with("Source:") { + let prefix_len = if line.starts_with("URL:") { 4 } else { 7 }; + let url_str = line[prefix_len..].trim(); + current_url = Some(url_str.to_owned()); + } else { + current_body.push_str(line); + current_body.push('\n'); + } + } + + // Finalize the last section if any. + if let Some(title) = current_title { + let base_url_str = current_url.unwrap_or_else(|| base_url.to_owned()); + // URL-encode the title and append it as a fragment + let encoded_title = utf8_percent_encode(&title, NON_ALPHANUMERIC).to_string(); + let url = format!("{base_url_str}#{encoded_title}"); + let metadata = CrawledMetadata { + title: title.into(), + description: base_url_str.into(), + }; + docs.push(CrawledDocument::new( + url, + current_body.trim().to_owned(), + metadata, + )); + } + + docs +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_split_llms_content_with_url() { + // Test a section that provides a URL. + let content = "\ +# Test Title with URL +URL: https://developers.cloudflare.com +This is a test body. +More text on the same section. +"; + let base_url = "example.com"; + let docs = split_llms_content(content, base_url); + assert_eq!(docs.len(), 1, "Should produce one document"); + + let doc = &docs[0]; + // The title is taken from the heading. + assert_eq!(doc.metadata.title, Some("Test Title with URL".to_string())); + // The URL should be extracted from the URL: line with encoded title appended. + assert_eq!( + doc.url, + "https://developers.cloudflare.com#Test%20Title%20with%20URL" + ); + // The body should contain the text after the URL line. + assert_eq!( + doc.markdown, + "This is a test body.\nMore text on the same section." + ); + } + + #[test] + fn test_split_llms_content_with_source() { + // Test a section that provides a Source. + let content = "\ +# Test Title with Source +Source: https://docs.perplexity.ai +This is another test body. +Line two of body. +"; + let base_url = "example.com"; + let docs = split_llms_content(content, base_url); + assert_eq!(docs.len(), 1, "Should produce one document"); + + let doc = &docs[0]; + assert_eq!( + doc.metadata.title, + Some("Test Title with Source".to_string()) + ); + // The URL should be extracted from the Source: line with encoded title appended. + assert_eq!( + doc.url, + "https://docs.perplexity.ai#Test%20Title%20with%20Source" + ); + assert_eq!( + doc.markdown, + "This is another test body.\nLine two of body." + ); + } + + #[test] + fn test_split_llms_content_without_metadata() { + // Test a section with no URL or Source line; should fallback to base_url. + let content = "\ +# Test Title without URL or Source +This is test body with no explicit URL. +Additional content line. +"; + let base_url = "example.com"; + let docs = split_llms_content(content, base_url); + assert_eq!(docs.len(), 1, "Should produce one document"); + + let doc = &docs[0]; + assert_eq!( + doc.metadata.title, + Some("Test Title without URL or Source".to_string()) + ); + // Fallback to the provided base_url with encoded title appended. + assert_eq!( + doc.url, + "example.com#Test%20Title%20without%20URL%20or%20Source" + ); + assert_eq!( + doc.markdown, + "This is test body with no explicit URL.\nAdditional content line." + ); + } + + #[test] + fn test_split_llms_content_multiple_sections() { + // Test multiple sections with mixed metadata. + let content = "\ +# Section One +URL: https://developers.cloudflare.com +Content for section one. + +# Section Two +Source: https://docs.perplexity.ai +Content for section two. + +# Section Three +Content for section three with no metadata. +"; + let base_url = "example.com"; + let docs = split_llms_content(content, base_url); + assert_eq!(docs.len(), 3, "Should produce three documents"); + + // Section One. + let doc1 = &docs[0]; + assert_eq!(doc1.metadata.title, Some("Section One".to_string())); + assert_eq!(doc1.url, "https://developers.cloudflare.com#Section%20One"); + assert!(doc1.markdown.contains("Content for section one.")); + + // Section Two. + let doc2 = &docs[1]; + assert_eq!(doc2.metadata.title, Some("Section Two".to_string())); + assert_eq!(doc2.url, "https://docs.perplexity.ai#Section%20Two"); + assert!(doc2.markdown.contains("Content for section two.")); + + // Section Three. + let doc3 = &docs[2]; + assert_eq!(doc3.metadata.title, Some("Section Three".to_string())); + // Since no URL/Source is provided, fallback to base_url with encoded title appended. + assert_eq!(doc3.url, "example.com#Section%20Three"); + assert!(doc3 + .markdown + .contains("Content for section three with no metadata.")); + } + + #[test] + fn test_base_url_only_generates_unique_fragment_links() { + // Test that when only base URL is available, each section gets a unique link with fragment + let content = "\ +# Getting Started +This is the getting started guide. + +# Configuration +This explains how to configure the system. + +# Advanced Usage +Advanced usage scenarios. +"; + let base_url = "https://docs.example.com"; + let docs = split_llms_content(content, base_url); + assert_eq!(docs.len(), 3, "Should produce three documents"); + + let doc1 = &docs[0]; + let doc2 = &docs[1]; + let doc3 = &docs[2]; + + // All should use base URL with encoded title fragments + assert_eq!(doc1.url, "https://docs.example.com#Getting%20Started"); + assert_eq!(doc2.url, "https://docs.example.com#Configuration"); + assert_eq!(doc3.url, "https://docs.example.com#Advanced%20Usage"); + + // All URLs should be different despite same base URL + assert_ne!(doc1.url, doc2.url); + assert_ne!(doc2.url, doc3.url); + assert_ne!(doc1.url, doc3.url); + + // All should have the same description (base URL) + assert_eq!(doc1.metadata.description, Some(base_url.to_string())); + assert_eq!(doc2.metadata.description, Some(base_url.to_string())); + assert_eq!(doc3.metadata.description, Some(base_url.to_string())); + } +} diff --git a/crates/tabby-crawler/src/types.rs b/crates/tabby-crawler/src/types.rs new file mode 100644 index 000000000000..654b08f6035f --- /dev/null +++ b/crates/tabby-crawler/src/types.rs @@ -0,0 +1,68 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Debug)] +pub struct KatanaRequestResponse { + pub timestamp: String, + pub request: KatanaRequest, + pub response: KatanaResponse, +} + +#[derive(Deserialize, Debug)] +pub struct KatanaRequest { + pub method: String, + pub endpoint: String, + pub raw: String, +} + +#[derive(Deserialize, Debug)] +pub struct KatanaResponse { + pub status_code: Option, + pub headers: HashMap, + pub body: Option, + pub technologies: Option>, + pub raw: Option, +} + +#[derive(Serialize)] +pub struct CrawledMetadata { + pub title: Option, + pub description: Option, +} + +impl From for CrawledMetadata { + fn from(metadata: readable_readability::Metadata) -> Self { + // Trim all ascii special chars from title + let trim_title_chars = [ + '#', '$', '%', '&', '*', '+', ',', '/', ':', ';', '=', '?', '@', '[', ']', '^', '`', + '{', '|', '}', '~', '\n', ' ', + ]; + let title = metadata + .article_title + .or(metadata.page_title) + .map(|x| x.trim_matches(trim_title_chars).to_owned()); + Self { + title, + description: metadata.description, + } + } +} + +#[derive(Serialize)] +pub struct CrawledDocument { + pub url: String, + pub markdown: String, + + pub metadata: CrawledMetadata, +} + +impl CrawledDocument { + pub fn new(url: String, markdown: String, metadata: CrawledMetadata) -> Self { + Self { + url, + markdown, + metadata, + } + } +} diff --git a/crates/tabby-download/Cargo.toml b/crates/tabby-download/Cargo.toml index a841d2570d4d..b27568645add 100644 --- a/crates/tabby-download/Cargo.toml +++ b/crates/tabby-download/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tabby-download" -version = "0.8.0" +version = "0.33.0-dev.0" edition = "2021" [dependencies] @@ -8,5 +8,7 @@ aim-downloader = { path = "../aim-downloader" } tabby-common = { path = "../tabby-common" } anyhow = { workspace = true } tracing = { workspace = true } -tokio-retry = "0.3.0" -sha256 = "1.4.0" +tokio-retry = { workspace = true } + +[dev-dependencies] +serial_test = { workspace = true } diff --git a/crates/tabby-download/src/lib.rs b/crates/tabby-download/src/lib.rs index da69efbfa408..51d944c91229 100644 --- a/crates/tabby-download/src/lib.rs +++ b/crates/tabby-download/src/lib.rs @@ -1,79 +1,486 @@ //! Responsible for downloading ML models for use with tabby. -use std::{fs, path::Path}; +use std::{fs, io}; -use aim_downloader::{bar::WrappedBar, https}; -use anyhow::{anyhow, Result}; -use tabby_common::registry::{parse_model_id, ModelRegistry}; +use aim_downloader::{bar::WrappedBar, error::DownloadError, hash::HashChecker, https}; +use anyhow::{bail, Context, Result}; +use tabby_common::registry::{parse_model_id, ModelInfo, ModelRegistry}; use tokio_retry::{ strategy::{jitter, ExponentialBackoff}, Retry, }; use tracing::{info, warn}; +pub fn get_download_host() -> String { + std::env::var("TABBY_DOWNLOAD_HOST").unwrap_or_else(|_| "huggingface.co".to_string()) +} + +pub fn get_huggingface_mirror_host() -> Option { + std::env::var("TABBY_HUGGINGFACE_HOST_OVERRIDE").ok() +} + +pub fn filter_download_address(model_info: &ModelInfo) -> Vec<(Option, String)> { + let download_host = get_download_host(); + if let Some(urls) = &model_info.urls { + if !urls.is_empty() { + let url = model_info + .urls + .iter() + .flatten() + .find(|f| f.contains(&download_host)) + .map(|url| { + if let Some(mirror_host) = get_huggingface_mirror_host() { + url.replace("huggingface.co", &mirror_host) + } else { + url.to_owned() + } + }); + + return vec![(url, model_info.sha256.clone().unwrap_or_default())]; + }; + }; + + model_info + .partition_urls + .iter() + .flatten() + .map(|x| -> (Option, String) { + let url = x + .urls + .iter() + .find(|f| f.contains(&download_host)) + .map(|url| { + if let Some(mirror_host) = get_huggingface_mirror_host() { + url.replace("huggingface.co", &mirror_host) + } else { + url.to_owned() + } + }); + + (url, x.sha256.clone()) + }) + .collect() +} + +macro_rules! partitioned_file_name { + ($index:expr, $total:expr) => { + format!("model-{:05}-of-{:05}.gguf", $index + 1, $total) + }; +} + async fn download_model_impl( registry: &ModelRegistry, name: &str, prefer_local_file: bool, ) -> Result<()> { let model_info = registry.get_model_info(name); - registry.save_model_info(name); + registry.migrate_legacy_model_path(name)?; + + let urls = filter_download_address(model_info); - let model_path = registry.get_model_path(name); - if model_path.exists() { - if !prefer_local_file { - info!("Checking model integrity.."); - let checksum = sha256::try_digest(&model_path).unwrap(); - if checksum == model_info.sha256 { - return Ok(()); + let mut model_existed = true; + for (index, _) in urls.iter().enumerate() { + if fs::metadata( + registry + .get_model_store_dir(name) + .join(partitioned_file_name!(index, urls.len())), + ) + .is_err() + { + model_existed = false; + break; + } + } + + if model_existed && prefer_local_file { + return Ok(()); + } + + if model_existed { + info!("Checking model integrity.."); + + let mut sha256_matched = true; + for (index, url) in urls.iter().enumerate() { + if HashChecker::check(partitioned_file_name!(index, urls.len()).as_str(), &url.1) + .is_err() + { + sha256_matched = false; + break; } + } - warn!( - "Checksum doesn't match for <{}/{}>, re-downloading...", - registry.name, name - ); - fs::remove_file(&model_path)?; - } else { + if sha256_matched { return Ok(()); } + + warn!( + "Checksum doesn't match for <{}/{}>, re-downloading...", + registry.name, name + ); } - let registry = std::env::var("TABBY_DOWNLOAD_HOST").unwrap_or("huggingface.co".to_owned()); - let Some(model_url) = model_info.urls.iter().find(|x| x.contains(®istry)) else { - return Err(anyhow!( - "Invalid mirror <{}> for model urls: {:?}", - registry, - model_info.urls - )); - }; + if urls.iter().any(|url| url.0.is_none()) { + bail!( + "No download URLs available for <{}/{}>", + registry.name, + model_info.name + ); + } - let strategy = ExponentialBackoff::from_millis(100).map(jitter).take(2); - let download_job = Retry::spawn(strategy, || download_file(model_url, model_path.as_path())); - download_job.await?; - Ok(()) -} + match fs::remove_dir_all(registry.get_model_dir(name)) { + Ok(_) => Ok(()), + // Ignore "Not Found" error, when newly download, the model directory may not exist + Err(ref e) if e.kind() == io::ErrorKind::NotFound => Ok(()), + Err(e) => Err(e), + }?; -async fn download_file(url: &str, path: &Path) -> Result<()> { - let dir = path.parent().unwrap(); + // prepare for download + let dir = registry.get_model_store_dir(name); fs::create_dir_all(dir)?; + registry.save_model_info(name); + + for (index, url) in urls.iter().enumerate() { + let dir = registry + .get_model_store_dir(name) + .to_string_lossy() + .into_owned(); + let filename: String = partitioned_file_name!(index, urls.len()); + let strategy = ExponentialBackoff::from_millis(100).map(jitter).take(2); + let address = url.clone().0.unwrap(); - let filename = path.to_str().unwrap(); - let intermediate_filename = filename.to_owned() + ".tmp"; + Retry::spawn(strategy, move || { + let dir = dir.clone(); + let filename = filename.clone(); + let address = address.clone(); - let mut bar = WrappedBar::new(0, url, false); + // it's ok to use unwrap here, because we've checked the availability of URLs + download_file(address, dir, filename, &url.1) + }) + .await?; + } + + Ok(()) +} - https::HTTPSHandler::get(url, &intermediate_filename, &mut bar, "").await?; +async fn download_file( + url: String, + dir: String, + filename: String, + expected_sha256: &str, +) -> Result<()> { + let fullpath = format! {"{}{}{}", dir, std::path::MAIN_SEPARATOR, filename}; + let intermediate_filename = fullpath.clone() + ".tmp"; + let mut bar = WrappedBar::new(0, &url, false); + if let Err(e) = + https::HTTPSHandler::get(&url, &intermediate_filename, &mut bar, expected_sha256).await + { + match e { + DownloadError::HttpError { name, code } => { + bail!("Fetching '{name}' failed: Server returned {code} HTTP status") + } + DownloadError::Validate { source } => { + bail!("Failed to validate '{source}'") + } + } + } - fs::rename(intermediate_filename, filename)?; + fs::rename(intermediate_filename, fullpath)?; Ok(()) } -pub async fn download_model(model_id: &str, prefer_local_file: bool) { +pub enum ModelKind { + Embedding, + Completion, + Chat, +} + +pub async fn download_model(model_id: &str, prefer_local_file: bool, kind: Option) { let (registry, name) = parse_model_id(model_id); let registry = ModelRegistry::new(registry).await; - let handler = |err| panic!("Failed to fetch model '{}' due to '{}'", model_id, err); + if let Some(kind) = kind { + let model_info = registry.get_model_info(name); + validate_model_kind(kind, model_info) + .context( + "Model validation has failed. For TabbyML models, please consult https://github.com/tabbyml/registry-tabby to locate the appropriate models.", + ) + .unwrap(); + } + + let handler = |err| panic!("Failed to fetch model '{model_id}' due to '{err}'"); download_model_impl(®istry, name, prefer_local_file) .await .unwrap_or_else(handler) } + +fn validate_model_kind(kind: ModelKind, info: &ModelInfo) -> Result<()> { + match kind { + ModelKind::Embedding => Ok(()), + ModelKind::Completion => info + .prompt_template + .as_ref() + .ok_or_else(|| { + anyhow::anyhow!( + "Model '{}' is not a completion model; it does not have a prompt template.", + info.name + ) + }) + .map(|_| ()), + ModelKind::Chat => info + .chat_template + .as_ref() + .ok_or_else(|| { + anyhow::anyhow!( + "Model '{}' is not a chat model, it does not have a chat template", + info.name + ) + }) + .map(|_| ()), + } +} + +#[cfg(test)] +mod tests { + // filter_download_address tests should be serial because they rely on environment variables + use serial_test::serial; + use tabby_common::registry::{ModelInfo, PartitionModelUrl}; + + #[test] + #[serial(filter_download_address)] + fn test_filter_download_address() { + // multiple urls + let model_info = ModelInfo { + name: "test".to_string(), + urls: Some(vec![ + "https://huggingface.co/test".to_string(), + "https://huggingface.co/test2".to_string(), + "https://modelscope.co/test2".to_string(), + ]), + sha256: Some("test_sha256".to_string()), + prompt_template: None, + chat_template: None, + partition_urls: None, + }; + let urls = super::filter_download_address(&model_info); + assert_eq!(urls.len(), 1); + assert_eq!(urls[0].0, Some("https://huggingface.co/test".into())); + + // single url + let model_info = ModelInfo { + name: "test".to_string(), + urls: Some(vec![ + "https://huggingface.co/test".to_string(), + "https://modelscope.co/test2".to_string(), + ]), + sha256: Some("test_sha256".to_string()), + prompt_template: None, + chat_template: None, + partition_urls: None, + }; + let urls = super::filter_download_address(&model_info); + assert_eq!(urls.len(), 1); + assert_eq!(urls[0].0, Some("https://huggingface.co/test".into())); + } + + #[test] + #[serial(filter_download_address)] + fn test_filter_download_address_no_host() { + std::env::set_var("TABBY_DOWNLOAD_HOST", "not-existed.com"); + // multiple urls + let model_info = ModelInfo { + name: "test".to_string(), + urls: Some(vec![ + "https://huggingface.co/test".to_string(), + "https://huggingface.co/test2".to_string(), + "https://modelscope.co/test2".to_string(), + ]), + sha256: Some("test_sha256".to_string()), + prompt_template: None, + chat_template: None, + partition_urls: None, + }; + let urls = super::filter_download_address(&model_info); + assert_eq!(urls.len(), 1); + assert_eq!(urls[0].0, None); + + // single url + let model_info = ModelInfo { + name: "test".to_string(), + urls: Some(vec![ + "https://huggingface.co/test".to_string(), + "https://modelscope.co/test2".to_string(), + ]), + sha256: Some("test_sha256".to_string()), + prompt_template: None, + chat_template: None, + partition_urls: None, + }; + let urls = super::filter_download_address(&model_info); + assert_eq!(urls.len(), 1); + assert_eq!(urls[0].0, None); + + std::env::remove_var("TABBY_DOWNLOAD_HOST"); + } + + #[test] + #[serial(filter_download_address)] + fn test_filter_download_address_multiple_partitions() { + let model_info = ModelInfo { + name: "test".to_string(), + urls: None, + sha256: Some("test_sha256".to_string()), + prompt_template: None, + chat_template: None, + partition_urls: Some(vec![ + PartitionModelUrl { + urls: vec![ + "https://huggingface.co/part1".to_string(), + "https://modelscope.co/part1".to_string(), + ], + sha256: "test_sha256_1".to_string(), + }, + PartitionModelUrl { + urls: vec![ + "https://huggingface.co/part2".to_string(), + "https://modelscope.co/part2".to_string(), + ], + sha256: "test_sha256_2".to_string(), + }, + ]), + }; + let urls = super::filter_download_address(&model_info); + assert_eq!(urls.len(), 2); + assert_eq!(urls[0].0, Some("https://huggingface.co/part1".into())); + assert_eq!(urls[0].1, "test_sha256_1"); + assert_eq!(urls[1].0, Some("https://huggingface.co/part2".into())); + assert_eq!(urls[1].1, "test_sha256_2"); + } + + #[test] + #[serial(filter_download_address)] + fn test_filter_download_address_single_partition() { + let model_info = ModelInfo { + name: "test".to_string(), + urls: None, + sha256: Some("test_sha256".to_string()), + prompt_template: None, + chat_template: None, + partition_urls: Some(vec![PartitionModelUrl { + urls: vec!["https://huggingface.co/part1".to_string()], + sha256: "test_sha256_1".to_string(), + }]), + }; + let urls = super::filter_download_address(&model_info); + assert_eq!(urls.len(), 1); + assert_eq!(urls[0].0, Some("https://huggingface.co/part1".into())); + assert_eq!(urls[0].1, "test_sha256_1"); + } + + #[test] + #[serial(filter_download_address)] + fn test_filter_download_address_prefer_urls() { + let model_info = ModelInfo { + name: "test".to_string(), + urls: Some(vec!["https://huggingface.co/test".to_string()]), + sha256: Some("test_sha256".to_string()), + prompt_template: None, + chat_template: None, + partition_urls: Some(vec![PartitionModelUrl { + urls: vec!["https://modelscope.co/test".to_string()], + sha256: "test_sha256".to_string(), + }]), + }; + let urls = super::filter_download_address(&model_info); + assert_eq!(urls.len(), 1); + assert_eq!(urls[0].0, Some("https://huggingface.co/test".into())); + assert_eq!(urls[0].1, "test_sha256"); + } + + #[test] + #[serial(filter_download_address)] + fn test_filter_download_address_huggingface_override_urls() { + std::env::set_var("TABBY_HUGGINGFACE_HOST_OVERRIDE", "modelscope.co"); + let model_info = ModelInfo { + name: "test".to_string(), + urls: Some(vec!["https://huggingface.co/test".to_string()]), + sha256: Some("test_sha256".to_string()), + prompt_template: None, + chat_template: None, + partition_urls: None, + }; + let urls = super::filter_download_address(&model_info); + assert_eq!(urls.len(), 1); + assert_eq!(urls[0].0, Some("https://modelscope.co/test".into())); + assert_eq!(urls[0].1, "test_sha256"); + // must reset the env, or it will affect other tests + std::env::remove_var("TABBY_HUGGINGFACE_HOST_OVERRIDE"); + } + + #[test] + #[serial(filter_download_address)] + fn test_filter_download_address_huggingface_override_partitioned() { + std::env::set_var("TABBY_HUGGINGFACE_HOST_OVERRIDE", "modelscope.co"); + let model_info = ModelInfo { + name: "test".to_string(), + urls: None, + sha256: Some("test_sha256".to_string()), + prompt_template: None, + chat_template: None, + partition_urls: Some(vec![ + PartitionModelUrl { + urls: vec!["https://huggingface.co/part1".to_string()], + sha256: "test_sha256_1".to_string(), + }, + PartitionModelUrl { + urls: vec!["https://huggingface.co/part2".to_string()], + sha256: "test_sha256_2".to_string(), + }, + ]), + }; + let urls = super::filter_download_address(&model_info); + assert_eq!(urls.len(), 2); + assert_eq!(urls[0].0, Some("https://modelscope.co/part1".into())); + assert_eq!(urls[0].1, "test_sha256_1"); + assert_eq!(urls[1].0, Some("https://modelscope.co/part2".into())); + assert_eq!(urls[1].1, "test_sha256_2"); + // must reset the env, or it will affect other tests + std::env::remove_var("TABBY_HUGGINGFACE_HOST_OVERRIDE"); + } + + #[test] + #[serial(filter_download_address)] + fn test_filter_download_address_download_host() { + std::env::set_var("TABBY_DOWNLOAD_HOST", "modelscope.co"); + let model_info = ModelInfo { + name: "test".to_string(), + urls: None, + sha256: Some("test_sha256".to_string()), + prompt_template: None, + chat_template: None, + partition_urls: Some(vec![ + PartitionModelUrl { + urls: vec![ + "https://huggingface.co/part1".to_string(), + "https://modelscope.co/part1".to_string(), + ], + sha256: "test_sha256_1".to_string(), + }, + PartitionModelUrl { + urls: vec![ + "https://huggingface.co/part2".to_string(), + "https://modelscope.co/part2".to_string(), + ], + sha256: "test_sha256_2".to_string(), + }, + ]), + }; + let urls = super::filter_download_address(&model_info); + assert_eq!(urls.len(), 2); + assert_eq!(urls[0].0, Some("https://modelscope.co/part1".into())); + assert_eq!(urls[0].1, "test_sha256_1"); + assert_eq!(urls[1].0, Some("https://modelscope.co/part2".into())); + assert_eq!(urls[1].1, "test_sha256_2"); + // must reset the env, or it will affect other tests + std::env::remove_var("TABBY_DOWNLOAD_HOST"); + } +} diff --git a/crates/tabby-git/Cargo.toml b/crates/tabby-git/Cargo.toml new file mode 100644 index 000000000000..6aa9567794c5 --- /dev/null +++ b/crates/tabby-git/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "tabby-git" +version.workspace = true +edition.workspace = true +authors.workspace = true +homepage.workspace = true + +[dependencies] +anyhow.workspace = true +chrono.workspace = true +git2.workspace = true +nucleo.workspace = true +serde.workspace = true +temp_testdir.workspace = true +axum.workspace = true +serde_json.workspace = true +mime_guess.workspace = true +futures.workspace = true +async-stream.workspace = true +tokio.workspace = true +tracing.workspace = true +ignore.workspace = true +grep = "0.3.1" + +[dev-dependencies] +assert_matches.workspace = true diff --git a/crates/tabby-git/src/commit.rs b/crates/tabby-git/src/commit.rs new file mode 100644 index 000000000000..6edd80333182 --- /dev/null +++ b/crates/tabby-git/src/commit.rs @@ -0,0 +1,101 @@ +use anyhow::Result; +use async_stream::stream; +use chrono::{DateTime, TimeZone, Utc}; +use futures::stream::{BoxStream, StreamExt}; +use git2::{Repository, Sort}; +use tokio::sync::{mpsc, oneshot}; + +#[derive(Debug, Clone)] +pub struct Commit { + pub id: String, + pub message: String, + pub author_name: String, + pub author_email: String, + pub author_at: DateTime, +} + +fn commit_from_git2(commit: &git2::Commit) -> Commit { + let author = commit.author(); + + Commit { + id: commit.id().to_string(), + message: commit.message().unwrap_or("").to_string(), + author_name: author.name().unwrap_or("").to_string(), + author_email: author.email().unwrap_or("").to_string(), + author_at: Utc + .timestamp_opt(author.when().seconds(), 0) + .single() + .unwrap_or_default(), + } +} + +pub fn stream_commits( + repo_path: String, +) -> (BoxStream<'static, Result>, oneshot::Sender<()>) { + let (stop_tx, stop_rx) = oneshot::channel(); + let (tx, mut rx) = mpsc::channel(16); + + // Spawn git operations in a tokio task + tokio::spawn({ + let mut stop_rx = stop_rx; + let tx_data = tx.clone(); + async move { + // Keep all git operations inside spawn_blocking + let result = tokio::task::spawn_blocking(move || { + let repo = match Repository::open(&repo_path) { + Ok(repo) => repo, + Err(e) => return Err(anyhow::anyhow!("Failed to open repository: {}", e)), + }; + + let mut revwalk = match repo.revwalk() { + Ok(walk) => walk, + Err(e) => return Err(anyhow::anyhow!("Failed to create revwalk: {}", e)), + }; + + revwalk.push_head().ok(); + revwalk.set_sorting(Sort::TIME).ok(); + + // Process commits inside the blocking task + for oid in revwalk { + if stop_rx.try_recv().is_ok() { + break; + } + + match oid.and_then(|oid| repo.find_commit(oid)) { + Ok(commit) => { + let commit: Commit = commit_from_git2(&commit); + if tx_data.blocking_send(Ok(commit)).is_err() { + break; + } + } + Err(e) => { + if tx_data + .blocking_send(Err(anyhow::anyhow!("Failed to get commit: {}", e))) + .is_err() + { + break; + } + } + } + } + Ok(()) + }) + .await; + + if let Err(e) = result { + tx.send(Err(anyhow::anyhow!("Task failed: {}", e))) + .await + .ok(); + } + } + }); + + let s = stream! { + while let Some(result) = rx.recv().await { + yield result; + } + } + .boxed(); + + (s, stop_tx) +} diff --git a/crates/tabby-git/src/file_search.rs b/crates/tabby-git/src/file_search.rs new file mode 100644 index 000000000000..c9419f289b76 --- /dev/null +++ b/crates/tabby-git/src/file_search.rs @@ -0,0 +1,169 @@ +use std::path::PathBuf; + +use async_stream::stream; +use futures::{Stream, StreamExt}; + +use super::rev_to_commit; +use crate::bytes2path; + +pub struct GitFileSearch { + pub r#type: &'static str, + pub path: String, + + /// matched indices for fuzzy search query. + pub indices: Vec, +} + +impl GitFileSearch { + fn new(r#type: &'static str, path: String, indices: Vec) -> Self { + Self { + r#type, + path, + indices: indices.into_iter().map(|i| i as i32).collect(), + } + } +} + +fn walk( + repository: git2::Repository, + rev: Option<&str>, + tx: tokio::sync::mpsc::Sender<(bool, PathBuf)>, +) -> anyhow::Result<()> { + use std::collections::VecDeque; + + let commit = rev_to_commit(&repository, rev)?; + let tree = commit.tree()?; + + // Initialize queue with root tree + let mut queue = VecDeque::new(); + queue.push_back(("".to_string(), tree)); + + while let Some((current_path, current_tree)) = queue.pop_front() { + // Process all entries in current directory + for entry in current_tree.iter() { + let entry_name = bytes2path(entry.name_bytes()); + let path = if current_path.is_empty() { + PathBuf::from(&entry_name) + } else { + PathBuf::from(¤t_path).join(entry_name) + }; + + match entry.kind() { + Some(git2::ObjectType::Blob) => { + tx.blocking_send((true, path)) + .map_err(|_| anyhow::anyhow!("Failed to send path to channel"))?; + } + Some(git2::ObjectType::Tree) => { + tx.blocking_send((false, path.clone())) + .map_err(|_| anyhow::anyhow!("Failed to send path to channel"))?; + + if let Ok(obj) = entry.to_object(&repository) { + if let Ok(subtree) = obj.peel_to_tree() { + queue.push_back((path.display().to_string(), subtree)); + } + } + } + _ => continue, + } + } + } + + Ok(()) +} + +async fn walk_stream( + repository: git2::Repository, + rev: Option<&str>, +) -> impl Stream { + let (tx, mut rx) = tokio::sync::mpsc::channel(1); + + let rev = rev.map(|s| s.to_owned()); + let task = tokio::task::spawn_blocking(move || walk(repository, rev.as_deref(), tx)); + + stream! { + while let Some(value) = rx.recv().await { + yield value; + } + + let _ = task.await; + } +} + +pub async fn search( + repository: git2::Repository, + rev: Option<&str>, + pattern: &str, + limit: usize, +) -> anyhow::Result> { + let mut scored_entries: Vec<(_, _)> = stream! { + let mut nucleo = nucleo::Matcher::new(nucleo::Config::DEFAULT.match_paths()); + let needle = nucleo::pattern::Pattern::new( + pattern, + nucleo::pattern::CaseMatching::Ignore, + nucleo::pattern::Normalization::Smart, + nucleo::pattern::AtomKind::Fuzzy, + ); + + for await (is_file, basepath) in walk_stream(repository, rev).await { + let r#type = if is_file { "file" } else { "dir" }; + let basepath = basepath.display().to_string(); + let haystack: nucleo::Utf32String = basepath.clone().into(); + let mut indices = Vec::new(); + let score = needle.indices(haystack.slice(..), &mut nucleo, &mut indices); + if let Some(score) = score { + yield (score, GitFileSearch::new(r#type, basepath, indices)); + } + } + } + // Ensure there's at least 1000 entries with scores > 0 for quality. + .take(1000) + .collect() + .await; + + scored_entries.sort_by_key(|x| -(x.0 as i32)); + let entries = scored_entries + .into_iter() + .map(|x| x.1) + .take(limit) + .collect(); + + Ok(entries) +} + +pub async fn list( + repository: git2::Repository, + rev: Option<&str>, + limit: Option, +) -> anyhow::Result<(Vec, bool)> { + let entries: Vec = stream! { + for await (is_file, basepath) in walk_stream(repository, rev).await { + let r#type = if is_file { "file" } else { "dir" }; + let basepath = basepath.display().to_string(); + yield GitFileSearch::new(r#type, basepath, Vec::new()); + } + } + .take(limit.unwrap_or(usize::MAX)) + .collect() + .await; + + let truncated = limit.map(|l| entries.len() >= l).unwrap_or(false); + Ok((entries, truncated)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::testutils::TempGitRepository; + + #[tokio::test] + async fn it_search() { + let root = TempGitRepository::default(); + + let result = search(root.repository(), None, "moonscript_lora md", 5) + .await + .unwrap(); + assert_eq!(result.len(), 1); + assert_eq!(result[0].r#type, "file"); + assert_eq!(result[0].path, "201_lm_moonscript_lora/README.md"); + } +} diff --git a/crates/tabby-git/src/grep/mod.rs b/crates/tabby-git/src/grep/mod.rs new file mode 100644 index 000000000000..3fdecd911187 --- /dev/null +++ b/crates/tabby-git/src/grep/mod.rs @@ -0,0 +1,207 @@ +mod output; +mod query; +mod searcher; + +use std::path::PathBuf; + +use anyhow::Context; +use async_stream::stream; +use futures::Stream; +use git2::TreeWalkResult; +pub use query::GrepQuery; +use searcher::GrepSearcher; +use tracing::{debug, warn}; + +use super::{bytes2path, rev_to_commit}; + +pub struct GrepFile { + pub path: PathBuf, + pub lines: Vec, +} + +pub struct GrepLine { + /// Content of the line. + pub line: GrepTextOrBase64, + + /// Byte offset in the file to the start of the line. + pub byte_offset: usize, + + /// Line number in the file, starting from 1. + pub line_number: usize, + + /// The matches in the line. + pub sub_matches: Vec, +} + +pub enum GrepTextOrBase64 { + Text(String), + Base64(Vec), +} + +pub struct GrepSubMatch { + // Byte offsets in the line + pub bytes_start: usize, + pub bytes_end: usize, +} + +pub fn grep( + repository: git2::Repository, + rev: Option<&str>, + query: &GrepQuery, +) -> anyhow::Result> { + let (tx, mut rx) = tokio::sync::mpsc::channel(1); + + let rev = rev.map(|s| s.to_owned()); + let query = query.clone(); + debug!("{:?}", query); + let searcher = query.searcher()?; + let task = + tokio::task::spawn_blocking(move || grep_impl(repository, rev.as_deref(), searcher, tx)); + + Ok(stream! { + while let Some(file) = rx.recv().await { + yield file; + } + + if let Err(err) = task.await { + warn!("Error grepping repository: {}", err); + } + }) +} + +fn grep_impl( + repository: git2::Repository, + rev: Option<&str>, + mut searcher: GrepSearcher, + tx: tokio::sync::mpsc::Sender, +) -> anyhow::Result<()> { + let commit = rev_to_commit(&repository, rev)?; + let tree = commit.tree()?; + + tree.walk(git2::TreeWalkMode::PreOrder, |path, entry| { + // Skip non-blob entries + if entry.kind() != Some(git2::ObjectType::Blob) { + return TreeWalkResult::Ok; + } + + match grep_file(&repository, &mut searcher, path, entry, tx.clone()) { + Ok(()) => {} + Err(e) => { + warn!("Error grepping file: {}", e); + } + } + TreeWalkResult::Ok + })?; + Ok(()) +} + +fn grep_file( + repository: &git2::Repository, + searcher: &mut GrepSearcher, + path: &str, + entry: &git2::TreeEntry, + tx: tokio::sync::mpsc::Sender, +) -> anyhow::Result<()> { + let object = entry.to_object(repository)?; + let content = object.as_blob().context("Not a blob")?.content(); + + let path = PathBuf::from(path).join(bytes2path(entry.name_bytes())); + + let mut output = output::GrepOutput::new(path.clone(), tx.clone()); + searcher.search(content, &mut output)?; + output.flush( + searcher.require_file_match, + searcher.require_content_match, + content, + ); + + Ok(()) +} +#[cfg(test)] +mod tests { + use futures::StreamExt; + + use super::*; + use crate::testutils::TempGitRepository; + + #[tokio::test] + async fn test_grep() { + let root = TempGitRepository::default(); + let query = GrepQuery::builder().pattern("crosscodeeval_data").build(); + + let files: Vec<_> = grep(root.repository(), None, &query) + .unwrap() + .collect() + .await; + assert_eq!(files.len(), 1); + assert_eq!(files[0].path, PathBuf::from("203_llm_evaluation/README.md")); + + let query = GrepQuery::builder().pattern("ideas").build(); + let files: Vec<_> = grep(root.repository(), None, &query) + .unwrap() + .collect() + .await; + assert_eq!(files.len(), 1); + assert_eq!(files[0].path, PathBuf::from("README.md")); + + let query = GrepQuery::builder() + .file_type("markdown") + .file_pattern("llm_evaluation") + .build(); + let files: Vec<_> = grep(root.repository(), None, &query) + .unwrap() + .collect() + .await; + assert_eq!(files.len(), 1); + assert_eq!(files[0].path, PathBuf::from("203_llm_evaluation/README.md")); + + // File patterns are AND-ed. + let query = GrepQuery::builder() + .file_pattern(".md") + .file_pattern("llm_evaluation") + .build(); + let files: Vec<_> = grep(root.repository(), None, &query) + .unwrap() + .collect() + .await; + assert_eq!(files.len(), 1); + assert_eq!(files[0].path, PathBuf::from("203_llm_evaluation/README.md")); + + // When positive condition provided, return nothing if no matches. + let query = GrepQuery::builder() + .file_type("markdown") + .pattern("non_exist_pattern") + .build(); + let files: Vec<_> = grep(root.repository(), None, &query) + .unwrap() + .collect() + .await; + assert_eq!(files.len(), 0); + + // When no positive condition provided, all + // files not matching negative conditions should be returned. + let query = GrepQuery::builder() + .negative_file_type("rust") + .negative_pattern("non_exist_pattern") + .build(); + let files: Vec<_> = grep(root.repository(), None, &query) + .unwrap() + .collect() + .await; + assert_eq!(files.len(), 9); + + let query = GrepQuery::builder() + .pattern("non_exist_pattern") + .negative_file_pattern("non_exist_pattern") + .negative_pattern("ideas") + .build(); + let files: Vec<_> = grep(root.repository(), None, &query) + .unwrap() + .collect() + .await; + assert_eq!(files.len(), 0); + + let query = GrepQuery::builder().build(); + assert!(grep(root.repository(), None, &query).is_err()); + } +} diff --git a/crates/tabby-git/src/grep/output.rs b/crates/tabby-git/src/grep/output.rs new file mode 100644 index 000000000000..a4c4c66b9499 --- /dev/null +++ b/crates/tabby-git/src/grep/output.rs @@ -0,0 +1,215 @@ +use std::{ + io::BufRead, + path::{Path, PathBuf}, +}; + +use grep::{matcher::Matcher, regex::RegexMatcher, searcher::Sink}; +use tracing::debug; + +use super::{GrepFile, GrepLine, GrepSubMatch, GrepTextOrBase64}; + +pub struct GrepOutput { + path: PathBuf, + lines: Vec, + + tx: tokio::sync::mpsc::Sender, + + content_matched: bool, + content_negated: bool, + + pub file_matched: bool, + pub file_negated: bool, +} + +impl std::fmt::Debug for GrepOutput { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("GrepOutput") + .field("content_matched", &self.content_matched) + .field("content_negated", &self.content_negated) + .field("file_matched", &self.file_matched) + .field("file_negated", &self.file_negated) + .finish() + } +} + +impl GrepOutput { + pub fn new(path: PathBuf, tx: tokio::sync::mpsc::Sender) -> Self { + Self { + path: path.to_owned(), + lines: Vec::new(), + tx, + + file_matched: false, + file_negated: false, + + content_matched: false, + content_negated: false, + } + } + + pub fn path(&self) -> &Path { + &self.path + } + + pub fn sink<'output, 'a>( + &'output mut self, + matcher: &'a RegexMatcher, + ) -> GrepMatchSink<'output, 'a> { + GrepMatchSink { + output: self, + matcher, + } + } + + pub fn negative_sink(&mut self) -> GrepNegativeMatchSink<'_> { + GrepNegativeMatchSink { output: self } + } + + fn record(&mut self, line: GrepLine) { + self.lines.push(line); + } + + pub fn flush(&mut self, require_file_match: bool, require_content_match: bool, content: &[u8]) { + // If file or content is negated, we don't want to send the file. + if self.file_negated || self.content_negated { + return; + } + + if require_file_match && !self.file_matched { + return; + } + + if require_content_match && !self.content_matched { + return; + } + + let lines = if self.content_matched { + std::mem::take(&mut self.lines) + } else { + match read_lines(content) { + Ok(lines) => lines, + Err(e) => { + debug!("Failed to read file: {:?}", e); + vec![] + } + } + }; + + let file = GrepFile { + path: self.path.clone(), + lines, + }; + + match self.tx.blocking_send(file) { + Ok(_) => {} + Err(_) => { + // Request got cancelled, we can safely ignore the error. + } + } + } +} + +fn read_lines(content: &[u8]) -> anyhow::Result> { + let reader = std::io::BufReader::new(content); + let line_reader = reader.lines().take(5); + + let mut lines = vec![]; + let mut line_number = 1; + let mut byte_offset = 0; + for line in line_reader { + let line = line? + "\n"; + let bytes_length = line.len(); + lines.push(GrepLine { + line: GrepTextOrBase64::Text(line), + byte_offset, + line_number, + sub_matches: vec![], + }); + + byte_offset += bytes_length; + line_number += 1; + } + + Ok(lines) +} + +pub struct GrepMatchSink<'output, 'a> { + output: &'output mut GrepOutput, + matcher: &'a RegexMatcher, +} + +impl Sink for GrepMatchSink<'_, '_> { + type Error = std::io::Error; + + fn matched( + &mut self, + _searcher: &grep::searcher::Searcher, + mat: &grep::searcher::SinkMatch<'_>, + ) -> Result { + self.output.content_matched = true; + + // 1. Search is always done in single-line mode. + let line = mat.lines().next().expect("Have at least one line"); + + // 2. Collect all matches in the line. + let mut matches: Vec = vec![]; + self.matcher.find_iter(line, |m| { + matches.push(GrepSubMatch { + bytes_start: m.start(), + bytes_end: m.end(), + }); + true + })?; + + let line = GrepTextOrBase64::Base64(line.to_owned()); + + // 3. Create a GrepLine object and add it to the file. + let line = GrepLine { + line, + byte_offset: mat.absolute_byte_offset() as usize, + line_number: mat.line_number().expect("Have line number") as usize, + sub_matches: matches, + }; + + self.output.record(line); + Ok(true) + } + + fn context( + &mut self, + _searcher: &grep::searcher::Searcher, + context: &grep::searcher::SinkContext<'_>, + ) -> Result { + let line = context.bytes(); + + let line = match std::str::from_utf8(line) { + Ok(s) => GrepTextOrBase64::Text(s.to_owned()), + Err(_) => GrepTextOrBase64::Base64(line.to_owned()), + }; + + self.output.record(GrepLine { + line, + byte_offset: context.absolute_byte_offset() as usize, + line_number: context.line_number().expect("Have line number") as usize, + sub_matches: vec![], + }); + Ok(true) + } +} + +pub struct GrepNegativeMatchSink<'output> { + output: &'output mut GrepOutput, +} + +impl Sink for GrepNegativeMatchSink<'_> { + type Error = std::io::Error; + + fn matched( + &mut self, + _searcher: &grep::searcher::Searcher, + _mat: &grep::searcher::SinkMatch<'_>, + ) -> Result { + self.output.content_negated = true; + Ok(false) + } +} diff --git a/crates/tabby-git/src/grep/query.rs b/crates/tabby-git/src/grep/query.rs new file mode 100644 index 000000000000..191d6d0d4413 --- /dev/null +++ b/crates/tabby-git/src/grep/query.rs @@ -0,0 +1,296 @@ +use std::str::FromStr; + +use anyhow::bail; +use grep::{ + regex::{RegexMatcher, RegexMatcherBuilder}, + searcher::{BinaryDetection, SearcherBuilder}, +}; +use ignore::types::TypesBuilder; + +use super::searcher::GrepSearcher; + +#[derive(Default, Clone, Debug)] +pub struct GrepQuery { + patterns: Vec, + negative_patterns: Vec, + + file_patterns: Vec, + negative_file_patterns: Vec, + + file_types: Vec, + negative_file_types: Vec, +} + +impl GrepQuery { + #[cfg(test)] + pub fn builder() -> GrepQueryBuilder { + GrepQueryBuilder::default() + } + + pub fn searcher(&self) -> anyhow::Result { + let pattern_matcher = if self.patterns.is_empty() { + None + } else { + let pattern = self.patterns.join("|"); + let case_insensitive = !has_uppercase_literal(&pattern); + Some( + RegexMatcherBuilder::new() + .case_insensitive(case_insensitive) + .line_terminator(Some(b'\n')) + .build(&self.patterns.join("|"))?, + ) + }; + + let negative_pattern_matcher = if self.negative_patterns.is_empty() { + None + } else { + Some(RegexMatcher::new_line_matcher( + &self.negative_patterns.join("|"), + )?) + }; + + let file_pattern_matcher = if self.file_patterns.is_empty() { + vec![] + } else { + let mut matchers = vec![]; + for p in &self.file_patterns { + let case_insensitive = !has_uppercase_literal(p); + let matcher = RegexMatcherBuilder::new() + .case_insensitive(case_insensitive) + .line_terminator(Some(b'\n')) + .build(p)?; + matchers.push(matcher); + } + matchers + }; + + let negative_file_pattern_matcher = if self.negative_file_patterns.is_empty() { + None + } else { + Some(RegexMatcher::new_line_matcher( + &self.negative_file_patterns.join("|"), + )?) + }; + + if pattern_matcher.is_none() + && negative_pattern_matcher.is_none() + && file_pattern_matcher.is_empty() + && negative_file_pattern_matcher.is_none() + && self.file_types.is_empty() + && self.negative_file_types.is_empty() + { + bail!("No patterns specified") + } + + let file_type_matcher = if self.file_types.is_empty() && self.negative_file_types.is_empty() + { + None + } else { + let mut types_builder = TypesBuilder::new(); + types_builder.add_defaults(); + for file_type in &self.file_types { + types_builder.select(file_type); + } + for file_type in &self.negative_file_types { + types_builder.negate(file_type); + } + + Some(types_builder.build()?) + }; + + let searcher = SearcherBuilder::new() + .binary_detection(BinaryDetection::quit(b'\x00')) + .before_context(3) + .line_number(true) + .after_context(3) + .build(); + + Ok(GrepSearcher::new( + !self.file_patterns.is_empty() || !self.file_types.is_empty(), + !self.patterns.is_empty(), + pattern_matcher, + negative_pattern_matcher, + file_pattern_matcher, + negative_file_pattern_matcher, + file_type_matcher, + searcher, + )) + } +} + +impl FromStr for GrepQuery { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let mut builder = GrepQueryBuilder::default(); + for (negative, part) in tokenize_query(s) { + if negative { + match part { + _ if part.starts_with("lang:") => { + builder = builder.negative_file_type(&part[5..]) + } + _ if part.starts_with("f:") => { + builder = builder.negative_file_pattern(&part[2..]) + } + _ => builder = builder.negative_pattern(part), + } + } else { + match part { + _ if part.starts_with("lang:") => builder = builder.file_type(&part[5..]), + _ if part.starts_with("f:") => builder = builder.file_pattern(&part[2..]), + _ => builder = builder.pattern(part), + } + } + } + + Ok(builder.build()) + } +} + +#[derive(Default)] +pub struct GrepQueryBuilder { + query: GrepQuery, +} + +impl GrepQueryBuilder { + pub fn pattern>(mut self, pattern: T) -> Self { + let pattern = pattern.into(); + if !pattern.is_empty() { + self.query.patterns.push(pattern); + } + self + } + + pub fn negative_pattern>(mut self, pattern: T) -> Self { + let pattern = pattern.into(); + if !pattern.is_empty() { + self.query.negative_patterns.push(pattern); + } + self + } + + pub fn file_pattern>(mut self, pattern: T) -> Self { + let pattern = pattern.into(); + if !pattern.is_empty() { + self.query.file_patterns.push(pattern); + } + self + } + + pub fn negative_file_pattern>(mut self, pattern: T) -> Self { + let pattern = pattern.into(); + if !pattern.is_empty() { + self.query.negative_file_patterns.push(pattern); + } + self + } + + pub fn file_type>(mut self, file_type: T) -> Self { + self.query.file_types.push(file_type.into()); + self + } + + pub fn negative_file_type>(mut self, file_type: T) -> Self { + self.query.negative_file_types.push(file_type.into()); + self + } + + pub fn build(self) -> GrepQuery { + self.query + } +} + +fn has_uppercase_literal(expr: &str) -> bool { + expr.chars().any(|c| c.is_ascii_uppercase()) +} + +/// Tokenize a query string, and respectes quoted strings. +/// When a token is prefixed with a `-`, it is considered a negative pattern. +/// +/// Quote characters can be escaped with a backslash. +/// Returns the list of tokens, and whether they are negative patterns. +fn tokenize_query(query: &str) -> Vec<(bool, String)> { + let mut tokens = vec![]; + let mut current = String::new(); + let mut negative = false; + let mut quoted = false; + let mut escaped = false; + + for c in query.chars() { + if escaped { + current.push(c); + escaped = false; + continue; + } + + match c { + ' ' if !quoted => { + if !current.is_empty() { + tokens.push((negative, current.clone())); + current.clear(); + negative = false; + } + } + '-' if !quoted => { + if !current.is_empty() { + tokens.push((negative, current.clone())); + current.clear(); + } + negative = true; + } + '"' => { + if quoted { + tokens.push((negative, current.clone())); + current.clear(); + } + quoted = !quoted; + } + '\\' => { + escaped = true; + } + _ => { + current.push(c); + } + } + } + + if !current.is_empty() { + tokens.push((negative, current)); + } + + tokens +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_query() { + let query: GrepQuery = "lang:rust -f:*.rs foo bar -baz".parse().unwrap(); + assert_eq!(query.patterns, vec!["foo", "bar"]); + assert_eq!(query.negative_patterns, vec!["baz"]); + assert_eq!(query.negative_file_patterns, vec!["*.rs"]); + assert_eq!(query.file_types, vec!["rust"]); + } + + #[test] + fn test_tokenize_query() { + let query = r#"lang:rust -f:*.rs foo bar -baz "qux quux", -"corge grault" "\"abc\" dd""#; + let tokens = tokenize_query(query); + assert_eq!( + tokens, + vec![ + (false, "lang:rust".to_owned()), + (true, "f:*.rs".to_owned()), + (false, "foo".to_owned()), + (false, "bar".to_owned()), + (true, "baz".to_owned()), + (false, "qux quux".to_owned()), + (false, ",".to_owned()), + (true, "corge grault".to_owned()), + (true, "\"abc\" dd".to_owned()) + ] + ); + } +} diff --git a/crates/tabby-git/src/grep/searcher.rs b/crates/tabby-git/src/grep/searcher.rs new file mode 100644 index 000000000000..fc1e749baab4 --- /dev/null +++ b/crates/tabby-git/src/grep/searcher.rs @@ -0,0 +1,109 @@ +use std::path::Path; + +use grep::{matcher::Matcher, regex::RegexMatcher}; +use ignore::types::Types; + +use super::output::GrepOutput; + +pub struct GrepSearcher { + pub require_file_match: bool, + pub require_content_match: bool, + + pattern_matcher: Option, + negative_pattern_matcher: Option, + + file_pattern_matcher: Vec, + negative_file_pattern_matcher: Option, + + file_type_matcher: Option, + searcher: grep::searcher::Searcher, +} + +pub enum GrepFileMatch { + NoPattern, + Matched, + NotMatched, +} + +impl GrepSearcher { + pub fn new( + require_file_match: bool, + require_content_match: bool, + pattern_matcher: Option, + negative_pattern_matcher: Option, + file_pattern_matcher: Vec, + negative_file_pattern_matcher: Option, + file_type_matcher: Option, + searcher: grep::searcher::Searcher, + ) -> Self { + Self { + require_file_match, + require_content_match, + pattern_matcher, + negative_pattern_matcher, + file_pattern_matcher, + negative_file_pattern_matcher, + file_type_matcher, + searcher, + } + } + + fn file_matched(&self, path: &Path) -> anyhow::Result { + let path_bytes = path.display().to_string().into_bytes(); + if let Some(ref matcher) = self.negative_file_pattern_matcher { + if matcher.is_match(&path_bytes)? { + return Ok(GrepFileMatch::NotMatched); + } + } + + let mut matched = GrepFileMatch::NoPattern; + if let Some(ref file_type_matcher) = self.file_type_matcher { + match file_type_matcher.matched(path, false) { + ignore::Match::None => { + // Do nothing. + } + ignore::Match::Ignore(_) => { + return Ok(GrepFileMatch::NotMatched); + } + ignore::Match::Whitelist(_glob) => { + matched = GrepFileMatch::Matched; + } + }; + }; + + for matcher in &self.file_pattern_matcher { + if matcher.is_match(&path_bytes)? { + matched = GrepFileMatch::Matched; + } else { + matched = GrepFileMatch::NotMatched; + break; + } + } + + Ok(matched) + } + + pub fn search(&mut self, content: &[u8], output: &mut GrepOutput) -> anyhow::Result<()> { + let file_matched = self.file_matched(output.path())?; + if let GrepFileMatch::NotMatched = file_matched { + output.file_negated = true; + return Ok(()); + } + + if let GrepFileMatch::Matched = file_matched { + output.file_matched = true; + } + + if let Some(ref matcher) = self.pattern_matcher { + self.searcher + .search_reader(matcher, content, output.sink(matcher))?; + }; + + if let Some(ref matcher) = self.negative_pattern_matcher { + self.searcher + .search_reader(matcher, content, output.negative_sink())?; + } + + Ok(()) + } +} diff --git a/crates/tabby-git/src/lib.rs b/crates/tabby-git/src/lib.rs new file mode 100644 index 000000000000..affab71ea926 --- /dev/null +++ b/crates/tabby-git/src/lib.rs @@ -0,0 +1,233 @@ +mod commit; +mod file_search; +mod grep; +mod serve_git; + +use std::{fs, path::Path, process::Command}; + +use anyhow::bail; +use axum::{ + body::Body, + http::{Response, StatusCode}, +}; +pub use commit::{stream_commits, Commit}; +use file_search::GitFileSearch; +use futures::Stream; +pub use grep::{GrepFile, GrepLine, GrepSubMatch, GrepTextOrBase64}; +use tracing::warn; + +pub async fn search_files( + root: &Path, + rev: Option<&str>, + pattern: &str, + limit: usize, +) -> anyhow::Result> { + file_search::search(git2::Repository::open(root)?, rev, pattern, limit).await +} + +pub struct ListFile { + pub files: Vec, + pub truncated: bool, +} + +pub async fn list_files( + root: &Path, + rev: Option<&str>, + limit: Option, +) -> anyhow::Result { + let (files, truncated) = file_search::list(git2::Repository::open(root)?, rev, limit).await?; + Ok(ListFile { files, truncated }) +} + +pub async fn grep( + root: &Path, + rev: Option<&str>, + query: &str, +) -> anyhow::Result> { + let repository = git2::Repository::open(root)?; + let query: grep::GrepQuery = query.parse()?; + grep::grep(repository, rev, &query) +} + +pub fn serve_file( + root: &Path, + commit: Option<&str>, + path: Option<&str>, +) -> std::result::Result, StatusCode> { + let repository = git2::Repository::open(root).map_err(|_| StatusCode::NOT_FOUND)?; + serve_git::serve(&repository, commit, path) +} + +#[derive(Debug)] +pub struct GitReference { + pub name: String, + pub commit: String, +} + +pub fn list_refs(root: &Path) -> anyhow::Result> { + let repository = git2::Repository::open(root)?; + let refs = repository.references()?; + Ok(refs + .filter_map(|r| r.ok()) + .filter_map(|r| { + let name = r.name()?.to_string(); + let commit = r.target()?.to_string(); + Some(GitReference { name, commit }) + }) + // Filter out remote refs + .filter(|r| !r.name.starts_with("refs/remotes/")) + .collect()) +} + +pub fn get_head_name(root: &Path) -> anyhow::Result { + let repository = git2::Repository::open(root)?; + let head = repository.head()?; + let name = head.name().ok_or(anyhow::anyhow!("HEAD has no name"))?; + Ok(name.to_string()) +} + +pub fn sync_refs(root: &Path, url: &str, refs: &Vec) -> anyhow::Result<()> { + if !root.exists() { + fs::create_dir_all(root)?; + let status = Command::new("git") + .current_dir(root.parent().expect("Must not be in root directory")) + .arg("clone") + .arg(url) + .arg(root) + .status()?; + + if let Some(code) = status.code() { + if code != 0 { + warn!( + "Failed to clone `{}`. Please check your repository configuration.", + url + ); + fs::remove_dir_all(root).expect("Failed to remove directory"); + + bail!("Failed to clone `{}`", url); + } + } + } + + for ref_name in refs { + let branch = if let Some(branch) = ref_name.strip_prefix("refs/heads/") { + branch + } else if let Some(tag) = ref_name.strip_prefix("refs/tags/") { + tag + } else { + ref_name + }; + + // get the current branch name without refs/ prefix + + let output = Command::new("git") + .current_dir(root) + .arg("symbolic-ref") + .arg("--short") + .arg("HEAD") + .output() + .ok(); + + let current_branch = output + .filter(|o| o.status.success()) + .and_then(|o| String::from_utf8(o.stdout).ok()) + .map(|s| s.trim().to_string()); + + let status = if current_branch.as_deref() == Some(branch) { + Command::new("git") + .current_dir(root) + .arg("pull") + .arg("origin") + .arg(branch) + .status()? + } else { + // Use `git fetch origin +ref:ref` to create or update the local branch from the remote. + // The + ensures that the local branch is updated (forced) even if it's not a fast-forward, + // and it creates the branch if it doesn't exist locally. + Command::new("git") + .current_dir(root) + .arg("fetch") + .arg("origin") + .arg(format!("+{branch}:{branch}")) + .status()? + }; + if !status.success() { + return Err(anyhow::anyhow!("Failed to fetch origin {}", branch)); + } + } + + Ok(()) +} + +fn rev_to_commit<'a>( + repository: &'a git2::Repository, + rev: Option<&str>, +) -> anyhow::Result> { + let commit = match rev { + Some(rev) => repository.revparse_single(rev)?.peel_to_commit()?, + None => repository.head()?.peel_to_commit()?, + }; + Ok(commit) +} + +#[cfg(unix)] +pub fn bytes2path(b: &[u8]) -> &Path { + use std::os::unix::prelude::*; + Path::new(std::ffi::OsStr::from_bytes(b)) +} +#[cfg(windows)] +pub fn bytes2path(b: &[u8]) -> &Path { + use std::str; + Path::new(str::from_utf8(b).unwrap()) +} + +#[cfg(test)] +mod testutils { + use std::process::{Command, Stdio}; + + use temp_testdir::TempDir; + + pub struct TempGitRepository { + tempdir: TempDir, + } + + impl TempGitRepository { + pub fn repository(&self) -> git2::Repository { + git2::Repository::open(self.path()).unwrap() + } + + pub fn path(&self) -> std::path::PathBuf { + self.tempdir.join("interview-questions") + } + } + + impl Default for TempGitRepository { + fn default() -> Self { + let tempdir = TempDir::default(); + + Command::new("git") + .current_dir(&tempdir) + .arg("clone") + .args(["--depth", "1"]) + .arg("https://github.com/TabbyML/interview-questions") + .stderr(Stdio::null()) + .stdout(Stdio::null()) + .status() + .unwrap(); + + Self { tempdir } + } + } +} + +#[cfg(test)] +mod tests { + use crate::{list_refs, testutils::TempGitRepository}; + + #[test] + fn test_list_refs() { + let root = TempGitRepository::default(); + let refs = list_refs(&root.path()).unwrap(); + assert_eq!(refs.len(), 1); + } +} diff --git a/crates/tabby-git/src/serve_git.rs b/crates/tabby-git/src/serve_git.rs new file mode 100644 index 000000000000..ba1fbbda93c1 --- /dev/null +++ b/crates/tabby-git/src/serve_git.rs @@ -0,0 +1,167 @@ +use std::path::Path; + +use anyhow::{bail, Context}; +use axum::{ + body::Body, + http::{header, StatusCode}, + response::Response, +}; +use git2::{AttrCheckFlags, Blob}; +use mime_guess::Mime; +use serde::Serialize; + +use super::rev_to_commit; + +const DIRECTORY_MIME_TYPE: &str = "application/vnd.directory+json"; + +fn resolve<'a>( + repository: &'a git2::Repository, + rev: Option<&str>, + relpath_str: Option<&str>, +) -> anyhow::Result> { + let commit = rev_to_commit(repository, rev)?; + let tree = commit.tree()?; + + let relpath = Path::new(relpath_str.unwrap_or("")); + let object = if relpath_str.is_some() { + tree.get_path(relpath)?.to_object(repository)? + } else { + tree.as_object().clone() + }; + + match object.kind() { + Some(git2::ObjectType::Blob) => { + let filter = repository.get_attr( + relpath, + "filter", + AttrCheckFlags::INDEX_ONLY | AttrCheckFlags::NO_SYSTEM, + )?; + + let blob = object.as_blob().context("failed to resolve blob")?; + if filter == Some("lfs") { + Ok(Resolve::File( + "text/plain".parse().expect("failed to parse mime"), + blob.clone(), + )) + } else { + let mime = mime_guess::from_path(relpath).first_or_octet_stream(); + Ok(Resolve::File(mime, blob.clone())) + } + } + Some(git2::ObjectType::Tree) => Ok(Resolve::Dir( + object + .as_tree() + .context("failed to resolve tree")? + .iter() + .map(|entry| { + let kind = if entry.kind() == Some(git2::ObjectType::Tree) { + DirEntryKind::Dir + } else { + DirEntryKind::File + }; + DirEntry { + kind, + basename: relpath + .join(entry.name().expect("failed to resolve entry name")) + .display() + .to_string() + .replace("\\", "/"), + } + }) + .collect::>(), + )), + _ => { + bail!("unsupported object type"); + } + } +} + +pub fn serve( + repository: &git2::Repository, + rev: Option<&str>, + relpath: Option<&str>, +) -> std::result::Result, StatusCode> { + let resolve = match resolve(repository, rev, relpath) { + Ok(resolve) => resolve, + Err(_) => { + return Err(StatusCode::NOT_FOUND); + } + }; + + let resp = match resolve { + Resolve::Dir(entries) => { + let json = + serde_json::to_string(&ListDir { entries }).expect("failed to serialize response"); + Response::builder() + .header(header::CONTENT_TYPE, DIRECTORY_MIME_TYPE) + .body(Body::from(json)) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + } + Resolve::File(mime, blob) => { + let body = Body::from(blob.content().to_owned()); + Response::builder() + .header(header::CONTENT_TYPE, mime.as_ref()) + .body(body) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + } + }; + + Ok(resp) +} + +#[derive(Debug)] +pub enum Resolve<'a> { + Dir(Vec), + File(Mime, Blob<'a>), +} + +#[derive(Serialize)] +struct ListDir { + entries: Vec, +} + +#[derive(Serialize, Debug)] +#[serde(rename_all = "lowercase")] +pub enum DirEntryKind { + File, + Dir, +} + +#[derive(Serialize, Debug)] +pub struct DirEntry { + kind: DirEntryKind, + basename: String, +} + +#[cfg(test)] +mod tests { + use assert_matches::assert_matches; + + use super::*; + use crate::testutils::TempGitRepository; + + #[test] + fn test_resolve() { + let root = TempGitRepository::default(); + let repo = root.repository(); + + assert_matches!(resolve(&repo, None, None), Ok(Resolve::Dir(_))); + assert_matches!( + resolve(&repo, None, Some("README.md")), + Ok(Resolve::File(_, _)) + ); + } + + #[test] + fn test_serve() { + let root = TempGitRepository::default(); + let repo = root.repository(); + + assert_matches!(serve(&repo, None, None), Ok(_)); + assert_matches!(serve(&repo, None, Some("README.md")), Ok(_)); + assert_matches!( + serve(&repo, None, Some("NotExists")), + Err(StatusCode::NOT_FOUND) + ); + } +} diff --git a/crates/tabby-index-cli/Cargo.toml b/crates/tabby-index-cli/Cargo.toml new file mode 100644 index 000000000000..30cc341d930f --- /dev/null +++ b/crates/tabby-index-cli/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "tabby-index-cli" +version.workspace = true +edition.workspace = true +authors.workspace = true +homepage.workspace = true + +[dependencies] +clap = { workspace = true, features = ["derive"]} +tantivy.workspace = true +tabby-common = { path = "../tabby-common" } +anyhow.workspace = true +serde.workspace = true +serde_json.workspace = true diff --git a/crates/tabby-index-cli/README.md b/crates/tabby-index-cli/README.md new file mode 100644 index 000000000000..54a6cf3d815f --- /dev/null +++ b/crates/tabby-index-cli/README.md @@ -0,0 +1,3 @@ +# tabby-index-cli + +A cli tool to help debugging tabby's index, deriving from https://github.com/quickwit-oss/tantivy-cli \ No newline at end of file diff --git a/crates/tabby-index-cli/assets/queries.txt b/crates/tabby-index-cli/assets/queries.txt new file mode 100644 index 000000000000..88b7c851329c --- /dev/null +++ b/crates/tabby-index-cli/assets/queries.txt @@ -0,0 +1 @@ +chunk_tokens:embedding_one_1 AND chunk_tokens:embedding_zero_2 \ No newline at end of file diff --git a/crates/tabby-index-cli/src/commands/bench.rs b/crates/tabby-index-cli/src/commands/bench.rs new file mode 100644 index 000000000000..753771b39f31 --- /dev/null +++ b/crates/tabby-index-cli/src/commands/bench.rs @@ -0,0 +1,108 @@ +use std::{ + fs::File, + io, + io::{BufRead, BufReader}, + path::{Path, PathBuf}, +}; + +use clap::Args; +use tantivy::{ + collector::{Count, TopDocs}, + query::QueryParser, + schema::{Field, Schema}, + Index, TantivyDocument, +}; + +use crate::timer::TimerTree; + +#[derive(Args)] +pub struct BenchArgs { + /// Path to the queries file + #[clap(short, long)] + queries: PathBuf, + + /// Number of times to repeat the benchmark + #[clap(short, long, default_value = "1")] + num_repeat: usize, +} + +pub fn run_bench_cli(index_path: &Path, args: &BenchArgs) -> Result<(), String> { + run_bench(index_path, &args.queries, args.num_repeat) +} + +fn extract_search_fields(schema: &Schema) -> Vec { + schema + .fields() + .filter(|&(_, field_entry)| field_entry.is_indexed()) + .map(|(field, _)| field) + .collect() +} + +fn read_query_file(query_path: &Path) -> io::Result> { + let query_file: File = File::open(query_path)?; + let file = BufReader::new(&query_file); + let mut queries = Vec::new(); + for line_res in file.lines() { + queries.push(line_res?); + } + Ok(queries) +} + +fn run_bench(index_path: &Path, query_filepath: &Path, num_repeat: usize) -> Result<(), String> { + println!("Index : {index_path:?}"); + println!("Query : {query_filepath:?}"); + println!("-------------------------------\n\n\n"); + + let index = + Index::open_in_dir(index_path).map_err(|e| format!("Failed to open index.\n{e:?}"))?; + let searcher = index.reader().map_err(|err| format!("{err:?}"))?.searcher(); + let default_search_fields: Vec = extract_search_fields(&index.schema()); + let queries = read_query_file(query_filepath) + .map_err(|e| format!("Failed reading the query file: {e}"))?; + let query_parser = QueryParser::new( + index.schema(), + default_search_fields, + index.tokenizers().clone(), + ); + + println!("SEARCH\n"); + println!("query\tnum hits\ttime in microsecs"); + for _ in 0..num_repeat { + for query_txt in &queries { + let query = query_parser + .parse_query(query_txt) + .unwrap_or_else(|x| panic!("Failed to parse query {query_txt:?}.\n\n{x:?}")); + let mut timing = TimerTree::default(); + let (_top_docs, count) = { + let _search = timing.open("search"); + searcher + .search(&query, &(TopDocs::with_limit(10), Count)) + .map_err(|e| format!("Failed while searching query {query_txt:?}.\n\n{e:?}"))? + }; + println!("{}\t{}\t{}", query_txt, count, timing.total_time()); + } + } + + println!("\n\nFETCH STORE\n"); + println!("query\ttime in microsecs"); + for _ in 0..num_repeat { + for query_txt in &queries { + let query = query_parser.parse_query(query_txt).unwrap(); + let top_docs = searcher + .search(&*query, &TopDocs::with_limit(10)) + .map_err(|e| { + format!("Failed while retrieving document for query {query:?}.\n{e:?}") + })?; + let mut timer = TimerTree::default(); + { + let _scoped_timer_ = timer.open("total"); + for (_score, doc_address) in top_docs { + searcher.doc::(doc_address).unwrap(); + } + } + println!("{}\t{}", query_txt, timer.total_time()); + } + } + + Ok(()) +} diff --git a/crates/tabby-index-cli/src/commands/head.rs b/crates/tabby-index-cli/src/commands/head.rs new file mode 100644 index 000000000000..853e5f5e06ea --- /dev/null +++ b/crates/tabby-index-cli/src/commands/head.rs @@ -0,0 +1,72 @@ +use std::path::Path; + +use clap::Args; +use tabby_common::index::IndexSchema; +use tantivy::{DocAddress, DocSet, Document, Index, TantivyDocument, Term, TERMINATED}; + +#[derive(Args)] +pub struct HeadArgs { + /// Number of documents to display + #[clap(short, long, default_value = "1")] + num_docs: usize, + + #[clap(short, long, default_value = "code")] + corpus: String, +} + +pub fn run_head_cli(index_path: &Path, args: &HeadArgs) -> anyhow::Result<()> { + let index = Index::open_in_dir(index_path)?; + + let searcher = index.reader()?.searcher(); + let schema = IndexSchema::instance(); + + let mut count = 0; + 'outer: for (segment_ordinal, segment_reader) in searcher.segment_readers().iter().enumerate() { + let Ok(inverted_index) = segment_reader.inverted_index(schema.field_corpus) else { + continue; + }; + + let term_corpus = Term::from_field_text(schema.field_corpus, &args.corpus); + let Ok(Some(mut postings)) = + inverted_index.read_postings(&term_corpus, tantivy::schema::IndexRecordOption::Basic) + else { + continue; + }; + + let mut doc_id = postings.doc(); + while doc_id != TERMINATED { + if !segment_reader.is_deleted(doc_id) { + let doc_address = DocAddress::new(segment_ordinal as u32, doc_id); + let doc: TantivyDocument = + searcher.doc(doc_address).expect("Failed to read document"); + + let json_value = to_json_value(doc, &schema.schema); + + println!("{json_value}"); + + count += 1; + if count >= args.num_docs { + break 'outer; + } + } + doc_id = postings.advance(); + } + } + + Ok(()) +} + +fn to_json_value(doc: TantivyDocument, schema: &tantivy::schema::Schema) -> serde_json::Value { + let json = doc.to_json(schema); + let mut doc: serde_json::Value = serde_json::from_str(&json).expect("Failed to parse JSON"); + + for (_, value) in doc.as_object_mut().expect("Expected object").iter_mut() { + if let Some(array) = value.as_array_mut() { + if array.len() == 1 { + *value = array[0].clone(); + } + } + } + + doc +} diff --git a/crates/tabby-index-cli/src/commands/inspect.rs b/crates/tabby-index-cli/src/commands/inspect.rs new file mode 100644 index 000000000000..340e4724c968 --- /dev/null +++ b/crates/tabby-index-cli/src/commands/inspect.rs @@ -0,0 +1,85 @@ +use std::path::Path; + +use tantivy::{self, schema::Schema, space_usage::PerFieldSpaceUsage, Index}; + +pub fn run_inspect_cli(directory: &Path) -> tantivy::Result<()> { + let index = Index::open_in_dir(directory)?; + let schema = index.schema(); + let searcher = index.reader()?.searcher(); + let segments = searcher.segment_readers(); + println!(); + println!("==============================================================================="); + println!("Inspect index report"); + println!("===============================================================================\n"); + println!("1. General infos"); + println!("==============================================================================="); + println!("Index directory: {directory:?}"); + println!("Number of segments: {}", segments.len()); + let space_usage = searcher.space_usage()?; + println!("Total bytes: {}", space_usage.total()); + println!(); + for (i, (segment_reader, segment_space_usage)) in segments + .iter() + .zip(space_usage.segments().iter()) + .enumerate() + { + let section_count = i + 2; + println!( + "{}. Space usage for segment: `{}`", + section_count, + segment_reader.segment_id().uuid_string() + ); + println!("=============================================================================="); + println!("Num docs: {}", segment_space_usage.num_docs()); + println!("Store space usage:"); + println!("Total bytes: {}", segment_space_usage.store().total()); + println!( + "Offset bytes: {}", + segment_space_usage.store().offsets_usage() + ); + println!(); + println!("{section_count}.1 Term dictionnary space usage"); + println!("--------------------------------"); + let per_field_space_usage = segment_space_usage.termdict(); + println!("Total bytes: {}", per_field_space_usage.total()); + print_fields_space_usage(&schema, per_field_space_usage); + println!(); + + println!("{section_count}.2 Fast field space usage"); + println!("--------------------------------"); + let fast_field_space_usage = segment_space_usage.fast_fields(); + println!("Total bytes: {}", fast_field_space_usage.total()); + print_fields_space_usage(&schema, fast_field_space_usage); + println!(); + + println!("{section_count}.3 Postings space usage"); + println!("--------------------------------"); + let postings_space_usage = segment_space_usage.postings(); + println!("Total bytes: {}", postings_space_usage.total()); + print_fields_space_usage(&schema, postings_space_usage); + println!(); + + println!("{section_count}.4 Positions space usage"); + println!("--------------------------------"); + let positions_space_usage = segment_space_usage.positions(); + println!("Total bytes: {}", positions_space_usage.total()); + print_fields_space_usage(&schema, positions_space_usage); + println!(); + } + + println!(); + println!("----------------------------- END OF REPORT ----------------------------------"); + Ok(()) +} + +fn print_fields_space_usage(schema: &Schema, per_field_space_usage: &PerFieldSpaceUsage) { + println!("Total bytes: {}", per_field_space_usage.total()); + for (field, field_space_usage) in per_field_space_usage.fields() { + let field_name = schema.get_field_name(*field); + println!( + "Field `{}` bytes: {}", + field_name, + field_space_usage.total() + ); + } +} diff --git a/crates/tabby-index-cli/src/commands/mod.rs b/crates/tabby-index-cli/src/commands/mod.rs new file mode 100644 index 000000000000..58e31d4897a0 --- /dev/null +++ b/crates/tabby-index-cli/src/commands/mod.rs @@ -0,0 +1,9 @@ +mod bench; +mod head; +mod inspect; + +pub use self::{ + bench::{run_bench_cli, BenchArgs}, + head::{run_head_cli, HeadArgs}, + inspect::run_inspect_cli, +}; diff --git a/crates/tabby-index-cli/src/main.rs b/crates/tabby-index-cli/src/main.rs new file mode 100644 index 000000000000..b579980a047b --- /dev/null +++ b/crates/tabby-index-cli/src/main.rs @@ -0,0 +1,45 @@ +mod commands; +mod timer; + +use std::path::PathBuf; + +use clap::{Parser, Subcommand}; +use commands::{BenchArgs, HeadArgs}; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +#[command(propagate_version = true)] +struct Cli { + #[command(subcommand)] + command: Commands, + + /// Path to the index directory + #[clap(short, long)] + index_dir: Option, +} + +#[derive(Subcommand)] +pub enum Commands { + Inspect, + Bench(BenchArgs), + Head(HeadArgs), +} + +fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + let index_dir = cli.index_dir.unwrap_or(tabby_common::path::index_dir()); + + match cli.command { + Commands::Inspect => { + commands::run_inspect_cli(&index_dir)?; + } + Commands::Bench(args) => { + commands::run_bench_cli(&index_dir, &args).map_err(|e| anyhow::anyhow!("{}", e))?; + } + Commands::Head(args) => { + commands::run_head_cli(&index_dir, &args)?; + } + }; + + Ok(()) +} diff --git a/crates/tabby-index-cli/src/timer.rs b/crates/tabby-index-cli/src/timer.rs new file mode 100644 index 000000000000..d6eac127d755 --- /dev/null +++ b/crates/tabby-index-cli/src/timer.rs @@ -0,0 +1,89 @@ +use serde::Serialize; +use tantivy::time::Instant; + +pub struct OpenTimer<'a> { + name: &'static str, + timer_tree: &'a mut TimerTree, + start: Instant, + depth: u32, +} + +impl OpenTimer<'_> { + /// Starts timing a new named subtask + /// + /// The timer is stopped automatically + /// when the `OpenTimer` is dropped. + pub fn open(&mut self, name: &'static str) -> OpenTimer<'_> { + OpenTimer { + name, + timer_tree: self.timer_tree, + start: Instant::now(), + depth: self.depth + 1, + } + } +} + +impl Drop for OpenTimer<'_> { + fn drop(&mut self) { + self.timer_tree.timings.push(Timing { + name: self.name, + duration: self.start.elapsed().whole_microseconds() as i64, + depth: self.depth, + }); + } +} + +/// Timing recording +#[derive(Debug, Serialize)] +pub struct Timing { + name: &'static str, + duration: i64, + depth: u32, +} + +/// Timer tree +#[derive(Debug, Serialize, Default)] +pub struct TimerTree { + timings: Vec, +} + +impl TimerTree { + /// Returns the total time elapsed in microseconds + pub fn total_time(&self) -> i64 { + self.timings.last().unwrap().duration + } + + /// Open a new named subtask + pub fn open(&mut self, name: &'static str) -> OpenTimer<'_> { + OpenTimer { + name, + timer_tree: self, + start: Instant::now(), + depth: 0, + } + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_timer() { + let mut timer_tree = TimerTree::default(); + { + let mut a = timer_tree.open("a"); + { + let mut ab = a.open("b"); + { + let _abc = ab.open("c"); + } + { + let _abd = ab.open("d"); + } + } + } + assert_eq!(timer_tree.timings.len(), 4); + } +} diff --git a/crates/tabby-index/Cargo.toml b/crates/tabby-index/Cargo.toml new file mode 100644 index 000000000000..2ebe5962f115 --- /dev/null +++ b/crates/tabby-index/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "tabby-index" +version.workspace = true +edition.workspace = true +authors.workspace = true +homepage.workspace = true + +[dependencies] +anyhow = { workspace = true } +tabby-common = { path = "../tabby-common" } +tabby-git ={ path = "../tabby-git" } +tantivy = { workspace = true } +tracing = { workspace = true } +tree-sitter-tags = "0.22.6" +lazy_static = { workspace = true } +tree-sitter-scala = "0.22.1" +tree-sitter-python = "0.21.0" +tree-sitter-java = "0.21.0" +tree-sitter-kotlin = "0.3.6" +tree-sitter-rust = "0.21.2" +tree-sitter-typescript = "0.21.1" +tree-sitter-go = "0.21.0" +tree-sitter-ruby = "0.21.0" +tree-sitter-c = { git = "https://github.com/tree-sitter/tree-sitter-c/", rev = "00ed08f" } +tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "d29fbff" } +tree-sitter-c-sharp = "0.21.2" +tree-sitter-solidity = { git = "https://github.com/JoranHonig/tree-sitter-solidity", rev = "0e86ae647bda22c9bee00ec59752df7b3d3b000b" } +tree-sitter-lua = "0.1.0" +tree-sitter-elixir = "0.2.0" +tree-sitter-gdscript = { git = "https://github.com/faceCutWall/tree-sitter-gdscript", rev = "8a8c067899d734840e8ce86fdeeeadbe8088446b" } +ignore.workspace = true +tokio = { workspace = true, features = ["process"] } +text-splitter = { version = "0.13.3", features = ["code"] } +serde.workspace = true +serde_json.workspace = true +futures.workspace = true +async-stream.workspace = true +tabby-inference = { path = "../tabby-inference" } +git2.workspace = true +insta.workspace = true +async-trait.workspace = true +logkit.workspace = true +chrono.workspace = true + +[dev-dependencies] +temp_testdir = { workspace = true } +tabby-common = { path = "../tabby-common", features = [ "testutils" ] } +tracing-test.workspace = true +tokio = { workspace = true, features = ["rt", "macros", "rt-multi-thread"] } +serde_json = { workspace = true } +async-trait = { workspace = true } +tracing-subscriber = { workspace = true } +serial_test = { workspace = true } diff --git a/crates/tabby-index/README.md b/crates/tabby-index/README.md new file mode 100644 index 000000000000..ed04f778f924 --- /dev/null +++ b/crates/tabby-index/README.md @@ -0,0 +1,3 @@ +# tabby-index + +This package contains indexer definition for Code / Documentation corpus. \ No newline at end of file diff --git a/crates/tabby-scheduler/queries/csharp.scm b/crates/tabby-index/queries/csharp.scm similarity index 100% rename from crates/tabby-scheduler/queries/csharp.scm rename to crates/tabby-index/queries/csharp.scm diff --git a/crates/tabby-index/queries/gdscript.scm b/crates/tabby-index/queries/gdscript.scm new file mode 100644 index 000000000000..9da7e0f1da3f --- /dev/null +++ b/crates/tabby-index/queries/gdscript.scm @@ -0,0 +1,12 @@ + +( + (class_definition (name) @name) @definition.class +) + +( + (function_definition (name) @name) @definition.function +) + +( + (call (identifier) @name) @reference.call +) diff --git a/crates/tabby-scheduler/queries/go.scm b/crates/tabby-index/queries/go.scm similarity index 100% rename from crates/tabby-scheduler/queries/go.scm rename to crates/tabby-index/queries/go.scm diff --git a/crates/tabby-scheduler/queries/kotlin.scm b/crates/tabby-index/queries/kotlin.scm similarity index 100% rename from crates/tabby-scheduler/queries/kotlin.scm rename to crates/tabby-index/queries/kotlin.scm diff --git a/crates/tabby-index/queries/ocaml.scm b/crates/tabby-index/queries/ocaml.scm new file mode 100644 index 000000000000..bc60aacecc46 --- /dev/null +++ b/crates/tabby-index/queries/ocaml.scm @@ -0,0 +1,115 @@ +; Modules +;-------- + +( + (comment)? @doc . + (module_definition (module_binding (module_name) @name) @definition.module) + (#strip! @doc "^\\(\\*\\*?\\s*|\\s\\*\\)$") +) + +(module_path (module_name) @name) @reference.module + +; Module types +;-------------- + +( + (comment)? @doc . + (module_type_definition (module_type_name) @name) @definition.interface + (#strip! @doc "^\\(\\*\\*?\\s*|\\s\\*\\)$") +) + +(module_type_path (module_type_name) @name) @reference.implementation + +; Functions +;---------- + +( + (comment)? @doc . + (value_definition + [ + (let_binding + pattern: (value_name) @name + (parameter)) + (let_binding + pattern: (value_name) @name + body: [(fun_expression) (function_expression)]) + ] @definition.function + ) + (#strip! @doc "^\\(\\*\\*?\\s*|\\s\\*\\)$") +) + +( + (comment)? @doc . + (external (value_name) @name) @definition.function + (#strip! @doc "^\\(\\*\\*?\\s*|\\s\\*\\)$") +) + +(application_expression + function: (value_path (value_name) @name)) @reference.call + +(infix_expression + left: (value_path (value_name) @name) + operator: (concat_operator) @reference.call + (#eq? @reference.call "@@")) + +(infix_expression + operator: (rel_operator) @reference.call + right: (value_path (value_name) @name) + (#eq? @reference.call "|>")) + +; Operator +;--------- + +( + (comment)? @doc . + (value_definition + (let_binding + pattern: (parenthesized_operator (_) @name)) @definition.function) + (#strip! @doc "^\\(\\*\\*?\\s*|\\s\\*\\)$") +) + +[ + (prefix_operator) + (sign_operator) + (pow_operator) + (mult_operator) + (add_operator) + (concat_operator) + (rel_operator) + (and_operator) + (or_operator) + (assign_operator) + (hash_operator) + (indexing_operator) + (let_operator) + (let_and_operator) + (match_operator) +] @name @reference.call + +; Classes +;-------- + +( + (comment)? @doc . + [ + (class_definition (class_binding (class_name) @name) @definition.class) + (class_type_definition (class_type_binding (class_type_name) @name) @definition.class) + ] + (#strip! @doc "^\\(\\*\\*?\\s*|\\s\\*\\)$") +) + +[ + (class_path (class_name) @name) + (class_type_path (class_type_name) @name) +] @reference.class + +; Methods +;-------- + +( + (comment)? @doc . + (method_definition (method_name) @name) @definition.method + (#strip! @doc "^\\(\\*\\*?\\s*|\\s\\*\\)$") +) + +(method_invocation (method_name) @name) @reference.call diff --git a/crates/tabby-scheduler/queries/rust.scm b/crates/tabby-index/queries/rust.scm similarity index 100% rename from crates/tabby-scheduler/queries/rust.scm rename to crates/tabby-index/queries/rust.scm diff --git a/crates/tabby-index/queries/scala.scm b/crates/tabby-index/queries/scala.scm new file mode 100644 index 000000000000..687b4f8dbf5e --- /dev/null +++ b/crates/tabby-index/queries/scala.scm @@ -0,0 +1,68 @@ + +(class_definition + name: (identifier) @name) @definition.class + +(enum_definition + name: (identifier) @name) @definition.enum + +(object_definition + name: (identifier) @name) @definition.object + +(object_definition + name: (identifier) @name) @definition.trait + + +(class_parameter + name: (identifier) @name) @definition.class_parameter + +(self_type (identifier) @name) @definition.parameter + +(type_definition + name: (type_identifier) @name) @definition.type + + +(val_definition + pattern: (identifier) @name) @definition.val + +(var_definition + pattern: (identifier) @name) @definition.var + +(val_declaration + name: (identifier) @name) @definition.val_decl + +(var_declaration + name: (identifier) @name) @definition.var_decl + + + +(call_expression + function: (identifier) @name) @reference.call + +(call_expression + function: (operator_identifier) @name) @reference.call + +(call_expression + function: (field_expression + field: (identifier) @name)) @reference.call + +((call_expression + function: (identifier) @name) + (#match? @name "^[A-Z]")) @reference.constructor + +(generic_function + function: (identifier) @name) @reference.call + +(interpolated_string_expression + interpolator: (identifier) @name) @reference.call + + +(function_definition + name: (identifier) @name) @definition.function + + +(function_declaration + name: (identifier) @name) @definition.function + +(function_definition + name: (identifier) @name) @definition.function + diff --git a/crates/tabby-index/queries/solidity.scm b/crates/tabby-index/queries/solidity.scm new file mode 100644 index 000000000000..c63b76f55a85 --- /dev/null +++ b/crates/tabby-index/queries/solidity.scm @@ -0,0 +1,45 @@ +;; Copied from https://github.com/JoranHonig/tree-sitter-solidity/blob/master/queries/tags.scm +;; +;; Method and Function declarations +(contract_declaration (_ + (function_definition + name: (identifier) @name) @definition.method)) + +(source_file + (function_definition + name: (identifier) @name) @definition.function) + +;; Contract, struct, enum and interface declarations +(contract_declaration + name: (identifier) @name) @definition.class + +(interface_declaration + name: (identifier) @name) @definition.interface + +(library_declaration + name: (identifier) @name) @definition.interface + +(struct_declaration name: (identifier) @name) @definition.class +(enum_declaration name: (identifier) @name) @definition.class +(event_definition name: (identifier) @name) @definition.class + +;; Function calls +(call_expression (expression (identifier)) @name ) @reference.call + +(call_expression + (expression (member_expression + property: (_) @name ))) @reference.call + +;; Log emit +(emit_statement name: (_) @name) @reference.class + + +;; Inheritance + +(inheritance_specifier + ancestor: (user_defined_type (_) @name . )) @reference.class + + +;; Imports ( note that unknown is not standardised ) +(import_directive + import_name: (_) @name ) @reference.unknown \ No newline at end of file diff --git a/crates/tabby-scheduler/queries/tsx.scm b/crates/tabby-index/queries/tsx.scm similarity index 100% rename from crates/tabby-scheduler/queries/tsx.scm rename to crates/tabby-index/queries/tsx.scm diff --git a/crates/tabby-index/src/code/index.rs b/crates/tabby-index/src/code/index.rs new file mode 100644 index 000000000000..6959746b28ee --- /dev/null +++ b/crates/tabby-index/src/code/index.rs @@ -0,0 +1,263 @@ +use std::{path::Path, pin::pin, sync::Arc}; + +use anyhow::Result; +use async_stream::stream; +use futures::StreamExt; +use ignore::{DirEntry, Walk}; +use tabby_common::index::{code, corpus}; +use tabby_inference::Embedding; +use tracing::warn; + +use super::{ + create_code_builder, + intelligence::{CodeIntelligence, SourceCode}, + repository, CodeRepository, +}; +use crate::{ + code::repository::resolve_commits, + indexer::{Indexer, TantivyDocBuilder}, +}; + +// Magic numbers +static MAX_LINE_LENGTH_THRESHOLD: usize = 300; +static AVG_LINE_LENGTH_THRESHOLD: f32 = 150f32; +static MIN_ALPHA_NUM_FRACTION: f32 = 0.25f32; +static MAX_NUMBER_OF_LINES: usize = 100000; +static MAX_NUMBER_FRACTION: f32 = 0.5f32; + +pub async fn index_repository(embedding: Arc, repository: &CodeRepository) { + let refs = resolve_commits(repository); + // resolve_commits would return the current default branch, + // so it should never be empty here. + if refs.is_empty() { + logkit::error!( + "no branches found for repository {}", + repository.canonical_git_url() + ); + return; + } + + let mut count_files = 0; + let mut count_chunks = 0; + + for (ref_name, sha) in refs { + if let Err(e) = repository::checkout(repository, &ref_name) { + warn!("Failed to checkout ref {}: {}", ref_name, e); + continue; + } + + logkit::info!("Indexing branch {} with commit {}", ref_name, &sha); + + let file_stream = stream! { + for file in Walk::new(repository.dir()) { + let file = match file { + Ok(file) => file, + Err(e) => { + warn!("Failed to walk file tree for indexing: {e}"); + continue; + } + }; + + yield file; + } + } + // Commit every 100 files + .chunks(100); + + let mut file_stream = pin!(file_stream); + + while let Some(files) = file_stream.next().await { + count_files += files.len(); + count_chunks += add_changed_documents(repository, &sha, embedding.clone(), files).await; + logkit::info!("Processed {count_files} files, updated {count_chunks} chunks",); + } + } +} + +// garbage collection use blob id to check files, +// does NOT have to checkout branch locally. +pub async fn garbage_collection() { + let index = Indexer::new(corpus::CODE); + stream! { + let mut num_to_keep = 0; + let mut num_to_delete = 0; + + for await (_, id) in index.iter_ids() { + let Some(source_file_id) = SourceCode::source_file_id_from_id(&id) else { + warn!("Failed to extract source file id from index id: {id}"); + num_to_delete += 1; + index.delete(&id); + continue; + }; + + if CodeIntelligence::check_source_file_id_matched(source_file_id) { + num_to_keep += 1; + } else { + num_to_delete += 1; + index.delete(&id); + } + } + + logkit::info!("Finished garbage collection for code index: {num_to_keep} items kept, {num_to_delete} items removed"); + index.commit(); + }.collect::<()>().await; +} + +async fn add_changed_documents( + repository: &CodeRepository, + commit: &str, + embedding: Arc, + files: Vec, +) -> usize { + let concurrency = std::cmp::max(std::thread::available_parallelism().unwrap().get() * 4, 64); + let builder = Arc::new(create_code_builder(Some(embedding))); + let index = Arc::new(Indexer::new(corpus::CODE)); + let cloned_index = index.clone(); + + let mut count_docs = 0; + stream! { + for file in files { + let Some(key) = CodeIntelligence::compute_source_file_id(file.path()) else { + continue; + }; + + let id = SourceCode::to_index_id(&repository.source_id, &key).id; + + // Skip if already indexed and has no failed chunks, + // when skip, we should check if the document needs to be backfilled. + if !require_updates(cloned_index.clone(), &id) { + backfill_commit_in_doc_if_needed( + builder.clone(), + cloned_index.clone(), + &id, + repository, + commit, + file.path()).await.unwrap_or_else(|e| { + warn!("Failed to backfill commit for {id}: {e}"); + } + ); + continue; + } + + let Some(code) = CodeIntelligence::compute_source_file(repository, commit, file.path()) else { + continue; + }; + + if !is_valid_file(&code) { + continue; + } + + let (_, s) = builder.build(code).await; + // must delete before adding, otherwise the some fields like failed_chunks_count will remain + cloned_index.delete(&id); + for await task in s { + yield task; + } + } + } + .buffer_unordered(concurrency) + .filter_map(|x| async { x.ok().flatten() }) + .for_each(|x| { + count_docs += 1; + index.add(x) + }) + .await; + + match Arc::try_unwrap(index) { + Ok(index) => index.commit(), + Err(_) => { + panic!("Failed to commit code index"); + } + }; + + count_docs +} + +fn require_updates(indexer: Arc, id: &str) -> bool { + if indexer.is_indexed(id) && !indexer.has_failed_chunks(id) { + return false; + }; + + true +} + +// v0.23.0 add the commit field to the code document. +async fn backfill_commit_in_doc_if_needed( + builder: Arc>, + indexer: Arc, + id: &str, + repository: &CodeRepository, + commit: &str, + path: &Path, +) -> Result<()> { + if indexer.has_attribute_field(id, code::fields::COMMIT) { + return Ok(()); + } + + let code = CodeIntelligence::compute_source_file(repository, commit, path) + .ok_or_else(|| anyhow::anyhow!("Failed to compute source file"))?; + if !is_valid_file(&code) { + anyhow::bail!("Invalid file"); + } + + let origin = indexer.get_doc(id).await?; + indexer.delete_doc(id); + indexer + .add(builder.backfill_doc_attributes(&origin, &code).await) + .await; + + Ok(()) +} + +fn is_valid_file(file: &SourceCode) -> bool { + file.max_line_length <= MAX_LINE_LENGTH_THRESHOLD + && file.avg_line_length <= AVG_LINE_LENGTH_THRESHOLD + && file.alphanum_fraction >= MIN_ALPHA_NUM_FRACTION + && file.num_lines <= MAX_NUMBER_OF_LINES + && file.number_fraction <= MAX_NUMBER_FRACTION +} + +#[cfg(test)] +mod tests { + use futures::StreamExt; + use insta::assert_snapshot; + + use crate::code::intelligence::CodeIntelligence; + + #[tokio::test] + async fn test_code_splitter() { + // First file, tabby-inference/src/decoding.rs + let file_contents = include_str!("../../../tabby-inference/src/decoding.rs"); + + let rust_chunks = CodeIntelligence::chunks(file_contents, "rust") + .map(|(_, chunk)| chunk) + .collect::>() + .await; + + assert_snapshot!(format!("{:#?}", rust_chunks)); + + let text_chunks = CodeIntelligence::chunks(file_contents, "unknown") + .map(|(_, chunk)| chunk) + .collect::>() + .await; + + assert_snapshot!(format!("{:#?}", text_chunks)); + + // Second file, tabby-db/src/cache.rs + let file_contents2 = include_str!("../../../../ee/tabby-db/src/cache.rs"); + + let rust_chunks2 = CodeIntelligence::chunks(file_contents2, "rust") + .map(|(_, chunk)| chunk) + .collect::>() + .await; + + assert_snapshot!(format!("{:#?}", rust_chunks2)); + + let text_chunks2 = CodeIntelligence::chunks(file_contents2, "unknown") + .map(|(_, chunk)| chunk) + .collect::>() + .await; + + assert_snapshot!(format!("{:#?}", text_chunks2)); + } +} diff --git a/crates/tabby-index/src/code/intelligence.rs b/crates/tabby-index/src/code/intelligence.rs new file mode 100644 index 000000000000..3976d3c4e646 --- /dev/null +++ b/crates/tabby-index/src/code/intelligence.rs @@ -0,0 +1,278 @@ +mod id; + +use std::{fs::read_to_string, path::Path}; + +use async_stream::stream; +use futures::Stream; +use id::SourceFileId; +use tabby_common::languages::get_language_by_ext; +use text_splitter::{CodeSplitter, TextSplitter}; +use tracing::warn; +use tree_sitter_tags::TagsContext; + +pub use super::types::{Point, SourceCode, Tag}; +use super::{ + languages::{self}, + CodeRepository, +}; + +pub struct CodeIntelligence; + +const CHUNK_SIZE: usize = 512; + +impl CodeIntelligence { + fn find_tags(language: &str, content: &str) -> Vec { + let config = languages::get(language); + let empty = Vec::new(); + + let Some(config) = config else { + return empty; + }; + + let mut context = TagsContext::new(); + + let Ok((tags, has_error)) = context.generate_tags(&config.0, content.as_bytes(), None) + else { + return empty; + }; + + if has_error { + return empty; + } + + tags.filter_map(|x| x.ok()) + .map(|x| Tag { + range: x.range, + name_range: x.name_range, + utf16_column_range: x.utf16_column_range, + line_range: x.line_range, + docs: x.docs, + is_definition: x.is_definition, + syntax_type_name: config.0.syntax_type_name(x.syntax_type_id).to_owned(), + span: Point::new(x.span.start.row, x.span.start.column) + ..Point::new(x.span.end.row, x.span.end.column), + }) + .collect() + } + + pub fn compute_source_file_id(path: &Path) -> Option { + SourceFileId::try_from(path).map(|key| key.to_string()).ok() + } + + pub fn check_source_file_id_matched(item_key: &str) -> bool { + let Ok(key) = item_key.parse::() else { + warn!("Failed to parse key: {}", item_key); + return false; + }; + + let Ok(file_key) = SourceFileId::try_from(key.path()) else { + return false; + }; + + // If key doesn't match, means file has been removed / modified. + file_key.to_string() == item_key + } + + pub fn compute_source_file( + config: &CodeRepository, + commit: &str, + path: &Path, + ) -> Option { + let source_file_id = Self::compute_source_file_id(path)?; + + if path.is_dir() || !path.exists() { + warn!("Path {} is not a file or does not exist", path.display()); + return None; + } + let relative_path = path + .strip_prefix(config.dir()) + .expect("Paths always begin with the prefix"); + + let Some(ext) = relative_path.extension() else { + return None; + }; + + let Some(language_info) = get_language_by_ext(ext) else { + warn!("Unknown language for extension {:?}", ext); + return None; + }; + + let language = language_info.language(); + let contents = match read_to_string(path) { + Ok(x) => x, + Err(_) => { + warn!("Failed to read {path:?}, skipping..."); + return None; + } + }; + + let metrics::Metrics { + max_line_length, + avg_line_length, + alphanum_fraction, + num_lines, + number_fraction, + } = metrics::compute_metrics(&contents); + + let source_file = SourceCode { + source_file_id, + source_id: config.source_id.clone(), + git_url: config.canonical_git_url(), + commit: commit.to_owned(), + basedir: config.dir().display().to_string(), + filepath: relative_path.display().to_string(), + max_line_length, + avg_line_length, + alphanum_fraction, + number_fraction, + tags: Self::find_tags(language, &contents), + num_lines, + language: language.into(), + }; + Some(source_file) + } + + fn stream_text_chunks(text: &str) -> impl Stream { + let text = text.to_owned(); + let splitter = TextSplitter::new(CHUNK_SIZE); + stream! { + for (offset, chunk) in splitter.chunk_indices(&text) { + yield (offset, chunk.to_owned()); + } + } + } + + fn stream_code_chunks( + text: &str, + language: &str, + ) -> Option> { + let Some(config) = languages::get(language) else { + return None; + }; + let chunk_size = tabby_common::languages::get_language(language) + .chunk_size + .unwrap_or(CHUNK_SIZE); + let text = text.to_owned(); + let splitter = CodeSplitter::new(config.0.language.clone(), chunk_size) + .expect("Failed to create code splitter"); + Some(stream! { + for (offset, chunk) in splitter.chunk_indices(&text) { + yield (offset, chunk.to_owned()); + } + }) + } + + pub fn chunks(text: &str, language: &str) -> impl Stream { + let text = text.to_owned(); + let language = language.to_owned(); + stream! { + let mut last_offset = 0; + let mut last_line_number = 1; + if let Some(stream) = Self::stream_code_chunks(&text, &language) { + for await (offset, chunk) in stream { + last_line_number = line_number_from_byte_offset(&text, last_offset, last_line_number, offset); + last_offset = offset; + yield (last_line_number, chunk); + } + } else { + for await (offset, chunk) in Self::stream_text_chunks(&text) { + last_line_number = line_number_from_byte_offset(&text, last_offset, last_line_number, offset); + last_offset = offset; + yield (last_line_number, chunk); + } + } + } + } +} + +fn line_number_from_byte_offset( + s: &str, + last_offset: usize, + last_line_number: usize, + byte_offset: usize, +) -> usize { + let mut line_number = last_line_number; // Start counting from line 1 + let mut current_offset = last_offset; + + for c in s[last_offset..].chars() { + if c == '\n' { + line_number += 1; + } + current_offset += c.len_utf8(); + if current_offset >= byte_offset { + break; + } + } + + line_number +} + +mod metrics { + use std::cmp::max; + + pub struct Metrics { + pub max_line_length: usize, + pub avg_line_length: f32, + pub alphanum_fraction: f32, + pub number_fraction: f32, + pub num_lines: usize, + } + + pub fn compute_metrics(content: &str) -> Metrics { + let mut metrics = Metrics { + max_line_length: 0, + avg_line_length: 0.0, + alphanum_fraction: 0.0, + number_fraction: 0.0, + num_lines: 0, + }; + // Compute metrics in single loop. + for x in content.lines() { + metrics.num_lines += 1; + let line_length = x.len(); + metrics.max_line_length = max(metrics.max_line_length, line_length); + metrics.avg_line_length += line_length as f32; + for c in x.chars() { + if c.is_alphanumeric() { + metrics.alphanum_fraction += 1.0; + } + if c.is_numeric() { + metrics.number_fraction += 1.0; + } + } + } + + metrics.avg_line_length /= metrics.num_lines as f32; + metrics.alphanum_fraction /= content.len() as f32; + metrics.number_fraction /= content.len() as f32; + + metrics + } +} + +#[cfg(test)] +mod tests { + use serial_test::file_serial; + use tabby_common::path::set_tabby_root; + use tracing_test::traced_test; + + use super::*; + use crate::testutils::{get_repository_config, get_rust_source_file, get_tabby_root}; + + #[test] + #[traced_test] + #[file_serial(set_tabby_root)] + fn test_create_source_file() { + set_tabby_root(get_tabby_root()); + let config = get_repository_config(); + let source_file = + CodeIntelligence::compute_source_file(&config, "commit", &get_rust_source_file()) + .expect("Failed to create source file"); + + // check source_file properties + assert_eq!(source_file.language, "rust"); + assert_eq!(source_file.tags.len(), 3); + assert_eq!(source_file.filepath, "rust.rs"); + assert_eq!(source_file.commit, "commit"); + } +} diff --git a/crates/tabby-index/src/code/intelligence/id.rs b/crates/tabby-index/src/code/intelligence/id.rs new file mode 100644 index 000000000000..5df4ea1b934c --- /dev/null +++ b/crates/tabby-index/src/code/intelligence/id.rs @@ -0,0 +1,60 @@ +use std::{ + path::{Path, PathBuf}, + str::FromStr, +}; + +use anyhow::{bail, Context, Result}; +use serde::{Deserialize, Serialize}; +use tabby_common::languages::get_language_by_ext; + +fn get_git_hash(path: &Path) -> Result { + Ok(git2::Oid::hash_file(git2::ObjectType::Blob, path)?.to_string()) +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct SourceFileId { + path: PathBuf, + language: String, + git_hash: String, +} + +impl SourceFileId { + pub fn path(&self) -> &Path { + &self.path + } +} + +impl FromStr for SourceFileId { + type Err = serde_json::Error; + + fn from_str(s: &str) -> Result { + serde_json::from_str(s) + } +} + +impl TryFrom<&Path> for SourceFileId { + type Error = anyhow::Error; + + fn try_from(path: &Path) -> Result { + if !path.is_file() { + bail!("Path is not a file"); + } + + let git_hash = get_git_hash(path)?; + let ext = path.extension().context("Failed to get extension")?; + let Some(lang) = get_language_by_ext(ext) else { + bail!("Unknown language for extension {:?}", ext); + }; + Ok(Self { + path: path.to_owned(), + language: lang.language().to_string(), + git_hash: git_hash.to_string(), + }) + } +} + +impl ToString for SourceFileId { + fn to_string(&self) -> String { + serde_json::to_string(&self).expect("Failed to serialize SourceFileKey") + } +} diff --git a/crates/tabby-index/src/code/languages.rs b/crates/tabby-index/src/code/languages.rs new file mode 100644 index 000000000000..6139dfa7e57f --- /dev/null +++ b/crates/tabby-index/src/code/languages.rs @@ -0,0 +1,185 @@ +use std::collections::HashMap; + +use lazy_static::lazy_static; +use tree_sitter_tags::TagsConfiguration; + +// Mark TagsConfiguration as thread sync / safe. +pub(crate) struct TagsConfigurationSync(pub TagsConfiguration); +unsafe impl Send for TagsConfigurationSync {} +unsafe impl Sync for TagsConfigurationSync {} + +pub fn get(language: &str) -> Option<&TagsConfigurationSync> { + LANGUAGE_TAGS.get(language) +} + +lazy_static! { + static ref LANGUAGE_TAGS: HashMap<&'static str, TagsConfigurationSync> = { + HashMap::from([ + ( + "python", + TagsConfigurationSync( + TagsConfiguration::new( + tree_sitter_python::language(), + tree_sitter_python::TAGS_QUERY, + "", + ) + .unwrap(), + ), + ), + ( + "rust", + TagsConfigurationSync( + TagsConfiguration::new( + tree_sitter_rust::language(), + include_str!("../../queries/rust.scm"), + "", + ) + .unwrap(), + ), + ), + ( + "java", + TagsConfigurationSync( + TagsConfiguration::new( + tree_sitter_java::language(), + tree_sitter_java::TAGS_QUERY, + "", + ) + .unwrap(), + ), + ), + ( + "scala", + TagsConfigurationSync( + TagsConfiguration::new( + tree_sitter_scala::language(), + include_str!("../../queries/scala.scm"), + "", + ) + .unwrap(), + ), + ), + ( + "kotlin", + TagsConfigurationSync( + TagsConfiguration::new( + tree_sitter_kotlin::language(), + include_str!("../../queries/kotlin.scm"), + "", + ) + .unwrap(), + ), + ), + ( + "javascript-typescript", + TagsConfigurationSync( + TagsConfiguration::new( + tree_sitter_typescript::language_tsx(), + include_str!("../../queries/tsx.scm"), + "", + ) + .unwrap(), + ), + ), + ( + "elixir", + TagsConfigurationSync( + TagsConfiguration::new( + tree_sitter_elixir::language(), + tree_sitter_elixir::TAGS_QUERY, + "", + ) + .unwrap(), + ), + ), + ( + "go", + TagsConfigurationSync( + TagsConfiguration::new( + tree_sitter_go::language(), + include_str!("../../queries/go.scm"), + "", + ) + .unwrap(), + ), + ), + ( + "ruby", + TagsConfigurationSync( + TagsConfiguration::new( + tree_sitter_ruby::language(), + tree_sitter_ruby::TAGS_QUERY, + "", + ) + .unwrap(), + ), + ), + ( + "c", + TagsConfigurationSync( + TagsConfiguration::new( + tree_sitter_c::language(), + tree_sitter_c::TAGS_QUERY, + "", + ) + .unwrap(), + ), + ), + ( + "cpp", + TagsConfigurationSync( + TagsConfiguration::new( + tree_sitter_cpp::language(), + tree_sitter_cpp::TAGS_QUERY, + "", + ) + .unwrap(), + ), + ), + ( + "csharp", + TagsConfigurationSync( + TagsConfiguration::new( + tree_sitter_c_sharp::language(), + include_str!("../../queries/csharp.scm"), + "", + ) + .unwrap(), + ), + ), + ( + "solidity", + TagsConfigurationSync( + TagsConfiguration::new( + tree_sitter_solidity::language(), + include_str!("../../queries/solidity.scm"), + "", + ) + .unwrap(), + ), + ), + ( + "lua", + TagsConfigurationSync( + TagsConfiguration::new( + tree_sitter_lua::language(), + tree_sitter_lua::TAGS_QUERY, + "", + ) + .unwrap(), + ), + ), + ( + "gdscript", + TagsConfigurationSync( + TagsConfiguration::new( + tree_sitter_gdscript::language(), + include_str!("../../queries/gdscript.scm"), + "", + ) + .unwrap(), + ), + ), + ]) + }; +} diff --git a/crates/tabby-index/src/code/mod.rs b/crates/tabby-index/src/code/mod.rs new file mode 100644 index 000000000000..a87f421dc04a --- /dev/null +++ b/crates/tabby-index/src/code/mod.rs @@ -0,0 +1,157 @@ +use std::sync::Arc; + +use anyhow::{bail, Result}; +use async_stream::stream; +use async_trait::async_trait; +use futures::stream::BoxStream; +use serde_json::json; +use tabby_common::{ + config::CodeRepository, + index::{code, corpus}, +}; +use tabby_inference::Embedding; +use tokio::task::JoinHandle; +use tracing::{info_span, warn, Instrument}; + +use self::intelligence::SourceCode; +use crate::{ + code::intelligence::CodeIntelligence, indexer::TantivyDocBuilder, IndexAttributeBuilder, +}; + +// Modules for creating code search index. +mod index; +pub mod intelligence; +mod languages; +mod repository; +mod types; + +#[derive(Default)] +pub struct CodeIndexer {} + +impl CodeIndexer { + pub async fn refresh( + &mut self, + embedding: Arc, + repository: &CodeRepository, + ) -> anyhow::Result<()> { + repository::sync_repository(repository)?; + + logkit::info!( + "Building source code index: {}", + repository.canonical_git_url() + ); + index::index_repository(embedding, repository).await; + index::garbage_collection().await; + + Ok(()) + } + + pub async fn garbage_collection(&mut self, repositories: &[CodeRepository]) { + repository::garbage_collection(repositories); + } +} +struct CodeBuilder { + embedding: Option>, +} + +impl CodeBuilder { + fn new(embedding: Option>) -> Self { + Self { embedding } + } +} + +#[async_trait] +impl IndexAttributeBuilder for CodeBuilder { + async fn build_attributes(&self, source_code: &SourceCode) -> serde_json::Value { + json!({ + code::fields::COMMIT: source_code.commit, + }) + } + + async fn build_chunk_attributes<'a>( + &self, + source_code: &'a SourceCode, + ) -> BoxStream<'a, JoinHandle, serde_json::Value)>>> { + let text = match source_code.read_content() { + Ok(content) => content, + Err(e) => { + warn!( + "Failed to read content of '{}': {}", + source_code.filepath, e + ); + + return Box::pin(stream! { + let path = source_code.filepath.clone(); + yield tokio::spawn(async move { + bail!("Failed to read content of '{}': {}", path, e); + }); + }); + } + }; + + let Some(embedding) = self.embedding.clone() else { + warn!("No embedding service found for code indexing"); + return Box::pin(stream! { + yield tokio::spawn(async move { + bail!("No embedding service found for code indexing"); + }); + }); + }; + + let source_code = source_code.clone(); + let s = stream! { + for await (start_line, body) in CodeIntelligence::chunks(&text, &source_code.language) { + let mut attributes = json!({ + code::fields::CHUNK_FILEPATH: source_code.filepath, + code::fields::CHUNK_GIT_URL: source_code.git_url, + code::fields::CHUNK_LANGUAGE: source_code.language, + code::fields::CHUNK_BODY: body, + }); + + // When text length is not equal to body length, it means this chunk is not the entire + // content of the file, thus we need to record the start line. + if text.len() != body.len() { + attributes[code::fields::CHUNK_START_LINE] = start_line.into(); + } + let embedding = embedding.clone(); + let rewritten_body = format!("```{}\n{}\n```", source_code.filepath, body); + yield tokio::spawn(async move { + match build_binarize_embedding_tokens(embedding.clone(), &rewritten_body).await { + Ok(tokens) => Ok((tokens, attributes)), + Err(err) => Err(err), + } + }); + } + }; + + Box::pin(s) + } +} + +async fn build_binarize_embedding_tokens( + embedding: Arc, + body: &str, +) -> Result> { + let embedding = match embedding + .embed(body) + .instrument(info_span!("index_compute_embedding", corpus = corpus::CODE)) + .await + { + Ok(x) => x, + Err(err) => { + bail!("Failed to embed chunk text: {}", err); + } + }; + + let mut tokens = code::tokenize_code(body); + for token in tabby_common::index::binarize_embedding(embedding.iter()) { + tokens.push(token); + } + + Ok(tokens) +} + +pub fn create_code_builder(embedding: Option>) -> TantivyDocBuilder { + let builder = CodeBuilder::new(embedding); + TantivyDocBuilder::new(corpus::CODE, builder) +} diff --git a/crates/tabby-index/src/code/repository.rs b/crates/tabby-index/src/code/repository.rs new file mode 100644 index 000000000000..04b5af6b6efb --- /dev/null +++ b/crates/tabby-index/src/code/repository.rs @@ -0,0 +1,139 @@ +use std::{ + collections::HashSet, + fs::{self}, +}; + +use anyhow::bail; +use tabby_common::path::repositories_dir; +use tabby_git::sync_refs; +use tracing::warn; + +use super::CodeRepository; + +trait RepositoryExt { + fn sync(&self) -> anyhow::Result<()>; +} + +impl RepositoryExt for CodeRepository { + // sync clones the repository if it doesn't exist, otherwise it pulls the remote. + fn sync(&self) -> anyhow::Result<()> { + if let Err(e) = sync_refs( + self.dir().as_path(), + &self.canonical_git_url(), + &self.git_refs, + ) { + logkit::error!("Failed to clone repository: {}", e); + return Err(e); + } + Ok(()) + } +} + +pub fn sync_repository(repository: &CodeRepository) -> anyhow::Result<()> { + if repository.is_local_dir() { + if !repository.dir().exists() { + bail!("Directory {} does not exist", repository.dir().display()); + } + } else { + repository.sync()?; + } + + Ok(()) +} + +/// Resolve commits for the given repository. +/// +/// This function inspects the git repository and returns a list of (ref_name, commit_sha) +/// pairs that should be indexed. +/// If no specific refs are configured, it defaults to HEAD. +pub fn resolve_commits(repository: &CodeRepository) -> Vec<(String, String)> { + let repo = match git2::Repository::open(repository.dir()) { + Ok(repo) => repo, + Err(e) => { + logkit::error!( + "failed to open repo {}: {}", + repository.canonical_git_url(), + e + ); + return vec![]; + } + }; + + let mut commits = Vec::new(); + + // if no refs specified, use the default branch and commits directly + if repository.git_refs.is_empty() { + if let Ok(head) = repo.head() { + if let Ok(commit) = head.peel_to_commit() { + commits.push(( + head.name().unwrap_or("HEAD").to_string(), + commit.id().to_string(), + )); + } + } + return commits; + } + + for ref_name in &repository.git_refs { + let reference = match repo + .find_reference(&format!("refs/heads/{ref_name}")) + .or_else(|_| repo.find_reference(&format!("refs/tags/{ref_name}"))) + .or_else(|_| repo.find_reference(ref_name)) + { + Ok(reference) => reference, + Err(e) => { + logkit::error!("failed to find ref {}: {}", ref_name, e); + continue; + } + }; + + let commit = match reference.peel_to_commit() { + Ok(commit) => commit, + Err(e) => { + logkit::error!("failed to get commit for ref {}: {}", ref_name, e); + continue; + } + }; + commits.push((ref_name.clone(), commit.id().to_string())); + } + commits +} + +pub fn checkout(repository: &CodeRepository, branch: &str) -> anyhow::Result<()> { + let repo = git2::Repository::open(repository.dir())?; + let reference = repo + .find_reference(&format!("refs/heads/{branch}")) + .or_else(|_| repo.find_reference(&format!("refs/tags/{branch}"))) + .or_else(|_| repo.find_reference(branch))?; + + let mut checkout_builder = git2::build::CheckoutBuilder::new(); + checkout_builder.force(); + + repo.set_head(reference.name().unwrap())?; + repo.checkout_head(Some(&mut checkout_builder))?; + Ok(()) +} + +pub fn garbage_collection(repositories: &[CodeRepository]) { + let names = repositories.iter().map(|r| r.dir()).collect::>(); + + let Ok(dir) = fs::read_dir(repositories_dir()) else { + return; + }; + + for file in dir.filter_map(Result::ok) { + let metadata = file.metadata().expect("Failed to read metadata"); + let filename = file.file_name(); + if metadata.is_file() { + warn!("An unrelated file {:?} was found in repositories directory, It will now be removed...", filename); + // There shouldn't be any files under repositories dir. + fs::remove_file(file.path()) + .unwrap_or_else(|_| panic!("Failed to remove file {filename:?}")) + } else if metadata.is_dir() && !names.contains(&file.path()) { + warn!("An unrelated directory {:?} was found in repositories directory, It will now be removed...", file.path().display()); + fs::remove_dir_all(file.path()).unwrap_or_else(|_| { + panic!("Failed to remove directory {:?}", file.path().display()) + }); + } + } +} diff --git a/crates/tabby-index/src/code/snapshots/tabby_index__code__index__tests__code_splitter-2.snap b/crates/tabby-index/src/code/snapshots/tabby_index__code__index__tests__code_splitter-2.snap new file mode 100644 index 000000000000..560a8ea950eb --- /dev/null +++ b/crates/tabby-index/src/code/snapshots/tabby_index__code__index__tests__code_splitter-2.snap @@ -0,0 +1,18 @@ +--- +source: crates/tabby-index/src/code/index.rs +expression: "format!(\"{:#?}\", text_chunks)" +--- +[ + "use dashmap::DashMap;\nuse tabby_common::languages::Language;\nuse trie_rs::{Trie, TrieBuilder};\n\npub struct StopConditionFactory {\n stop_trie_cache: DashMap>,\n stop_words_from_model_config: Vec,\n}\n\nfn reverse(s: T) -> String\nwhere\n T: Into,\n{\n s.into().chars().rev().collect()\n}", + "impl Default for StopConditionFactory {\n fn default() -> Self {\n Self {\n stop_trie_cache: DashMap::new(),\n stop_words_from_model_config: vec![],\n }\n }\n}\n\ntype CachedTrie<'a> = dashmap::mapref::one::Ref<'a, String, Trie>;\n\nimpl StopConditionFactory {\n pub fn with_stop_words(stop_words: Vec) -> Self {\n Self {\n stop_trie_cache: DashMap::new(),\n stop_words_from_model_config: stop_words,\n }\n }", + "pub fn create(&self, text: &str, language: Option<&'static Language>) -> StopCondition<'_> {\n if let Some(language) = language {\n StopCondition::new(self.get_trie(language), text)\n } else {\n StopCondition::new(None, text)\n }\n }", + "fn get_trie<'a>(&'a self, language: &'static Language) -> Option> {\n let mut stop_words = language.get_stop_words();\n // append model stop words\n stop_words.extend(self.stop_words_from_model_config.iter().cloned());", + "if stop_words.is_empty() {\n None\n } else {\n let hashkey = language.language().to_owned();\n let mut trie = self.stop_trie_cache.get(&hashkey);\n if trie.is_none() {\n self.stop_trie_cache\n .insert(hashkey.clone(), create_stop_trie(stop_words));\n trie = self.stop_trie_cache.get(&hashkey);\n }\n\n trie\n }\n }\n}", + "fn create_stop_trie(stop_words: Vec) -> Trie {\n let mut builder = TrieBuilder::new();\n for word in stop_words {\n builder.push(reverse(word))\n }\n builder.build()\n}\n\npub struct StopCondition<'a> {\n stop_trie: Option>,\n reversed_text: String,\n num_decoded: usize,\n}", + "impl<'a> StopCondition<'a> {\n pub fn new(stop_trie: Option>, text: &str) -> Self {\n Self {\n stop_trie,\n reversed_text: reverse(text),\n num_decoded: 0,\n }\n }\n\n pub fn should_stop(&mut self, new_text: &str) -> (bool, usize) {\n self.num_decoded += 1;\n if !new_text.is_empty() {\n self.reversed_text = reverse(new_text) + &self.reversed_text;", + "if let Some(re) = &self.stop_trie {\n let matches = re.common_prefix_search(&self.reversed_text);\n let matched_length = matches.into_iter().map(|x| x.len()).max();\n if let Some(matched_length) = matched_length {\n return (true, matched_length);\n }\n }\n }\n (false, 0)\n }\n}\n\n#[cfg(test)]\nmod tests {\n\n use tabby_common::languages::UNKNOWN_LANGUAGE;\n\n use super::*;", + "#[test]\n fn test_trie_works() {\n let text = reverse(\"void write_u32(std::uint32_t val) const {\\n write_raw(&val, sizeof(val));\\n }\\n\\n ~llama_file() {\\n if (fp) {\\n std::fclose(fp);\\n }\\n }\\n};\\n\\nvoid\");\n\n let trie = create_stop_trie(vec![\"\\n\\n\".to_owned(), \"\\n\\n \".to_owned()]);\n assert!(trie.common_prefix_search(&text).is_empty());", + "let trie = create_stop_trie(vec![\n \"\\n\\n\".to_owned(),\n \"\\n\\n \".to_owned(),\n \"\\nvoid\".to_owned(),\n \"<|file_sep|>\".to_owned(), // qwen 2.5 coder style\n ]);\n assert!(!trie.common_prefix_search(&text).is_empty());\n\n let qwen25coder = reverse(\"qwen25 style stop words;<|file_sep|>\");\n assert!(!trie.common_prefix_search(qwen25coder).is_empty());\n }", + "#[test]\n fn test_stop_condition_max_length() {\n let factory = StopConditionFactory::default();\n let mut cond = factory.create(\"\", Some(&UNKNOWN_LANGUAGE));\n let (should_stop, _) = cond.should_stop(\"1\");\n assert!(!should_stop);\n let (should_stop, _) = cond.should_stop(\"2\");\n assert!(!should_stop);\n let (should_stop, _) = cond.should_stop(\"3\");\n assert!(!should_stop);\n let (should_stop, _) = cond.should_stop(\"4\");\n assert!(!should_stop)", + "}\n\n #[test]\n fn test_stop_condition_additional_stop_words() {\n let factory = StopConditionFactory::with_stop_words(vec![\"<|endoftext|>\".to_owned()]);\n let mut cond = factory.create(\"\", Some(&UNKNOWN_LANGUAGE));\n let (should_stop, _) = cond.should_stop(\"1\");\n assert!(!should_stop);\n let (should_stop, _) = cond.should_stop(\"<|endoftext|>\");\n assert!(should_stop);\n }\n}", +] diff --git a/crates/tabby-index/src/code/snapshots/tabby_index__code__index__tests__code_splitter-3.snap b/crates/tabby-index/src/code/snapshots/tabby_index__code__index__tests__code_splitter-3.snap new file mode 100644 index 000000000000..f5f37dbb4958 --- /dev/null +++ b/crates/tabby-index/src/code/snapshots/tabby_index__code__index__tests__code_splitter-3.snap @@ -0,0 +1,10 @@ +--- +source: crates/tabby-index/src/code/index.rs +expression: "format!(\"{:#?}\", rust_chunks2)" +--- +[ + "use std::future::Future;\n\nuse tokio::sync::RwLock;\n\n#[derive(Default)]\npub struct Cache {\n value: RwLock>,\n}", + "impl Cache", + "{\n pub async fn new() -> Self {\n Cache {\n value: Default::default(),\n }\n }\n\n pub async fn invalidate(&self) {\n *self.value.write().await = None;\n }", + "pub async fn get_or_refresh(&self, refresh: impl Fn() -> F) -> Result\n where\n T: Clone,\n F: Future>,\n {\n let value = self.value.read().await;\n if let Some(value) = &*value {\n Ok(value.clone())\n } else {\n drop(value);\n let mut value = self.value.write().await;\n let generated = refresh().await?;\n *value = Some(generated.clone());\n Ok(generated)\n }\n }\n}", +] diff --git a/crates/tabby-index/src/code/snapshots/tabby_index__code__index__tests__code_splitter-4.snap b/crates/tabby-index/src/code/snapshots/tabby_index__code__index__tests__code_splitter-4.snap new file mode 100644 index 000000000000..063bb11d48f7 --- /dev/null +++ b/crates/tabby-index/src/code/snapshots/tabby_index__code__index__tests__code_splitter-4.snap @@ -0,0 +1,8 @@ +--- +source: crates/tabby-index/src/code/index.rs +expression: "format!(\"{:#?}\", text_chunks2)" +--- +[ + "use std::future::Future;\n\nuse tokio::sync::RwLock;\n\n#[derive(Default)]\npub struct Cache {\n value: RwLock>,\n}\n\nimpl Cache {\n pub async fn new() -> Self {\n Cache {\n value: Default::default(),\n }\n }\n\n pub async fn invalidate(&self) {\n *self.value.write().await = None;\n }", + "pub async fn get_or_refresh(&self, refresh: impl Fn() -> F) -> Result\n where\n T: Clone,\n F: Future>,\n {\n let value = self.value.read().await;\n if let Some(value) = &*value {\n Ok(value.clone())\n } else {\n drop(value);\n let mut value = self.value.write().await;\n let generated = refresh().await?;\n *value = Some(generated.clone());\n Ok(generated)\n }\n }\n}", +] diff --git a/crates/tabby-index/src/code/snapshots/tabby_index__code__index__tests__code_splitter.snap b/crates/tabby-index/src/code/snapshots/tabby_index__code__index__tests__code_splitter.snap new file mode 100644 index 000000000000..8e76443f2a68 --- /dev/null +++ b/crates/tabby-index/src/code/snapshots/tabby_index__code__index__tests__code_splitter.snap @@ -0,0 +1,26 @@ +--- +source: crates/tabby-index/src/code/index.rs +expression: "format!(\"{:#?}\", rust_chunks)" +--- +[ + "use dashmap::DashMap;\nuse tabby_common::languages::Language;\nuse trie_rs::{Trie, TrieBuilder};\n\npub struct StopConditionFactory {\n stop_trie_cache: DashMap>,\n stop_words_from_model_config: Vec,\n}\n\nfn reverse(s: T) -> String\nwhere\n T: Into,\n{\n s.into().chars().rev().collect()\n}", + "impl Default for StopConditionFactory {\n fn default() -> Self {\n Self {\n stop_trie_cache: DashMap::new(),\n stop_words_from_model_config: vec![],\n }\n }\n}\n\ntype CachedTrie<'a> = dashmap::mapref::one::Ref<'a, String, Trie>;", + "impl StopConditionFactory", + "{\n pub fn with_stop_words(stop_words: Vec) -> Self {\n Self {\n stop_trie_cache: DashMap::new(),\n stop_words_from_model_config: stop_words,\n }\n }\n\n pub fn create(&self, text: &str, language: Option<&'static Language>) -> StopCondition<'_> {\n if let Some(language) = language {\n StopCondition::new(self.get_trie(language), text)\n } else {\n StopCondition::new(None, text)\n }\n }", + "fn get_trie<'a>(&'a self, language: &'static Language) -> Option>", + "{\n let mut stop_words = language.get_stop_words();\n // append model stop words\n stop_words.extend(self.stop_words_from_model_config.iter().cloned());", + "if stop_words.is_empty() {\n None\n } else {\n let hashkey = language.language().to_owned();\n let mut trie = self.stop_trie_cache.get(&hashkey);\n if trie.is_none() {\n self.stop_trie_cache\n .insert(hashkey.clone(), create_stop_trie(stop_words));\n trie = self.stop_trie_cache.get(&hashkey);\n }\n\n trie\n }\n }\n}", + "fn create_stop_trie(stop_words: Vec) -> Trie {\n let mut builder = TrieBuilder::new();\n for word in stop_words {\n builder.push(reverse(word))\n }\n builder.build()\n}\n\npub struct StopCondition<'a> {\n stop_trie: Option>,\n reversed_text: String,\n num_decoded: usize,\n}", + "impl<'a> StopCondition<'a>", + "{\n pub fn new(stop_trie: Option>, text: &str) -> Self {\n Self {\n stop_trie,\n reversed_text: reverse(text),\n num_decoded: 0,\n }\n }", + "pub fn should_stop(&mut self, new_text: &str) -> (bool, usize)", + "{\n self.num_decoded += 1;\n if !new_text.is_empty() {\n self.reversed_text = reverse(new_text) + &self.reversed_text;\n\n if let Some(re) = &self.stop_trie {\n let matches = re.common_prefix_search(&self.reversed_text);\n let matched_length = matches.into_iter().map(|x| x.len()).max();\n if let Some(matched_length) = matched_length {\n return (true, matched_length);\n }\n }\n }", + "(false, 0)\n }\n}\n\n#[cfg(test)]", + "mod tests", + "{\n\n use tabby_common::languages::UNKNOWN_LANGUAGE;\n\n use super::*;\n\n #[test]", + "fn test_trie_works()", + "{\n let text = reverse(\"void write_u32(std::uint32_t val) const {\\n write_raw(&val, sizeof(val));\\n }\\n\\n ~llama_file() {\\n if (fp) {\\n std::fclose(fp);\\n }\\n }\\n};\\n\\nvoid\");\n\n let trie = create_stop_trie(vec![\"\\n\\n\".to_owned(), \"\\n\\n \".to_owned()]);\n assert!(trie.common_prefix_search(&text).is_empty());", + "let trie = create_stop_trie(vec![\n \"\\n\\n\".to_owned(),\n \"\\n\\n \".to_owned(),\n \"\\nvoid\".to_owned(),\n \"<|file_sep|>\".to_owned(), // qwen 2.5 coder style\n ]);\n assert!(!trie.common_prefix_search(&text).is_empty());\n\n let qwen25coder = reverse(\"qwen25 style stop words;<|file_sep|>\");\n assert!(!trie.common_prefix_search(qwen25coder).is_empty());\n }\n\n #[test]", + "fn test_stop_condition_max_length() {\n let factory = StopConditionFactory::default();\n let mut cond = factory.create(\"\", Some(&UNKNOWN_LANGUAGE));\n let (should_stop, _) = cond.should_stop(\"1\");\n assert!(!should_stop);\n let (should_stop, _) = cond.should_stop(\"2\");\n assert!(!should_stop);\n let (should_stop, _) = cond.should_stop(\"3\");\n assert!(!should_stop);\n let (should_stop, _) = cond.should_stop(\"4\");\n assert!(!should_stop)\n }", + "#[test]\n fn test_stop_condition_additional_stop_words() {\n let factory = StopConditionFactory::with_stop_words(vec![\"<|endoftext|>\".to_owned()]);\n let mut cond = factory.create(\"\", Some(&UNKNOWN_LANGUAGE));\n let (should_stop, _) = cond.should_stop(\"1\");\n assert!(!should_stop);\n let (should_stop, _) = cond.should_stop(\"<|endoftext|>\");\n assert!(should_stop);\n }\n}", +] diff --git a/crates/tabby-index/src/code/types.rs b/crates/tabby-index/src/code/types.rs new file mode 100644 index 000000000000..8dc1cd584b74 --- /dev/null +++ b/crates/tabby-index/src/code/types.rs @@ -0,0 +1,80 @@ +use std::{ + ops::Range, + path::{Path, PathBuf}, +}; + +use serde::{Deserialize, Serialize}; + +use crate::indexer::{IndexId, ToIndexId}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct SourceCode { + pub source_file_id: String, + pub source_id: String, + pub git_url: String, + pub commit: String, + pub basedir: String, + pub filepath: String, + pub language: String, + pub max_line_length: usize, + pub avg_line_length: f32, + pub alphanum_fraction: f32, + pub number_fraction: f32, + pub num_lines: usize, + pub tags: Vec, +} + +impl ToIndexId for SourceCode { + fn to_index_id(&self) -> IndexId { + Self::to_index_id(&self.source_id, &self.source_file_id) + } +} + +impl SourceCode { + pub fn read_content(&self) -> std::io::Result { + let path = self.absolute_path(); + std::fs::read_to_string(path) + } + + pub fn absolute_path(&self) -> PathBuf { + Path::new(&self.basedir).join(&self.filepath) + } + + pub fn source_file_id_from_id(id: &str) -> Option<&str> { + id.split(":::").nth(1) + } + + pub fn to_index_id(source_id: &str, source_file_id: &str) -> IndexId { + IndexId { + source_id: source_id.to_owned(), + // Source file id might be duplicated across different source_ids, we prefix it with + // source_id to make it unique within corpus. + id: format!("{source_id}:::{source_file_id}"), + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Point { + pub row: usize, + pub column: usize, +} + +impl Point { + pub fn new(row: usize, column: usize) -> Self { + Self { row, column } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Tag { + pub range: Range, + pub name_range: Range, + pub utf16_column_range: Range, + pub span: Range, + pub line_range: Range, + #[serde(skip_serializing_if = "Option::is_none")] + pub docs: Option, + pub is_definition: bool, + pub syntax_type_name: String, +} diff --git a/crates/tabby-index/src/indexer.rs b/crates/tabby-index/src/indexer.rs new file mode 100644 index 000000000000..b57dbd5e4445 --- /dev/null +++ b/crates/tabby-index/src/indexer.rs @@ -0,0 +1,529 @@ +use std::collections::HashSet; + +use anyhow::{bail, Result}; +use async_stream::stream; +use futures::{stream::BoxStream, Stream, StreamExt}; +use serde_json::json; +use tabby_common::{ + index::{structured_doc::fields::KIND, IndexSchema, FIELD_SOURCE_ID}, + path, +}; +use tantivy::{ + aggregation::{ + agg_req::Aggregation, + agg_result::{AggregationResult, BucketResult}, + AggregationCollector, Key, + }, + collector::TopDocs, + doc, + query::AllQuery, + schema::{self, document::CompactDocValue, Value}, + DateTime, DocAddress, DocSet, IndexWriter, Searcher, TantivyDocument, Term, TERMINATED, +}; +use tokio::{sync::mpsc, task::JoinHandle}; +use tracing::{debug, warn}; + +use crate::tantivy_utils::open_or_create_index; + +#[derive(Debug)] +pub struct IndexId { + pub source_id: String, + pub id: String, +} + +pub trait ToIndexId { + fn to_index_id(&self) -> IndexId; +} + +#[async_trait::async_trait] +pub trait IndexAttributeBuilder: Send + Sync { + /// Build document level attributes, these attributes are only stored but not indexed. + async fn build_attributes(&self, document: &T) -> serde_json::Value; + + /// Build chunk level attributes, these attributes are stored and indexed. + async fn build_chunk_attributes<'a>( + &self, + document: &'a T, + ) -> BoxStream<'a, JoinHandle, serde_json::Value)>>>; +} + +pub struct TantivyDocBuilder { + corpus: &'static str, + builder: Box>, +} + +impl TantivyDocBuilder { + pub fn new(corpus: &'static str, builder: impl IndexAttributeBuilder + 'static) -> Self { + Self { + corpus, + builder: Box::new(builder), + } + } + + pub async fn build( + &self, + document: T, + ) -> ( + String, + impl Stream>> + '_, + ) { + let schema = IndexSchema::instance(); + let IndexId { source_id, id } = document.to_index_id(); + + let now = tantivy::time::OffsetDateTime::now_utc(); + let updated_at = tantivy::DateTime::from_utc(now); + + let cloned_id = id.clone(); + let doc_id = id.clone(); + let doc_attributes = self.builder.build_attributes(&document).await; + let s = stream! { + let (tx, mut rx) = mpsc::channel(32); + + for await chunk_doc in self.build_chunks(cloned_id, source_id.clone(), updated_at, document).await { + let tx = tx.clone(); + let doc_id = doc_id.clone(); + yield tokio::spawn(async move { + match chunk_doc.await { + Ok(Ok(doc)) => { + Some(doc) + } + Ok(Err(e)) => { + warn!("Failed to build chunk for document '{}': {}", doc_id, e); + tx.send(()).await.unwrap_or_else(|e| { + warn!("Failed to send error signal for document '{}': {}", doc_id, e); + }); + None + } + Err(e) => { + warn!("Failed to call build chunk '{}': {}", doc_id, e); + tx.send(()).await.unwrap_or_else(|e| { + warn!("Failed to send error signal for document '{}': {}", doc_id, e); + }); + None + } + } + }); + }; + + // drop tx to signal the end of the stream + // the cloned is dropped in its own thread + drop(tx); + + let mut doc = doc! { + schema.field_id => doc_id, + schema.field_source_id => source_id, + schema.field_corpus => self.corpus, + schema.field_attributes => doc_attributes, + schema.field_updated_at => updated_at, + }; + + yield tokio::spawn(async move { + let mut failed_count = 0; + while (rx.recv().await).is_some() { + failed_count += 1; + } + if failed_count > 0 { + doc.add_u64(schema.field_failed_chunks_count, failed_count as u64); + } + Some(doc) + }); + }; + + (id, s) + } + + async fn build_chunks( + &self, + id: String, + source_id: String, + updated_at: tantivy::DateTime, + document: T, + ) -> impl Stream>> + '_ { + let kind = self.corpus; + stream! { + let schema = IndexSchema::instance(); + for await (chunk_id, task) in self.builder.build_chunk_attributes(&document).await.enumerate() { + let id = id.clone(); + let source_id = source_id.clone(); + + yield tokio::spawn(async move { + let built_chunk_attributes_result = task.await?; + let (tokens, chunk_attributes) = built_chunk_attributes_result?; + + let mut doc = doc! { + schema.field_id => id, + schema.field_source_id => source_id, + schema.field_corpus => kind, + schema.field_updated_at => updated_at, + schema.field_chunk_id => format!("{}-{}", id, chunk_id), + schema.field_chunk_attributes => chunk_attributes, + }; + + for token in &tokens { + doc.add_text(schema.field_chunk_tokens, token); + } + + Ok(doc) + }); + } + } + } + + pub async fn backfill_doc_attributes( + &self, + origin: &TantivyDocument, + doc: &T, + ) -> TantivyDocument { + let schema = IndexSchema::instance(); + let mut doc = doc! { + schema.field_id => get_text(origin, schema.field_id), + schema.field_source_id => get_text(origin, schema.field_source_id).to_string(), + schema.field_corpus => get_text(origin, schema.field_corpus).to_string(), + schema.field_attributes => self.builder.build_attributes(doc).await, + schema.field_updated_at => get_date(origin, schema.field_updated_at), + }; + if let Some(failed_chunks) = get_number_optional(origin, schema.field_failed_chunks_count) { + doc.add_u64(schema.field_failed_chunks_count, failed_chunks as u64); + } + + doc + } +} + +pub struct Indexer { + corpus: String, + searcher: Searcher, + writer: IndexWriter, +} + +impl Indexer { + pub fn new(corpus: &str) -> Self { + let doc = IndexSchema::instance(); + let (_, index) = open_or_create_index(&doc.schema, &path::index_dir()); + let writer = index + .writer(150_000_000) + .expect("Failed to create index writer"); + let reader = index.reader().expect("Failed to create index reader"); + + Self { + corpus: corpus.to_owned(), + searcher: reader.searcher(), + writer, + } + } + + pub async fn add(&self, document: TantivyDocument) { + self.writer + .add_document(document) + .expect("Failed to add document"); + } + + pub async fn get_doc(&self, id: &str) -> Result { + let schema = IndexSchema::instance(); + let query = schema.doc_query(&self.corpus, id); + let docs = match self.searcher.search(&query, &TopDocs::with_limit(1)) { + Ok(docs) => docs, + Err(e) => { + debug!("query tantivy error: {}", e); + return Err(e.into()); + } + }; + if docs.is_empty() { + bail!("Document not found: {}", id); + } + + self.searcher + .doc(docs.first().unwrap().1) + .map_err(|e| e.into()) + } + + // `get_doc_kind` returns the kind of a structured_doc, and `None` for a code. + pub async fn get_doc_kind<'a>(&self, id: &str) -> Result> { + let doc = self.get_doc(id).await?; + let schema = IndexSchema::instance(); + Ok(get_json_text_optional(&doc, schema.field_attributes, KIND).map(|v| v.to_owned())) + } + + /// Lists the latest document IDs based on the given source ID, key-value pairs, and datetime field. + /// + /// The IDs are sorted by the datetime field in descending order and filtered by the given constraints. + pub async fn list_latest_ids( + &self, + source_id: &str, + kvs: &Vec<(&str, &str)>, + datetime_field: &str, + offset: usize, + ) -> Result> { + let schema = IndexSchema::instance(); + let query = schema.doc_with_attribute_field(&self.corpus, source_id, kvs); + let docs = match self + .searcher + .search(&query, &TopDocs::with_limit(u16::MAX as usize)) + { + Ok(docs) => docs, + Err(e) => { + debug!("query tantivy error: {}", e); + return Err(e.into()); + } + }; + if docs.is_empty() { + bail!("No document found: {:?}", kvs); + } + + let mut documents = Vec::new(); + for (_, doc_address) in docs { + let doc: TantivyDocument = self.searcher.doc(doc_address)?; + documents.push(( + get_text(&doc, schema.field_id).to_owned(), + get_json_date_field(&doc, schema.field_attributes, datetime_field), + )); + } + + documents.sort_by(|a, b| b.1.cmp(&a.1)); + + Ok(documents + .iter() + .skip(offset) + .map(|(id, _)| id.to_owned()) + .collect()) + } + + pub async fn count_doc_by_attribute( + &self, + source_id: &str, + kvs: &Vec<(&str, &str)>, + ) -> Result { + let schema = IndexSchema::instance(); + let query = schema.doc_with_attribute_field(&self.corpus, source_id, kvs); + + let count = self.searcher.search(&query, &tantivy::collector::Count)?; + Ok(count) + } + + pub fn delete(&self, id: &str) { + let schema = IndexSchema::instance(); + let _ = self + .writer + .delete_query(Box::new(schema.doc_query_with_chunks(&self.corpus, id))); + } + + pub fn delete_doc(&self, id: &str) { + let schema = IndexSchema::instance(); + let _ = self + .writer + .delete_query(Box::new(schema.doc_query(&self.corpus, id))); + } + + pub fn commit(mut self) { + self.writer.commit().expect("Failed to commit changes"); + self.writer + .wait_merging_threads() + .expect("Failed to wait for merging threads"); + } + + // Check whether the document ID presents in the corpus. + pub fn is_indexed(&self, id: &str) -> bool { + let schema = IndexSchema::instance(); + let query = schema.doc_query(&self.corpus, id); + let Ok(docs) = self.searcher.search(&query, &TopDocs::with_limit(1)) else { + return false; + }; + !docs.is_empty() + } + + /// Iterates over all the document IDs in the corpus. + pub fn iter_ids(&self) -> impl Stream + '_ { + let schema = IndexSchema::instance(); + + stream! { + // Based on https://github.com/quickwit-oss/tantivy/blob/main/examples/iterating_docs_and_positions.rs + for (segment_ordinal, segment_reader) in self.searcher.segment_readers().iter().enumerate() { + let Ok(inverted_index) = segment_reader.inverted_index(schema.field_corpus) else { + continue; + }; + + let term_corpus = Term::from_field_text(schema.field_corpus, &self.corpus); + let Ok(Some(mut postings)) = inverted_index.read_postings(&term_corpus, tantivy::schema::IndexRecordOption::Basic) else { + continue; + }; + + let mut doc_id = postings.doc(); + while doc_id != TERMINATED { + if !segment_reader.is_deleted(doc_id) { + let doc_address = DocAddress::new(segment_ordinal as u32, doc_id); + let doc: TantivyDocument = self.searcher.doc(doc_address).expect("Failed to read document"); + + // Skip chunks, as we only want to iterate over the main docs + if doc.get_first(schema.field_chunk_id).is_none() { + let id = get_text(&doc, schema.field_id); + let source = get_text(&doc, schema.field_source_id); + yield (source.to_owned(), id.to_owned()); + } + } + doc_id = postings.advance(); + } + } + } + } + + pub fn is_indexed_after(&self, id: &str, time: chrono::DateTime) -> bool { + let schema = IndexSchema::instance(); + let query = schema.doc_indexed_after(&self.corpus, id, time); + let Ok(docs) = self.searcher.search(&query, &TopDocs::with_limit(1)) else { + return false; + }; + + !docs.is_empty() + } + + /// Check whether the document has failed chunks. + /// + /// failed chunks tracks the number of embedding indexing failed chunks for a document. + pub fn has_failed_chunks(&self, id: &str) -> bool { + let schema = IndexSchema::instance(); + let query = schema.doc_has_failed_chunks(&self.corpus, id); + let Ok(docs) = self.searcher.search(&query, &TopDocs::with_limit(1)) else { + return false; + }; + + !docs.is_empty() + } + + // Check whether the document has attribute field. + pub fn has_attribute_field(&self, id: &str, field: &str) -> bool { + let schema = IndexSchema::instance(); + let query = schema.doc_has_attribute_field(&self.corpus, id, field); + match self.searcher.search(&query, &TopDocs::with_limit(1)) { + Ok(docs) => !docs.is_empty(), + Err(e) => { + debug!("query tantivy error: {}", e); + false + } + } + } +} + +pub struct IndexGarbageCollector { + searcher: Searcher, + writer: IndexWriter, +} + +impl IndexGarbageCollector { + pub fn new() -> Self { + let doc = IndexSchema::instance(); + let (_, index) = open_or_create_index(&doc.schema, &path::index_dir()); + let writer = index + .writer(150_000_000) + .expect("Failed to create index writer"); + let reader = index.reader().expect("Failed to create index reader"); + + Self { + searcher: reader.searcher(), + writer, + } + } + + pub fn garbage_collect(&self, active_source_ids: &[String]) -> anyhow::Result<()> { + let source_ids: HashSet<_> = active_source_ids.iter().collect(); + + let count_aggregation: Aggregation = serde_json::from_value(json!({ + "terms": { + "field": FIELD_SOURCE_ID, + } + })) + .unwrap(); + + let collector = AggregationCollector::from_aggs( + vec![("count".to_owned(), count_aggregation)] + .into_iter() + .collect(), + Default::default(), + ); + + let res = self.searcher.search(&AllQuery, &collector)?; + let Some(AggregationResult::BucketResult(BucketResult::Terms { buckets, .. })) = + res.0.get("count") + else { + bail!("Failed to get source_id count"); + }; + for source_id in buckets { + let count = source_id.doc_count; + let Key::Str(source_id) = &source_id.key else { + warn!("Failed to get source_id key as string"); + continue; + }; + + if !source_ids.contains(source_id) { + debug!("Deleting {} documents for source_id: {}", count, source_id,); + self.delete_by_source_id(source_id); + } + } + + Ok(()) + } + + fn delete_by_source_id(&self, source_id: &str) { + let schema = IndexSchema::instance(); + let _ = self + .writer + .delete_query(Box::new(schema.source_id_query(source_id))); + } + + pub fn commit(mut self) { + self.writer.commit().expect("Failed to commit changes"); + self.writer + .wait_merging_threads() + .expect("Failed to wait for merging threads"); + } +} + +fn get_text(doc: &TantivyDocument, field: schema::Field) -> &str { + doc.get_first(field).unwrap().as_str().unwrap() +} + +fn get_date(doc: &TantivyDocument, field: schema::Field) -> tantivy::DateTime { + doc.get_first(field).unwrap().as_datetime().unwrap() +} + +fn get_number_optional(doc: &TantivyDocument, field: schema::Field) -> Option { + doc.get_first(field)?.as_i64() +} + +fn get_json_field<'a>( + doc: &'a TantivyDocument, + field: schema::Field, + name: &str, +) -> CompactDocValue<'a> { + doc.get_first(field) + .unwrap() + .as_object() + .unwrap() + .find(|(k, _)| *k == name) + .unwrap() + .1 +} + +fn get_json_date_field(doc: &TantivyDocument, field: schema::Field, name: &str) -> DateTime { + get_json_field(doc, field, name).as_datetime().unwrap() +} + +fn get_json_field_optional<'a>( + doc: &'a TantivyDocument, + field: schema::Field, + name: &str, +) -> Option> { + Some( + doc.get_first(field)? + .as_object()? + .find(|(k, _)| *k == name)? + .1, + ) +} + +fn get_json_text_optional<'a>( + doc: &'a TantivyDocument, + field: schema::Field, + name: &str, +) -> Option<&'a str> { + get_json_field_optional(doc, field, name).map(|v| v.as_str().unwrap()) +} diff --git a/crates/tabby-index/src/indexer_tests.rs b/crates/tabby-index/src/indexer_tests.rs new file mode 100644 index 000000000000..fd1e9a5feef9 --- /dev/null +++ b/crates/tabby-index/src/indexer_tests.rs @@ -0,0 +1,359 @@ +mod mock_embedding { + use anyhow::Result; + use async_trait::async_trait; + use tabby_inference::Embedding; + + pub struct MockEmbedding { + result: Vec, + error: bool, + } + + impl MockEmbedding { + pub fn new(result: Vec, error: bool) -> Self { + Self { result, error } + } + } + + #[async_trait] + impl Embedding for MockEmbedding { + async fn embed(&self, prompt: &str) -> Result> { + if self.error { + Err(anyhow::anyhow!( + "Mock error, prompt length {}", + prompt.len() + )) + } else { + Ok(self.result.clone()) + } + } + } +} + +mod structured_doc_tests { + use std::sync::Arc; + + use serial_test::file_serial; + use tabby_common::index::{corpus, structured_doc::fields as StructuredDocIndexFields}; + use temp_testdir::TempDir; + + use super::mock_embedding::MockEmbedding; + use crate::{ + indexer::Indexer, + public::StructuredDocState, + structured_doc::public::{ + StructuredDoc, StructuredDocFields, StructuredDocIndexer, StructuredDocIssueFields, + }, + }; + + /// the document should be indexed even no embedding is provided + /// the document itself could be used for search + #[test] + #[file_serial(set_tabby_root)] + fn test_structured_doc_empty_embedding() { + let root = tabby_common::path::tabby_root(); + let temp_dir = TempDir::default(); + tabby_common::path::set_tabby_root(temp_dir.to_owned()); + + let id = "structured_doc_empty_embedding"; + let embedding = MockEmbedding::new(vec![], true); + let embedding = Arc::new(embedding); + let indexer = StructuredDocIndexer::new(embedding.clone()); + let doc = StructuredDoc { + source_id: "source".to_owned(), + fields: StructuredDocFields::Issue(StructuredDocIssueFields { + link: id.to_owned(), + title: "title".to_owned(), + author_email: Some("author_email".to_owned()), + body: "body".to_owned(), + closed: false, + }), + }; + + let updated_at = chrono::Utc::now(); + let res = tokio::runtime::Runtime::new().unwrap().block_on(async { + let updated = indexer + .presync(&StructuredDocState { + id: doc.id().to_string(), + updated_at, + deleted: false, + }) + .await + && indexer.sync(doc).await; + println!("{updated}"); + updated + }); + assert!(res); + indexer.commit(); + + let validator = Indexer::new(corpus::STRUCTURED_DOC); + + assert!(validator.is_indexed(id)); + assert!(validator.has_failed_chunks(id)); + + tabby_common::path::set_tabby_root(root); + } + + #[test] + #[file_serial(set_tabby_root)] + fn test_structured_doc_with_embedding() { + let root = tabby_common::path::tabby_root(); + let temp_dir = TempDir::default(); + tabby_common::path::set_tabby_root(temp_dir.to_owned()); + + let id = "structured_doc_with_embedding"; + let embedding = MockEmbedding::new(vec![1.0], false); + let embedding = Arc::new(embedding); + let indexer = StructuredDocIndexer::new(embedding.clone()); + let doc = StructuredDoc { + source_id: "source".to_owned(), + fields: StructuredDocFields::Issue(StructuredDocIssueFields { + link: id.to_owned(), + title: "title".to_owned(), + author_email: Some("author_email".to_owned()), + body: "body".to_owned(), + closed: false, + }), + }; + + let updated_at = chrono::Utc::now(); + let res = tokio::runtime::Runtime::new().unwrap().block_on(async { + let updated = indexer + .presync(&StructuredDocState { + id: doc.id().to_string(), + updated_at, + deleted: false, + }) + .await + && indexer.sync(doc).await; + println!("{updated}"); + updated + }); + assert!(res); + indexer.commit(); + + let validator = Indexer::new(corpus::STRUCTURED_DOC); + + assert!(validator.is_indexed(id)); + assert!(!validator.has_failed_chunks(id)); + + tabby_common::path::set_tabby_root(root); + } + + #[test] + #[file_serial(set_tabby_root)] + fn test_structured_doc_has_attribute_field() { + let root = tabby_common::path::tabby_root(); + let temp_dir = TempDir::default(); + tabby_common::path::set_tabby_root(temp_dir.to_owned()); + + let id = "structured_doc_has_attribute_field"; + let embedding = MockEmbedding::new(vec![1.0], false); + let embedding = Arc::new(embedding); + let indexer = StructuredDocIndexer::new(embedding.clone()); + let doc = StructuredDoc { + source_id: "source".to_owned(), + fields: StructuredDocFields::Issue(StructuredDocIssueFields { + link: id.to_owned(), + title: "title".to_owned(), + author_email: Some("author_email".to_owned()), + body: "body".to_owned(), + closed: false, + }), + }; + + let res = tokio::runtime::Runtime::new() + .unwrap() + .block_on(async { indexer.sync(doc).await }); + assert!(res); + indexer.commit(); + + let validator = Indexer::new(corpus::STRUCTURED_DOC); + + assert!(validator.is_indexed(id)); + assert!(validator.has_attribute_field(id, StructuredDocIndexFields::issue::AUTHOR_EMAIL)); + + tabby_common::path::set_tabby_root(root); + } +} + +mod builder_tests { + use std::sync::Arc; + + use futures::StreamExt; + use serial_test::file_serial; + use tabby_common::index::{corpus, IndexSchema}; + use tantivy::schema::Value; + use temp_testdir::TempDir; + + use super::mock_embedding::MockEmbedding; + use crate::{ + code::{create_code_builder, intelligence::CodeIntelligence}, + indexer::{TantivyDocBuilder, ToIndexId}, + structured_doc::{ + public::{StructuredDoc, StructuredDocFields, StructuredDocIssueFields}, + StructuredDocBuilder, + }, + testutils::{get_repository_config, get_rust_source_file, get_tabby_root}, + }; + + #[test] + #[file_serial(set_tabby_root)] + fn test_builder_code_empty_embedding() { + let origin_root = tabby_common::path::tabby_root(); + tabby_common::path::set_tabby_root(get_tabby_root()); + + let embedding = MockEmbedding::new(vec![], true); + let builder = Arc::new(create_code_builder(Some(Arc::new(embedding)))); + + let repo = get_repository_config(); + let code = CodeIntelligence::compute_source_file(&repo, "commit", &get_rust_source_file()) + .unwrap(); + let index_id = code.to_index_id(); + + let (id, s) = tokio::runtime::Runtime::new() + .unwrap() + .block_on(async { builder.build(code).await }); + assert_eq!(id, index_id.id); + + let res = tokio::runtime::Runtime::new().unwrap().block_on(async { + s.buffer_unordered(std::cmp::max( + std::thread::available_parallelism().unwrap().get() * 2, + 32, + )) + .map(|handler| handler.unwrap()) + .collect::>() + .await + .into_iter() + .flatten() + .collect::>() + }); + + // the chunks should be failed as no embedding is provided + // the last element is the document itself + assert_eq!(res.len(), 1); + let doc = res.last().unwrap(); + + let schema = IndexSchema::instance(); + let failed_count = doc + .get_first(schema.field_failed_chunks_count) + .and_then(|v| v.as_u64()) + .unwrap(); + + // the first three are the chunks and failed, counted as 3 + assert_eq!(failed_count, 3); + + tabby_common::path::set_tabby_root(origin_root); + } + + /// Test that the indexer return the document and none itself + /// when the embedding is empty + #[test] + #[file_serial(set_tabby_root)] + fn test_builder_empty_embedding() { + let root = tabby_common::path::tabby_root(); + let temp_dir = TempDir::default(); + tabby_common::path::set_tabby_root(temp_dir.to_owned()); + + let test_id = "builder_empty_embedding"; + let embedding = MockEmbedding::new(vec![], true); + let builder = StructuredDocBuilder::new(Arc::new(embedding)); + let tantivy_builder = TantivyDocBuilder::new(corpus::STRUCTURED_DOC, builder); + + let doc = StructuredDoc { + source_id: "source".to_owned(), + fields: StructuredDocFields::Issue(StructuredDocIssueFields { + link: test_id.to_owned(), + title: "title".to_owned(), + author_email: Some("author_email".to_owned()), + body: "body".to_owned(), + closed: false, + }), + }; + + let (id, s) = tokio::runtime::Runtime::new() + .unwrap() + .block_on(async { tantivy_builder.build(doc).await }); + assert_eq!(id, test_id); + + let res = tokio::runtime::Runtime::new().unwrap().block_on(async { + s.buffer_unordered(std::cmp::max( + std::thread::available_parallelism().unwrap().get() * 2, + 32, + )) + .map(|handler| handler.unwrap()) + .collect::>() + .await + .into_iter() + .flatten() + .collect::>() + }); + + // The last element is the document itself, + // while the preceding elements are the chunks. + // Given that the embedding is empty, + // all chunks should be considered failed and skipped. + assert_eq!(res.len(), 1); + let doc = res.last().unwrap(); + + let schema = IndexSchema::instance(); + let failed_count = doc + .get_first(schema.field_failed_chunks_count) + .and_then(|v| v.as_u64()) + .unwrap(); + + assert_eq!(failed_count, 1); + + tabby_common::path::set_tabby_root(root); + } + + /// Test that the indexer returns the document and the chunk + /// when the embedding is not empty. + /// when there are embeddings, the failed count should not be existed + #[test] + #[file_serial(set_tabby_root)] + fn test_builder_with_embedding() { + let root = tabby_common::path::tabby_root(); + let temp_dir = TempDir::default(); + tabby_common::path::set_tabby_root(temp_dir.to_owned()); + + let test_id = "builder_with_embedding"; + let embedding = MockEmbedding::new(vec![1.0], false); + let builder = StructuredDocBuilder::new(Arc::new(embedding)); + let tantivy_builder = TantivyDocBuilder::new(corpus::STRUCTURED_DOC, builder); + + let doc = StructuredDoc { + source_id: "source".to_owned(), + fields: StructuredDocFields::Issue(StructuredDocIssueFields { + link: test_id.to_owned(), + title: "title".to_owned(), + author_email: Some("author_email".to_owned()), + body: "body".to_owned(), + closed: false, + }), + }; + + let (id, s) = tokio::runtime::Runtime::new() + .unwrap() + .block_on(async { tantivy_builder.build(doc).await }); + + assert_eq!(id, test_id); + + let res = tokio::runtime::Runtime::new().unwrap().block_on(async { + s.buffer_unordered(std::cmp::max( + std::thread::available_parallelism().unwrap().get() * 2, + 32, + )) + .collect::>() + .await + }); + + // the last element is the document itself + assert_eq!(res.len(), 2); + let doc = res[1].as_ref().unwrap().as_ref().unwrap(); + + let schema = IndexSchema::instance(); + assert!(doc.get_first(schema.field_failed_chunks_count).is_none()); + + tabby_common::path::set_tabby_root(root); + } +} diff --git a/crates/tabby-index/src/lib.rs b/crates/tabby-index/src/lib.rs new file mode 100644 index 000000000000..ddcde1b545ff --- /dev/null +++ b/crates/tabby-index/src/lib.rs @@ -0,0 +1,38 @@ +//! Responsible for scheduling all of the background jobs for tabby. +//! Includes syncing respositories and updating indices. + +mod code; +mod indexer; +mod tantivy_utils; + +#[cfg(test)] +mod testutils; + +use indexer::{IndexAttributeBuilder, Indexer}; + +mod structured_doc; + +#[cfg(test)] +mod indexer_tests; + +pub mod public { + use indexer::IndexGarbageCollector; + + use super::*; + pub use super::{ + code::CodeIndexer, + structured_doc::public::{ + StructuredDoc, StructuredDocCommitFields, StructuredDocFields, + StructuredDocGarbageCollector, StructuredDocIndexer, StructuredDocIngestedFields, + StructuredDocIssueFields, StructuredDocPageFields, StructuredDocPullDocumentFields, + StructuredDocState, StructuredDocWebFields, KIND_COMMIT as STRUCTURED_DOC_KIND_COMMIT, + }, + }; + + pub fn run_index_garbage_collection(active_sources: Vec) -> anyhow::Result<()> { + let index_garbage_collector = IndexGarbageCollector::new(); + index_garbage_collector.garbage_collect(&active_sources)?; + index_garbage_collector.commit(); + Ok(()) + } +} diff --git a/crates/tabby-index/src/structured_doc/mod.rs b/crates/tabby-index/src/structured_doc/mod.rs new file mode 100644 index 000000000000..8501734cdaae --- /dev/null +++ b/crates/tabby-index/src/structured_doc/mod.rs @@ -0,0 +1,52 @@ +pub mod public; +mod types; + +use std::sync::Arc; + +use anyhow::Result; +use async_trait::async_trait; +use futures::stream::BoxStream; +use serde_json::json; +use tabby_common::index::{corpus, structured_doc}; +use tabby_inference::Embedding; +use tokio::task::JoinHandle; +use types::{BuildStructuredDoc, StructuredDoc}; + +use crate::{indexer::TantivyDocBuilder, IndexAttributeBuilder}; + +pub struct StructuredDocBuilder { + embedding: Arc, +} + +impl StructuredDocBuilder { + pub fn new(embedding: Arc) -> Self { + Self { embedding } + } +} + +#[async_trait] +impl IndexAttributeBuilder for StructuredDocBuilder { + async fn build_attributes(&self, document: &StructuredDoc) -> serde_json::Value { + let mut attributes = document.build_attributes().await; + attributes + .as_object_mut() + .unwrap() + .insert(structured_doc::fields::KIND.into(), json!(document.kind())); + attributes + } + + async fn build_chunk_attributes<'a>( + &self, + document: &'a StructuredDoc, + ) -> BoxStream<'a, JoinHandle, serde_json::Value)>>> { + let embedding = self.embedding.clone(); + document.build_chunk_attributes(embedding).await + } +} + +fn create_structured_doc_builder( + embedding: Arc, +) -> TantivyDocBuilder { + let builder = StructuredDocBuilder::new(embedding); + TantivyDocBuilder::new(corpus::STRUCTURED_DOC, builder) +} diff --git a/crates/tabby-index/src/structured_doc/public.rs b/crates/tabby-index/src/structured_doc/public.rs new file mode 100644 index 000000000000..dc2294ffa7a6 --- /dev/null +++ b/crates/tabby-index/src/structured_doc/public.rs @@ -0,0 +1,229 @@ +use std::{future::Future, sync::Arc}; + +use anyhow::Result; +use async_stream::stream; +use chrono::{DateTime, Utc}; +use futures::StreamExt; +use tabby_common::index::{corpus, structured_doc::fields as StructuredDocIndexFields}; +use tabby_inference::Embedding; +use tracing::debug; + +pub use super::types::{ + commit::CommitDocument as StructuredDocCommitFields, + ingested::IngestedDocument as StructuredDocIngestedFields, + issue::IssueDocument as StructuredDocIssueFields, + page::PageDocument as StructuredDocPageFields, + pull::PullDocument as StructuredDocPullDocumentFields, + web::WebDocument as StructuredDocWebFields, StructuredDoc, StructuredDocFields, KIND_COMMIT, + KIND_INGESTED, +}; +use super::{create_structured_doc_builder, types::BuildStructuredDoc}; +use crate::{indexer::TantivyDocBuilder, Indexer}; + +/// StructuredDocState tracks the state of the document source. +/// It helps determine whether the document should be updated or deleted. +pub struct StructuredDocState { + // id is the unique identifier of the document. + // It is used to track the document in the indexer. + pub id: String, + + // updated_at is the time when the document was last updated. + // when the updated_at is earlier than the document's index time, + // the update will be skipped. + pub updated_at: DateTime, + + // deleted indicates whether the document should be removed from the indexer. + // For instance, a closed pull request will be marked as deleted, + // prompting the indexer to remove it from the index. + pub deleted: bool, +} + +pub struct StructuredDocIndexer { + builder: TantivyDocBuilder, + indexer: Indexer, +} + +impl StructuredDocIndexer { + pub fn new(embedding: Arc) -> Self { + let builder = create_structured_doc_builder(embedding); + let indexer = Indexer::new(corpus::STRUCTURED_DOC); + Self { indexer, builder } + } + + // Runs pre-sync checks to determine if the document needs to be updated. + // Returns false if `sync` is not required to be called. + pub async fn presync(&self, state: &StructuredDocState) -> bool { + if state.deleted { + self.indexer.delete(&state.id); + return false; + } + + if self.indexer.is_indexed_after(&state.id, state.updated_at) + && !self.indexer.has_failed_chunks(&state.id) + { + return false; + }; + + true + } + + // The sync process updates the document in the indexer incrementally. + // It first determines whether the document requires an update. + // + // If an update is needed, it checks the deletion state of the document. + // If the document is marked as deleted, it will be removed. + // Next, the document is rebuilt, the original is deleted, and the newly indexed document is added. + pub async fn sync(&self, document: StructuredDoc) -> bool { + if !self.require_updates(&document).await { + return false; + } + + stream! { + let (id, s) = self.builder.build(document).await; + self.indexer.delete(&id); + + for await doc in s.buffer_unordered(std::cmp::max(std::thread::available_parallelism().unwrap().get() * 2, 32)) { + if let Ok(Some(doc)) = doc { + self.indexer.add(doc).await; + } + } + }.count().await; + true + } + + pub async fn delete(&self, id: &str) -> bool { + if self.indexer.is_indexed(id) { + self.indexer.delete(id); + true + } else { + false + } + } + + pub async fn count_doc(&self, source_id: &str, kind: &str) -> Result { + let attributes = vec![(StructuredDocIndexFields::KIND, kind)]; + self.indexer + .count_doc_by_attribute(source_id, &attributes) + .await + } + + pub async fn list_latest_ids( + &self, + source_id: &str, + kind: &str, + datetime_field: &str, + offset: usize, + ) -> Result> { + self.indexer + .list_latest_ids( + source_id, + &vec![(StructuredDocIndexFields::KIND, kind)], + datetime_field, + offset, + ) + .await + } + + pub fn commit(self) { + self.indexer.commit(); + } + + async fn require_updates(&self, document: &StructuredDoc) -> bool { + if document.should_skip() { + return false; + } + + if self.should_backfill(document) { + return true; + } + + if let StructuredDocFields::Commit(_commit) = &document.fields { + if self.indexer.get_doc(document.id()).await.is_ok() { + return false; + } + } + + true + } + + fn should_backfill(&self, document: &StructuredDoc) -> bool { + // v0.22.0 add the author field to the issue and pull documents. + match &document.fields { + StructuredDocFields::Issue(issue) => { + if issue.author_email.is_some() + && !self.indexer.has_attribute_field( + document.id(), + StructuredDocIndexFields::issue::AUTHOR_EMAIL, + ) + { + return true; + } + } + StructuredDocFields::Pull(pull) => { + if pull.author_email.is_some() + && !self.indexer.has_attribute_field( + document.id(), + StructuredDocIndexFields::pull::AUTHOR_EMAIL, + ) + { + return true; + } + } + _ => (), + } + + false + } +} + +pub struct StructuredDocGarbageCollector { + indexer: Indexer, +} + +impl Default for StructuredDocGarbageCollector { + fn default() -> Self { + Self { + indexer: Indexer::new(corpus::STRUCTURED_DOC), + } + } +} + +impl StructuredDocGarbageCollector { + pub async fn run(self, should_keep_ingested: F) -> anyhow::Result<()> + where + F: Fn(String, String) -> Fut + Send + Sync, + Fut: Future + Send, + { + stream! { + let mut num_to_delete = 0; + + for await (source_id, id) in self.indexer.iter_ids() { + let kind = if let Ok(Some(kind)) = self.indexer.get_doc_kind(&id).await { + kind + } else { + continue + }; + + if kind.as_str() == KIND_INGESTED { + let doc_id = if let Some(doc_id) = id.strip_prefix(&format!("{source_id}/")) { + doc_id + } else { + debug!("ingested doc has incorrect id format, deleting, id: {}, source: {}", id, source_id); + num_to_delete += 1; + self.indexer.delete(&id); + continue; + }; + if !should_keep_ingested(source_id.clone(), doc_id.to_owned()).await { + num_to_delete += 1; + self.indexer.delete(&id); + } + } + } + + self.indexer.commit(); + logkit::info!("Finished garbage collection for structured doc index: {num_to_delete} items removed"); + }.collect::<()>().await; + + Ok(()) + } +} diff --git a/crates/tabby-index/src/structured_doc/types.rs b/crates/tabby-index/src/structured_doc/types.rs new file mode 100644 index 000000000000..a5169bbeafed --- /dev/null +++ b/crates/tabby-index/src/structured_doc/types.rs @@ -0,0 +1,138 @@ +pub mod commit; +pub mod ingested; +pub mod issue; +pub mod page; +pub mod pull; +pub mod web; + +use std::sync::Arc; + +use anyhow::{bail, Result}; +use async_trait::async_trait; +use futures::stream::BoxStream; +use tabby_inference::Embedding; +use tokio::task::JoinHandle; +use tracing::warn; + +use crate::indexer::{IndexId, ToIndexId}; + +pub struct StructuredDoc { + pub source_id: String, + pub fields: StructuredDocFields, +} + +pub const KIND_WEB: &str = "web"; +pub const KIND_ISSUE: &str = "issue"; +pub const KIND_PULL: &str = "pull"; +pub const KIND_COMMIT: &str = "commit"; +pub const KIND_PAGE: &str = "page"; +pub const KIND_INGESTED: &str = "ingested"; + +impl StructuredDoc { + pub fn id(&self) -> &str { + match &self.fields { + StructuredDocFields::Web(web) => &web.link, + StructuredDocFields::Issue(issue) => &issue.link, + StructuredDocFields::Pull(pull) => &pull.link, + StructuredDocFields::Commit(commit) => &commit.sha, + StructuredDocFields::Page(page) => &page.link, + StructuredDocFields::Ingested(ingested) => &ingested.id, + } + } + + pub fn kind(&self) -> &'static str { + match &self.fields { + StructuredDocFields::Web(_) => KIND_WEB, + StructuredDocFields::Issue(_) => KIND_ISSUE, + StructuredDocFields::Pull(_) => KIND_PULL, + StructuredDocFields::Commit(_) => KIND_COMMIT, + StructuredDocFields::Page(_) => KIND_PAGE, + StructuredDocFields::Ingested(_) => KIND_INGESTED, + } + } +} + +impl ToIndexId for StructuredDoc { + fn to_index_id(&self) -> IndexId { + IndexId { + source_id: self.source_id.clone(), + id: self.id().to_owned(), + } + } +} + +#[async_trait] +pub trait BuildStructuredDoc { + fn should_skip(&self) -> bool; + + async fn build_attributes(&self) -> serde_json::Value; + async fn build_chunk_attributes( + &self, + embedding: Arc, + ) -> BoxStream<'life0, JoinHandle, serde_json::Value)>>>; +} + +pub enum StructuredDocFields { + Web(web::WebDocument), + Issue(issue::IssueDocument), + Pull(pull::PullDocument), + Commit(commit::CommitDocument), + Page(page::PageDocument), + Ingested(ingested::IngestedDocument), +} + +#[async_trait] +impl BuildStructuredDoc for StructuredDoc { + fn should_skip(&self) -> bool { + match &self.fields { + StructuredDocFields::Web(doc) => doc.should_skip(), + StructuredDocFields::Issue(doc) => doc.should_skip(), + StructuredDocFields::Pull(doc) => doc.should_skip(), + StructuredDocFields::Commit(doc) => doc.should_skip(), + StructuredDocFields::Page(doc) => doc.should_skip(), + StructuredDocFields::Ingested(doc) => doc.should_skip(), + } + } + + async fn build_attributes(&self) -> serde_json::Value { + match &self.fields { + StructuredDocFields::Web(doc) => doc.build_attributes().await, + StructuredDocFields::Issue(doc) => doc.build_attributes().await, + StructuredDocFields::Pull(doc) => doc.build_attributes().await, + StructuredDocFields::Commit(doc) => doc.build_attributes().await, + StructuredDocFields::Page(doc) => doc.build_attributes().await, + StructuredDocFields::Ingested(doc) => doc.build_attributes().await, + } + } + + async fn build_chunk_attributes( + &self, + embedding: Arc, + ) -> BoxStream<'life0, JoinHandle, serde_json::Value)>>> { + match &self.fields { + StructuredDocFields::Web(doc) => doc.build_chunk_attributes(embedding).await, + StructuredDocFields::Issue(doc) => doc.build_chunk_attributes(embedding).await, + StructuredDocFields::Pull(doc) => doc.build_chunk_attributes(embedding).await, + StructuredDocFields::Commit(doc) => doc.build_chunk_attributes(embedding).await, + StructuredDocFields::Page(doc) => doc.build_chunk_attributes(embedding).await, + StructuredDocFields::Ingested(doc) => doc.build_chunk_attributes(embedding).await, + } + } +} + +async fn build_tokens(embedding: Arc, text: &str) -> Result> { + let embedding = match embedding.embed(text).await { + Ok(embedding) => embedding, + Err(err) => { + warn!("Failed to embed chunk text: {}", err); + bail!("Failed to embed chunk text: {}", err); + } + }; + + let mut chunk_embedding_tokens = vec![]; + for token in tabby_common::index::binarize_embedding(embedding.iter()) { + chunk_embedding_tokens.push(token); + } + + Ok(chunk_embedding_tokens) +} diff --git a/crates/tabby-index/src/structured_doc/types/commit.rs b/crates/tabby-index/src/structured_doc/types/commit.rs new file mode 100644 index 000000000000..dce870e8b45f --- /dev/null +++ b/crates/tabby-index/src/structured_doc/types/commit.rs @@ -0,0 +1,55 @@ +use std::sync::Arc; + +use anyhow::Result; +use async_stream::stream; +use async_trait::async_trait; +use chrono::{DateTime, Utc}; +use futures::stream::BoxStream; +use serde_json::json; +use tabby_common::index::structured_doc::fields::commit; +use tabby_inference::Embedding; +use tokio::task::JoinHandle; + +use super::{build_tokens, BuildStructuredDoc}; + +#[derive(Debug)] +pub struct CommitDocument { + pub sha: String, + pub message: String, + pub author_email: String, + pub author_at: DateTime, +} + +#[async_trait] +impl BuildStructuredDoc for CommitDocument { + fn should_skip(&self) -> bool { + false + } + + async fn build_attributes(&self) -> serde_json::Value { + json!({ + commit::SHA: self.sha, + commit::MESSAGE: self.message, + commit::AUTHOR_EMAIL: self.author_email, + commit::AUTHOR_AT: self.author_at, + }) + } + + async fn build_chunk_attributes( + &self, + embedding: Arc, + ) -> BoxStream<'life0, JoinHandle, serde_json::Value)>>> { + let s = stream! { + let embedding = embedding.clone(); + let body = self.message.clone(); + yield tokio::spawn(async move { + match build_tokens(embedding.clone(), &body).await { + Ok(tokens) => Ok((tokens, json!({}))), + Err(err) => Err(err), + } + }); + }; + + Box::pin(s) + } +} diff --git a/crates/tabby-index/src/structured_doc/types/ingested.rs b/crates/tabby-index/src/structured_doc/types/ingested.rs new file mode 100644 index 000000000000..9ae7ac29607b --- /dev/null +++ b/crates/tabby-index/src/structured_doc/types/ingested.rs @@ -0,0 +1,75 @@ +use std::sync::Arc; + +use anyhow::Result; +use async_stream::stream; +use async_trait::async_trait; +use futures::stream::BoxStream; +use serde_json::json; +use tabby_common::index::structured_doc::fields; +use tabby_inference::Embedding; +use text_splitter::TextSplitter; +use tokio::task::JoinHandle; + +use super::{build_tokens, BuildStructuredDoc}; + +pub struct IngestedDocument { + // the link of the document is optional, + // so we use source/doc_id as the unique identifier. + pub id: String, + pub title: String, + pub body: String, + pub link: Option, +} + +#[async_trait] +impl BuildStructuredDoc for IngestedDocument { + fn should_skip(&self) -> bool { + self.body.trim().is_empty() + } + + async fn build_attributes(&self) -> serde_json::Value { + let mut attr = json!({ + fields::ingested::TITLE: self.title, + }); + if let Some(link) = &self.link { + attr.as_object_mut() + .unwrap() + .insert(fields::ingested::LINK.to_string(), json!(link)); + }; + + attr + } + + async fn build_chunk_attributes( + &self, + embedding: Arc, + ) -> BoxStream<'life0, JoinHandle, serde_json::Value)>>> { + let content = format!("{}\n\n{}", self.title, self.body); + + let chunks: Vec<_> = TextSplitter::new(2048) + .chunks(&content) + .map(|x| x.to_owned()) + .collect(); + + let s = stream! { + for chunk_text in chunks { + let embedding = embedding.clone(); + yield tokio::spawn(async move { + let tokens = match build_tokens(embedding.clone(), &chunk_text).await { + Ok(tokens) => tokens, + Err(e) => { + return Err(anyhow::anyhow!("Failed to build tokens for chunk: {}", e)); + } + }; + let chunk = json!({ + fields::ingested::CHUNK_BODY: chunk_text, + }); + + Ok((tokens, chunk)) + }); + } + }; + + Box::pin(s) + } +} diff --git a/crates/tabby-index/src/structured_doc/types/issue.rs b/crates/tabby-index/src/structured_doc/types/issue.rs new file mode 100644 index 000000000000..fb1d2a6b41f0 --- /dev/null +++ b/crates/tabby-index/src/structured_doc/types/issue.rs @@ -0,0 +1,58 @@ +use std::sync::Arc; + +use anyhow::Result; +use async_stream::stream; +use async_trait::async_trait; +use futures::stream::BoxStream; +use serde_json::json; +use tabby_common::index::structured_doc::fields; +use tabby_inference::Embedding; +use tokio::task::JoinHandle; + +use super::{build_tokens, BuildStructuredDoc}; + +pub struct IssueDocument { + pub link: String, + pub title: String, + pub author_email: Option, + pub body: String, + pub closed: bool, +} + +#[async_trait] +impl BuildStructuredDoc for IssueDocument { + fn should_skip(&self) -> bool { + false + } + + async fn build_attributes(&self) -> serde_json::Value { + json!({ + fields::issue::LINK: self.link, + fields::issue::TITLE: self.title, + fields::issue::AUTHOR_EMAIL: self.author_email, + fields::issue::BODY: self.body, + fields::issue::CLOSED: self.closed, + }) + } + + async fn build_chunk_attributes( + &self, + embedding: Arc, + ) -> BoxStream<'life0, JoinHandle, serde_json::Value)>>> { + let text = format!("{}\n\n{}", self.title, self.body); + let s = stream! { + yield tokio::spawn(async move { + let tokens = match build_tokens(embedding, &text).await{ + Ok(tokens) => tokens, + Err(e) => { + return Err(anyhow::anyhow!("Failed to build tokens for text: {}", e)); + } + }; + let chunk_attributes = json!({}); + Ok((tokens, chunk_attributes)) + }) + }; + + Box::pin(s) + } +} diff --git a/crates/tabby-index/src/structured_doc/types/page.rs b/crates/tabby-index/src/structured_doc/types/page.rs new file mode 100644 index 000000000000..4df89df5c633 --- /dev/null +++ b/crates/tabby-index/src/structured_doc/types/page.rs @@ -0,0 +1,66 @@ +use std::sync::Arc; + +use anyhow::Result; +use async_stream::stream; +use async_trait::async_trait; +use futures::stream::BoxStream; +use serde_json::json; +use tabby_common::index::structured_doc::fields; +use tabby_inference::Embedding; +use text_splitter::TextSplitter; +use tokio::task::JoinHandle; + +use super::{build_tokens, BuildStructuredDoc}; + +pub struct PageDocument { + pub link: String, + pub title: String, + pub content: String, +} + +#[async_trait] +impl BuildStructuredDoc for PageDocument { + fn should_skip(&self) -> bool { + self.content.trim().is_empty() + } + + async fn build_attributes(&self) -> serde_json::Value { + json!({ + fields::page::LINK: self.link, + fields::page::TITLE: self.title, + }) + } + + async fn build_chunk_attributes( + &self, + embedding: Arc, + ) -> BoxStream<'life0, JoinHandle, serde_json::Value)>>> { + let content = format!("{}\n\n{}", self.title, self.content); + + let chunks: Vec<_> = TextSplitter::new(2048) + .chunks(&content) + .map(|x| x.to_owned()) + .collect(); + + let s = stream! { + for chunk_text in chunks { + let embedding = embedding.clone(); + yield tokio::spawn(async move { + let tokens = match build_tokens(embedding.clone(), &chunk_text).await { + Ok(tokens) => tokens, + Err(e) => { + return Err(anyhow::anyhow!("Failed to build tokens for chunk: {}", e)); + } + }; + let chunk = json!({ + fields::page::CHUNK_CONTENT: chunk_text, + }); + + Ok((tokens, chunk)) + }); + } + }; + + Box::pin(s) + } +} diff --git a/crates/tabby-index/src/structured_doc/types/pull.rs b/crates/tabby-index/src/structured_doc/types/pull.rs new file mode 100644 index 000000000000..aa2df53ea339 --- /dev/null +++ b/crates/tabby-index/src/structured_doc/types/pull.rs @@ -0,0 +1,68 @@ +use std::sync::Arc; + +use anyhow::Result; +use async_stream::stream; +use async_trait::async_trait; +use futures::stream::BoxStream; +use serde_json::json; +use tabby_common::index::structured_doc::fields; +use tabby_inference::Embedding; +use tokio::task::JoinHandle; + +use super::{build_tokens, BuildStructuredDoc}; + +pub struct PullDocument { + pub link: String, + pub title: String, + pub author_email: Option, + pub body: String, + + /// The diff represents the code changes in this PR, + /// including metadata, affected line ranges, and added (+) or removed (-) lines. + /// For more details on the diff format, refer to: + /// https://git-scm.com/docs/diff-format#_combined_diff_format + /// + /// The diff is only stored if its size is less than or equal to 1MB + pub diff: Option, + pub merged: bool, +} + +#[async_trait] +impl BuildStructuredDoc for PullDocument { + fn should_skip(&self) -> bool { + false + } + + async fn build_attributes(&self) -> serde_json::Value { + json!({ + fields::pull::LINK: self.link, + fields::pull::TITLE: self.title, + fields::pull::AUTHOR_EMAIL: self.author_email, + fields::pull::BODY: self.body, + fields::pull::DIFF: self.diff, + fields::pull::MERGED: self.merged, + }) + } + + async fn build_chunk_attributes( + &self, + embedding: Arc, + ) -> BoxStream<'life0, JoinHandle, serde_json::Value)>>> { + // currently not indexing the diff + let text = format!("{}\n\n{}", self.title, self.body); + let s = stream! { + yield tokio::spawn(async move { + let tokens = match build_tokens(embedding, &text).await{ + Ok(tokens) => tokens, + Err(e) => { + return Err(anyhow::anyhow!("Failed to build tokens for text: {}", e)); + } + }; + let chunk_attributes = json!({}); + Ok((tokens, chunk_attributes)) + }) + }; + + Box::pin(s) + } +} diff --git a/crates/tabby-index/src/structured_doc/types/web.rs b/crates/tabby-index/src/structured_doc/types/web.rs new file mode 100644 index 000000000000..2c8e652bf8da --- /dev/null +++ b/crates/tabby-index/src/structured_doc/types/web.rs @@ -0,0 +1,82 @@ +use std::{collections::HashSet, sync::Arc}; + +use anyhow::Result; +use async_stream::stream; +use async_trait::async_trait; +use futures::stream::BoxStream; +use serde_json::json; +use tabby_common::index::structured_doc::fields; +use tabby_inference::Embedding; +use text_splitter::TextSplitter; +use tokio::task::JoinHandle; + +use super::{build_tokens, BuildStructuredDoc}; + +pub struct WebDocument { + pub link: String, + pub title: String, + pub body: String, +} + +#[async_trait] +impl BuildStructuredDoc for WebDocument { + fn should_skip(&self) -> bool { + self.body.trim().is_empty() + } + + async fn build_attributes(&self) -> serde_json::Value { + json!({ + fields::web::TITLE: self.title, + fields::web::LINK: self.link, + }) + } + + async fn build_chunk_attributes( + &self, + embedding: Arc, + ) -> BoxStream<'life0, JoinHandle, serde_json::Value)>>> { + let chunks: Vec<_> = TextSplitter::new(2048) + .chunks(&self.body) + .map(|x| x.to_owned()) + .collect(); + + let title_embedding_tokens = match build_tokens(embedding.clone(), &self.title).await { + Ok(tokens) => tokens, + Err(e) => { + return Box::pin(stream! { + yield tokio::spawn(async move { + Err(anyhow::anyhow!("Failed to build tokens for title: {}", e)) + }); + }); + } + }; + let s = stream! { + for chunk_text in chunks { + let title_embedding_tokens = title_embedding_tokens.clone(); + let embedding = embedding.clone(); + yield tokio::spawn(async move { + let chunk_embedding_tokens = match build_tokens(embedding.clone(), &chunk_text).await { + Ok(tokens) => tokens, + Err(e) => { + return Err(anyhow::anyhow!("Failed to build tokens for chunk: {}", e)); + } + }; + let chunk = json!({ + fields::web::CHUNK_TEXT: chunk_text, + }); + + // Title embedding tokens are merged with chunk embedding tokens to enhance the search results. + let tokens = merge_tokens(vec![title_embedding_tokens, chunk_embedding_tokens]); + Ok((tokens, chunk)) + }); + } + }; + + Box::pin(s) + } +} + +pub fn merge_tokens(tokens: Vec>) -> Vec { + let tokens = tokens.into_iter().flatten().collect::>(); + tokens.into_iter().collect() +} diff --git a/crates/tabby-index/src/tantivy_utils.rs b/crates/tabby-index/src/tantivy_utils.rs new file mode 100644 index 000000000000..862f5902e316 --- /dev/null +++ b/crates/tabby-index/src/tantivy_utils.rs @@ -0,0 +1,31 @@ +use std::{fs, path::Path}; + +use tantivy::{directory::MmapDirectory, schema::Schema, Index}; +use tracing::{debug, warn}; + +pub fn open_or_create_index(code: &Schema, path: &Path) -> (bool, Index) { + let (recreated, index) = match open_or_create_index_impl(code, path) { + Ok(index) => (false, index), + Err(err) => { + warn!( + "Failed to open index repositories: {}, removing index directory '{}'...", + err, + path.display() + ); + fs::remove_dir_all(path).expect("Failed to remove index directory"); + + debug!("Reopening index repositories..."); + ( + true, + open_or_create_index_impl(code, path).expect("Failed to open index"), + ) + } + }; + (recreated, index) +} + +fn open_or_create_index_impl(code: &Schema, path: &Path) -> tantivy::Result { + fs::create_dir_all(path).expect("Failed to create index directory"); + let directory = MmapDirectory::open(path).expect("Failed to open index directory"); + Index::open_or_create(directory, code.clone()) +} diff --git a/crates/tabby-index/src/testutils.rs b/crates/tabby-index/src/testutils.rs new file mode 100644 index 000000000000..33b199826438 --- /dev/null +++ b/crates/tabby-index/src/testutils.rs @@ -0,0 +1,25 @@ +use std::path::PathBuf; + +use tabby_common::config::{config_index_to_id, CodeRepository}; + +pub fn get_tabby_root() -> PathBuf { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("testdata"); + path +} + +pub fn get_repository_config() -> CodeRepository { + CodeRepository::new( + "https://github.com/TabbyML/tabby", + &config_index_to_id(0), + vec![], + ) +} + +pub fn get_rust_source_file() -> PathBuf { + let mut path = get_tabby_root(); + path.push("repositories"); + path.push("https_github.com_TabbyML_tabby"); + path.push("rust.rs"); + path +} diff --git a/crates/tabby-index/testdata/repositories/https_github.com_TabbyML_tabby/rust.rs b/crates/tabby-index/testdata/repositories/https_github.com_TabbyML_tabby/rust.rs new file mode 100644 index 000000000000..dcf12a742f39 --- /dev/null +++ b/crates/tabby-index/testdata/repositories/https_github.com_TabbyML_tabby/rust.rs @@ -0,0 +1,34 @@ +mod metrics { + use std::cmp::max; + + pub fn max_line_length(content: &str) -> usize { + content.lines().map(|x| x.len()).reduce(max).unwrap_or(0) + } + + pub fn avg_line_length(content: &str) -> f32 { + let mut total = 0; + let mut len = 0; + for x in content.lines() { + len += 1; + total += x.len(); + } + + if len > 0 { + total as f32 / len as f32 + } else { + 0.0 + } + } + + pub fn alphanum_fraction(content: &str) -> f32 { + let num_alphanumn: f32 = content + .chars() + .map(|x| f32::from(u8::from(x.is_alphanumeric()))) + .sum(); + if !content.is_empty() { + num_alphanumn / content.len() as f32 + } else { + 0.0 + } + } +} \ No newline at end of file diff --git a/crates/tabby-inference/Cargo.toml b/crates/tabby-inference/Cargo.toml index 2ddbb64cbfdb..6b9854ba848d 100644 --- a/crates/tabby-inference/Cargo.toml +++ b/crates/tabby-inference/Cargo.toml @@ -1,15 +1,22 @@ [package] name = "tabby-inference" -version = "0.8.0" -edition = "2021" +version.workspace = true +edition.workspace = true +authors.workspace = true +homepage.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +anyhow.workspace = true async-stream = { workspace = true } async-trait = { workspace = true } dashmap = "5.5.3" -derive_builder = "0.12.0" +derive_builder.workspace = true futures = { workspace = true } -regex.workspace = true tabby-common = { path = "../tabby-common" } +trie-rs = "0.1.1" +async-openai-alt.workspace = true +secrecy = "0.8" +reqwest.workspace = true +tracing.workspace = true diff --git a/crates/tabby-inference/src/chat.rs b/crates/tabby-inference/src/chat.rs new file mode 100644 index 000000000000..b76b0b2f9f89 --- /dev/null +++ b/crates/tabby-inference/src/chat.rs @@ -0,0 +1,146 @@ +use async_openai_alt::{ + config::OpenAIConfig, + error::OpenAIError, + types::{ + ChatCompletionResponseStream, CreateChatCompletionRequest, CreateChatCompletionResponse, + }, +}; +use async_trait::async_trait; +use derive_builder::Builder; +use tracing::warn; + +#[async_trait] +pub trait ChatCompletionStream: Sync + Send { + async fn chat( + &self, + request: CreateChatCompletionRequest, + ) -> Result; + + async fn chat_stream( + &self, + request: CreateChatCompletionRequest, + ) -> Result; +} + +#[derive(Builder, Clone)] +pub struct ExtendedOpenAIConfig { + #[builder(default)] + kind: String, + + base: OpenAIConfig, + + #[builder(setter(into))] + model_name: String, + + #[builder(setter(into))] + supported_models: Option>, +} + +impl ExtendedOpenAIConfig { + pub fn builder() -> ExtendedOpenAIConfigBuilder { + ExtendedOpenAIConfigBuilder::default() + } + + fn process_request( + &self, + mut request: CreateChatCompletionRequest, + ) -> CreateChatCompletionRequest { + if request.model.is_empty() { + request.model = self.model_name.clone(); + } else if let Some(supported_models) = &self.supported_models { + if !supported_models.contains(&request.model) { + warn!( + "Warning: {} model is not supported, falling back to {}", + request.model, self.model_name + ); + request.model = self.model_name.clone(); + } + } + + match self.kind.as_str() { + "mistral/chat" => { + request.presence_penalty = None; + request.user = None; + request.stream_options = None; + } + "openai/chat" => { + request = process_request_openai(request); + } + _ => {} + } + + request + } +} + +fn process_request_openai(request: CreateChatCompletionRequest) -> CreateChatCompletionRequest { + let mut request = request; + + // Check for specific O-series model prefixes + if request.model.starts_with("o1") || request.model.starts_with("o3-mini") { + request.presence_penalty = None; + request.frequency_penalty = None; + } + + request +} + +impl async_openai_alt::config::Config for ExtendedOpenAIConfig { + fn headers(&self) -> reqwest::header::HeaderMap { + self.base.headers() + } + + fn url(&self, path: &str) -> String { + self.base.url(path) + } + + fn query(&self) -> Vec<(&str, &str)> { + self.base.query() + } + + fn api_base(&self) -> &str { + self.base.api_base() + } + + fn api_key(&self) -> &secrecy::Secret { + self.base.api_key() + } +} + +#[async_trait] +impl ChatCompletionStream for async_openai_alt::Client { + async fn chat( + &self, + request: CreateChatCompletionRequest, + ) -> Result { + let request = self.config().process_request(request); + self.chat().create(request).await + } + + async fn chat_stream( + &self, + request: CreateChatCompletionRequest, + ) -> Result { + let request = self.config().process_request(request); + self.chat().create_stream(request).await + } +} + +#[async_trait] +impl ChatCompletionStream for async_openai_alt::Client { + async fn chat( + &self, + request: CreateChatCompletionRequest, + ) -> Result { + let request = process_request_openai(request); + self.chat().create(request).await + } + + async fn chat_stream( + &self, + request: CreateChatCompletionRequest, + ) -> Result { + let request = process_request_openai(request); + self.chat().create_stream(request).await + } +} diff --git a/crates/tabby-inference/src/code.rs b/crates/tabby-inference/src/code.rs new file mode 100644 index 000000000000..90e8e45f2b2a --- /dev/null +++ b/crates/tabby-inference/src/code.rs @@ -0,0 +1,102 @@ +use std::sync::Arc; + +use async_stream::stream; +use derive_builder::Builder; +use futures::StreamExt; +use tabby_common::{config::ModelConfig, languages::Language}; + +use crate::{ + clip_prompt, decoding::StopConditionFactory, CompletionOptionsBuilder, CompletionStream, +}; + +#[derive(Builder, Debug)] +pub struct CodeGenerationOptions { + #[builder(default = "1024")] + pub max_input_length: usize, + + #[builder(default = "256")] + pub max_decoding_tokens: i32, + + #[builder(default = "0.1")] + pub sampling_temperature: f32, + + #[builder(default = "crate::default_seed()")] + pub seed: u64, + + #[builder(default = "None")] + pub language: Option<&'static Language>, + + #[builder(default = "\"standard\".to_string()")] + pub mode: String, +} + +/// CodeGeneration utilizes the CompletionStream to generate code completions. +/// It employs the StopConditionFactory to maintain a list of stop conditions by language, then +/// reads and decodes the stream, ceasing code generation when a stop condition is met. +pub struct CodeGeneration { + imp: Arc, + stop_condition_factory: StopConditionFactory, +} + +impl CodeGeneration { + pub fn new(imp: Arc, config: Option) -> Self { + let additional_stop_words = match config { + Some(ModelConfig::Local(config)) => config.additional_stop_words.unwrap_or_default(), + Some(ModelConfig::Http(config)) => config.additional_stop_words.unwrap_or_default(), + _ => vec![], + }; + let stop_condition_factory = StopConditionFactory::with_stop_words(additional_stop_words); + + Self { + imp, + stop_condition_factory, + } + } +} + +impl CodeGeneration { + pub async fn generate(&self, prompt: &str, options: CodeGenerationOptions) -> String { + // Clip prompt by options.max_input_length (truncate from beginning) + let prompt = if options.max_input_length > 0 { + clip_prompt(prompt, options.max_input_length) + } else { + prompt + }; + + let completion_options = CompletionOptionsBuilder::default() + .max_decoding_tokens(options.max_decoding_tokens) + .sampling_temperature(options.sampling_temperature) + .seed(options.seed) + .build() + .expect("Failed to build completion options"); + + if options.mode == "next_edit_suggestion" { + tracing::debug!("Using generate_sync for next_edit_suggestion mode"); + return self.imp.generate_sync(prompt, completion_options).await; + } + + // For standard mode, use streaming with stop conditions + let s = stream! { + let mut text = String::new(); + let mut stop_condition = self.stop_condition_factory.create( + prompt, + options.language, + ); + + for await new_text in self.imp.generate(prompt, completion_options).await { + let (should_stop, stop_length) = stop_condition.should_stop(&new_text); + text += &new_text; + if should_stop { + // stop condition matched against prompt + generated text. There's a chance that stop_length >= text.len(); + let new_text_length = text.len().checked_sub(stop_length).unwrap_or_default(); + text.truncate(new_text_length); + break; + } + } + + yield text; + }; + + Box::pin(s).into_future().await.0.unwrap_or_default() + } +} diff --git a/crates/tabby-inference/src/completion.rs b/crates/tabby-inference/src/completion.rs new file mode 100644 index 000000000000..599ff03de59a --- /dev/null +++ b/crates/tabby-inference/src/completion.rs @@ -0,0 +1,33 @@ +use async_trait::async_trait; +use derive_builder::Builder; +use futures::{stream::BoxStream, StreamExt}; + +#[derive(Builder, Debug)] +pub struct CompletionOptions { + pub max_decoding_tokens: i32, + + pub sampling_temperature: f32, + + pub seed: u64, + + #[builder(default = "0.0")] + pub presence_penalty: f32, +} + +#[async_trait] +pub trait CompletionStream: Sync + Send { + /// Generate a completion in streaming mode + async fn generate(&self, prompt: &str, options: CompletionOptions) + -> BoxStream<'life0, String>; + + /// Generate a completion in non-streaming mode + /// Returns the full completion as a single string + async fn generate_sync(&self, prompt: &str, options: CompletionOptions) -> String { + let mut stream = self.generate(prompt, options).await; + let mut result = String::new(); + while let Some(chunk) = stream.next().await { + result.push_str(&chunk); + } + result + } +} diff --git a/crates/tabby-inference/src/decoding.rs b/crates/tabby-inference/src/decoding.rs index d40c571929ac..514ea5eed5c1 100644 --- a/crates/tabby-inference/src/decoding.rs +++ b/crates/tabby-inference/src/decoding.rs @@ -1,9 +1,10 @@ use dashmap::DashMap; -use regex::Regex; use tabby_common::languages::Language; +use trie_rs::{Trie, TrieBuilder}; pub struct StopConditionFactory { - stop_regex_cache: DashMap, + stop_trie_cache: DashMap>, + stop_words_from_model_config: Vec, } fn reverse(s: T) -> String @@ -16,115 +17,138 @@ where impl Default for StopConditionFactory { fn default() -> Self { Self { - stop_regex_cache: DashMap::new(), + stop_trie_cache: DashMap::new(), + stop_words_from_model_config: vec![], } } } +type CachedTrie<'a> = dashmap::mapref::one::Ref<'a, String, Trie>; + impl StopConditionFactory { - pub fn create( - &self, - text: &str, - max_decoding_length: usize, - language: Option<&'static Language>, - ) -> StopCondition { - if let Some(language) = language { - StopCondition::new(self.get_re(language), max_decoding_length, text) - } else { - StopCondition::new(None, max_decoding_length, text) + pub fn with_stop_words(stop_words: Vec) -> Self { + Self { + stop_trie_cache: DashMap::new(), + stop_words_from_model_config: stop_words, } } - fn get_re(&self, language: &'static Language) -> Option { - let stop_words = language.get_stop_words(); - if stop_words.is_empty() { - None + pub fn create(&self, text: &str, language: Option<&'static Language>) -> StopCondition<'_> { + if let Some(language) = language { + StopCondition::new(self.get_trie(language), text) } else { - let hashkey = language.get_hashkey(); - let mut re = self.stop_regex_cache.get(&hashkey); - if re.is_none() { - self.stop_regex_cache - .insert(hashkey.clone(), create_stop_regex(stop_words)); - re = self.stop_regex_cache.get(&hashkey); - } - re.map(|x| x.value().clone()) + StopCondition::new(None, text) } } - pub fn trim_stop_words(&self, language: &'static Language, text: &str) -> Option { - let Some(re) = self.get_re(language) else { - return None; - }; - - let text = reverse(text); + fn get_trie<'a>(&'a self, language: &'static Language) -> Option> { + let mut stop_words = language.get_stop_words(); + // append model stop words + stop_words.extend(self.stop_words_from_model_config.iter().cloned()); - let text = if let Some(m) = re.find_at(&text, 0) { - &text[m.end()..] + if stop_words.is_empty() { + None } else { - &text - }; + let hashkey = language.language().to_owned(); + let mut trie = self.stop_trie_cache.get(&hashkey); + if trie.is_none() { + self.stop_trie_cache + .insert(hashkey.clone(), create_stop_trie(stop_words)); + trie = self.stop_trie_cache.get(&hashkey); + } - Some(reverse(text)) + trie + } } } -fn create_stop_regex(stop_words: Vec) -> Regex { - // (?m) enables multi-line matching mode. - // \A means absolute begins of string. - let reversed_stop_words: Vec<_> = stop_words - .iter() - .map(|x| regex::escape(&reverse(x))) - .collect(); - let regex_string = r"(?m)\A".to_owned() + "((" + &reversed_stop_words.join(")|(") + "))"; - Regex::new(®ex_string).expect("Failed to create regex") +fn create_stop_trie(stop_words: Vec) -> Trie { + let mut builder = TrieBuilder::new(); + for word in stop_words { + builder.push(reverse(word)) + } + builder.build() } -pub struct StopCondition { - stop_re: Option, - max_decoding_length: usize, +pub struct StopCondition<'a> { + stop_trie: Option>, reversed_text: String, num_decoded: usize, } -impl StopCondition { - pub fn new(stop_re: Option, max_decoding_length: usize, text: &str) -> Self { +impl<'a> StopCondition<'a> { + pub fn new(stop_trie: Option>, text: &str) -> Self { Self { - stop_re, - max_decoding_length, + stop_trie, reversed_text: reverse(text), num_decoded: 0, } } - pub fn should_stop(&mut self, new_text: &str) -> bool { + pub fn should_stop(&mut self, new_text: &str) -> (bool, usize) { + self.num_decoded += 1; if !new_text.is_empty() { self.reversed_text = reverse(new_text) + &self.reversed_text; - if let Some(re) = &self.stop_re { - if re.is_match(&self.reversed_text) { - return true; + if let Some(re) = &self.stop_trie { + let matches = re.common_prefix_search(&self.reversed_text); + let matched_length = matches.into_iter().map(|x| x.len()).max(); + if let Some(matched_length) = matched_length { + return (true, matched_length); } } } - - self.num_decoded += 1; - self.num_decoded >= self.max_decoding_length + (false, 0) } } #[cfg(test)] mod tests { + + use tabby_common::languages::UNKNOWN_LANGUAGE; + use super::*; #[test] - fn test_it_works() { + fn test_trie_works() { let text = reverse("void write_u32(std::uint32_t val) const {\n write_raw(&val, sizeof(val));\n }\n\n ~llama_file() {\n if (fp) {\n std::fclose(fp);\n }\n }\n};\n\nvoid"); - assert!(!create_stop_regex(vec!["\n\n".to_owned(), "\n\n ".to_owned()]).is_match(&text)); - assert!(create_stop_regex(vec![ + + let trie = create_stop_trie(vec!["\n\n".to_owned(), "\n\n ".to_owned()]); + assert!(trie.common_prefix_search(&text).is_empty()); + + let trie = create_stop_trie(vec![ "\n\n".to_owned(), "\n\n ".to_owned(), - "\nvoid".to_owned() - ]) - .is_match(&text)); + "\nvoid".to_owned(), + "<|file_sep|>".to_owned(), // qwen 2.5 coder style + ]); + assert!(!trie.common_prefix_search(&text).is_empty()); + + let qwen25coder = reverse("qwen25 style stop words;<|file_sep|>"); + assert!(!trie.common_prefix_search(qwen25coder).is_empty()); + } + + #[test] + fn test_stop_condition_max_length() { + let factory = StopConditionFactory::default(); + let mut cond = factory.create("", Some(&UNKNOWN_LANGUAGE)); + let (should_stop, _) = cond.should_stop("1"); + assert!(!should_stop); + let (should_stop, _) = cond.should_stop("2"); + assert!(!should_stop); + let (should_stop, _) = cond.should_stop("3"); + assert!(!should_stop); + let (should_stop, _) = cond.should_stop("4"); + assert!(!should_stop) + } + + #[test] + fn test_stop_condition_additional_stop_words() { + let factory = StopConditionFactory::with_stop_words(vec!["<|endoftext|>".to_owned()]); + let mut cond = factory.create("", Some(&UNKNOWN_LANGUAGE)); + let (should_stop, _) = cond.should_stop("1"); + assert!(!should_stop); + let (should_stop, _) = cond.should_stop("<|endoftext|>"); + assert!(should_stop); } } diff --git a/crates/tabby-inference/src/embedding.rs b/crates/tabby-inference/src/embedding.rs new file mode 100644 index 000000000000..428a4195f112 --- /dev/null +++ b/crates/tabby-inference/src/embedding.rs @@ -0,0 +1,6 @@ +use async_trait::async_trait; + +#[async_trait] +pub trait Embedding: Sync + Send { + async fn embed(&self, prompt: &str) -> anyhow::Result>; +} diff --git a/crates/tabby-inference/src/lib.rs b/crates/tabby-inference/src/lib.rs index 180f792aa806..1ad1947b241a 100644 --- a/crates/tabby-inference/src/lib.rs +++ b/crates/tabby-inference/src/lib.rs @@ -1,68 +1,69 @@ //! Lays out the abstract definition of a text generation model, and utilities for encodings. -pub mod decoding; +mod chat; +mod code; +mod completion; +mod decoding; +mod embedding; -use async_trait::async_trait; -use derive_builder::Builder; -use futures::stream::BoxStream; -use tabby_common::languages::Language; +pub use chat::{ChatCompletionStream, ExtendedOpenAIConfig}; +pub use code::{CodeGeneration, CodeGenerationOptions, CodeGenerationOptionsBuilder}; +pub use completion::{CompletionOptions, CompletionOptionsBuilder, CompletionStream}; +pub use embedding::Embedding; -#[derive(Builder, Debug)] -pub struct TextGenerationOptions { - #[builder(default = "1024")] - pub max_input_length: usize, - - #[builder(default = "256")] - pub max_decoding_length: usize, - - #[builder(default = "0.1")] - pub sampling_temperature: f32, - - #[builder(default = "0")] - pub seed: u64, - - #[builder(default = "None")] - pub language: Option<&'static Language>, +fn default_seed() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|x| x.as_millis() as u64) + .unwrap_or_default() } -impl TextGenerationOptions { - pub fn default_seed() -> u64 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_millis() as u64 +/// Clip the prompt and retain only the latter part of the prompt, +/// limiting the content to a maximum of `max_length` characters, +/// ensuring that the &str is valid UTF-8. +/// +/// This is necessary because the prompt may be split in the middle of a multi-byte character +/// which would cause an panic. +pub fn clip_prompt(prompt: &str, max_length: usize) -> &str { + if prompt.len() <= max_length { + return prompt; } -} -#[async_trait] -pub trait TextGeneration: Sync + Send { - async fn generate(&self, prompt: &str, options: TextGenerationOptions) -> String; - async fn generate_stream( - &self, - prompt: &str, - options: TextGenerationOptions, - ) -> BoxStream; -} + let mut start = prompt.len() - max_length; + while !prompt.is_char_boundary(start) { + start += 1; + } -pub mod helpers { - use async_stream::stream; - use futures::{pin_mut, stream::BoxStream, Stream, StreamExt}; + &prompt[start..] +} - pub async fn stream_to_string(s: impl Stream) -> String { - pin_mut!(s); +#[cfg(test)] +mod tests { + use super::*; - let mut text = "".to_owned(); - while let Some(value) = s.next().await { - text += &value; - } + #[test] + fn test_clip_prompt() { + assert_eq!(clip_prompt("hello", 5), "hello"); + assert_eq!(clip_prompt("hello", 3), "llo"); - text - } + // assert_eq!("é".as_bytes().len(), 2); // Latin-1 Supplement has length 2 + assert_eq!(clip_prompt("1é2", 1), "2"); + assert_eq!(clip_prompt("1é2", 2), "2"); + assert_eq!(clip_prompt("1é2", 3), "é2"); + assert_eq!(clip_prompt("1é2", 4), "1é2"); - pub async fn string_to_stream(s: String) -> BoxStream<'static, String> { - let stream = stream! { - yield s - }; + // assert_eq!("世".as_bytes().len(), 3); // CJK has length 3 + assert_eq!(clip_prompt("1世2", 1), "2"); + assert_eq!(clip_prompt("1世2", 2), "2"); + assert_eq!(clip_prompt("1世2", 3), "2"); + assert_eq!(clip_prompt("1世2", 4), "世2"); + assert_eq!(clip_prompt("1世2", 5), "1世2"); - Box::pin(stream) + // assert_eq!("😀".as_bytes().len(), 4); // Emoji has length 4 + assert_eq!(clip_prompt("1😀2", 1), "2"); + assert_eq!(clip_prompt("1😀2", 2), "2"); + assert_eq!(clip_prompt("1😀2", 3), "2"); + assert_eq!(clip_prompt("1😀2", 4), "2"); + assert_eq!(clip_prompt("1😀2", 5), "😀2"); + assert_eq!(clip_prompt("1😀2", 6), "1😀2"); } } diff --git a/crates/tabby-scheduler/Cargo.toml b/crates/tabby-scheduler/Cargo.toml deleted file mode 100644 index a9b49788caca..000000000000 --- a/crates/tabby-scheduler/Cargo.toml +++ /dev/null @@ -1,37 +0,0 @@ -[package] -name = "tabby-scheduler" -version = "0.8.0" -edition = "2021" - -[dependencies] -anyhow = { workspace = true } -job_scheduler = "1.2.1" -tabby-common = { path = "../tabby-common" } -tantivy = { workspace = true } -tracing = { workspace = true } -tree-sitter-tags = "0.20.2" -lazy_static = { workspace = true } -serde-jsonlines = { workspace = true } -file-rotate = "0.7.5" -tree-sitter-python = "0.20.2" -tree-sitter-java = "0.20.2" -tree-sitter-kotlin = "0.3.1" -tree-sitter-rust = "0.20.3" -tree-sitter-typescript = "0.20.3" -tree-sitter-go = "0.20.0" -tree-sitter-ruby = "0.20.0" -tree-sitter-c = { git = "https://github.com/tree-sitter/tree-sitter-c/", rev = "212a80f" } -tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "a714740" } -tree-sitter-c-sharp = "0.20.0" -ignore = "0.4.20" -kdam = { version = "0.5.0" } -requirements = "0.3.0" -serdeconv.workspace = true -cargo-lock = { version = "9.0.0", features = ["dependency-tree"] } - -[dev-dependencies] -temp_testdir = "0.2" -tabby-common = { path = "../tabby-common", features = [ "testutils" ] } -tracing-test = "0.1" -tokio = { workspace = true, features = ["rt", "macros"] } -serde_json = { workspace = true } diff --git a/crates/tabby-scheduler/src/dataset.rs b/crates/tabby-scheduler/src/dataset.rs deleted file mode 100644 index 238cdc3a48b5..000000000000 --- a/crates/tabby-scheduler/src/dataset.rs +++ /dev/null @@ -1,197 +0,0 @@ -mod deps; -mod tags; - -use std::{ - collections::HashMap, - ffi::OsStr, - fs::{self, read_to_string}, - io::{IsTerminal, Write}, -}; - -use anyhow::Result; -use file_rotate::{compression::Compression, suffix::AppendCount, ContentLimit, FileRotate}; -use ignore::{DirEntry, Walk}; -use kdam::BarExt; -use lazy_static::lazy_static; -use serde_jsonlines::WriteExt; -use tabby_common::{ - config::RepositoryConfig, - path::{dataset_dir, dependency_file}, - DependencyFile, SourceFile, -}; -use tracing::error; -use tree_sitter_tags::TagsContext; - -use crate::utils::tqdm; - -trait RepositoryExt { - fn create_dataset(&self, writer: &mut impl Write) -> Result<()>; -} - -impl RepositoryExt for RepositoryConfig { - fn create_dataset(&self, writer: &mut impl Write) -> Result<()> { - let dir = self.dir(); - - let walk_dir_iter = || { - Walk::new(dir.as_path()) - .filter_map(Result::ok) - .filter(is_source_code) - }; - - let mut pb = std::io::stdout() - .is_terminal() - .then(|| tqdm(walk_dir_iter().count())); - let walk_dir = walk_dir_iter(); - - let mut context = TagsContext::new(); - for entry in walk_dir { - pb.as_mut().map(|b| b.update(1)).transpose()?; - - let relative_path = entry.path().strip_prefix(dir.as_path()).unwrap(); - let language = get_language(relative_path.extension().unwrap()) - .unwrap() - .to_owned(); - match read_to_string(entry.path()) { - Ok(file_content) => { - let source_file = SourceFile { - git_url: self.git_url.clone(), - filepath: relative_path.display().to_string(), - max_line_length: metrics::max_line_length(&file_content), - avg_line_length: metrics::avg_line_length(&file_content), - alphanum_fraction: metrics::alphanum_fraction(&file_content), - tags: tags::collect(&mut context, &language, &file_content), - language, - content: file_content, - }; - writer.write_json_lines([source_file])?; - } - Err(e) => { - error!("Cannot read {relative_path:?}: {e:?}"); - } - } - } - - Ok(()) - } -} - -fn get_language(ext: &OsStr) -> Option<&str> { - let ext = ext.to_str().unwrap_or(""); - EXTENSION_LANGUAGE.get(ext).copied() -} - -fn is_source_code(entry: &DirEntry) -> bool { - if entry.file_type().is_some_and(|x| x.is_file()) { - entry.path().extension().and_then(get_language).is_some() - } else { - false - } -} - -pub fn create_dataset(config: &Vec) -> Result<()> { - fs::remove_dir_all(dataset_dir()).ok(); - fs::create_dir_all(dataset_dir())?; - let mut writer = FileRotate::new( - SourceFile::files_jsonl(), - AppendCount::new(usize::max_value()), - ContentLimit::Lines(1000), - Compression::None, - #[cfg(unix)] - None, - ); - - let mut deps = DependencyFile::default(); - for repository in config { - deps::collect(repository.dir().as_path(), &mut deps); - repository.create_dataset(&mut writer)?; - } - - serdeconv::to_json_file(&deps, dependency_file())?; - - writer.flush()?; - Ok(()) -} - -mod metrics { - use std::cmp::max; - - pub fn max_line_length(content: &str) -> usize { - content.lines().map(|x| x.len()).reduce(max).unwrap_or(0) - } - - pub fn avg_line_length(content: &str) -> f32 { - let mut total = 0; - let mut len = 0; - for x in content.lines() { - len += 1; - total += x.len(); - } - - if len > 0 { - total as f32 / len as f32 - } else { - 0.0 - } - } - - pub fn alphanum_fraction(content: &str) -> f32 { - let num_alphanumn: f32 = content - .chars() - .map(|x| f32::from(u8::from(x.is_alphanumeric()))) - .sum(); - if !content.is_empty() { - num_alphanumn / content.len() as f32 - } else { - 0.0 - } - } -} - -lazy_static! { - static ref LANGUAGE_EXTENSION: HashMap<&'static str, Vec<&'static str>> = { - HashMap::from([ - ("c", vec!["c", "h"]), - ("csharp", vec!["cs"]), - ( - "cpp", - vec!["cpp", "hpp", "c++", "h++", "cc", "hh", "C", "H", "tcc"], - ), - ("css", vec!["css"]), - ("dockerfile", vec!["Dockerfile"]), - ("go", vec!["go"]), - ("haskell", vec!["hs"]), - ("html", vec!["html"]), - ("java", vec!["java"]), - ("kotlin", vec!["kt", "kts"]), - ("julia", vec!["jl"]), - ("lua", vec!["lua"]), - ("makefile", vec!["Makefile"]), - ("markdown", vec!["md", "markdown"]), - ("php", vec!["php", "php3", "php4", "php5", "phps", "phpt"]), - ("perl", vec!["pl", "pm", "pod", "perl"]), - ("powershell", vec!["ps1", "psd1", "psm1"]), - ("python", vec!["py"]), - ("ruby", vec!["rb"]), - ("rust", vec!["rs"]), - ("sql", vec!["sql"]), - ("scala", vec!["scala"]), - ("shellscript", vec!["sh", "bash", "command", "zsh"]), - ( - "javascript-typescript", - vec!["ts", "mts", "js", "mjs", "jsx", "tsx"], - ), - ("tex", vec!["tex"]), - ("vb", vec!["vb"]), - ]) - }; - static ref EXTENSION_LANGUAGE: HashMap<&'static str, &'static str> = { - let mut map = HashMap::new(); - for (lang, exts) in &*LANGUAGE_EXTENSION { - for ext in exts { - map.insert(*ext, *lang); - } - } - - map - }; -} diff --git a/crates/tabby-scheduler/src/dataset/deps/mod.rs b/crates/tabby-scheduler/src/dataset/deps/mod.rs deleted file mode 100644 index c4e29af5361c..000000000000 --- a/crates/tabby-scheduler/src/dataset/deps/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -mod python; -mod rust; - -use std::{collections::HashSet, path::Path}; - -use tabby_common::DependencyFile; - -pub fn collect(path: &Path, file: &mut DependencyFile) { - if let Ok(mut deps) = python::process_requirements_txt(path) { - file.direct.append(&mut deps); - } - - if let Ok(mut deps) = rust::process_cargo(path) { - file.direct.append(&mut deps); - } - - // Remove duplicates across sources. - let deps = file.direct.clone().into_iter().collect::>(); - file.direct = deps.into_iter().collect(); -} diff --git a/crates/tabby-scheduler/src/dataset/deps/python.rs b/crates/tabby-scheduler/src/dataset/deps/python.rs deleted file mode 100644 index a8cdb3d0b95a..000000000000 --- a/crates/tabby-scheduler/src/dataset/deps/python.rs +++ /dev/null @@ -1,30 +0,0 @@ -use std::{collections::HashSet, path::Path}; - -use anyhow::Result; -use tabby_common::Package; -use tracing::warn; - -pub fn process_requirements_txt(path: &Path) -> Result> { - let requirements_txt = path.join("requirements.txt"); - let content = std::fs::read_to_string(requirements_txt)?; - - let mut deps = HashSet::new(); - match requirements::parse_str(&content) { - Ok(requirements) => { - for requirement in requirements { - if let Some(name) = requirement.name { - deps.insert(Package { - language: "python".to_owned(), - name, - version: None, // requirements.txt doesn't come with accurate version information. - }); - } - } - } - Err(err) => { - warn!("Failed to parse requirements.txt: {}", err); - } - } - - Ok(deps.into_iter().collect()) -} diff --git a/crates/tabby-scheduler/src/dataset/deps/rust.rs b/crates/tabby-scheduler/src/dataset/deps/rust.rs deleted file mode 100644 index 19da9d1bfed2..000000000000 --- a/crates/tabby-scheduler/src/dataset/deps/rust.rs +++ /dev/null @@ -1,42 +0,0 @@ -use std::path::Path; - -use anyhow::Result; -use cargo_lock::dependency::graph::EdgeDirection; -use tabby_common::Package; - -fn extract_deps<'a, I>(packages: I) -> Vec -where - I: IntoIterator, -{ - let mut res = packages - .into_iter() - .map(|package| Package { - language: String::from("rust"), - name: package.name.to_string(), - version: Some(package.version.to_string()), - }) - .collect::>() - .into_iter() - .collect::>(); - res.sort_unstable(); - res -} - -pub fn process_cargo(path: &Path) -> Result> { - let cargo_lock_file = path.join("Cargo.lock"); - - let lockfile = cargo_lock::Lockfile::load(cargo_lock_file)?; - - let tree = lockfile.dependency_tree()?; - let graph = tree.graph(); - - let root_pkg_idx = graph - .externals(EdgeDirection::Incoming) - .collect::>(); - let direct_deps_idx = root_pkg_idx - .iter() - .flat_map(|idx| graph.neighbors_directed(*idx, EdgeDirection::Outgoing)) - .collect::>(); - let deps = extract_deps(direct_deps_idx.iter().map(|dep_idx| &graph[*dep_idx])); - Ok(deps) -} diff --git a/crates/tabby-scheduler/src/dataset/tags.rs b/crates/tabby-scheduler/src/dataset/tags.rs deleted file mode 100644 index 1f7a85594ad5..000000000000 --- a/crates/tabby-scheduler/src/dataset/tags.rs +++ /dev/null @@ -1,158 +0,0 @@ -use std::collections::HashMap; - -use lazy_static::lazy_static; -use tabby_common::{Point, Tag}; -use tree_sitter_tags::{TagsConfiguration, TagsContext}; - -pub fn collect(context: &mut TagsContext, language: &str, content: &str) -> Vec { - let config = LANGUAGE_TAGS.get(language); - let empty = Vec::new(); - - let Some(config) = config else { - return empty; - }; - - let Ok((tags, has_error)) = context.generate_tags(&config.0, content.as_bytes(), None) else { - return empty; - }; - - if has_error { - return empty; - } - - tags.filter_map(|x| x.ok()) - .map(|x| Tag { - range: x.range, - name_range: x.name_range, - utf16_column_range: x.utf16_column_range, - line_range: x.line_range, - docs: x.docs, - is_definition: x.is_definition, - syntax_type_name: config.0.syntax_type_name(x.syntax_type_id).to_owned(), - span: Point::new(x.span.start.row, x.span.start.column) - ..Point::new(x.span.end.row, x.span.end.column), - }) - .collect() -} - -// Mark TagsConfiguration as thread sync / safe. -struct TagsConfigurationSync(TagsConfiguration); -unsafe impl Send for TagsConfigurationSync {} -unsafe impl Sync for TagsConfigurationSync {} - -lazy_static! { - static ref LANGUAGE_TAGS: HashMap<&'static str, TagsConfigurationSync> = { - HashMap::from([ - ( - "python", - TagsConfigurationSync( - TagsConfiguration::new( - tree_sitter_python::language(), - tree_sitter_python::TAGGING_QUERY, - "", - ) - .unwrap(), - ), - ), - ( - "rust", - TagsConfigurationSync( - TagsConfiguration::new( - tree_sitter_rust::language(), - include_str!("../../queries/rust.scm"), - "", - ) - .unwrap(), - ), - ), - ( - "java", - TagsConfigurationSync( - TagsConfiguration::new( - tree_sitter_java::language(), - tree_sitter_java::TAGGING_QUERY, - "", - ) - .unwrap(), - ), - ), - ( - "kotlin", - TagsConfigurationSync( - TagsConfiguration::new( - tree_sitter_kotlin::language(), - include_str!("../../queries/kotlin.scm"), - "", - ) - .unwrap(), - ), - ), - ( - "javascript-typescript", - TagsConfigurationSync( - TagsConfiguration::new( - tree_sitter_typescript::language_tsx(), - include_str!("../../queries/tsx.scm"), - "", - ) - .unwrap(), - ), - ), - ( - "go", - TagsConfigurationSync( - TagsConfiguration::new( - tree_sitter_go::language(), - include_str!("../../queries/go.scm"), - "", - ) - .unwrap(), - ), - ), - ( - "ruby", - TagsConfigurationSync( - TagsConfiguration::new( - tree_sitter_ruby::language(), - tree_sitter_ruby::TAGGING_QUERY, - "", - ) - .unwrap(), - ), - ), - ( - "c", - TagsConfigurationSync( - TagsConfiguration::new( - tree_sitter_c::language(), - tree_sitter_c::TAGS_QUERY, - "", - ) - .unwrap(), - ), - ), - ( - "cpp", - TagsConfigurationSync( - TagsConfiguration::new( - tree_sitter_cpp::language(), - tree_sitter_cpp::TAGS_QUERY, - "", - ) - .unwrap(), - ), - ), - ( - "csharp", - TagsConfigurationSync( - TagsConfiguration::new( - tree_sitter_c_sharp::language(), - include_str!("../../queries/csharp.scm"), - "", - ) - .unwrap(), - ), - ), - ]) - }; -} diff --git a/crates/tabby-scheduler/src/index.rs b/crates/tabby-scheduler/src/index.rs deleted file mode 100644 index 11ea66275af6..000000000000 --- a/crates/tabby-scheduler/src/index.rs +++ /dev/null @@ -1,199 +0,0 @@ -use std::{fs, io::IsTerminal}; - -use anyhow::Result; -use kdam::BarExt; -use tabby_common::{ - config::RepositoryConfig, - index::{register_tokenizers, CodeSearchSchema}, - path::index_dir, - SourceFile, -}; -use tantivy::{directory::MmapDirectory, doc, Index}; - -use crate::utils::tqdm; - -// Magic numbers -static MAX_LINE_LENGTH_THRESHOLD: usize = 300; -static AVG_LINE_LENGTH_THRESHOLD: f32 = 150f32; -static MAX_BODY_LINES_THRESHOLD: usize = 15; - -pub fn index_repositories(_config: &Vec) -> Result<()> { - let code = CodeSearchSchema::new(); - - fs::create_dir_all(index_dir())?; - let directory = MmapDirectory::open(index_dir())?; - let index = Index::open_or_create(directory, code.schema)?; - register_tokenizers(&index); - - // Initialize the search index writer with an initial arena size of 150 MB. - let mut writer = index.writer(150_000_000)?; - writer.delete_all_documents()?; - - let mut pb = std::io::stdout() - .is_terminal() - .then(SourceFile::all) - .transpose()? - .map(|iter| tqdm(iter.count())); - for file in SourceFile::all()? { - pb.as_mut().map(|b| b.update(1)).transpose()?; - - if file.max_line_length > MAX_LINE_LENGTH_THRESHOLD { - continue; - } - - if file.avg_line_length > AVG_LINE_LENGTH_THRESHOLD { - continue; - } - - for doc in from_source_file(file) { - writer.add_document(doc!( - code.field_git_url => doc.git_url, - code.field_filepath => doc.filepath, - code.field_language => doc.language, - code.field_name => doc.name, - code.field_body => doc.body, - code.field_kind => doc.kind, - ))?; - } - } - - writer.commit()?; - writer.wait_merging_threads()?; - - Ok(()) -} - -/// Atomic repository document in index. -struct IndexedDocument { - git_url: String, - filepath: String, - language: String, - name: String, - body: String, - kind: String, -} - -fn from_source_file(file: SourceFile) -> impl Iterator { - file.tags.into_iter().filter_map(move |tag| { - let name = file.content.get(tag.name_range).unwrap().to_owned(); - let body = file.content.get(tag.range).unwrap().to_owned(); - - if body.lines().collect::>().len() > MAX_BODY_LINES_THRESHOLD { - return None; - } - - Some(IndexedDocument { - git_url: file.git_url.clone(), - filepath: file.filepath.clone(), - language: file.language.clone(), - name, - body, - kind: tag.syntax_type_name, - }) - }) -} - -#[cfg(test)] -mod tests { - use serde_json::{from_value, json}; - - use super::*; - - fn test_source_file() -> SourceFile { - from_value(json!( - { - "git_url": "https://fake.com/tabbyml.git", - "filepath": "python/tabby/trainer.py", - "content": "import os\nimport glob\nfrom dataclasses import dataclass, field\nfrom typing import List\n\nimport peft\nimport torch\nfrom transformers import (\n AutoModelForCausalLM,\n AutoTokenizer,\n HfArgumentParser,\n Trainer,\n TrainingArguments,\n)\nfrom datasets import Dataset, load_dataset\n\n\nclass ConstantLengthDataset:\n \"\"\"\n Iterable dataset that returns constant length chunks of tokens from stream of text files.\n Args:\n tokenizer (Tokenizer): The processor used for proccessing the data.\n dataset (dataset.Dataset): Dataset with text files.\n infinite (bool): If True the iterator is reset after dataset reaches end else stops.\n seq_length (int): Length of token sequences to return.\n num_of_sequences (int): Number of token sequences to keep in buffer.\n chars_per_token (int): Number of characters per token used to estimate number of tokens in text buffer.\n \"\"\"\n\n def __init__(\n self,\n tokenizer,\n dataset,\n infinite=False,\n seq_length=1024,\n num_of_sequences=1024,\n chars_per_token=3.6,\n content_field=\"content\",\n ):\n self.tokenizer = tokenizer\n self.concat_token_id = tokenizer.eos_token_id\n self.dataset = dataset\n self.seq_length = seq_length\n self.infinite = infinite\n self.current_size = 0\n self.max_buffer_size = seq_length * chars_per_token * num_of_sequences\n self.content_field = content_field\n\n def __call__(self):\n def gen():\n for x in self:\n yield x\n\n return gen()\n\n def __iter__(self):\n for buffer in self._read_dataset_into_buffer():\n yield from self._tokenize(buffer)\n\n def _tokenize(self, buffer):\n tokenized_inputs = self.tokenizer(buffer, truncation=False)[\"input_ids\"]\n\n all_token_ids = []\n for tokenized_input in tokenized_inputs:\n all_token_ids.extend(tokenized_input + [self.concat_token_id])\n\n for i in range(0, len(all_token_ids), self.seq_length):\n input_ids = all_token_ids[i : i + self.seq_length]\n\n if len(input_ids) < self.seq_length:\n input_ids = all_token_ids[-self.seq_length :]\n\n if len(input_ids) == self.seq_length:\n self.current_size += 1\n yield dict(input_ids=input_ids, labels=input_ids)\n\n def _read_dataset_into_buffer(self):\n iterator = iter(self.dataset)\n more_examples = True\n while more_examples:\n buffer, buffer_len = [], 0\n while True:\n if buffer_len >= self.max_buffer_size:\n break\n try:\n buffer.append(next(iterator)[self.content_field])\n buffer_len += len(buffer[-1])\n except StopIteration:\n if self.infinite:\n iterator = iter(self.dataset)\n else:\n more_examples = False\n break\n yield buffer\n\n\n", - "language": "python", - "max_line_length": 115, - "avg_line_length": 32.388393, - "alphanum_fraction": 0.6066319, - "tags": [ - { - "range": { - "start": 290, - "end": 320 - }, - "name_range": { - "start": 296, - "end": 317 - }, - "utf16_column_range": { - "start": 6, - "end": 27 - }, - "span": { - "start": { - "row": 17, - "column": 6 - }, - "end": { - "row": 17, - "column": 27 - } - }, - "line_range": { - "start": 290, - "end": 318 - }, - "is_definition": true, - "syntax_type_name": "class" - }, - { - "range": { - "start": 953, - "end": 970 - }, - "name_range": { - "start": 957, - "end": 965 - }, - "utf16_column_range": { - "start": 8, - "end": 16 - }, - "span": { - "start": { - "row": 29, - "column": 8 - }, - "end": { - "row": 29, - "column": 16 - } - }, - "line_range": { - "start": 953, - "end": 966 - }, - "is_definition": true, - "syntax_type_name": "function" - }, - ] - })).unwrap() - } - - #[test] - fn it_create_documents() { - let source_file: SourceFile = test_source_file(); - let docs: Vec<_> = from_source_file(source_file).collect(); - assert_eq!(docs.len(), 2); - - assert_eq!(docs[0].name, "ConstantLengthDataset"); - assert_eq!(docs[0].kind, "class"); - assert!( - docs[0].body.starts_with("class ConstantLengthDataset"), - "body: {:?}", - docs[0].body - ); - - assert_eq!(docs[1].name, "__init__"); - assert_eq!(docs[1].kind, "function"); - assert!( - docs[1].body.starts_with("def __init__"), - "body: {:?}", - docs[1].body - ); - } -} diff --git a/crates/tabby-scheduler/src/lib.rs b/crates/tabby-scheduler/src/lib.rs deleted file mode 100644 index a8e7bbd7e452..000000000000 --- a/crates/tabby-scheduler/src/lib.rs +++ /dev/null @@ -1,66 +0,0 @@ -//! Responsible for scheduling all of the background jobs for tabby. -//! Includes syncing respositories and updating indices. -mod dataset; -mod index; -mod repository; -mod utils; - -use anyhow::Result; -use job_scheduler::{Job, JobScheduler}; -use tabby_common::config::RepositoryConfig; -use tracing::{error, info}; - -pub async fn scheduler(now: bool, access: Vec) -> Result<()> { - let mut scheduler = JobScheduler::new(); - - let job1 = || job_sync(&access); - - let job2 = || job_index(&access); - - if now { - job1(); - job2(); - } else { - // Every 5 minutes. - scheduler.add(Job::new("0 1/5 * * * * *".parse().unwrap(), job1)); - - // Every 5 hours. - scheduler.add(Job::new("0 0 1/5 * * * *".parse().unwrap(), job2)); - - info!("Scheduler activated..."); - loop { - scheduler.tick(); - let duration = scheduler.time_till_next_job(); - info!("Sleep {:?} for next job ...", duration); - std::thread::sleep(duration); - } - } - - Ok(()) -} - -pub fn job_index(repositories: &Vec) { - println!("Indexing repositories..."); - let ret = index::index_repositories(repositories); - if let Err(err) = ret { - error!("Failed to index repositories, err: '{}'", err); - } - println!(); -} - -pub fn job_sync(repositories: &Vec) { - println!("Syncing repositories..."); - let repositories = repositories; - let ret = repository::sync_repositories(repositories); - if let Err(err) = ret { - error!("Failed to sync repositories, err: '{}'", err); - return; - } - - println!("Building dataset..."); - let ret = dataset::create_dataset(repositories); - if let Err(err) = ret { - error!("Failed to build dataset, err: '{}'", err); - } - println!(); -} diff --git a/crates/tabby-scheduler/src/repository.rs b/crates/tabby-scheduler/src/repository.rs deleted file mode 100644 index 18504de03ec6..000000000000 --- a/crates/tabby-scheduler/src/repository.rs +++ /dev/null @@ -1,72 +0,0 @@ -use std::{collections::HashSet, fs, process::Command}; - -use anyhow::{anyhow, Result}; -use tabby_common::{config::RepositoryConfig, path::repositories_dir}; -use tracing::warn; - -trait RepositoryExt { - fn sync(&self) -> Result<()>; -} - -impl RepositoryExt for RepositoryConfig { - fn sync(&self) -> Result<()> { - let dir = self.dir(); - let dir_string = dir.display().to_string(); - let status = if dir.exists() { - Command::new("git").current_dir(&dir).arg("pull").status() - } else { - std::fs::create_dir_all(&dir) - .unwrap_or_else(|_| panic!("Failed to create dir {}", dir_string)); - Command::new("git") - .current_dir(dir.parent().unwrap()) - .arg("clone") - .arg(&self.git_url) - .arg(dir) - .status() - }; - - if let Some(code) = status?.code() { - if code != 0 { - return Err(anyhow!( - "Failed to pull remote '{}'. Consider remove dir '{}' and retry", - &self.git_url, - &dir_string - )); - } - } - - Ok(()) - } -} - -pub fn sync_repositories(repositories: &Vec) -> Result<()> { - let mut names = HashSet::new(); - for repository in repositories { - names.insert(repository.name()); - if repository.is_local_dir() { - if !repository.dir().exists() { - panic!("Directory {} does not exist", repository.dir().display()); - } - } else { - repository.sync()?; - } - } - - for file in fs::read_dir(repositories_dir())?.filter_map(Result::ok) { - let metadata = file.metadata()?; - let filename = file.file_name(); - if metadata.is_file() { - warn!("An unrelated file {:?} was found in repositories directory, It will now be removed...", filename); - // There shouldn't be any files under repositories dir. - fs::remove_file(file.path())?; - } else if metadata.is_dir() { - let filename = filename.to_str().ok_or(anyhow!("Invalid file name"))?; - if !names.contains(filename) { - warn!("An unrelated directory {:?} was found in repositories directory, It will now be removed...", filename); - fs::remove_dir_all(file.path())?; - } - } - } - - Ok(()) -} diff --git a/crates/tabby-scheduler/src/utils.rs b/crates/tabby-scheduler/src/utils.rs deleted file mode 100644 index b0f3b821d8f5..000000000000 --- a/crates/tabby-scheduler/src/utils.rs +++ /dev/null @@ -1,5 +0,0 @@ -use kdam::{tqdm, Bar}; - -pub fn tqdm(total: usize) -> Bar { - tqdm!(total = total, ncols = 40, force_refresh = true) -} diff --git a/crates/tabby-scheduler/tests/integration_test.rs b/crates/tabby-scheduler/tests/integration_test.rs deleted file mode 100644 index df816145b66c..000000000000 --- a/crates/tabby-scheduler/tests/integration_test.rs +++ /dev/null @@ -1,29 +0,0 @@ -#[cfg(test)] -mod tests { - use std::fs::create_dir_all; - - use tabby_common::{ - config::{Config, RepositoryConfig, ServerConfig}, - path::set_tabby_root, - }; - use temp_testdir::*; - use tracing_test::traced_test; - - #[traced_test] - #[tokio::test] - async fn end_to_end() { - let root = TempDir::default(); - create_dir_all(&root).expect("Failed to create tabby root"); - set_tabby_root(root.to_path_buf()); - - let config = Config { - repositories: vec![RepositoryConfig::new( - "https://github.com/TabbyML/interview-questions".to_owned(), - )], - server: ServerConfig::default(), - }; - - let res = tabby_scheduler::scheduler(true, config.repositories).await; - res.expect("Failed to run scheduler"); - } -} diff --git a/crates/tabby/Cargo.toml b/crates/tabby/Cargo.toml index 0af7c66d1e77..c62b69856315 100644 --- a/crates/tabby/Cargo.toml +++ b/crates/tabby/Cargo.toml @@ -1,62 +1,67 @@ [package] name = "tabby" -version = "0.8.0" -edition = "2021" +version.workspace = true +edition.workspace = true +authors.workspace = true +homepage.workspace = true +default-run = "tabby" [features] -default = ["ee", "experimental-http"] +default = ["ee", "llama-cpp-server/binary"] ee = ["dep:tabby-webserver"] -cuda = ["llama-cpp-bindings/cuda"] -rocm = ["llama-cpp-bindings/rocm"] -experimental-http = ["dep:http-api-bindings"] +cuda = ["llama-cpp-server/cuda"] +rocm = ["llama-cpp-server/rocm"] +vulkan = ["llama-cpp-server/vulkan"] # If compiling on a system without OpenSSL installed, or cross-compiling for a different # architecture, enable this feature to compile OpenSSL as part of the build. # See https://docs.rs/openssl/#vendored for more. static-ssl = ['openssl/vendored'] -prod-db = ['tabby-webserver/prod-db'] +prod = ["ee", 'tabby-webserver/prod'] [dependencies] tabby-common = { path = "../tabby-common" } -tabby-scheduler = { path = "../tabby-scheduler" } tabby-download = { path = "../tabby-download" } tabby-inference = { path = "../tabby-inference" } axum.workspace = true +axum-extra = {workspace = true, features = ["typed-header"]} hyper = { workspace = true } -tokio = { workspace = true, features = ["full"] } +tokio = { workspace = true } utoipa = { workspace = true, features = ["axum_extras", "preserve_order"] } -utoipa-swagger-ui = { version = "3.1", features = ["axum"] } +utoipa-swagger-ui = { version = "9", features = ["axum"] } serde = { workspace = true } serdeconv = { workspace = true } serde_json = { workspace = true } -tower-http = { version = "0.4.0", features = ["cors", "timeout"] } -clap = { version = "4.3.0", features = ["derive"] } +tower-http = { workspace = true, features = ["cors", "timeout"] } +clap = { workspace = true, features = ["derive"] } lazy_static = { workspace = true } -strum = { version = "0.24", features = ["derive"] } +strum = { workspace = true } strfmt = "0.2.4" tracing = { workspace = true } tracing-subscriber = { workspace = true } -opentelemetry = { version = "0.18.0", features = ["rt-tokio"] } -opentelemetry-otlp = "0.11.0" -axum-tracing-opentelemetry = "0.10.0" -tracing-opentelemetry = "0.18.0" +tracing-opentelemetry.workspace = true +opentelemetry.workspace = true +opentelemetry_sdk.workspace = true +opentelemetry-otlp.workspace = true +opentelemetry-semantic-conventions.workspace = true tantivy = { workspace = true } anyhow = { workspace = true } -sysinfo = "0.29.8" +sysinfo = "0.33.0" nvml-wrapper = "0.9.0" -http-api-bindings = { path = "../http-api-bindings", optional = true } # included when build with `experimental-http` feature +http-api-bindings = { path = "../http-api-bindings" } async-stream = { workspace = true } -minijinja = { version = "1.0.8", features = ["loader"] } -textdistance = "1.0.2" -regex.workspace = true -llama-cpp-bindings = { path = "../llama-cpp-bindings" } +llama-cpp-server = { path = "../llama-cpp-server" } futures.workspace = true async-trait.workspace = true tabby-webserver = { path = "../../ee/tabby-webserver", optional = true } thiserror.workspace = true chrono.workspace = true -axum-prometheus = "0.4.0" +axum-prometheus = "0.6" uuid.workspace = true -serial_test = "3.0.0" +color-eyre = { version = "0.6.3" } +reqwest.workspace = true +async-openai-alt.workspace = true +spinners = "4.1.1" +regex.workspace = true [dependencies.openssl] optional = true @@ -67,9 +72,11 @@ vergen = { version = "8.0.0", features = ["build", "git", "gitcl"] } [dev-dependencies] assert-json-diff = "2.0.2" -insta = { version = "1.34.0", features = ["yaml", "redactions"] } +insta = { workspace = true, features = ["yaml", "redactions"] } reqwest.workspace = true serde-jsonlines = "0.5.0" +reqwest-eventsource = { workspace = true } +serial_test = { workspace = true } [package.metadata.cargo-machete] ignored = ["openssl"] diff --git a/crates/tabby/build.rs b/crates/tabby/build.rs index dc138d230671..34c22d7bced2 100644 --- a/crates/tabby/build.rs +++ b/crates/tabby/build.rs @@ -7,7 +7,10 @@ fn main() -> Result<(), Box> { EmitBuilder::builder() .all_build() .all_git() - .git_describe(true, true, Some("v*")) + // TODO(kweizh): we encounter a issue with match_pattern in vergen on Windows + // will add the match_pattern back when the issue is resolved + // ref: https://github.com/rustyhorde/vergen/issues/402 + .git_describe(false, true, None) .emit()?; Ok(()) } diff --git a/crates/tabby/src/download.rs b/crates/tabby/src/download.rs index dcc36c1d116e..47ca391a21f8 100644 --- a/crates/tabby/src/download.rs +++ b/crates/tabby/src/download.rs @@ -14,6 +14,6 @@ pub struct DownloadArgs { } pub async fn main(args: &DownloadArgs) { - download_model(&args.model, args.prefer_local_file).await; + download_model(&args.model, args.prefer_local_file, None).await; info!("model '{}' is ready", args.model); } diff --git a/crates/tabby/src/job.rs b/crates/tabby/src/job.rs deleted file mode 100644 index 7a3e493e9d1a..000000000000 --- a/crates/tabby/src/job.rs +++ /dev/null @@ -1,38 +0,0 @@ -use tabby_common::config::{Config, RepositoryConfig}; -use tabby_webserver::public::{ConnectHubRequest, RepositoryAccess}; - -pub async fn get_repositories( - url: Option, - token: Option, - config: &Vec, -) -> Vec { - match url.zip(token) { - Some((addr, token)) => { - let client = - tabby_webserver::public::create_client(&addr, &token, ConnectHubRequest::Job).await; - - RepositoryAccess::get_repositories(&client) - .await - .expect("Must be able to load repositories") - } - None => config.to_vec(), - } -} - -pub async fn start_sync_job(args: JobArgs, config: &Config) { - let repositories = get_repositories(args.url, args.token, &config.repositories).await; - tabby_scheduler::job_sync(&repositories) -} - -pub async fn start_index_job(args: JobArgs, config: &Config) { - let repositories = get_repositories(args.url, args.token, &config.repositories).await; - tabby_scheduler::job_index(&repositories) -} - -#[derive(clap::Args)] -pub struct JobArgs { - #[clap(long, requires = "url")] - pub token: Option, - #[clap(long, requires = "token")] - pub url: Option, -} diff --git a/crates/tabby/src/main.rs b/crates/tabby/src/main.rs index fd3dcae3721f..8d2c870edbb9 100644 --- a/crates/tabby/src/main.rs +++ b/crates/tabby/src/main.rs @@ -1,24 +1,15 @@ -//! Core tabby functionality. Defines primary API and CLI behavior. +mod otel; mod routes; mod services; mod download; mod serve; -mod job; -#[cfg(feature = "ee")] -mod worker; +#[cfg(target_family = "unix")] +use std::os::unix::fs::PermissionsExt; use clap::{Parser, Subcommand}; -use job::{start_index_job, start_sync_job, JobArgs}; -use opentelemetry::{ - global, - sdk::{propagation::TraceContextPropagator, trace, trace::Sampler, Resource}, - KeyValue, -}; -use opentelemetry_otlp::WithExportConfig; -use tabby_common::config::Config; -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer}; +use tabby_common::config::{Config, ModelConfig}; #[derive(Parser)] #[command(author, version, about, long_about = None)] @@ -28,7 +19,7 @@ struct Cli { command: Commands, /// Open Telemetry endpoint. - #[clap(long)] + #[clap(hide = true, long)] otlp_endpoint: Option, } @@ -39,36 +30,6 @@ pub enum Commands { /// Download the language model for serving. Download(download::DownloadArgs), - - /// Run scheduler progress for cron jobs integrating external code repositories. - Scheduler(SchedulerArgs), - - /// Run completion model as worker - #[cfg(feature = "ee")] - #[clap(name = "worker::completion")] - WorkerCompletion(worker::WorkerArgs), - - /// Run chat model as worker - #[cfg(feature = "ee")] - #[clap(name = "worker::chat")] - WorkerChat(worker::WorkerArgs), - - /// Execute the repository sync job. - #[cfg(feature = "ee")] - #[clap(name = "job::sync")] - JobSync(JobArgs), - - /// Execute the index job. - #[cfg(feature = "ee")] - #[clap(name = "job::index")] - JobIndex(JobArgs), -} - -#[derive(clap::Args)] -pub struct SchedulerArgs { - /// If true, runs scheduler jobs immediately. - #[clap(long, default_value_t = false)] - now: bool, } #[derive(clap::ValueEnum, strum::Display, PartialEq, Clone)] @@ -76,78 +37,40 @@ pub enum Device { #[strum(serialize = "cpu")] Cpu, - #[cfg(feature = "cuda")] #[strum(serialize = "cuda")] Cuda, - #[cfg(feature = "rocm")] #[strum(serialize = "rocm")] Rocm, - #[cfg(all(target_os = "macos", target_arch = "aarch64"))] #[strum(serialize = "metal")] Metal, - #[cfg(feature = "experimental-http")] - #[strum(serialize = "experimental_http")] - #[clap(hide = true)] - ExperimentalHttp, -} - -impl Device { - #[cfg(all(target_os = "macos", target_arch = "aarch64"))] - pub fn ggml_use_gpu(&self) -> bool { - *self == Device::Metal - } - - #[cfg(feature = "cuda")] - pub fn ggml_use_gpu(&self) -> bool { - *self == Device::Cuda - } - - #[cfg(feature = "rocm")] - pub fn ggml_use_gpu(&self) -> bool { - *self == Device::Rocm - } - - #[cfg(not(any( - all(target_os = "macos", target_arch = "aarch64"), - feature = "cuda", - feature = "rocm", - )))] - pub fn ggml_use_gpu(&self) -> bool { - false - } + #[strum(serialize = "vulkan")] + Vulkan, } #[tokio::main] async fn main() { - let cli = Cli::parse(); - init_logging(cli.otlp_endpoint); + color_eyre::install().expect("Must be able to install color_eyre"); - let config = Config::load().unwrap_or_default(); + let cli = Cli::parse(); + let _guard = otel::init_tracing_subscriber(cli.otlp_endpoint); + + let config = Config::load().expect("Must be able to load config"); + let root = tabby_common::path::tabby_root(); + std::fs::create_dir_all(&root).expect("Must be able to create tabby root"); + #[cfg(target_family = "unix")] + { + let mut permissions = std::fs::metadata(&root).unwrap().permissions(); + permissions.set_mode(0o700); + std::fs::set_permissions(&root, permissions).unwrap(); + } match cli.command { Commands::Serve(ref args) => serve::main(&config, args).await, Commands::Download(ref args) => download::main(args).await, - Commands::Scheduler(args) => tabby_scheduler::scheduler(args.now, config.repositories) - .await - .unwrap_or_else(|err| fatal!("Scheduler failed due to '{}'", err)), - #[cfg(feature = "ee")] - Commands::JobSync(args) => start_sync_job(args, &config).await, - #[cfg(feature = "ee")] - Commands::JobIndex(args) => start_index_job(args, &config).await, - #[cfg(feature = "ee")] - Commands::WorkerCompletion(ref args) => { - worker::main(tabby_webserver::public::WorkerKind::Completion, args).await - } - #[cfg(feature = "ee")] - Commands::WorkerChat(ref args) => { - worker::main(tabby_webserver::public::WorkerKind::Chat, args).await - } } - - opentelemetry::global::shutdown_tracer_provider(); } #[macro_export] @@ -167,49 +90,18 @@ macro_rules! fatal { }; } -fn init_logging(otlp_endpoint: Option) { - let mut layers = Vec::new(); - - let fmt_layer = tracing_subscriber::fmt::layer() - .with_file(true) - .with_line_number(true) - .boxed(); - - layers.push(fmt_layer); - - if let Some(otlp_endpoint) = &otlp_endpoint { - global::set_text_map_propagator(TraceContextPropagator::new()); - - let tracer = opentelemetry_otlp::new_pipeline() - .tracing() - .with_exporter( - opentelemetry_otlp::new_exporter() - .tonic() - .with_endpoint(otlp_endpoint), - ) - .with_trace_config( - trace::config() - .with_resource(Resource::new(vec![KeyValue::new( - "service.name", - "tabby.server", - )])) - .with_sampler(Sampler::AlwaysOn), - ) - .install_batch(opentelemetry::runtime::Tokio); - - if let Ok(tracer) = tracer { - layers.push(tracing_opentelemetry::layer().with_tracer(tracer).boxed()); - axum_tracing_opentelemetry::init_propagator().unwrap(); - }; - } - - let env_filter = EnvFilter::from_default_env() - .add_directive("tabby=info".parse().unwrap()) - .add_directive("axum_tracing_opentelemetry=info".parse().unwrap()) - .add_directive("otel=debug".parse().unwrap()); +fn to_local_config(model: &str, parallelism: u8, device: &Device) -> ModelConfig { + let num_gpu_layers = if *device != Device::Cpu { + std::env::var("LLAMA_CPP_N_GPU_LAYERS") + .map(|s| s.parse::().ok()) + .ok() + .flatten() + .unwrap_or(9999) + } else { + 0 + }; + // This only works when the the model is included in cli arguments. + let enable_fast_attention = Some(std::env::var("LLAMA_CPP_FAST_ATTENTION").is_ok()); - tracing_subscriber::registry() - .with(layers) - .with(env_filter) - .init(); + ModelConfig::new_local(model, parallelism, num_gpu_layers, enable_fast_attention) } diff --git a/crates/tabby/src/otel.rs b/crates/tabby/src/otel.rs new file mode 100644 index 000000000000..1821df79bb97 --- /dev/null +++ b/crates/tabby/src/otel.rs @@ -0,0 +1,104 @@ +use opentelemetry::{trace::TracerProvider as _, KeyValue}; +use opentelemetry_otlp::WithExportConfig; +use opentelemetry_sdk::{ + runtime, + trace::{RandomIdGenerator, Sampler, TracerProvider}, + Resource, +}; +use opentelemetry_semantic_conventions::{ + attribute::{DEPLOYMENT_ENVIRONMENT_NAME, SERVICE_NAME, SERVICE_VERSION}, + SCHEMA_URL, +}; +use tracing::level_filters::LevelFilter; +use tracing_opentelemetry::OpenTelemetryLayer; +use tracing_subscriber::{ + layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer, Registry, +}; + +// Create a Resource that captures information about the entity for which telemetry is recorded. +fn resource() -> Resource { + Resource::from_schema_url( + [ + KeyValue::new(SERVICE_NAME, env!("CARGO_PKG_NAME")), + KeyValue::new(SERVICE_VERSION, env!("CARGO_PKG_VERSION")), + KeyValue::new(DEPLOYMENT_ENVIRONMENT_NAME, "develop"), + ], + SCHEMA_URL, + ) +} + +// Construct TracerProvider for OpenTelemetryLayer +fn init_tracer_provider(otlp_endpoint: String) -> TracerProvider { + let exporter = opentelemetry_otlp::SpanExporter::builder() + .with_tonic() + .with_endpoint(otlp_endpoint) + .with_timeout(std::time::Duration::from_secs(3)) + .build() + .unwrap(); + + TracerProvider::builder() + // Customize sampling strategy + .with_sampler(Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased( + 1.0, + )))) + // If export trace to AWS X-Ray, you can use XrayIdGenerator + .with_id_generator(RandomIdGenerator::default()) + .with_resource(resource()) + .with_batch_exporter(exporter, runtime::Tokio) + .build() +} + +// Initialize tracing-subscriber and return OtelGuard for opentelemetry-related termination processing +pub fn init_tracing_subscriber(otlp_endpoint: Option) -> OtelGuard { + let mut layers: Vec + Send + Sync>> = Vec::new(); + + let tracer_provider = if let Some(endpoint) = otlp_endpoint { + let tracer_provider = init_tracer_provider(endpoint); + let tracer = tracer_provider.tracer("tracing-otel-subscriber"); + layers.push(Box::new(OpenTelemetryLayer::new(tracer))); + Some(tracer_provider) + } else { + None + }; + + let fmt_layer = tracing_subscriber::fmt::layer() + .with_file(true) + .with_line_number(true) + .boxed(); + layers.push(fmt_layer); + + let mut dirs = if cfg!(feature = "prod") { + "tabby=info,otel=debug,http_api_bindings=info,llama_cpp_server=info".into() + } else { + "tabby=debug,otel=debug,http_api_bindings=debug,llama_cpp_server=debug".into() + }; + + if let Ok(env) = std::env::var(EnvFilter::DEFAULT_ENV) { + dirs = format!("{dirs},{env}") + }; + + let env_filter = EnvFilter::builder() + .with_default_directive(LevelFilter::WARN.into()) + .parse_lossy(dirs); + + tracing_subscriber::registry() + .with(layers) + .with(env_filter) + .init(); + + OtelGuard { tracer_provider } +} + +pub struct OtelGuard { + tracer_provider: Option, +} + +impl Drop for OtelGuard { + fn drop(&mut self) { + if let Some(tracer_provider) = self.tracer_provider.take() { + if let Err(err) = tracer_provider.shutdown() { + eprintln!("{err:?}"); + } + } + } +} diff --git a/crates/tabby/src/routes/chat.rs b/crates/tabby/src/routes/chat.rs index 05a7431edbf2..df96185d87e3 100644 --- a/crates/tabby/src/routes/chat.rs +++ b/crates/tabby/src/routes/chat.rs @@ -1,24 +1,28 @@ use std::sync::Arc; +use async_openai_alt::error::OpenAIError; use axum::{ - body::StreamBody, extract::State, - response::{IntoResponse, Response}, + response::sse::{Event, KeepAlive, Sse}, Json, }; -use futures::StreamExt; -use tracing::instrument; - -use crate::services::chat::{ChatCompletionRequest, ChatService}; +use axum_extra::TypedHeader; +use futures::{Stream, StreamExt}; +use hyper::StatusCode; +use tabby_common::{ + api::event::{Event as LoggerEvent, EventLogger}, + axum::MaybeUser, +}; +use tabby_inference::ChatCompletionStream; +use tracing::{error, instrument, warn}; #[utoipa::path( post, - path = "/v1beta/chat/completions", - request_body = ChatCompletionRequest, + path = "/v1/chat/completions", operation_id = "chat_completions", - tag = "v1beta", + tag = "v1", responses( - (status = 200, description = "Success", body = ChatCompletionChunk, content_type = "text/event-stream"), + (status = 200, description = "Success", content_type = "text/event-stream"), (status = 405, description = "When chat model is not specified, the endpoint returns 405 Method Not Allowed"), (status = 422, description = "When the prompt is malformed, the endpoint returns 422 Unprocessable Entity") ), @@ -26,23 +30,57 @@ use crate::services::chat::{ChatCompletionRequest, ChatService}; ("token" = []) ) )] +#[allow(unused)] +pub async fn chat_completions_utoipa(_request: Json) -> StatusCode { + unimplemented!() +} + +pub struct ChatState { + pub chat_completion: Arc, + pub logger: Arc, +} + #[instrument(skip(state, request))] pub async fn chat_completions( - State(state): State>, - Json(request): Json, -) -> Response { - let stream = state.generate(request).await; - let stream = match stream { + State(state): State>, + TypedHeader(MaybeUser(user)): TypedHeader, + Json(mut request): Json, +) -> Result>>, StatusCode> { + if let Some(user) = user { + request.user.replace(user); + } + let user = request.user.clone(); + + let s = match state.chat_completion.chat_stream(request).await { Ok(s) => s, - Err(_) => { - let mut response = StreamBody::default().into_response(); - *response.status_mut() = hyper::StatusCode::UNPROCESSABLE_ENTITY; - return response; + Err(err) => { + warn!("Error happens during chat completion: {}", err); + return Err(StatusCode::INTERNAL_SERVER_ERROR); + } + }; + + let s = async_stream::stream! { + let mut s = s; + while let Some(event) = s.next().await { + match event { + Ok(event) => { + yield Ok(Event::default().json_data(event)?); + } + Err(err) => { + if let OpenAIError::StreamError(content) = err { + if content == "Stream ended" { + break; + } + } else { + error!("Failed to get chat completion chunk: {:?}", err); + yield Err(err.into()); + } + } + } } }; - let s = stream.map(|chunk| match serde_json::to_string(&chunk) { - Ok(s) => Ok(format!("data: {s}\n\n")), - Err(e) => Err(anyhow::Error::from(e)), - }); - StreamBody::new(s).into_response() + + state.logger.log(user, LoggerEvent::ChatCompletion {}); + + Ok(Sse::new(s).keep_alive(KeepAlive::default())) } diff --git a/crates/tabby/src/routes/completions.rs b/crates/tabby/src/routes/completions.rs index 8dbf312a61b6..e1d8eea3ae21 100644 --- a/crates/tabby/src/routes/completions.rs +++ b/crates/tabby/src/routes/completions.rs @@ -1,8 +1,9 @@ use std::sync::Arc; -use axum::{extract::State, headers::Header, Json, TypedHeader}; +use axum::{extract::State, Extension, Json}; +use axum_extra::{headers, TypedHeader}; use hyper::StatusCode; -use tabby_webserver::public::USER_HEADER_FIELD_NAME; +use tabby_common::axum::{AllowedCodeRepository, MaybeUser}; use tracing::{instrument, warn}; use crate::services::completion::{CompletionRequest, CompletionResponse, CompletionService}; @@ -24,13 +25,21 @@ use crate::services::completion::{CompletionRequest, CompletionResponse, Complet #[instrument(skip(state, request))] pub async fn completions( State(state): State>, + Extension(allowed_code_repository): Extension, TypedHeader(MaybeUser(user)): TypedHeader, + user_agent: Option>, Json(mut request): Json, ) -> Result, StatusCode> { if let Some(user) = user { request.user.replace(user); } - match state.generate(&request).await { + + let user_agent = user_agent.map(|x| x.0.to_string()); + + match state + .generate(&request, &allowed_code_repository, user_agent.as_deref()) + .await + { Ok(resp) => Ok(Json(resp)), Err(err) => { warn!("{}", err); @@ -38,28 +47,3 @@ pub async fn completions( } } } - -#[derive(Debug)] -pub struct MaybeUser(Option); - -impl Header for MaybeUser { - fn name() -> &'static axum::http::HeaderName { - &USER_HEADER_FIELD_NAME - } - - fn decode<'i, I>(values: &mut I) -> Result - where - Self: Sized, - I: Iterator, - { - let Some(value) = values.next() else { - return Ok(MaybeUser(None)); - }; - let str = value.to_str().expect("User email is always a valid string"); - Ok(MaybeUser(Some(str.to_string()))) - } - - fn encode>(&self, _values: &mut E) { - todo!() - } -} diff --git a/crates/tabby/src/routes/events.rs b/crates/tabby/src/routes/events.rs index 78e9aaa784d9..0a044dcc58f3 100644 --- a/crates/tabby/src/routes/events.rs +++ b/crates/tabby/src/routes/events.rs @@ -4,8 +4,12 @@ use axum::{ extract::{Query, State}, Json, }; +use axum_extra::TypedHeader; use hyper::StatusCode; -use tabby_common::api::event::{Event, EventLogger, LogEventRequest, SelectKind}; +use tabby_common::{ + api::event::{Event, EventLogger, LogEventRequest, SelectKind}, + axum::MaybeUser, +}; #[utoipa::path( post, @@ -23,40 +27,50 @@ use tabby_common::api::event::{Event, EventLogger, LogEventRequest, SelectKind}; )] pub async fn log_event( State(logger): State>, + TypedHeader(MaybeUser(user)): TypedHeader, Query(params): Query>, Json(request): Json, ) -> StatusCode { if request.event_type == "view" { - logger.log(Event::View { - completion_id: request.completion_id, - choice_index: request.choice_index, - view_id: request.view_id, - }); + logger.log( + user, + Event::View { + completion_id: request.completion_id, + choice_index: request.choice_index, + view_id: request.view_id, + }, + ); StatusCode::OK } else if request.event_type == "select" { let is_line = params .get("select_kind") .map(|x| x == "line") .unwrap_or(false); - logger.log(Event::Select { - completion_id: request.completion_id, - choice_index: request.choice_index, - kind: if is_line { - Some(SelectKind::Line) - } else { - None + logger.log( + user, + Event::Select { + completion_id: request.completion_id, + choice_index: request.choice_index, + kind: if is_line { + Some(SelectKind::Line) + } else { + None + }, + view_id: request.view_id, + elapsed: request.elapsed, }, - view_id: request.view_id, - elapsed: request.elapsed, - }); + ); StatusCode::OK } else if request.event_type == "dismiss" { - logger.log(Event::Dismiss { - completion_id: request.completion_id, - choice_index: request.choice_index, - view_id: request.view_id, - elapsed: request.elapsed, - }); + logger.log( + user, + Event::Dismiss { + completion_id: request.completion_id, + choice_index: request.choice_index, + view_id: request.view_id, + elapsed: request.elapsed, + }, + ); StatusCode::OK } else { StatusCode::BAD_REQUEST diff --git a/crates/tabby/src/routes/health.rs b/crates/tabby/src/routes/health.rs index 055763726bd4..5388d89c4013 100644 --- a/crates/tabby/src/routes/health.rs +++ b/crates/tabby/src/routes/health.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use axum::{extract::State, Json}; -use crate::services::health; +use crate::services::health::HealthState; #[utoipa::path( get, @@ -15,6 +15,6 @@ use crate::services::health; ("token" = []) ) )] -pub async fn health(State(state): State>) -> Json { +pub async fn health(State(state): State>) -> Json { Json(state.as_ref().clone()) } diff --git a/crates/tabby/src/routes/mod.rs b/crates/tabby/src/routes/mod.rs index f8311e8776df..9cf78e49ce63 100644 --- a/crates/tabby/src/routes/mod.rs +++ b/crates/tabby/src/routes/mod.rs @@ -7,10 +7,7 @@ use std::{ use axum::{routing, Router}; use axum_prometheus::PrometheusMetricLayer; -use axum_tracing_opentelemetry::opentelemetry_tracing_layer; -use hyper::Server; use tower_http::cors::CorsLayer; -use tracing::info; use crate::fatal; @@ -18,7 +15,6 @@ pub async fn run_app(api: Router, ui: Option, host: IpAddr, port: u16) { let (prometheus_layer, prometheus_handle) = PrometheusMetricLayer::pair(); let app = api .layer(CorsLayer::permissive()) - .layer(opentelemetry_tracing_layer()) .layer(prometheus_layer) .route( "/metrics", @@ -32,21 +28,40 @@ pub async fn run_app(api: Router, ui: Option, host: IpAddr, port: u16) { }; let address = SocketAddr::from((host, port)); - info!("Listening at {}", address); - Server::bind(&address) - .serve(app.into_make_service_with_connect_info::()) - .await - .unwrap_or_else(|err| fatal!("Error happens during serving: {}", err)) + let version = env!("CARGO_PKG_VERSION"); + println!( + r#" +████████╗ █████╗ ██████╗ ██████╗ ██╗ ██╗ +╚══██╔══╝██╔══██╗██╔══██╗██╔══██╗╚██╗ ██╔╝ + ██║ ███████║██████╔╝██████╔╝ ╚████╔╝ + ██║ ██╔══██║██╔══██╗██╔══██╗ ╚██╔╝ + ██║ ██║ ██║██████╔╝██████╔╝ ██║ + ╚═╝ ╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═╝ + +📄 Version {version} +🚀 Listening at http://{address} +"# + ); + let listener = tokio::net::TcpListener::bind(address).await.unwrap(); + + axum::serve( + listener, + app.into_make_service_with_connect_info::(), + ) + .await + .unwrap_or_else(|err| fatal!("Error happens during serving: {}", err)) } mod chat; mod completions; mod events; mod health; -mod search; +mod models; +mod server_setting; pub use chat::*; pub use completions::*; pub use events::*; pub use health::*; -pub use search::*; +pub use models::*; +pub use server_setting::*; diff --git a/crates/tabby/src/routes/models.rs b/crates/tabby/src/routes/models.rs new file mode 100644 index 000000000000..e11fc66eecbc --- /dev/null +++ b/crates/tabby/src/routes/models.rs @@ -0,0 +1,54 @@ +use std::sync::Arc; + +use axum::{extract::State, Json}; +use serde::{Deserialize, Serialize}; +use tabby_common::api::server_setting::ServerSetting; +use utoipa::ToSchema; + +#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)] +pub struct ModelInfo { + completion: Option>, + chat: Option>, +} + +impl From for ModelInfo { + fn from(value: tabby_common::config::Config) -> Self { + let models = value.model; + let mut http_model_configs: ModelInfo = ModelInfo { + completion: None, + chat: None, + }; + + if let Some(tabby_common::config::ModelConfig::Http(completion_http_config)) = + models.completion + { + if let Some(models) = completion_http_config.supported_models { + http_model_configs.completion = Some(models.clone()); + } + } + + if let Some(tabby_common::config::ModelConfig::Http(chat_http_config)) = models.chat { + if let Some(models) = chat_http_config.supported_models { + http_model_configs.chat = Some(models.clone()); + } + } + + http_model_configs + } +} + +#[utoipa::path( + get, + path = "/v1beta/models", + tag = "v1beta", + operation_id = "config", + responses( + (status = 200, description = "Success", body = ServerSetting, content_type = "application/json"), + ), + security( + ("token" = []) + ) +)] +pub async fn models(State(state): State>) -> Json { + Json(state.as_ref().clone()) +} diff --git a/crates/tabby/src/routes/search.rs b/crates/tabby/src/routes/search.rs deleted file mode 100644 index a7ff29aa06db..000000000000 --- a/crates/tabby/src/routes/search.rs +++ /dev/null @@ -1,64 +0,0 @@ -use std::sync::Arc; - -use anyhow::Result; -use axum::{ - extract::{Query, State}, - Json, -}; -use hyper::StatusCode; -use serde::Deserialize; -use tabby_common::api::code::{CodeSearch, CodeSearchError, SearchResponse}; -use tracing::{instrument, warn}; -use utoipa::IntoParams; - -#[derive(Deserialize, IntoParams)] -pub struct SearchQuery { - #[param(default = "get")] - q: String, - - #[param(default = 20)] - limit: Option, - - #[param(default = 0)] - offset: Option, -} - -#[utoipa::path( - get, - params(SearchQuery), - path = "/v1beta/search", - operation_id = "search", - tag = "v1beta", - responses( - (status = 200, description = "Success" , body = SearchResponse, content_type = "application/json"), - (status = 501, description = "When code search is not enabled, the endpoint will returns 501 Not Implemented"), - ), - security( - ("token" = []) - ) -)] -#[instrument(skip(state, query))] -pub async fn search( - State(state): State>, - query: Query, -) -> Result, StatusCode> { - match state - .search( - &query.q, - query.limit.unwrap_or(20), - query.offset.unwrap_or(0), - ) - .await - { - Ok(serp) => Ok(Json(serp)), - Err(CodeSearchError::NotReady) => Err(StatusCode::NOT_IMPLEMENTED), - Err(CodeSearchError::TantivyError(err)) => { - warn!("{}", err); - Err(StatusCode::INTERNAL_SERVER_ERROR) - } - Err(CodeSearchError::QueryParserError(err)) => { - warn!("{}", err); - Err(StatusCode::BAD_REQUEST) - } - } -} diff --git a/crates/tabby/src/routes/server_setting.rs b/crates/tabby/src/routes/server_setting.rs new file mode 100644 index 000000000000..adf6c38ba926 --- /dev/null +++ b/crates/tabby/src/routes/server_setting.rs @@ -0,0 +1,21 @@ +use axum::Json; +use tabby_common::api::server_setting::ServerSetting; + +#[utoipa::path( + get, + path = "/v1beta/server_setting", + tag = "v1beta", + operation_id = "config", + responses( + (status = 200, description = "Success", body = ServerSetting, content_type = "application/json"), + ), + security( + ("token" = []) + ) +)] +pub async fn setting() -> Json { + let config = ServerSetting { + disable_client_side_telemetry: false, + }; + Json(config) +} diff --git a/crates/tabby/src/serve.rs b/crates/tabby/src/serve.rs index b1759abbb18b..1641473ce73a 100644 --- a/crates/tabby/src/serve.rs +++ b/crates/tabby/src/serve.rs @@ -1,17 +1,21 @@ use std::{net::IpAddr, sync::Arc, time::Duration}; -use axum::{routing, Router}; +use axum::{routing, Extension, Router}; use clap::Args; use hyper::StatusCode; +use spinners::{Spinner, Spinners, Stream}; use tabby_common::{ - api, - api::{code::CodeSearch, event::EventLogger}, - config::Config, + api::{self, code::CodeSearch, event::EventLogger}, + axum::AllowedCodeRepository, + config::{Config, ModelConfig}, usage, }; -use tokio::time::sleep; +use tabby_download::ModelKind; +#[cfg(feature = "ee")] +use tabby_webserver::EEApiDoc; +use tokio::{sync::oneshot::Sender, time::sleep}; use tower_http::timeout::TimeoutLayer; -use tracing::info; +use tracing::{debug, warn}; use utoipa::{ openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme}, Modify, OpenApi, @@ -19,16 +23,18 @@ use utoipa::{ use utoipa_swagger_ui::SwaggerUi; use crate::{ - routes::{self, run_app}, + routes::{self, run_app, ChatState}, services::{ - chat::{self, create_chat_service}, + self, code::create_code_search, - completion::{self, create_completion_service}, - event::create_logger, + completion::{self, create_completion_service_and_chat, CompletionService}, + embedding, + event::create_event_logger, health, model::download_model_if_needed, + tantivy::IndexReaderProvider, }, - Device, + to_local_config, Device, }; #[derive(OpenApi)] @@ -48,26 +54,27 @@ Install following IDE / Editor extensions to get started with [Tabby](https://gi servers( (url = "/", description = "Server"), ), - paths(routes::log_event, routes::completions, routes::chat_completions, routes::health, routes::search), + paths( + routes::log_event, + routes::completions, + routes::chat_completions_utoipa, + routes::health, + routes::setting, + ), components(schemas( api::event::LogEventRequest, completion::CompletionRequest, completion::CompletionResponse, completion::Segments, + completion::Declaration, completion::Choice, completion::Snippet, completion::DebugOptions, completion::DebugData, - chat::ChatCompletionRequest, - chat::ChatCompletionChoice, - chat::ChatCompletionDelta, - chat::Message, - chat::ChatCompletionChunk, + completion::EditHistory, health::HealthState, health::Version, - api::code::SearchResponse, - api::code::Hit, - api::code::HitDocument + api::server_setting::ServerSetting, )), modifiers(&SecurityAddon), )] @@ -93,6 +100,10 @@ pub struct ServeArgs { #[clap(long, default_value_t=Device::Cpu)] device: Device, + /// Device to run chat model [default equals --device arg] + #[clap(long, requires("chat_model"))] + chat_device: Option, + /// Parallelism for model serving - increasing this number will have a significant impact on the /// memory requirement e.g., GPU vRAM. #[clap(long, default_value_t = 1)] @@ -100,53 +111,140 @@ pub struct ServeArgs { #[cfg(feature = "ee")] #[clap(hide = true, long, default_value_t = false)] - webserver: bool, + no_webserver: bool, } pub async fn main(config: &Config, args: &ServeArgs) { - #[cfg(feature = "experimental-http")] - if args.device == Device::ExperimentalHttp { - tracing::warn!("HTTP device is unstable and does not comply with semver expectations."); - } else { - load_model(args).await; - } - #[cfg(not(feature = "experimental-http"))] - load_model(args).await; + let config = merge_args(config, args); - info!("Starting server, this might take a few minutes..."); + load_model(&config).await; - let logger = Arc::new(create_logger()); - let code = Arc::new(create_code_search()); + let tx = try_run_spinner(); - let api = api_router(args, config, logger.clone(), code.clone()).await; - let ui = Router::new() - .merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi())); + #[allow(unused_assignments)] + let mut webserver = None; #[cfg(feature = "ee")] - let (api, ui) = if args.webserver { - let (api, ui) = - tabby_webserver::public::attach_webserver(api, ui, logger, code, config, args.port) - .await; - (api, ui) + { + webserver = Some(!args.no_webserver) + } + + let embedding = if tabby_common::config::is_embedding_service_enabled() { + embedding::create(&config.model.embedding).await } else { - let ui = ui.fallback(|| async { axum::response::Redirect::temporary("/swagger-ui") }); - (api, ui) + None }; - #[cfg(not(feature = "ee"))] - let ui = ui.fallback(|| async { axum::response::Redirect::temporary("/swagger-ui") }); + #[cfg(feature = "ee")] + let ws = if !args.no_webserver { + Some( + tabby_webserver::public::Webserver::new(create_event_logger(), embedding.clone()).await, + ) + } else { + None + }; + + let mut logger: Arc = Arc::new(create_event_logger()); + + #[cfg(feature = "ee")] + if let Some(ws) = &ws { + logger = ws.logger(); + } + + let index_reader_provider = embedding + .is_some() + .then(|| Arc::new(IndexReaderProvider::default())); + + let docsearch = embedding.clone().zip(index_reader_provider.clone()).map( + |(embedding, index_reader_provider)| { + Arc::new(services::structured_doc::create( + embedding.clone(), + index_reader_provider.clone(), + )) as Arc + }, + ); + + let code = embedding + .zip(index_reader_provider) + .map(|(embedding, index_reader_provider)| { + Arc::new(create_code_search( + embedding.clone(), + index_reader_provider.clone(), + )) as Arc + }); - start_heartbeat(args); + let model = &config.model; + let (completion, completion_stream, chat) = create_completion_service_and_chat( + &config.completion, + code.clone(), + logger.clone(), + model.completion.clone(), + model.chat.clone(), + ) + .await; + + let chat_state = chat.as_ref().map(|c| { + Arc::new(ChatState { + chat_completion: c.clone(), + logger: logger.clone(), + }) + }); + let mut api = api_router( + args, + &config, + logger.clone(), + code.clone(), + completion, + chat_state, + webserver, + ) + .await; + let mut doc = ApiDoc::openapi(); + #[cfg(feature = "ee")] + doc.merge(EEApiDoc::openapi()); + let mut ui = Router::new() + .merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", doc)) + .fallback(|| async { axum::response::Redirect::temporary("/swagger-ui") }); + + #[cfg(feature = "ee")] + if let Some(ws) = &ws { + let (new_api, new_ui) = ws + .attach( + &config, + api, + ui, + code, + chat, + completion_stream, + docsearch, + |x| Box::new(services::structured_doc::create_serper(x)), + ) + .await; + api = new_api; + ui = new_ui; + }; + + if let Some(tx) = tx { + tx.send(()) + .unwrap_or_else(|_| warn!("Spinner channel is closed")); + } + start_heartbeat(args, &config, webserver); run_app(api, Some(ui), args.host, args.port).await } -async fn load_model(args: &ServeArgs) { - if let Some(model) = &args.model { - download_model_if_needed(model).await; +async fn load_model(config: &Config) { + if let Some(ModelConfig::Local(ref model)) = config.model.completion { + download_model_if_needed(&model.model_id, ModelKind::Completion).await; } - if let Some(chat_model) = &args.chat_model { - download_model_if_needed(chat_model).await + if let Some(ModelConfig::Local(ref model)) = config.model.chat { + download_model_if_needed(&model.model_id, ModelKind::Chat).await; + } + + if tabby_common::config::is_embedding_service_enabled() { + if let ModelConfig::Local(ref model) = config.model.embedding { + download_model_if_needed(&model.model_id, ModelKind::Embedding).await; + } } } @@ -154,37 +252,20 @@ async fn api_router( args: &ServeArgs, config: &Config, logger: Arc, - code: Arc, + _code: Option>, + completion_state: Option, + chat_state: Option>, + webserver: Option, ) -> Router { - let completion_state = if let Some(model) = &args.model { - Some(Arc::new( - create_completion_service( - code.clone(), - logger.clone(), - model, - &args.device, - args.parallelism, - ) - .await, - )) - } else { - None - }; - - let chat_state = if let Some(chat_model) = &args.chat_model { - Some(Arc::new( - create_chat_service(logger.clone(), chat_model, &args.device, args.parallelism).await, - )) - } else { - None - }; - let mut routers = vec![]; let health_state = Arc::new(health::HealthState::new( - args.model.as_deref(), - args.chat_model.as_deref(), + &config.model, &args.device, + args.chat_model + .as_deref() + .map(|_| args.chat_device.as_ref().unwrap_or(&args.device)), + webserver, )); routers.push({ @@ -201,19 +282,25 @@ async fn api_router( "/v1/health", routing::get(routes::health).with_state(health_state), ) + .route("/v1beta/models", routing::get(routes::models)) + .with_state(Arc::new(config.clone().into())) }); if let Some(completion_state) = completion_state { - routers.push({ - Router::new() - .route( - "/v1/completions", - routing::post(routes::completions).with_state(completion_state), - ) - .layer(TimeoutLayer::new(Duration::from_secs( - config.server.completion_timeout, - ))) - }); + let mut router = Router::new() + .route( + "/v1/completions", + routing::post(routes::completions).with_state(Arc::new(completion_state)), + ) + .layer(TimeoutLayer::new(Duration::from_secs( + config.server.completion_timeout, + ))); + + if webserver.is_none() || webserver.is_some_and(|x| !x) { + router = router.layer(Extension(AllowedCodeRepository::new_from_config())); + } + + routers.push(router); } else { routers.push({ Router::new().route( @@ -224,27 +311,46 @@ async fn api_router( } if let Some(chat_state) = chat_state { + routers.push({ + Router::new().route( + "/v1/chat/completions", + routing::post(routes::chat_completions).with_state(chat_state.clone()), + ) + }); + + // For forward compatibility of `/v1beta` route. routers.push({ Router::new().route( "/v1beta/chat/completions", routing::post(routes::chat_completions).with_state(chat_state), ) - }) + }); } else { + routers.push({ + Router::new().route( + "/v1/chat/completions", + routing::post(StatusCode::NOT_IMPLEMENTED), + ) + }); + routers.push({ Router::new().route( "/v1beta/chat/completions", routing::post(StatusCode::NOT_IMPLEMENTED), ) - }) + }); } - routers.push({ - Router::new().route( - "/v1beta/search", - routing::get(routes::search).with_state(code), - ) - }); + let server_setting_router = + Router::new().route("/v1beta/server_setting", routing::get(routes::setting)); + + #[cfg(feature = "ee")] + if args.no_webserver { + routers.push(server_setting_router) + } + + #[cfg(not(feature = "ee"))] + routers.push(server_setting_router); let mut root = Router::new(); for router in routers { @@ -253,12 +359,15 @@ async fn api_router( root } -fn start_heartbeat(args: &ServeArgs) { - let state = health::HealthState::new( - args.model.as_deref(), - args.chat_model.as_deref(), +fn start_heartbeat(args: &ServeArgs, config: &Config, webserver: Option) { + let state = Arc::new(health::HealthState::new( + &config.model, &args.device, - ); + args.chat_model + .as_deref() + .map(|_| args.chat_device.as_ref().unwrap_or(&args.device)), + webserver, + )); tokio::spawn(async move { loop { usage::capture("ServeHealth", &state).await; @@ -284,3 +393,45 @@ impl Modify for SecurityAddon { } } } + +fn merge_args(config: &Config, args: &ServeArgs) -> Config { + let mut config = (*config).clone(); + if let Some(model) = &args.model { + if config.model.completion.is_some() { + warn!("Overriding completion model from config.toml. The overriding behavior might surprise you. Consider setting the model in config.toml directly."); + } + config.model.completion = Some(to_local_config(model, args.parallelism, &args.device)); + }; + + if let Some(chat_model) = &args.chat_model { + if config.model.chat.is_some() { + warn!("Overriding chat model from config.toml. The overriding behavior might surprise you. Consider setting the model in config.toml directly."); + } + config.model.chat = Some(to_local_config( + chat_model, + args.parallelism, + args.chat_device.as_ref().unwrap_or(&args.device), + )); + } + + config +} + +fn try_run_spinner() -> Option> { + if cfg!(feature = "prod") { + let (tx, rx) = tokio::sync::oneshot::channel(); + tokio::task::spawn(async move { + let mut sp = Spinner::with_timer_and_stream( + Spinners::Dots, + "Starting...".into(), + Stream::Stdout, + ); + let _ = rx.await; + sp.stop_with_message("".into()); + }); + Some(tx) + } else { + debug!("Starting server, this might take a few minutes..."); + None + } +} diff --git a/crates/tabby/src/services/chat.rs b/crates/tabby/src/services/chat.rs deleted file mode 100644 index b9ed75fc5cd7..000000000000 --- a/crates/tabby/src/services/chat.rs +++ /dev/null @@ -1,183 +0,0 @@ -mod chat_prompt; - -use std::sync::Arc; - -use async_stream::stream; -use chat_prompt::ChatPromptBuilder; -use futures::stream::BoxStream; -use serde::{Deserialize, Serialize}; -use tabby_common::api::event::{Event, EventLogger}; -use tabby_inference::{TextGeneration, TextGenerationOptions, TextGenerationOptionsBuilder}; -use thiserror::Error; -use tracing::debug; -use utoipa::ToSchema; -use uuid::Uuid; - -use super::model; -use crate::{fatal, Device}; - -#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)] -#[schema(example=json!({ - "messages": [ - Message { role: "user".to_owned(), content: "What is tail recursion?".to_owned()}, - Message { role: "assistant".to_owned(), content: "It's a kind of optimization in compiler?".to_owned()}, - Message { role: "user".to_owned(), content: "Could you share more details?".to_owned()}, - ] -}))] -pub struct ChatCompletionRequest { - messages: Vec, - temperature: Option, - seed: Option, -} - -#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)] -pub struct Message { - role: String, - content: String, -} - -#[derive(Error, Debug)] -pub enum CompletionError { - #[error("failed to format prompt")] - MiniJinja(#[from] minijinja::Error), -} - -#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)] -pub struct ChatCompletionChunk { - id: String, - created: u64, - system_fingerprint: String, - object: &'static str, - model: &'static str, - choices: [ChatCompletionChoice; 1], -} - -#[derive(Serialize, Deserialize, Clone, Debug, ToSchema)] -pub struct ChatCompletionChoice { - index: usize, - logprobs: Option, - finish_reason: Option, - delta: ChatCompletionDelta, -} - -#[derive(Serialize, Deserialize, Clone, Debug, ToSchema)] -pub struct ChatCompletionDelta { - content: String, -} - -impl ChatCompletionChunk { - fn new(content: String, id: String, created: u64, last_chunk: bool) -> Self { - ChatCompletionChunk { - id, - created, - object: "chat.completion.chunk", - model: "unused-model", - system_fingerprint: "unused-system-fingerprint".into(), - choices: [ChatCompletionChoice { - index: 0, - delta: ChatCompletionDelta { content }, - logprobs: None, - finish_reason: last_chunk.then(|| "stop".into()), - }], - } - } -} - -pub struct ChatService { - engine: Arc, - logger: Arc, - prompt_builder: ChatPromptBuilder, -} - -impl ChatService { - fn new( - engine: Arc, - logger: Arc, - chat_template: String, - ) -> Self { - Self { - engine, - logger, - prompt_builder: ChatPromptBuilder::new(chat_template), - } - } - - fn text_generation_options(temperature: Option, seed: u64) -> TextGenerationOptions { - let mut builder = TextGenerationOptionsBuilder::default(); - builder - .max_input_length(2048) - .max_decoding_length(1920) - .seed(seed); - if let Some(temperature) = temperature { - builder.sampling_temperature(temperature); - } - builder.build().unwrap() - } - - pub async fn generate<'a>( - self: Arc, - request: ChatCompletionRequest, - ) -> Result, CompletionError> { - let mut event_output = String::new(); - let event_input = convert_messages(&request.messages); - - let prompt = self.prompt_builder.build(&request.messages)?; - let options = Self::text_generation_options( - request.temperature, - request - .seed - .unwrap_or_else(TextGenerationOptions::default_seed), - ); - let created = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .expect("Must be able to read system clock") - .as_secs(); - let id = format!("chatcmpl-{}", Uuid::new_v4()); - - debug!("PROMPT: {}", prompt); - let s = stream! { - for await content in self.engine.generate_stream(&prompt, options).await { - event_output.push_str(&content); - yield ChatCompletionChunk::new(content, id.clone(), created, false) - } - yield ChatCompletionChunk::new("".into(), id.clone(), created, true); - - self.logger.log(Event::ChatCompletion { completion_id: id, input: event_input, output: create_assistant_message(event_output) }); - }; - - Ok(Box::pin(s)) - } -} - -fn create_assistant_message(string: String) -> tabby_common::api::event::Message { - tabby_common::api::event::Message { - content: string, - role: "assistant".into(), - } -} - -fn convert_messages(input: &[Message]) -> Vec { - input - .iter() - .map(|m| tabby_common::api::event::Message { - content: m.content.clone(), - role: m.role.clone(), - }) - .collect() -} - -pub async fn create_chat_service( - logger: Arc, - model: &str, - device: &Device, - parallelism: u8, -) -> ChatService { - let (engine, model::PromptInfo { chat_template, .. }) = - model::load_text_generation(model, device, parallelism).await; - - let Some(chat_template) = chat_template else { - fatal!("Chat model requires specifying prompt template"); - }; - - ChatService::new(engine, logger, chat_template) -} diff --git a/crates/tabby/src/services/chat/chat_prompt.rs b/crates/tabby/src/services/chat/chat_prompt.rs deleted file mode 100644 index f3873dfad466..000000000000 --- a/crates/tabby/src/services/chat/chat_prompt.rs +++ /dev/null @@ -1,61 +0,0 @@ -use minijinja::{context, Environment}; - -use super::{CompletionError, Message}; - -pub struct ChatPromptBuilder { - env: Environment<'static>, -} - -impl ChatPromptBuilder { - pub fn new(prompt_template: String) -> Self { - let mut env = Environment::new(); - env.add_function("raise_exception", |e: String| panic!("{}", e)); - env.add_template_owned("prompt", prompt_template) - .expect("Failed to compile template"); - - Self { env } - } - - pub fn build(&self, messages: &[Message]) -> Result { - Ok(self.env.get_template("prompt")?.render(context!( - messages => messages - ))?) - } -} - -#[cfg(test)] -mod tests { - use super::*; - static PROMPT_TEMPLATE : &str = "{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ '[INST] ' + message['content'] + ' [/INST]' }}{% elif message['role'] == 'assistant' %}{{ message['content'] + ' ' }}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %}"; - - #[test] - fn test_it_works() { - let builder = ChatPromptBuilder::new(PROMPT_TEMPLATE.to_owned()); - let messages = vec![ - Message { - role: "user".to_owned(), - content: "What is tail recursion?".to_owned(), - }, - Message { - role: "assistant".to_owned(), - content: "It's a kind of optimization in compiler?".to_owned(), - }, - Message { - role: "user".to_owned(), - content: "Could you share more details?".to_owned(), - }, - ]; - assert_eq!(builder.build(&messages).unwrap(), "[INST] What is tail recursion? [/INST]It's a kind of optimization in compiler? [INST] Could you share more details? [/INST]") - } - - #[test] - #[should_panic] - fn test_it_panic() { - let builder = ChatPromptBuilder::new(PROMPT_TEMPLATE.to_owned()); - let messages = vec![Message { - role: "system".to_owned(), - content: "system".to_owned(), - }]; - builder.build(&messages).unwrap(); - } -} diff --git a/crates/tabby/src/services/code.rs b/crates/tabby/src/services/code.rs index 7ca31888aa65..e9c5963d0b96 100644 --- a/crates/tabby/src/services/code.rs +++ b/crates/tabby/src/services/code.rs @@ -1,192 +1,401 @@ -use std::{sync::Arc, time::Duration}; +use std::{collections::HashMap, sync::Arc}; use anyhow::Result; use async_trait::async_trait; use tabby_common::{ - api::code::{CodeSearch, CodeSearchError, Hit, HitDocument, SearchResponse}, - index::{self, register_tokenizers, CodeSearchSchema}, - path, + api::code::{ + CodeSearch, CodeSearchDocument, CodeSearchError, CodeSearchHit, CodeSearchParams, + CodeSearchQuery, CodeSearchResponse, CodeSearchScores, + }, + index::{ + self, + code::{self, tokenize_code}, + corpus, IndexSchema, + }, }; +use tabby_inference::Embedding; use tantivy::{ - collector::{Count, TopDocs}, - query::{BooleanQuery, QueryParser}, - query_grammar::Occur, - schema::Field, - DocAddress, Document, Index, IndexReader, + collector::TopDocs, + schema::{self, Value}, + IndexReader, TantivyDocument, }; -use tokio::{sync::Mutex, time::sleep}; -use tracing::{debug, log::info}; -struct CodeSearchImpl { - reader: IndexReader, - query_parser: QueryParser, +use super::tantivy::IndexReaderProvider; - schema: CodeSearchSchema, +struct CodeSearchImpl { + embedding: Arc, } impl CodeSearchImpl { - fn load() -> Result { - let code_schema = index::CodeSearchSchema::new(); - let index = Index::open_in_dir(path::index_dir())?; - register_tokenizers(&index); - - let query_parser = QueryParser::new( - code_schema.schema.clone(), - vec![code_schema.field_body], - index.tokenizers().clone(), - ); - let reader = index - .reader_builder() - .reload_policy(tantivy::ReloadPolicy::OnCommit) - .try_into()?; - Ok(Self { - reader, - query_parser, - schema: code_schema, - }) - } - - async fn load_async() -> CodeSearchImpl { - loop { - match CodeSearchImpl::load() { - Ok(code) => { - info!("Index is ready, enabling server..."); - return code; - } - Err(err) => { - debug!("Source code index is not ready `{}`", err); - } - }; - - sleep(Duration::from_secs(60)).await; - } - } - - fn create_hit(&self, score: f32, doc: Document, doc_address: DocAddress) -> Hit { - Hit { - score, - doc: HitDocument { - body: get_field(&doc, self.schema.field_body), - filepath: get_field(&doc, self.schema.field_filepath), - git_url: get_field(&doc, self.schema.field_git_url), - kind: get_field(&doc, self.schema.field_kind), - name: get_field(&doc, self.schema.field_name), - language: get_field(&doc, self.schema.field_language), - }, - id: doc_address.doc_id, - } + fn new(embedding: Arc) -> Self { + Self { embedding } } async fn search_with_query( &self, + reader: &IndexReader, q: &dyn tantivy::query::Query, limit: usize, - offset: usize, - ) -> Result { - let searcher = self.reader.searcher(); - let (top_docs, num_hits) = - { searcher.search(q, &(TopDocs::with_limit(limit).and_offset(offset), Count))? }; - let hits: Vec = { - top_docs - .iter() - .map(|(score, doc_address)| { - let doc = searcher.doc(*doc_address).unwrap(); - self.create_hit(*score, doc, *doc_address) - }) - .collect() + ) -> Result, CodeSearchError> { + let searcher = reader.searcher(); + let top_docs = { searcher.search(q, &(TopDocs::with_limit(limit)))? }; + let top_docs = top_docs + .iter() + .map(|(score, doc_address)| { + let doc: TantivyDocument = searcher.doc(*doc_address).unwrap(); + (*score, doc) + }) + .collect(); + Ok(top_docs) + } + + async fn search_in_language( + &self, + reader: &IndexReader, + query: CodeSearchQuery, + params: CodeSearchParams, + ) -> Result { + let docs_from_embedding = { + let embedding = self.embedding.embed(&query.content).await?; + let embedding_tokens_query = Box::new(index::embedding_tokens_query( + embedding.len(), + embedding.iter(), + )); + + let query = code::code_search_query(&query, embedding_tokens_query); + self.search_with_query(reader, &query, params.num_to_score) + .await? }; - Ok(SearchResponse { num_hits, hits }) + + let docs_from_bm25 = { + let body_tokens = tokenize_code(&query.content); + let body_query = code::body_query(&body_tokens); + + let query = code::code_search_query(&query, body_query); + self.search_with_query(reader, &query, params.num_to_score) + .await? + }; + + Ok( + merge_code_responses_by_rank(reader, ¶ms, docs_from_embedding, docs_from_bm25) + .await, + ) } } -#[async_trait] -impl CodeSearch for CodeSearchImpl { - async fn search( - &self, - q: &str, - limit: usize, - offset: usize, - ) -> Result { - let query = self.query_parser.parse_query(q)?; - self.search_with_query(&query, limit, offset).await +const RANK_CONSTANT: f32 = 60.0; + +async fn merge_code_responses_by_rank( + reader: &IndexReader, + params: &CodeSearchParams, + embedding_resp: Vec<(f32, TantivyDocument)>, + bm25_resp: Vec<(f32, TantivyDocument)>, +) -> CodeSearchResponse { + let mut scored_hits: HashMap = HashMap::default(); + + for (rank, embedding, doc) in compute_rank_score(embedding_resp).into_iter() { + let scores = CodeSearchScores { + rrf: rank, + embedding, + ..Default::default() + }; + + scored_hits.insert(get_chunk_id(&doc).to_owned(), (scores, doc)); } - async fn search_in_language( - &self, - language: &str, - tokens: &[String], - limit: usize, - offset: usize, - ) -> Result { - let language_query = self.schema.language_query(language); - let body_query = self.schema.body_query(tokens); - let query = BooleanQuery::new(vec![ - (Occur::Must, language_query), - (Occur::Must, body_query), - ]); - self.search_with_query(&query, limit, offset).await + for (rank, bm25, doc) in compute_rank_score(bm25_resp).into_iter() { + let chunk_id = get_chunk_id(&doc); + if let Some((score, _)) = scored_hits.get_mut(chunk_id) { + score.rrf += rank; + score.bm25 = bm25; + } else { + let scores = CodeSearchScores { + rrf: rank, + bm25, + ..Default::default() + }; + scored_hits.insert(chunk_id.to_owned(), (scores, doc)); + } + } + + let scored_hits_futures: Vec<_> = scored_hits + .into_values() + .map(|(scores, doc)| create_hit(reader, scores, doc)) + .collect(); + let mut scored_hits: Vec = futures::future::join_all(scored_hits_futures) + .await + .into_iter() + .collect(); + scored_hits.sort_by(|a, b| b.scores.rrf.total_cmp(&a.scores.rrf)); + retain_at_most_two_hits_per_file(&mut scored_hits); + + CodeSearchResponse { + hits: scored_hits + .into_iter() + .filter(|hit| { + hit.scores.bm25 > params.min_bm25_score + && hit.scores.embedding > params.min_embedding_score + && hit.scores.rrf > params.min_rrf_score + }) + .take(params.num_to_return) + .collect(), + } +} + +fn retain_at_most_two_hits_per_file(scored_hits: &mut Vec) { + let mut scored_hits_by_fileid: HashMap = HashMap::default(); + scored_hits.retain(|x| { + let count: usize = scored_hits_by_fileid + .get(&x.doc.file_id) + .copied() + .unwrap_or_default(); + scored_hits_by_fileid.insert(x.doc.file_id.clone(), count + 1); + count < 2 + }); +} + +fn compute_rank_score(resp: Vec<(f32, TantivyDocument)>) -> Vec<(f32, f32, TantivyDocument)> { + resp.into_iter() + .enumerate() + .map(|(rank, (score, doc))| (1.0 / (RANK_CONSTANT + (rank + 1) as f32), score, doc)) + .collect() +} + +fn get_chunk_id(doc: &TantivyDocument) -> &str { + let schema = IndexSchema::instance(); + get_text(doc, schema.field_chunk_id) +} + +async fn create_hit( + reader: &IndexReader, + scores: CodeSearchScores, + doc: TantivyDocument, +) -> CodeSearchHit { + let schema = IndexSchema::instance(); + let file_id = get_text(&doc, schema.field_id).to_owned(); + let commit = get_commit(reader, &file_id).await; + + let doc = CodeSearchDocument { + file_id, + chunk_id: get_text(&doc, schema.field_chunk_id).to_owned(), + body: get_json_text_field( + &doc, + schema.field_chunk_attributes, + code::fields::CHUNK_BODY, + ) + .to_owned(), + filepath: get_json_text_field( + &doc, + schema.field_chunk_attributes, + code::fields::CHUNK_FILEPATH, + ) + .to_owned(), + git_url: get_json_text_field( + &doc, + schema.field_chunk_attributes, + code::fields::CHUNK_GIT_URL, + ) + .to_owned(), + // commit is introduced in v0.23, but it is also a required field + // so we need to handle the case where it's not present + commit, + language: get_json_text_field( + &doc, + schema.field_chunk_attributes, + code::fields::CHUNK_LANGUAGE, + ) + .to_owned(), + start_line: get_optional_json_number_field( + &doc, + schema.field_chunk_attributes, + code::fields::CHUNK_START_LINE, + ), + }; + CodeSearchHit { scores, doc } +} + +async fn get_commit(reader: &IndexReader, id: &str) -> Option { + let schema = IndexSchema::instance(); + let query = schema.doc_query(corpus::CODE, id); + let doc = reader + .searcher() + .search(&query, &TopDocs::with_limit(1)) + .ok()?; + if doc.is_empty() { + return None; } + + let doc = reader.searcher().doc(doc[0].1).ok()?; + get_json_text_field_optional(&doc, schema.field_attributes, code::fields::COMMIT) + .map(|s| s.to_owned()) +} + +fn get_text(doc: &TantivyDocument, field: schema::Field) -> &str { + doc.get_first(field).unwrap().as_str().unwrap() } -fn get_field(doc: &Document, field: Field) -> String { +fn get_optional_json_number_field( + doc: &TantivyDocument, + field: schema::Field, + name: &str, +) -> Option { doc.get_first(field) - .and_then(|x| x.as_text()) .unwrap() - .to_owned() + .as_object() + .unwrap() + .find(|(k, _)| *k == name)? + .1 + .as_i64() + .map(|x| x as usize) } -struct CodeSearchService { - search: Arc>>, +fn get_json_text_field<'a>(doc: &'a TantivyDocument, field: schema::Field, name: &str) -> &'a str { + doc.get_first(field) + .unwrap() + .as_object() + .unwrap() + .find(|(k, _)| *k == name) + .unwrap() + .1 + .as_str() + .unwrap() } -impl CodeSearchService { - pub fn new() -> Self { - let search = Arc::new(Mutex::new(None)); - - let ret = Self { - search: search.clone(), - }; +fn get_json_text_field_optional<'a>( + doc: &'a TantivyDocument, + field: schema::Field, + name: &str, +) -> Option<&'a str> { + doc.get_first(field) + .and_then(|value| value.as_object()) + .and_then(|mut obj| obj.find(|(k, _)| *k == name)) + .and_then(|(_, v)| v.as_str()) +} - tokio::spawn(async move { - let code = CodeSearchImpl::load_async().await; - *search.lock().await = Some(code); - }); +struct CodeSearchService { + imp: CodeSearchImpl, + provider: Arc, +} - ret +impl CodeSearchService { + pub fn new(embedding: Arc, provider: Arc) -> Self { + Self { + imp: CodeSearchImpl::new(embedding), + provider, + } } } -pub fn create_code_search() -> impl CodeSearch { - CodeSearchService::new() +pub fn create_code_search( + embedding: Arc, + provider: Arc, +) -> impl CodeSearch { + CodeSearchService::new(embedding, provider) } #[async_trait] impl CodeSearch for CodeSearchService { - async fn search( + async fn search_in_language( &self, - q: &str, - limit: usize, - offset: usize, - ) -> Result { - if let Some(imp) = self.search.lock().await.as_ref() { - imp.search(q, limit, offset).await + query: CodeSearchQuery, + params: CodeSearchParams, + ) -> Result { + if let Some(reader) = self.provider.reader().await.as_ref() { + self.imp.search_in_language(reader, query, params).await } else { Err(CodeSearchError::NotReady) } } +} - async fn search_in_language( - &self, - language: &str, - tokens: &[String], - limit: usize, - offset: usize, - ) -> Result { - if let Some(imp) = self.search.lock().await.as_ref() { - imp.search_in_language(language, tokens, limit, offset) - .await - } else { - Err(CodeSearchError::NotReady) +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_retain_at_most_two_hits_per_file() { + let new_hit = |file: &str, body: &str| CodeSearchHit { + scores: Default::default(), + doc: CodeSearchDocument { + file_id: file.to_string(), + chunk_id: "chunk1".to_owned(), + body: body.to_string(), + filepath: "".to_owned(), + git_url: "".to_owned(), + commit: Some("".to_owned()), + language: "".to_owned(), + start_line: Some(0), + }, + }; + + let cases = vec![ + (vec![], vec![]), + ( + vec![new_hit("file1", "body1")], + vec![new_hit("file1", "body1")], + ), + ( + vec![new_hit("file1", "body1"), new_hit("file1", "body2")], + vec![new_hit("file1", "body1"), new_hit("file1", "body2")], + ), + ( + vec![ + new_hit("file1", "body1"), + new_hit("file1", "body2"), + new_hit("file1", "body3"), + ], + vec![new_hit("file1", "body1"), new_hit("file1", "body2")], + ), + ( + vec![ + new_hit("file1", "body1"), + new_hit("file1", "body2"), + new_hit("file1", "body3"), + new_hit("file2", "body4"), + ], + vec![ + new_hit("file1", "body1"), + new_hit("file1", "body2"), + new_hit("file2", "body4"), + ], + ), + ( + vec![ + new_hit("file1", "body1"), + new_hit("file1", "body2"), + new_hit("file1", "body3"), + new_hit("file2", "body4"), + new_hit("file2", "body5"), + ], + vec![ + new_hit("file1", "body1"), + new_hit("file1", "body2"), + new_hit("file2", "body4"), + new_hit("file2", "body5"), + ], + ), + ( + vec![ + new_hit("file1", "body1"), + new_hit("file1", "body2"), + new_hit("file1", "body3"), + new_hit("file2", "body4"), + new_hit("file2", "body5"), + new_hit("file2", "body6"), + ], + vec![ + new_hit("file1", "body1"), + new_hit("file1", "body2"), + new_hit("file2", "body4"), + new_hit("file2", "body5"), + ], + ), + ]; + + for (input, expected) in cases { + let mut input = input; + retain_at_most_two_hits_per_file(&mut input); + assert_eq!(input, expected); } } } diff --git a/crates/tabby/src/services/completion.rs b/crates/tabby/src/services/completion.rs index ea188309630e..e9d4f7bbd652 100644 --- a/crates/tabby/src/services/completion.rs +++ b/crates/tabby/src/services/completion.rs @@ -1,23 +1,28 @@ mod completion_prompt; +mod next_edit_prompt; use std::sync::Arc; +use regex::Regex; use serde::{Deserialize, Serialize}; use tabby_common::{ - api, api::{ + self, code::CodeSearch, event::{Event, EventLogger}, }, + axum::AllowedCodeRepository, + config::{CompletionConfig, ModelConfig}, languages::get_language, }; -use tabby_inference::{TextGeneration, TextGenerationOptions, TextGenerationOptionsBuilder}; +use tabby_inference::{ + ChatCompletionStream, CodeGeneration, CodeGenerationOptions, CodeGenerationOptionsBuilder, + CompletionStream, +}; use thiserror::Error; -use tracing::debug; use utoipa::ToSchema; use super::model; -use crate::Device; #[derive(Error, Debug)] pub enum CompletionError { @@ -53,6 +58,27 @@ pub struct CompletionRequest { /// The seed used for randomly selecting tokens seed: Option, + + /// The mode for completion. Use 'standard' for normal code completions or 'next_edit_suggestion' + /// to predict the next edit the user will make. + #[serde(default = "default_standard_mode")] + mode: String, +} + +pub fn default_standard_mode() -> String { + "standard".to_string() +} + +/// Contains information about edit history for next edit suggestion mode +#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)] +pub struct EditHistory { + original_code: String, + + /// Unified git-style diff of all edits made to the file + edits_diff: String, + + /// Current version of the code after all edits + current_version: String, } impl CompletionRequest { @@ -74,6 +100,11 @@ impl CompletionRequest { .as_ref() .is_some_and(|x| x.disable_retrieval_augmented_code_completion) } + + /// Returns true if the request is for next edit suggestion mode. + fn is_next_edit_suggestion_mode(&self) -> bool { + self.mode == "next_edit_suggestion" + } } #[derive(Serialize, Deserialize, ToSchema, Clone, Debug)] @@ -108,8 +139,47 @@ pub struct Segments { /// Content that appears after the cursor in the editor window. suffix: Option, + /// The relative path of the file that is being edited. + /// - When [Segments::git_url] is set, this is the path of the file in the git repository. + /// - When [Segments::git_url] is empty, this is the path of the file in the workspace. + filepath: Option, + + /// The remote URL of the current git repository. + /// Leave this empty if the file is not in a git repository, + /// or the git repository does not have a remote URL. + git_url: Option, + + /// The relevant declaration code snippets provided by the editor's LSP, + /// contain declarations of symbols extracted from [Segments::prefix]. + declarations: Option>, + + /// The relevant code snippets extracted from recently edited files. + /// These snippets are selected from candidates found within code chunks + /// based on the edited location. + /// The current editing file is excluded from the search candidates. + /// + /// When provided alongside [Segments::declarations], the snippets have + /// already been deduplicated to ensure no duplication with entries + /// in [Segments::declarations]. + /// + /// Sorted in descending order of [Snippet::score]. + relevant_snippets_from_changed_files: Option>, + + /// The relevant code snippets extracted from recently opened files. + /// These snippets are selected from candidates found within code chunks + /// based on the last visited location. + /// + /// Current Active file is excluded from the search candidates. + /// When provided with [Segments::relevant_snippets_from_changed_files], the snippets have + /// already been deduplicated to ensure no duplication with entries + /// in [Segments::relevant_snippets_from_changed_files]. + relevant_snippets_from_recently_opened_files: Option>, + /// Clipboard content when requesting code completion. clipboard: Option, + + /// Required when mode is 'next_edit_suggestion'. Contains information about edit history. + edit_history: Option, } impl From for api::event::Segments { @@ -118,6 +188,34 @@ impl From for api::event::Segments { prefix: val.prefix, suffix: val.suffix, clipboard: val.clipboard, + git_url: val.git_url, + declarations: val + .declarations + .map(|x| x.into_iter().map(Into::into).collect()), + filepath: val.filepath, + } + } +} + +/// A snippet of declaration code that is relevant to the current completion request. +#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)] +pub struct Declaration { + /// Filepath of the file where the snippet is from. + /// - When the file belongs to the same workspace as the current file, + /// this is a relative filepath, use the same rule as [Segments::filepath]. + /// - When the file located outside the workspace, such as in a dependency package, + /// this is a file URI with an absolute filepath. + pub filepath: String, + + /// Body of the snippet. + pub body: String, +} + +impl From for api::event::Declaration { + fn from(val: Declaration) -> Self { + Self { + filepath: val.filepath, + body: val.body, } } } @@ -134,7 +232,7 @@ impl Choice { } } -#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)] +#[derive(Serialize, Deserialize, ToSchema, Clone, Debug, PartialEq)] pub struct Snippet { filepath: String, body: String, @@ -152,14 +250,23 @@ pub struct CompletionResponse { #[serde(skip_serializing_if = "Option::is_none")] debug_data: Option, + + #[serde(default = "default_standard_mode")] + mode: String, } impl CompletionResponse { - pub fn new(id: String, choices: Vec, debug_data: Option) -> Self { + pub fn new( + id: String, + choices: Vec, + debug_data: Option, + mode: String, + ) -> Self { Self { id, choices, debug_data, + mode, } } } @@ -173,22 +280,34 @@ pub struct DebugData { prompt: Option, } +/// CompletionService enhances the CodeGeneration feature by adding Retrieval Augmented Code Completion capability. +/// It enables the retrieval of pertinent code snippets from the code repository, +/// which are then utilized as prompts for the code generation model. pub struct CompletionService { - engine: Arc, + config: CompletionConfig, + engine: Arc, logger: Arc, prompt_builder: completion_prompt::PromptBuilder, + next_edit_prompt_builder: next_edit_prompt::NextEditPromptBuilder, } impl CompletionService { fn new( - engine: Arc, - code: Arc, + config: CompletionConfig, + engine: Arc, + code: Option>, logger: Arc, prompt_template: Option, ) -> Self { Self { engine, - prompt_builder: completion_prompt::PromptBuilder::new(prompt_template, Some(code)), + prompt_builder: completion_prompt::PromptBuilder::new( + &config.code_search_params, + prompt_template, + code, + ), + next_edit_prompt_builder: next_edit_prompt::NextEditPromptBuilder::new(), + config, logger, } } @@ -197,80 +316,111 @@ impl CompletionService { &self, language: &str, segments: &Segments, + allowed_code_repository: &AllowedCodeRepository, disable_retrieval_augmented_code_completion: bool, ) -> Vec { - if !disable_retrieval_augmented_code_completion { - self.prompt_builder.collect(language, segments).await - } else { - vec![] + if disable_retrieval_augmented_code_completion { + return vec![]; } + + self.prompt_builder + .collect(language, segments, allowed_code_repository) + .await } fn text_generation_options( language: &str, temperature: Option, - seed: u64, - ) -> TextGenerationOptions { - let mut builder = TextGenerationOptionsBuilder::default(); + seed: Option, + max_input_length: usize, + max_output_tokens: usize, + mode: String, + ) -> CodeGenerationOptions { + let mut builder = CodeGenerationOptionsBuilder::default(); builder - .max_input_length(1024 + 512) - .max_decoding_length(128) - .seed(seed) + .max_input_length(max_input_length) + .max_decoding_tokens(max_output_tokens as i32) .language(Some(get_language(language))); - if let Some(temperature) = temperature { - builder.sampling_temperature(temperature); - } - builder.build().unwrap() + temperature.inspect(|x| { + builder.sampling_temperature(*x); + }); + seed.inspect(|x| { + builder.seed(*x); + }); + + builder.mode(mode); + + builder + .build() + .expect("Failed to create text generation options") } pub async fn generate( &self, request: &CompletionRequest, + allowed_code_repository: &AllowedCodeRepository, + user_agent: Option<&str>, ) -> Result { let completion_id = format!("cmpl-{}", uuid::Uuid::new_v4()); let language = request.language_or_unknown(); + + if request.is_next_edit_suggestion_mode() { + return self + .generate_next_edit_suggestion(request, completion_id, language, user_agent) + .await; + } + let options = Self::text_generation_options( language.as_str(), request.temperature, - request - .seed - .unwrap_or_else(TextGenerationOptions::default_seed), + request.seed, + self.config.max_input_length, + self.config.max_decoding_tokens, + request.mode.clone(), ); + let mut use_crlf = false; let (prompt, segments, snippets) = if let Some(prompt) = request.raw_prompt() { (prompt, None, vec![]) - } else if let Some(segments) = request.segments.clone() { - debug!("PREFIX: {}, SUFFIX: {:?}", segments.prefix, segments.suffix); + } else if let Some(segments) = request.segments.as_ref() { + if contains_crlf(segments) { + use_crlf = true; + } + let snippets = self .build_snippets( &language, - &segments, + segments, + allowed_code_repository, request.disable_retrieval_augmented_code_completion(), ) .await; let prompt = self .prompt_builder .build(&language, segments.clone(), &snippets); - (prompt, Some(segments), snippets) + + (override_prompt(prompt, use_crlf), Some(segments), snippets) } else { return Err(CompletionError::EmptyPrompt); }; - debug!("PROMPT: {}", prompt); - - let text = self.engine.generate(&prompt, options).await; - let segments = segments.map(|s| s.into()); - - self.logger.log(Event::Completion { - completion_id: completion_id.clone(), - language, - prompt: prompt.clone(), - segments, - choices: vec![api::event::Choice { - index: 0, - text: text.clone(), - }], - user: request.user.clone(), - }); + + let generated_text = + override_generated_text(self.engine.generate(&prompt, options).await, use_crlf); + + self.logger.log( + request.user.clone(), + Event::Completion { + completion_id: completion_id.clone(), + language, + prompt: prompt.clone(), + segments: segments.cloned().map(|x| x.into()), + choices: vec![api::event::Choice { + index: 0, + text: generated_text.clone(), + }], + user_agent: user_agent.map(|x| x.to_owned()), + }, + ); let debug_data = request .debug_options @@ -282,25 +432,322 @@ impl CompletionService { Ok(CompletionResponse::new( completion_id, - vec![Choice::new(text)], + vec![Choice::new(generated_text)], debug_data, + "standard".to_string(), + )) + } + + async fn generate_next_edit_suggestion( + &self, + request: &CompletionRequest, + completion_id: String, + language: String, + user_agent: Option<&str>, + ) -> Result { + let segments = request + .segments + .as_ref() + .ok_or(CompletionError::EmptyPrompt)?; + + let edit_history = segments + .edit_history + .as_ref() + .ok_or(CompletionError::EmptyPrompt)?; + + let prompt = self.next_edit_prompt_builder.build_prompt(edit_history); + + let options = Self::text_generation_options( + language.as_str(), + request.temperature, + request.seed, + self.config.max_input_length, + self.config.max_decoding_tokens * 2, + request.mode.clone(), + ); + + let generated_text = self.engine.generate(&prompt, options).await; + + self.logger.log( + request.user.clone(), + Event::Completion { + completion_id: completion_id.clone(), + language, + prompt: prompt.clone(), + segments: None, + choices: vec![api::event::Choice { + index: 0, + text: generated_text.clone(), + }], + user_agent: user_agent.map(|x| x.to_owned()), + }, + ); + + let debug_data = request + .debug_options + .as_ref() + .map(|debug_options| DebugData { + snippets: None, + prompt: debug_options.return_prompt.then_some(prompt), + }); + + Ok(CompletionResponse::new( + completion_id, + vec![Choice::new(generated_text)], + debug_data, + "next_edit_suggestion".to_string(), )) } } -pub async fn create_completion_service( - code: Arc, +fn contains_crlf(segments: &Segments) -> bool { + if segments.prefix.contains("\r\n") { + return true; + } + if let Some(suffix) = &segments.suffix { + if suffix.contains("\r\n") { + return true; + } + } + + false +} + +fn override_prompt(prompt: String, use_crlf: bool) -> String { + if use_crlf { + prompt.replace("\r\n", "\n") + } else { + prompt + } +} + +/// override_generated_text replaces \n with \r\n in the generated text if use_crlf is true. +/// This is used to ensure that the generated text has the same line endings as the prompt. +/// +/// Because there might be \r\n in the text, which also has a `\n` and should not be replaced, +/// we can not simply replace \n with \r\n. +fn override_generated_text(generated: String, use_crlf: bool) -> String { + if use_crlf { + let re = Regex::new(r"([^\r])\n").unwrap(); // Match \n that is preceded by anything except \r + re.replace_all(&generated, "$1\r\n").to_string() // Replace with captured character and \r\n + } else { + generated + } +} + +pub async fn create_completion_service_and_chat( + config: &CompletionConfig, + code: Option>, logger: Arc, - model: &str, - device: &Device, - parallelism: u8, -) -> CompletionService { - let ( - engine, - model::PromptInfo { - prompt_template, .. - }, - ) = model::load_text_generation(model, device, parallelism).await; - - CompletionService::new(engine.clone(), code, logger, prompt_template) + completion: Option, + chat: Option, +) -> ( + Option, + Option>, + Option>, +) { + let (code_generation, completion_stream, chat, prompt) = + model::load_code_generation_and_chat(completion, chat).await; + + let completion = code_generation.clone().map(|code_generation| { + CompletionService::new( + config.to_owned(), + code_generation.clone(), + code, + logger, + prompt + .unwrap_or_else(|| panic!("Prompt template is required for code completion")) + .prompt_template, + ) + }); + + (completion, completion_stream, chat) +} + +#[cfg(test)] +mod tests { + use api::code::CodeSearchParams; + use async_stream::stream; + use async_trait::async_trait; + use futures::stream::BoxStream; + use tabby_common::api::code::{CodeSearchError, CodeSearchQuery, CodeSearchResponse}; + use tabby_inference::{CompletionOptions, CompletionStream}; + + use super::*; + + struct MockEventLogger; + + impl EventLogger for MockEventLogger { + fn write(&self, _x: api::event::LogEntry) {} + } + + struct MockCompletionStream; + + #[async_trait] + impl CompletionStream for MockCompletionStream { + async fn generate(&self, _prompt: &str, _options: CompletionOptions) -> BoxStream { + let s = stream! { + yield r#""Hello, world!""#.into(); + }; + + Box::pin(s) + } + } + + struct MockCodeSearch; + + #[async_trait] + impl CodeSearch for MockCodeSearch { + async fn search_in_language( + &self, + _query: CodeSearchQuery, + _params: CodeSearchParams, + ) -> Result { + Ok(CodeSearchResponse { hits: vec![] }) + } + } + + fn mock_completion_service() -> CompletionService { + let generation = CodeGeneration::new(Arc::new(MockCompletionStream), None); + CompletionService::new( + CompletionConfig::default(), + Arc::new(generation), + Some(Arc::new(MockCodeSearch)), + Arc::new(MockEventLogger), + Some("
    {prefix}{suffix}".into()),
    +        )
    +    }
    +
    +    #[tokio::test]
    +    async fn test_completion_service() {
    +        let completion_service = mock_completion_service();
    +        let segment = Segments {
    +            prefix: "fn hello_world() -> &'static str {".into(),
    +            suffix: Some("}".into()),
    +            filepath: None,
    +            git_url: None,
    +            declarations: None,
    +            relevant_snippets_from_changed_files: None,
    +            relevant_snippets_from_recently_opened_files: None,
    +            clipboard: None,
    +            edit_history: None,
    +        };
    +        let request = CompletionRequest {
    +            language: Some("rust".into()),
    +            segments: Some(segment.clone()),
    +            user: None,
    +            debug_options: None,
    +            temperature: None,
    +            seed: None,
    +            mode: "standard".into(),
    +        };
    +
    +        let allowed_code_repository = AllowedCodeRepository::default();
    +        let response = completion_service
    +            .generate(&request, &allowed_code_repository, Some("test user agent"))
    +            .await
    +            .unwrap();
    +        assert_eq!(response.choices[0].text, r#""Hello, world!""#);
    +
    +        let prompt = completion_service
    +            .prompt_builder
    +            .build("rust", segment.clone(), &[]);
    +        assert_eq!(prompt, "
    fn hello_world() -> &'static str {}");
    +    }
    +
    +    #[test]
    +    fn test_contains_crlf() {
    +        let contained_crlf = vec![
    +            Segments {
    +                prefix: "fn hello_world() -> &'static str {\r\n".into(),
    +                suffix: Some("}".into()),
    +                filepath: None,
    +                git_url: None,
    +                declarations: None,
    +                relevant_snippets_from_changed_files: None,
    +                relevant_snippets_from_recently_opened_files: None,
    +                clipboard: None,
    +                edit_history: None,
    +            },
    +            Segments {
    +                prefix: "fn hello_world() -> &'static str {".into(),
    +                suffix: Some("}\r\n".into()),
    +                filepath: None,
    +                git_url: None,
    +                declarations: None,
    +                relevant_snippets_from_changed_files: None,
    +                relevant_snippets_from_recently_opened_files: None,
    +                clipboard: None,
    +                edit_history: None,
    +            },
    +            Segments {
    +                prefix: "fn hello_world() -> &'static str {\r\n".into(),
    +                suffix: Some("}\r\n".into()),
    +                filepath: None,
    +                git_url: None,
    +                declarations: None,
    +                relevant_snippets_from_changed_files: None,
    +                relevant_snippets_from_recently_opened_files: None,
    +                clipboard: None,
    +                edit_history: None,
    +            },
    +        ];
    +        for segments in contained_crlf {
    +            assert!(contains_crlf(&segments));
    +        }
    +
    +        let not_contained_crlf = vec![Segments {
    +            prefix: "fn hello_world() -> &'static str {\r".into(),
    +            suffix: Some("}\n".into()),
    +            filepath: None,
    +            git_url: None,
    +            declarations: None,
    +            relevant_snippets_from_changed_files: None,
    +            relevant_snippets_from_recently_opened_files: None,
    +            clipboard: None,
    +            edit_history: None,
    +        }];
    +        for segments in not_contained_crlf {
    +            assert!(!contains_crlf(&segments));
    +        }
    +    }
    +
    +    #[test]
    +    fn test_override_prompt() {
    +        let prompt = "fn hello_world() -> &'static str {\r\n".to_string();
    +        let use_crlf = true;
    +        assert_eq!(
    +            override_prompt(prompt.clone(), use_crlf),
    +            "fn hello_world() -> &'static str {\n"
    +        );
    +
    +        let use_crlf = false;
    +        assert_eq!(override_prompt(prompt.clone(), use_crlf), prompt);
    +    }
    +
    +    #[test]
    +    fn test_override_generated() {
    +        let cases = vec![
    +            (
    +                "fn hello_world() -> &'static str {\r\n".to_string(),
    +                "fn hello_world() -> &'static str {\r\n".to_string(),
    +            ),
    +            (
    +                "fn hello_world() -> &'static str {\n".to_string(),
    +                "fn hello_world() -> &'static str {\r\n".to_string(),
    +            ),
    +            (
    +                "fn hello_world() -> &'static str {\r".to_string(),
    +                "fn hello_world() -> &'static str {\r".to_string(),
    +            ),
    +            (
    +                "fn hello_world() -> &'static str {".to_string(),
    +                "fn hello_world() -> &'static str {".to_string(),
    +            ),
    +        ];
    +
    +        for (generated, expected) in cases {
    +            assert_eq!(override_generated_text(generated, true), expected);
    +        }
    +    }
     }
    diff --git a/crates/tabby/src/services/completion/completion_prompt.rs b/crates/tabby/src/services/completion/completion_prompt.rs
    index 00948772cd9e..a4c4b65adde3 100644
    --- a/crates/tabby/src/services/completion/completion_prompt.rs
    +++ b/crates/tabby/src/services/completion/completion_prompt.rs
    @@ -1,29 +1,29 @@
     use std::sync::Arc;
     
    -use lazy_static::lazy_static;
    -use regex::Regex;
     use strfmt::strfmt;
     use tabby_common::{
    -    api::code::{CodeSearch, CodeSearchError},
    +    api::code::{CodeSearch, CodeSearchError, CodeSearchParams, CodeSearchQuery},
    +    axum::AllowedCodeRepository,
         languages::get_language,
     };
    -use textdistance::Algorithm;
     use tracing::warn;
     
     use super::{Segments, Snippet};
     
    -static MAX_SNIPPETS_TO_FETCH: usize = 20;
    -static MAX_SNIPPET_CHARS_IN_PROMPT: usize = 768;
    -static MAX_SIMILARITY_THRESHOLD: f32 = 0.9;
    -
     pub struct PromptBuilder {
    +    code_search_params: CodeSearchParams,
         prompt_template: Option,
         code: Option>,
     }
     
     impl PromptBuilder {
    -    pub fn new(prompt_template: Option, code: Option>) -> Self {
    +    pub fn new(
    +        code_search_params: &CodeSearchParams,
    +        prompt_template: Option,
    +        code: Option>,
    +    ) -> Self {
             PromptBuilder {
    +            code_search_params: code_search_params.clone(),
                 prompt_template,
                 code,
             }
    @@ -37,12 +37,52 @@ impl PromptBuilder {
             strfmt!(prompt_template, prefix => prefix, suffix => suffix).unwrap()
         }
     
    -    pub async fn collect(&self, language: &str, segments: &Segments) -> Vec {
    -        if let Some(code) = &self.code {
    -            collect_snippets(code.as_ref(), language, &segments.prefix).await
    -        } else {
    -            vec![]
    +    pub async fn collect(
    +        &self,
    +        language: &str,
    +        segments: &Segments,
    +        allowed_code_repository: &AllowedCodeRepository,
    +    ) -> Vec {
    +        let quota_threshold_for_snippets_from_code_search = 256;
    +        let mut max_snippets_chars_in_prompt = 768;
    +        let mut snippets: Vec = vec![];
    +
    +        if let Some((count_characters, snippets_from_segments)) =
    +            extract_snippets_from_segments(max_snippets_chars_in_prompt, segments)
    +        {
    +            max_snippets_chars_in_prompt -= count_characters;
    +            snippets.extend(snippets_from_segments.into_iter());
    +        };
    +
    +        if max_snippets_chars_in_prompt <= quota_threshold_for_snippets_from_code_search {
    +            return snippets;
             }
    +
    +        let Some(code) = &self.code else {
    +            return snippets;
    +        };
    +
    +        let Some(git_url) = segments.git_url.as_ref() else {
    +            return snippets;
    +        };
    +
    +        let Some(source_id) = allowed_code_repository.closest_match(git_url) else {
    +            return snippets;
    +        };
    +
    +        let snippets_from_code_search = collect_snippets(
    +            &self.code_search_params,
    +            max_snippets_chars_in_prompt,
    +            code.as_ref(),
    +            source_id,
    +            segments.filepath.as_deref(),
    +            language,
    +            &segments.prefix,
    +        )
    +        .await;
    +
    +        snippets.extend(snippets_from_code_search.into_iter());
    +        snippets
         }
     
         pub fn build(&self, language: &str, segments: Segments, snippets: &[Snippet]) -> String {
    @@ -52,16 +92,9 @@ impl PromptBuilder {
     }
     
     fn get_default_suffix(suffix: Option) -> String {
    -    if suffix.is_none() {
    -        return "\n".to_owned();
    -    }
    -
    -    let suffix = suffix.unwrap();
    -    if suffix.is_empty() {
    -        "\n".to_owned()
    -    } else {
    -        suffix
    -    }
    +    suffix
    +        .filter(|s| !s.is_empty())
    +        .unwrap_or_else(|| "\n".to_string())
     }
     
     fn rewrite_with_snippets(language: &str, segments: Segments, snippets: &[Snippet]) -> Segments {
    @@ -78,7 +111,10 @@ fn build_prefix(language: &str, prefix: &str, snippets: &[Snippet]) -> String {
             return prefix.to_owned();
         }
     
    -    let comment_char = &get_language(language).line_comment;
    +    let Some(comment_char) = &get_language(language).line_comment else {
    +        return prefix.to_owned();
    +    };
    +
         let mut lines: Vec = vec![];
     
         for (i, snippet) in snippets.iter().enumerate() {
    @@ -98,20 +134,95 @@ fn build_prefix(language: &str, prefix: &str, snippets: &[Snippet]) -> String {
                 if x.is_empty() {
                     comment_char.to_string()
                 } else {
    -                format!("{} {}", comment_char, x)
    +                format!("{comment_char} {x}")
                 }
             })
             .collect();
         let comments = commented_lines.join("\n");
    -    format!("{}\n{}", comments, prefix)
    +    format!("{comments}\n{prefix}")
    +}
    +
    +fn extract_snippets_from_segments(
    +    max_snippets_chars: usize,
    +    segments: &Segments,
    +) -> Option<(usize, Vec)> {
    +    let mut count_characters = 0;
    +    let mut ret = Vec::new();
    +
    +    // declarations has highest priority.
    +    if let Some(declarations) = &segments.declarations {
    +        for declaration in declarations {
    +            if count_characters + declaration.body.len() > max_snippets_chars {
    +                break;
    +            }
    +            count_characters += declaration.body.len();
    +            ret.push(Snippet {
    +                filepath: declaration.filepath.clone(),
    +                body: declaration.body.clone(),
    +                score: 1.0,
    +            });
    +        }
    +    }
    +
    +    // then comes to the snippets from changed files.
    +    if let Some(relevant_snippets) = &segments.relevant_snippets_from_changed_files {
    +        for snippet in relevant_snippets {
    +            if count_characters + snippet.body.len() > max_snippets_chars {
    +                break;
    +            }
    +
    +            count_characters += snippet.body.len();
    +            ret.push(Snippet {
    +                filepath: snippet.filepath.clone(),
    +                body: snippet.body.clone(),
    +                score: 1.0,
    +            });
    +        }
    +    }
    +
    +    // then comes to the snippets from recently opened files.
    +    if let Some(relevant_snippets) = &segments.relevant_snippets_from_recently_opened_files {
    +        for snippet in relevant_snippets {
    +            if count_characters + snippet.body.len() > max_snippets_chars {
    +                break;
    +            }
    +
    +            count_characters += snippet.body.len();
    +            ret.push(Snippet {
    +                filepath: snippet.filepath.clone(),
    +                body: snippet.body.clone(),
    +                score: 1.0,
    +            });
    +        }
    +    }
    +
    +    if ret.is_empty() {
    +        None
    +    } else {
    +        Some((count_characters, ret))
    +    }
     }
     
    -async fn collect_snippets(code: &dyn CodeSearch, language: &str, text: &str) -> Vec {
    +async fn collect_snippets(
    +    code_search_params: &CodeSearchParams,
    +    max_snippets_chars: usize,
    +    code: &dyn CodeSearch,
    +    source_id: &str,
    +    filepath: Option<&str>,
    +    language: &str,
    +    content: &str,
    +) -> Vec {
    +    let query = CodeSearchQuery::new(
    +        filepath.map(|x| x.to_owned()),
    +        Some(language.to_owned()),
    +        content.to_owned(),
    +        source_id.to_owned(),
    +    );
    +
         let mut ret = Vec::new();
    -    let mut tokens = tokenize_text(text);
     
         let serp = match code
    -        .search_in_language(language, &tokens, MAX_SNIPPETS_TO_FETCH, 0)
    +        .search_in_language(query, code_search_params.clone())
             .await
         {
             Ok(serp) => serp,
    @@ -127,62 +238,38 @@ async fn collect_snippets(code: &dyn CodeSearch, language: &str, text: &str) ->
                 warn!("Failed to parse query: {}", err);
                 return ret;
             }
    +        Err(CodeSearchError::Other(err)) => {
    +            warn!("Failed to search: {}", err);
    +            return ret;
    +        }
         };
     
         let mut count_characters = 0;
         for hit in serp.hits {
             let body = hit.doc.body;
    -        let mut body_tokens = tokenize_text(&body);
     
    -        if count_characters + body.len() > MAX_SNIPPET_CHARS_IN_PROMPT {
    +        if count_characters + body.len() > max_snippets_chars {
                 break;
             }
     
    -        let similarity = if body_tokens.len() > tokens.len() {
    -            0.0
    -        } else {
    -            let distance = textdistance::LCSSeq::default()
    -                .for_iter(tokens.iter(), body_tokens.iter())
    -                .val() as f32;
    -            distance / body_tokens.len() as f32
    -        };
    -
    -        if similarity > MAX_SIMILARITY_THRESHOLD {
    -            // Exclude snippets presents in context window.
    -            continue;
    -        }
    -
    -        // Prepend body tokens and update tokens, so future similarity calculation will consider
    -        // added snippets.
    -        body_tokens.append(&mut tokens);
    -        tokens.append(&mut body_tokens);
    -
             count_characters += body.len();
             ret.push(Snippet {
                 filepath: hit.doc.filepath,
                 body,
    -            score: hit.score,
    +            score: hit.scores.rrf,
             });
         }
     
         ret
     }
     
    -lazy_static! {
    -    static ref TOKENIZER: Regex = Regex::new(r"[^\w]").unwrap();
    -}
    -
    -fn tokenize_text(text: &str) -> Vec {
    -    TOKENIZER
    -        .split(text)
    -        .map(|x| x.to_owned())
    -        .filter(|x| !x.is_empty())
    -        .collect()
    -}
    -
     #[cfg(test)]
     mod tests {
    +    use async_trait::async_trait;
    +    use tabby_common::api::code::CodeSearchResponse;
    +
         use super::*;
    +    use crate::services::completion::Declaration;
     
         fn create_prompt_builder(with_template: bool) -> PromptBuilder {
             let prompt_template = if with_template {
    @@ -193,17 +280,61 @@ mod tests {
             };
     
             // Init prompt builder with prompt rewrite disabled.
    -        PromptBuilder::new(prompt_template, None)
    +        PromptBuilder::new(&CodeSearchParams::default(), prompt_template, None)
         }
     
         fn make_segment(prefix: String, suffix: Option) -> Segments {
             Segments {
                 prefix,
                 suffix,
    +            filepath: None,
    +            git_url: None,
    +            declarations: None,
    +            relevant_snippets_from_changed_files: None,
    +            relevant_snippets_from_recently_opened_files: None,
                 clipboard: None,
    +            edit_history: None,
             }
         }
     
    +    struct MockCodeSearch(fn() -> Result);
    +
    +    #[async_trait]
    +    impl CodeSearch for MockCodeSearch {
    +        async fn search_in_language(
    +            &self,
    +            _query: CodeSearchQuery,
    +            _params: CodeSearchParams,
    +        ) -> Result {
    +            (self.0)()
    +        }
    +    }
    +
    +    #[tokio::test]
    +    async fn test_collect_snippets() {
    +        // Not ready error from CodeSearch should result in empty snippets, rather than error
    +        let search = MockCodeSearch(|| Err(CodeSearchError::NotReady));
    +        let snippets =
    +            collect_snippets(&CodeSearchParams::default(), 150, &search, "", None, "", "").await;
    +        assert_eq!(snippets, vec![]);
    +
    +        let search = MockCodeSearch(|| {
    +            Ok(CodeSearchResponse {
    +                hits: vec![Default::default()],
    +            })
    +        });
    +        let snippets =
    +            collect_snippets(&CodeSearchParams::default(), 150, &search, "", None, "", "").await;
    +        assert_eq!(
    +            snippets,
    +            vec![Snippet {
    +                filepath: "".into(),
    +                body: "".into(),
    +                score: 0.0
    +            }]
    +        );
    +    }
    +
         #[test]
         fn test_prompt_template() {
             let pb = create_prompt_builder(true);
    @@ -373,35 +504,50 @@ def this_is_prefix():\n";
             );
         }
     
    -    /// Empty strings tokens are not participating rag search and therefore could be removed.
         #[test]
    -    fn test_tokenized_text_filter() {
    -        let prefix = r#"public static String getFileExtension(String fullName) {
    -        String fileName = (new File(fullName)).getName();
    -        int dotIndex = fileName.lastIndexOf('.');
    -         }"#;
    +    fn it_extract_snippets_from_segments() {
    +        let segments = Segments {
    +            prefix: "def fib(n):\n    ".to_string(),
    +            suffix: Some("\n        return fib(n - 1) + fib(n - 2)".to_string()),
    +            filepath: None,
    +            git_url: None,
    +            declarations: None,
    +            relevant_snippets_from_changed_files: None,
    +            relevant_snippets_from_recently_opened_files: None,
    +            clipboard: None,
    +            edit_history: None,
    +        };
     
    -        // with filter
    -        assert_eq!(
    -            tokenize_text(prefix),
    -            [
    -                "public",
    -                "static",
    -                "String",
    -                "getFileExtension",
    -                "String",
    -                "fullName",
    -                "String",
    -                "fileName",
    -                "new",
    -                "File",
    -                "fullName",
    -                "getName",
    -                "int",
    -                "dotIndex",
    -                "fileName",
    -                "lastIndexOf",
    -            ]
    +        let max_snippets_chars = 768;
    +        assert!(extract_snippets_from_segments(max_snippets_chars, &segments).is_none());
    +
    +        let segments = Segments {
    +            prefix: "def fib(n):\n    ".to_string(),
    +            suffix: Some("\n        return fib(n - 1) + fib(n - 2)".to_string()),
    +            filepath: None,
    +            git_url: None,
    +            declarations: Some(vec![Declaration {
    +                filepath: "file:///path/to/file.py".to_string(),
    +                body: "def fib(n):\n    return n if n <= 1 else fib(n - 1) + fib(n - 2)"
    +                    .to_string(),
    +            }]),
    +            relevant_snippets_from_changed_files: Some(vec![Snippet {
    +                filepath: "a1.py".to_owned(),
    +                body: "res_1 = invoke_function_1(n)".to_owned(),
    +                score: 1.0,
    +            }]),
    +            relevant_snippets_from_recently_opened_files: Some(vec![Snippet {
    +                filepath: "b1.py".to_owned(),
    +                body: "res_1 = invoke_function_1(n)".to_owned(),
    +                score: 1.0,
    +            }]),
    +            clipboard: None,
    +            edit_history: None,
    +        };
    +
    +        assert!(
    +            extract_snippets_from_segments(max_snippets_chars, &segments)
    +                .is_some_and(|x| x.1.len() == 3)
             );
         }
     }
    diff --git a/crates/tabby/src/services/completion/next_edit_prompt.rs b/crates/tabby/src/services/completion/next_edit_prompt.rs
    new file mode 100644
    index 000000000000..0644c9f02591
    --- /dev/null
    +++ b/crates/tabby/src/services/completion/next_edit_prompt.rs
    @@ -0,0 +1,42 @@
    +use super::EditHistory;
    +
    +pub struct NextEditPromptBuilder;
    +
    +impl NextEditPromptBuilder {
    +    pub fn new() -> Self {
    +        Self
    +    }
    +
    +    pub fn build_prompt(&self, edit_history: &EditHistory) -> String {
    +        let prompt = format!("<|original_code|>\n{}\n<|edits_diff|>\n{}\n<|current_version|>\n{}\n<|next_version|>\n",
    +            edit_history.original_code,
    +            edit_history.edits_diff,
    +            edit_history.current_version
    +        );
    +
    +        prompt
    +    }
    +}
    +
    +#[cfg(test)]
    +mod tests {
    +    use super::*;
    +
    +    #[test]
    +    fn test_build_prompt() {
    +        let edit_history = EditHistory {
    +            original_code: "fn main() {\n    println!(\"Hello, world!\");\n}".to_string(),
    +            edits_diff: "---src/main.rs\n+++src/main.rs\n@@ -1,1 +1,2 @@\n    println!(\"Hello, world!\");\n    let x = 5;\n    println!(\"Hello, world!\");".to_string(),
    +            current_version: "fn main() {\n    let x = 5;\n    println!(\"Hello, world!\");\n}".to_string(),
    +        };
    +
    +        let builder = NextEditPromptBuilder::new();
    +        let prompt = builder.build_prompt(&edit_history);
    +
    +        assert!(prompt.contains("<|original_code|>"));
    +        assert!(prompt.contains("<|edits_diff|>"));
    +        assert!(prompt.contains("<|current_version|>"));
    +        assert!(prompt.contains("fn main()"));
    +        assert!(prompt.contains("let x = 5;"));
    +    }
    +}
    diff --git a/crates/tabby/src/services/embedding.rs b/crates/tabby/src/services/embedding.rs
    new file mode 100644
    index 000000000000..06f0d2b8b669
    --- /dev/null
    +++ b/crates/tabby/src/services/embedding.rs
    @@ -0,0 +1,10 @@
    +use std::sync::Arc;
    +
    +use tabby_common::config::ModelConfig;
    +use tabby_inference::Embedding;
    +
    +use super::model;
    +
    +pub async fn create(config: &ModelConfig) -> Option> {
    +    model::load_embedding(config).await
    +}
    diff --git a/crates/tabby/src/services/event.rs b/crates/tabby/src/services/event.rs
    index 90bb3fdf219f..156f9e30dbf3 100644
    --- a/crates/tabby/src/services/event.rs
    +++ b/crates/tabby/src/services/event.rs
    @@ -2,7 +2,10 @@ use std::{path::PathBuf, time::Duration};
     
     use chrono::Utc;
     use lazy_static::lazy_static;
    -use tabby_common::{api::event::RawEventLogger, path};
    +use tabby_common::{
    +    api::event::{EventLogger, LogEntry},
    +    path,
    +};
     use tokio::{
         io::AsyncWriteExt,
         sync::mpsc::{unbounded_channel, UnboundedSender},
    @@ -85,7 +88,7 @@ impl EventWriter {
     
             let writer = self.writer.as_mut().unwrap();
             writer
    -            .write_all(format!("{}\n", content).as_bytes())
    +            .write_all(format!("{content}\n").as_bytes())
                 .await
                 .unwrap();
         }
    @@ -98,15 +101,24 @@ impl EventWriter {
     
     struct EventService;
     
    -impl RawEventLogger for EventService {
    -    fn log(&self, content: String) {
    -        if let Err(err) = WRITER.send(content) {
    +impl EventLogger for EventService {
    +    fn write(&self, x: LogEntry) {
    +        let json = match serdeconv::to_json_string(&x) {
    +            Ok(json) => json,
    +            Err(err) => {
    +                error!("Failed to serialize event into json {}", err);
    +                return;
    +            }
    +        };
    +
    +        if let Err(err) = WRITER.send(json) {
                 error!("Failed to write event to file: {}", err);
             }
         }
     }
     
    -pub fn create_logger() -> impl RawEventLogger {
    +#[allow(unused)]
    +pub fn create_event_logger() -> impl EventLogger + 'static {
         EventService
     }
     
    diff --git a/crates/tabby/src/services/health.rs b/crates/tabby/src/services/health.rs
    index 5e48efe34b9c..005b1371041a 100644
    --- a/crates/tabby/src/services/health.rs
    +++ b/crates/tabby/src/services/health.rs
    @@ -3,7 +3,8 @@ use std::env::consts::ARCH;
     use anyhow::Result;
     use nvml_wrapper::Nvml;
     use serde::{Deserialize, Serialize};
    -use sysinfo::{CpuExt, System, SystemExt};
    +use sysinfo::System;
    +use tabby_common::config::{ModelConfig, ModelConfigGroup};
     use utoipa::ToSchema;
     
     use crate::Device;
    @@ -14,39 +15,151 @@ pub struct HealthState {
         model: Option,
         #[serde(skip_serializing_if = "Option::is_none")]
         chat_model: Option,
    +    #[serde(skip_serializing_if = "Option::is_none")]
    +    chat_device: Option,
         device: String,
    +    cuda_devices: Vec,
    +
    +    // Model health status; the above fields are slated for future deprecation.
    +    models: ModelsHealth,
    +
    +    // CPU information for Tabby server
         arch: String,
         cpu_info: String,
         cpu_count: usize,
    -    cuda_devices: Vec,
    +
         version: Version,
    +    webserver: Option,
    +}
    +
    +#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)]
    +pub struct ModelsHealth {
    +    #[serde(skip_serializing_if = "Option::is_none")]
    +    completion: Option,
    +
    +    #[serde(skip_serializing_if = "Option::is_none")]
    +    chat: Option,
    +
    +    embedding: ModelHealth,
    +}
    +
    +#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)]
    +enum ModelHealth {
    +    #[serde(rename = "remote")]
    +    Remote(RemoteModelHealth),
    +    #[serde(rename = "local")]
    +    Local(LocalModelHealth),
    +}
    +
    +#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)]
    +pub struct RemoteModelHealth {
    +    kind: String,
    +    #[serde(skip_serializing_if = "Option::is_none")]
    +    model_name: Option,
    +    api_endpoint: String,
    +}
    +
    +#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)]
    +pub struct LocalModelHealth {
    +    model_id: String,
    +    device: String,
    +    #[serde(skip_serializing_if = "Vec::is_empty")]
    +    cuda_devices: Vec,
    +}
    +
    +impl From<&ModelConfig> for ModelHealth {
    +    fn from(model_config: &ModelConfig) -> Self {
    +        match model_config {
    +            ModelConfig::Http(http) => ModelHealth::Remote(RemoteModelHealth {
    +                kind: http.kind.clone(),
    +                model_name: http.model_name.clone(),
    +                api_endpoint: http.api_endpoint.clone().unwrap_or_default(),
    +            }),
    +            ModelConfig::Local(llama) => ModelHealth::Local(LocalModelHealth {
    +                model_id: llama.model_id.clone(),
    +                device: String::new(),
    +                cuda_devices: vec![],
    +            }),
    +        }
    +    }
    +}
    +
    +impl From<&ModelConfigGroup> for ModelsHealth {
    +    fn from(model_config: &ModelConfigGroup) -> Self {
    +        let completion = model_config.completion.as_ref().map(ModelHealth::from);
    +        let chat = model_config.chat.as_ref().map(ModelHealth::from);
    +
    +        let embedding = ModelHealth::from(&model_config.embedding);
    +
    +        Self {
    +            completion,
    +            chat,
    +            embedding,
    +        }
    +    }
     }
     
     impl HealthState {
    -    pub fn new(model: Option<&str>, chat_model: Option<&str>, device: &Device) -> Self {
    +    pub fn new(
    +        model_config: &ModelConfigGroup,
    +        device: &Device,
    +        chat_device: Option<&Device>,
    +        webserver: Option,
    +    ) -> Self {
             let (cpu_info, cpu_count) = read_cpu_info();
     
    -        let cuda_devices = match read_cuda_devices() {
    -            Ok(s) => s,
    -            Err(_) => vec![],
    -        };
    +        let cuda_devices = read_cuda_devices().unwrap_or_default();
    +        let mut models = ModelsHealth::from(model_config);
    +        if let Some(model) = &mut models.completion {
    +            if let ModelHealth::Local(ref mut local) = model {
    +                local.device = device.to_string();
    +                local.cuda_devices = cuda_devices.clone();
    +            }
    +        }
    +        if let Some(model) = &mut models.chat {
    +            if let ModelHealth::Local(ref mut local) = model {
    +                local.device = chat_device.unwrap_or(device).to_string();
    +                local.cuda_devices = cuda_devices.clone();
    +            }
    +        }
    +        if let ModelHealth::Local(ref mut local) = models.embedding {
    +            local.device = device.to_string();
    +            local.cuda_devices = cuda_devices.clone();
    +        }
     
             Self {
    -            model: model.map(|x| x.to_owned()),
    -            chat_model: chat_model.map(|x| x.to_owned()),
    +            model: to_model_name(&model_config.completion),
    +            chat_model: to_model_name(&model_config.chat),
    +            chat_device: chat_device.map(|x| x.to_string()),
                 device: device.to_string(),
    +            models,
                 arch: ARCH.to_string(),
                 cpu_info,
                 cpu_count,
                 cuda_devices,
                 version: Version::new(),
    +            webserver,
             }
         }
     }
     
    +fn to_model_name(model: &Option) -> Option {
    +    if let Some(model) = model {
    +        match model {
    +            ModelConfig::Http(http) => http
    +                .model_name
    +                .clone()
    +                .or_else(|| Some("Remote".to_string())),
    +            ModelConfig::Local(llama) => Some(llama.model_id.clone()),
    +        }
    +    } else {
    +        None
    +    }
    +}
    +
     pub fn read_cpu_info() -> (String, usize) {
         let mut system = System::new_all();
    -    system.refresh_cpu();
    +    system.refresh_cpu_all();
         let cpus = system.cpus();
         let count = cpus.len();
         let info = if count > 0 {
    diff --git a/crates/tabby/src/services/mod.rs b/crates/tabby/src/services/mod.rs
    index e83cb0635dbc..2d572751a124 100644
    --- a/crates/tabby/src/services/mod.rs
    +++ b/crates/tabby/src/services/mod.rs
    @@ -1,6 +1,8 @@
    -pub mod chat;
     pub mod code;
     pub mod completion;
    +pub mod embedding;
     pub mod event;
     pub mod health;
     pub mod model;
    +pub mod structured_doc;
    +pub mod tantivy;
    diff --git a/crates/tabby/src/services/model.rs b/crates/tabby/src/services/model.rs
    deleted file mode 100644
    index fa026170ffc9..000000000000
    --- a/crates/tabby/src/services/model.rs
    +++ /dev/null
    @@ -1,97 +0,0 @@
    -use std::{fs, path::PathBuf, sync::Arc};
    -
    -use serde::Deserialize;
    -use tabby_common::{
    -    registry::{parse_model_id, ModelRegistry, GGML_MODEL_RELATIVE_PATH},
    -    terminal::{HeaderFormat, InfoMessage},
    -};
    -use tabby_download::download_model;
    -use tabby_inference::TextGeneration;
    -use tracing::info;
    -
    -use crate::{fatal, Device};
    -
    -pub async fn load_text_generation(
    -    model_id: &str,
    -    device: &Device,
    -    parallelism: u8,
    -) -> (Arc, PromptInfo) {
    -    #[cfg(feature = "experimental-http")]
    -    if device == &Device::ExperimentalHttp {
    -        let (engine, prompt_template) = http_api_bindings::create(model_id);
    -        return (
    -            engine,
    -            PromptInfo {
    -                prompt_template: Some(prompt_template),
    -                chat_template: None,
    -            },
    -        );
    -    }
    -
    -    if fs::metadata(model_id).is_ok() {
    -        let path = PathBuf::from(model_id);
    -        let model_path = path.join(GGML_MODEL_RELATIVE_PATH);
    -        let engine = create_ggml_engine(
    -            device,
    -            model_path.display().to_string().as_str(),
    -            parallelism,
    -        );
    -        let engine_info = PromptInfo::read(path.join("tabby.json"));
    -        (Arc::new(engine), engine_info)
    -    } else {
    -        let (registry, name) = parse_model_id(model_id);
    -        let registry = ModelRegistry::new(registry).await;
    -        let model_path = registry.get_model_path(name).display().to_string();
    -        let model_info = registry.get_model_info(name);
    -        let engine = create_ggml_engine(device, &model_path, parallelism);
    -        (
    -            Arc::new(engine),
    -            PromptInfo {
    -                prompt_template: model_info.prompt_template.clone(),
    -                chat_template: model_info.chat_template.clone(),
    -            },
    -        )
    -    }
    -}
    -
    -#[derive(Deserialize)]
    -pub struct PromptInfo {
    -    pub prompt_template: Option,
    -    pub chat_template: Option,
    -}
    -
    -impl PromptInfo {
    -    fn read(filepath: PathBuf) -> PromptInfo {
    -        serdeconv::from_json_file(&filepath)
    -            .unwrap_or_else(|_| fatal!("Invalid metadata file: {}", filepath.display()))
    -    }
    -}
    -
    -fn create_ggml_engine(device: &Device, model_path: &str, parallelism: u8) -> impl TextGeneration {
    -    if !device.ggml_use_gpu() {
    -        InfoMessage::new(
    -            "CPU Device",
    -            HeaderFormat::BoldBlue,
    -            &[
    -                "Tabby is currently running on the CPU. Completions may be slow, but it will suffice for testing purposes.",
    -                "For better performance, consider deploying Tabby on a GPU device."
    -            ],
    -        );
    -    }
    -    let options = llama_cpp_bindings::LlamaTextGenerationOptionsBuilder::default()
    -        .model_path(model_path.to_owned())
    -        .use_gpu(device.ggml_use_gpu())
    -        .parallelism(parallelism)
    -        .build()
    -        .unwrap();
    -
    -    llama_cpp_bindings::LlamaTextGeneration::new(options)
    -}
    -
    -pub async fn download_model_if_needed(model: &str) {
    -    if fs::metadata(model).is_ok() {
    -        info!("Loading model from local path {}", model);
    -    } else {
    -        download_model(model, true).await;
    -    }
    -}
    diff --git a/crates/tabby/src/services/model/mod.rs b/crates/tabby/src/services/model/mod.rs
    new file mode 100644
    index 000000000000..2da30c84eb87
    --- /dev/null
    +++ b/crates/tabby/src/services/model/mod.rs
    @@ -0,0 +1,89 @@
    +use std::{fs, sync::Arc};
    +
    +pub use llama_cpp_server::PromptInfo;
    +use tabby_common::config::ModelConfig;
    +use tabby_download::{download_model, ModelKind};
    +use tabby_inference::{ChatCompletionStream, CodeGeneration, CompletionStream, Embedding};
    +use tracing::info;
    +
    +pub async fn load_embedding(config: &ModelConfig) -> Option> {
    +    llama_cpp_server::create_embedding(config).await
    +}
    +
    +pub async fn load_code_generation_and_chat(
    +    completion_model: Option,
    +    chat_model: Option,
    +) -> (
    +    Option>,
    +    Option>,
    +    Option>,
    +    Option,
    +) {
    +    let (engine, prompt_info, chat) =
    +        load_completion_and_chat(completion_model.clone(), chat_model).await;
    +    let code = engine
    +        .clone()
    +        .map(|engine| Arc::new(CodeGeneration::new(engine, completion_model)));
    +    (code, engine, chat, prompt_info)
    +}
    +
    +async fn load_completion_and_chat(
    +    completion_model: Option,
    +    chat_model: Option,
    +) -> (
    +    Option>,
    +    Option,
    +    Option>,
    +) {
    +    if let (Some(ModelConfig::Local(completion)), Some(ModelConfig::Local(chat))) =
    +        (&completion_model, &chat_model)
    +    {
    +        let (completion, prompt, chat) =
    +            llama_cpp_server::create_completion_and_chat(completion, chat).await;
    +        return (Some(completion), Some(prompt), Some(chat));
    +    }
    +
    +    let (completion, prompt) = if let Some(completion_model) = completion_model {
    +        match completion_model {
    +            ModelConfig::Http(http) => {
    +                let engine = http_api_bindings::create(&http).await;
    +                let (prompt_template, chat_template) =
    +                    http_api_bindings::build_completion_prompt(&http);
    +                (
    +                    Some(engine),
    +                    Some(PromptInfo {
    +                        prompt_template,
    +                        chat_template,
    +                    }),
    +                )
    +            }
    +            ModelConfig::Local(llama) => {
    +                let (stream, prompt) = llama_cpp_server::create_completion(&llama).await;
    +                (Some(stream), Some(prompt))
    +            }
    +        }
    +    } else {
    +        (None, None)
    +    };
    +
    +    let chat = if let Some(chat_model) = chat_model {
    +        match chat_model {
    +            ModelConfig::Http(http) => Some(http_api_bindings::create_chat(&http).await),
    +            ModelConfig::Local(llama) => {
    +                Some(llama_cpp_server::create_chat_completion(&llama).await)
    +            }
    +        }
    +    } else {
    +        None
    +    };
    +
    +    (completion, prompt, chat)
    +}
    +
    +pub async fn download_model_if_needed(model: &str, kind: ModelKind) {
    +    if fs::metadata(model).is_ok() {
    +        info!("Loading model from local path {}", model);
    +    } else {
    +        download_model(model, true, Some(kind)).await;
    +    }
    +}
    diff --git a/crates/tabby/src/services/structured_doc/mod.rs b/crates/tabby/src/services/structured_doc/mod.rs
    new file mode 100644
    index 000000000000..e18aa26ee0a1
    --- /dev/null
    +++ b/crates/tabby/src/services/structured_doc/mod.rs
    @@ -0,0 +1,17 @@
    +mod serper;
    +mod tantivy;
    +
    +use std::sync::Arc;
    +
    +pub use tabby_common::api::structured_doc::DocSearch;
    +use tabby_inference::Embedding;
    +
    +use super::tantivy::IndexReaderProvider;
    +
    +pub fn create(embedding: Arc, provider: Arc) -> impl DocSearch {
    +    tantivy::DocSearchService::new(embedding, provider)
    +}
    +
    +pub fn create_serper(api_key: &str) -> impl DocSearch {
    +    serper::SerperService::new(api_key)
    +}
    diff --git a/crates/tabby/src/services/structured_doc/serper.rs b/crates/tabby/src/services/structured_doc/serper.rs
    new file mode 100644
    index 000000000000..515ad3b10609
    --- /dev/null
    +++ b/crates/tabby/src/services/structured_doc/serper.rs
    @@ -0,0 +1,91 @@
    +use async_trait::async_trait;
    +use serde::{Deserialize, Serialize};
    +use tabby_common::api::structured_doc::{
    +    DocSearch, DocSearchDocument, DocSearchError, DocSearchHit, DocSearchResponse,
    +    DocSearchWebDocument,
    +};
    +use tracing::warn;
    +
    +#[derive(Debug, Serialize)]
    +struct SerperRequest {
    +    q: String,
    +    num: usize,
    +    page: usize,
    +}
    +
    +#[derive(Debug, Deserialize)]
    +struct SerperResponse {
    +    organic: Vec,
    +}
    +
    +#[derive(Debug, Deserialize)]
    +struct SerperOrganicHit {
    +    title: String,
    +    snippet: String,
    +    link: String,
    +}
    +
    +pub struct SerperService {
    +    client: reqwest::Client,
    +}
    +
    +impl SerperService {
    +    pub fn new(api_key: &str) -> Self {
    +        let mut headers = reqwest::header::HeaderMap::new();
    +        headers.insert(
    +            "X-API-KEY",
    +            api_key.parse().expect("Failed to parse Serper API key"),
    +        );
    +        Self {
    +            client: reqwest::Client::builder()
    +                .default_headers(headers)
    +                .build()
    +                .expect("Failed to create reqwest client"),
    +        }
    +    }
    +}
    +
    +#[async_trait]
    +impl DocSearch for SerperService {
    +    async fn search(
    +        &self,
    +        source_ids: &[String],
    +        q: &str,
    +        limit: usize,
    +    ) -> Result {
    +        if !source_ids.is_empty() {
    +            warn!("Serper does not support source filtering");
    +        }
    +
    +        let request = SerperRequest {
    +            q: q.to_string(),
    +            num: limit,
    +            page: 0,
    +        };
    +        let response = self
    +            .client
    +            .post("https://google.serper.dev/search")
    +            .json(&request)
    +            .send()
    +            .await
    +            .map_err(|e| DocSearchError::Other(e.into()))?
    +            .json::()
    +            .await
    +            .map_err(|e| DocSearchError::Other(e.into()))?;
    +
    +        let hits = response
    +            .organic
    +            .into_iter()
    +            .map(|hit| DocSearchHit {
    +                score: 0.0,
    +                doc: DocSearchDocument::Web(DocSearchWebDocument {
    +                    title: hit.title,
    +                    link: hit.link,
    +                    snippet: hit.snippet,
    +                }),
    +            })
    +            .collect();
    +
    +        Ok(DocSearchResponse { hits })
    +    }
    +}
    diff --git a/crates/tabby/src/services/structured_doc/tantivy.rs b/crates/tabby/src/services/structured_doc/tantivy.rs
    new file mode 100644
    index 000000000000..762104efe284
    --- /dev/null
    +++ b/crates/tabby/src/services/structured_doc/tantivy.rs
    @@ -0,0 +1,160 @@
    +use std::{collections::HashSet, sync::Arc};
    +
    +use anyhow::Result;
    +use async_trait::async_trait;
    +use tabby_common::{
    +    api::structured_doc::{
    +        DocSearch, DocSearchDocument, DocSearchError, DocSearchHit, DocSearchResponse,
    +        FromTantivyDocument,
    +    },
    +    index::{self, corpus},
    +};
    +use tabby_inference::Embedding;
    +use tantivy::{
    +    collector::TopDocs,
    +    query::{BooleanQuery, ConstScoreQuery, Occur, Query},
    +    schema::{self, Value},
    +    IndexReader, TantivyDocument,
    +};
    +use tracing::warn;
    +
    +use crate::services::tantivy::IndexReaderProvider;
    +
    +struct DocSearchImpl {
    +    embedding: Arc,
    +}
    +
    +const EMBEDDING_SCORE_THRESHOLD: f32 = 0.75;
    +
    +impl DocSearchImpl {
    +    fn new(embedding: Arc) -> Self {
    +        Self { embedding }
    +    }
    +
    +    async fn search(
    +        &self,
    +        source_ids: &[String],
    +        reader: &IndexReader,
    +        q: &str,
    +        limit: usize,
    +    ) -> Result {
    +        let schema = index::IndexSchema::instance();
    +        let query = {
    +            let embedding = self.embedding.embed(q).await?;
    +            let embedding_tokens_query =
    +                index::embedding_tokens_query(embedding.len(), embedding.iter());
    +            let corpus_query = schema.corpus_query(corpus::STRUCTURED_DOC);
    +
    +            let mut query_clauses: Vec<(Occur, Box)> = vec![
    +                (
    +                    Occur::Must,
    +                    Box::new(ConstScoreQuery::new(corpus_query, 0.0)),
    +                ),
    +                (Occur::Must, Box::new(embedding_tokens_query)),
    +            ];
    +
    +            if !source_ids.is_empty() {
    +                let source_ids_query = Box::new(schema.source_ids_query(source_ids));
    +                let source_ids_query = ConstScoreQuery::new(source_ids_query, 0.0);
    +                query_clauses.push((Occur::Must, Box::new(source_ids_query)));
    +            }
    +            BooleanQuery::new(query_clauses)
    +        };
    +
    +        let searcher = reader.searcher();
    +        let top_chunks = searcher.search(&query, &TopDocs::with_limit(limit * 2))?;
    +
    +        let chunks = {
    +            // Extract all chunks.
    +            let mut chunks: Vec<_> = top_chunks
    +                .iter()
    +                .filter_map(|(score, chunk_address)| {
    +                    let chunk: TantivyDocument = searcher.doc(*chunk_address).ok()?;
    +                    let doc_id = get_text(&chunk, schema.field_id).to_owned();
    +                    Some(ScoredChunk {
    +                        score: *score,
    +                        chunk,
    +                        doc_id,
    +                    })
    +                })
    +                .collect();
    +
    +            // Sort by score in descending order.
    +            chunks.sort_unstable_by(|lhs, rhs| rhs.score.total_cmp(&lhs.score));
    +
    +            // Deduplicate by doc_id.
    +            let mut doc_ids = HashSet::new();
    +            chunks.retain(|x| doc_ids.insert(x.doc_id.clone()));
    +
    +            chunks
    +        };
    +
    +        let hits = chunks
    +            .iter()
    +            .filter_map(
    +                |ScoredChunk {
    +                     doc_id,
    +                     score,
    +                     chunk,
    +                 }| {
    +                    let doc_query = schema.doc_query(corpus::STRUCTURED_DOC, doc_id);
    +                    let top_docs = match searcher.search(&doc_query, &TopDocs::with_limit(1)) {
    +                        Err(err) => {
    +                            warn!("Failed to search doc `{}`: `{}`", doc_id, err);
    +                            return None;
    +                        }
    +                        Ok(top_docs) => top_docs,
    +                    };
    +                    let (_, doc_address) = top_docs.first()?;
    +                    let doc: TantivyDocument = searcher.doc(*doc_address).ok()?;
    +                    DocSearchDocument::from_tantivy_document(&doc, chunk)
    +                        .map(|doc| DocSearchHit { score: *score, doc })
    +                },
    +            )
    +            .filter(|x| x.score >= EMBEDDING_SCORE_THRESHOLD)
    +            .take(limit)
    +            .collect();
    +
    +        Ok(DocSearchResponse { hits })
    +    }
    +}
    +
    +fn get_text(doc: &TantivyDocument, field: schema::Field) -> &str {
    +    doc.get_first(field).unwrap().as_str().unwrap()
    +}
    +
    +struct ScoredChunk {
    +    doc_id: String,
    +    score: f32,
    +    chunk: TantivyDocument,
    +}
    +
    +pub struct DocSearchService {
    +    imp: DocSearchImpl,
    +    provider: Arc,
    +}
    +
    +impl DocSearchService {
    +    pub fn new(embedding: Arc, provider: Arc) -> Self {
    +        Self {
    +            imp: DocSearchImpl::new(embedding),
    +            provider,
    +        }
    +    }
    +}
    +
    +#[async_trait]
    +impl DocSearch for DocSearchService {
    +    async fn search(
    +        &self,
    +        source_ids: &[String],
    +        q: &str,
    +        limit: usize,
    +    ) -> Result {
    +        if let Some(reader) = self.provider.reader().await.as_ref() {
    +            self.imp.search(source_ids, reader, q, limit).await
    +        } else {
    +            Err(DocSearchError::NotReady)
    +        }
    +    }
    +}
    diff --git a/crates/tabby/src/services/tantivy.rs b/crates/tabby/src/services/tantivy.rs
    new file mode 100644
    index 000000000000..d9d27c956295
    --- /dev/null
    +++ b/crates/tabby/src/services/tantivy.rs
    @@ -0,0 +1,59 @@
    +use std::{sync::Arc, time::Duration};
    +
    +use tabby_common::{index::IndexSchema, path};
    +use tantivy::{Index, IndexReader};
    +use tokio::sync::RwLock;
    +use tracing::debug;
    +
    +pub struct IndexReaderProvider {
    +    provider: Arc>>,
    +    loader: tokio::task::JoinHandle<()>,
    +}
    +
    +impl IndexReaderProvider {
    +    pub fn reader(
    +        &self,
    +    ) -> impl futures::Future>> {
    +        self.provider.read()
    +    }
    +
    +    fn load() -> anyhow::Result {
    +        let index = Index::open_in_dir(path::index_dir())?;
    +
    +        if index.schema() != IndexSchema::instance().schema {
    +            return Err(anyhow::anyhow!("Index schema mismatch"));
    +        }
    +
    +        Ok(index.reader_builder().try_into()?)
    +    }
    +
    +    async fn load_async() -> IndexReader {
    +        loop {
    +            if let Ok(provider) = Self::load() {
    +                debug!("Index is ready, enabling search...");
    +                return provider;
    +            }
    +
    +            tokio::time::sleep(Duration::from_secs(60)).await;
    +        }
    +    }
    +}
    +
    +impl Default for IndexReaderProvider {
    +    fn default() -> Self {
    +        let provider = Arc::new(RwLock::new(None));
    +        let cloned_provider = provider.clone();
    +        let loader = tokio::spawn(async move {
    +            let doc = Self::load_async().await;
    +            *cloned_provider.write().await = Some(doc);
    +        });
    +
    +        Self { provider, loader }
    +    }
    +}
    +
    +impl Drop for IndexReaderProvider {
    +    fn drop(&mut self) {
    +        self.loader.abort()
    +    }
    +}
    diff --git a/crates/tabby/src/worker.rs b/crates/tabby/src/worker.rs
    deleted file mode 100644
    index 4f0b0cb0d8d0..000000000000
    --- a/crates/tabby/src/worker.rs
    +++ /dev/null
    @@ -1,119 +0,0 @@
    -use std::{env::consts::ARCH, net::IpAddr, sync::Arc};
    -
    -use axum::{routing, Router};
    -use clap::Args;
    -use tabby_common::api::{code::CodeSearch, event::EventLogger};
    -use tabby_webserver::public::{ConnectHubRequest, HubClient, RegisterWorkerRequest, WorkerKind};
    -use tracing::info;
    -
    -use crate::{
    -    routes::{self, run_app},
    -    services::{
    -        chat::create_chat_service,
    -        completion::create_completion_service,
    -        health::{read_cpu_info, read_cuda_devices},
    -        model::download_model_if_needed,
    -    },
    -    Device,
    -};
    -
    -#[derive(Args)]
    -pub struct WorkerArgs {
    -    /// URL to register this worker.
    -    #[clap(long)]
    -    url: String,
    -
    -    #[clap(long, default_value = "0.0.0.0")]
    -    host: IpAddr,
    -
    -    #[clap(long, default_value_t = 8080)]
    -    port: u16,
    -
    -    /// Server token to register this worker to.
    -    #[clap(long)]
    -    token: String,
    -
    -    /// Model id
    -    #[clap(long, help_heading=Some("Model Options"))]
    -    model: String,
    -
    -    /// Device to run model inference.
    -    #[clap(long, default_value_t=Device::Cpu, help_heading=Some("Model Options"))]
    -    device: Device,
    -
    -    /// Parallelism for model serving - increasing this number will have a significant impact on the
    -    /// memory requirement e.g., GPU vRAM.
    -    #[clap(long, default_value_t = 1, help_heading=Some("Model Options"))]
    -    parallelism: u8,
    -}
    -
    -async fn make_chat_route(logger: Arc, args: &WorkerArgs) -> Router {
    -    let chat_state =
    -        Arc::new(create_chat_service(logger, &args.model, &args.device, args.parallelism).await);
    -
    -    Router::new().route(
    -        "/v1beta/chat/completions",
    -        routing::post(routes::chat_completions).with_state(chat_state),
    -    )
    -}
    -
    -async fn make_completion_route(
    -    code: Arc,
    -    logger: Arc,
    -    args: &WorkerArgs,
    -) -> Router {
    -    let completion_state = Arc::new(
    -        create_completion_service(code, logger, &args.model, &args.device, args.parallelism).await,
    -    );
    -
    -    Router::new().route(
    -        "/v1/completions",
    -        routing::post(routes::completions).with_state(completion_state),
    -    )
    -}
    -
    -pub async fn main(kind: WorkerKind, args: &WorkerArgs) {
    -    download_model_if_needed(&args.model).await;
    -
    -    info!("Starting worker, this might take a few minutes...");
    -
    -    let context = WorkerContext::new(kind.clone(), args).await;
    -    let code = Arc::new(context.client);
    -    let logger = code.clone();
    -
    -    let app = match kind {
    -        WorkerKind::Completion => make_completion_route(code, logger.clone(), args).await,
    -        WorkerKind::Chat => make_chat_route(logger.clone(), args).await,
    -    };
    -
    -    run_app(app, None, args.host, args.port).await
    -}
    -
    -struct WorkerContext {
    -    client: HubClient,
    -}
    -
    -impl WorkerContext {
    -    async fn new(kind: WorkerKind, args: &WorkerArgs) -> Self {
    -        let (cpu_info, cpu_count) = read_cpu_info();
    -        let cuda_devices = read_cuda_devices().unwrap_or_default();
    -
    -        Self {
    -            client: tabby_webserver::public::create_client(
    -                &args.url,
    -                &args.token,
    -                ConnectHubRequest::Worker(RegisterWorkerRequest {
    -                    kind,
    -                    port: args.port,
    -                    name: args.model.to_owned(),
    -                    device: args.device.to_string(),
    -                    arch: ARCH.to_string(),
    -                    cpu_info,
    -                    cpu_count: cpu_count as i32,
    -                    cuda_devices,
    -                }),
    -            )
    -            .await,
    -        }
    -    }
    -}
    diff --git a/crates/tabby/tests/goldentests.rs b/crates/tabby/tests/goldentests.rs
    index 5af5afbda9ff..1e77064361a4 100644
    --- a/crates/tabby/tests/goldentests.rs
    +++ b/crates/tabby/tests/goldentests.rs
    @@ -20,8 +20,11 @@ fn workspace_dir() -> PathBuf {
             .output()
             .unwrap()
             .stdout;
    -    let cargo_path = std::path::Path::new(std::str::from_utf8(&output).unwrap().trim());
    -    cargo_path.parent().unwrap().to_path_buf()
    +    let cargo_path = std::path::Path::new(std::str::from_utf8(&output).expect("Valid path").trim());
    +    cargo_path
    +        .parent()
    +        .expect("Path must have a parent folder")
    +        .to_path_buf()
     }
     
     fn tabby_path() -> PathBuf {
    @@ -33,6 +36,7 @@ fn initialize_server(gpu_device: Option<&str>) {
         cmd.arg("serve")
             .arg("--model")
             .arg("TabbyML/StarCoder-1B")
    +        .arg("--no-webserver")
             .arg("--port")
             .arg("9090")
             .kill_on_drop(true);
    @@ -46,23 +50,26 @@ fn initialize_server(gpu_device: Option<&str>) {
                 .expect("Failed to start server")
                 .wait()
                 .await
    -            .unwrap();
    +            .expect("Failed to start server");
         });
     }
     
    -async fn wait_for_server(device: Option<&str>) {
    -    initialize_server(device);
    +async fn wait_for_server(gpu_device: Option<&str>) {
    +    initialize_server(gpu_device);
     
         loop {
             println!("Waiting for server to start...");
    -        let is_ok = reqwest::get("http://localhost:9090/v1/health")
    -            .await
    -            .is_ok();
    -        if is_ok {
    -            break;
    -        } else {
    -            sleep(Duration::from_secs(5)).await;
    +        match reqwest::get("http://127.0.0.1:9090/v1/health").await {
    +            Ok(resp) => {
    +                if resp.status().is_success() {
    +                    break;
    +                }
    +            }
    +            Err(e) => {
    +                println!("Waiting for server to start: {e:?}");
    +            }
             }
    +        sleep(Duration::from_secs(5)).await;
         }
     }
     
    @@ -75,8 +82,18 @@ async fn golden_test(body: serde_json::Value) -> serde_json::Value {
             }),
         );
     
    +    let resp = CLIENT
    +        .post("http://127.0.0.1:9090/v1/completions")
    +        .json(&body)
    +        .send()
    +        .await
    +        .unwrap();
    +
    +    let info = resp.text().await.unwrap();
    +    eprintln!("info {info}");
    +
         let actual: serde_json::Value = CLIENT
    -        .post("http://localhost:9090/v1/completions")
    +        .post("http://127.0.0.1:9090/v1/completions")
             .json(&body)
             .send()
             .await
    diff --git a/crates/tabby/tests/goldentests_chat.rs b/crates/tabby/tests/goldentests_chat.rs
    index 4e760b18878f..3d621af91b94 100644
    --- a/crates/tabby/tests/goldentests_chat.rs
    +++ b/crates/tabby/tests/goldentests_chat.rs
    @@ -1,6 +1,8 @@
    -use std::{io::BufRead, path::PathBuf};
    +use std::path::PathBuf;
     
    +use futures::StreamExt;
     use lazy_static::lazy_static;
    +use reqwest_eventsource::{Event, EventSource};
     use serde::Deserialize;
     use serde_json::json;
     use serial_test::serial;
    @@ -21,7 +23,7 @@ pub struct ChatCompletionChoice {
     
     #[derive(Deserialize)]
     pub struct ChatCompletionDelta {
    -    content: String,
    +    content: Option,
     }
     
     lazy_static! {
    @@ -49,6 +51,7 @@ fn initialize_server(gpu_device: Option<&str>) {
         cmd.arg("serve")
             .arg("--chat-model")
             .arg("TabbyML/Mistral-7B")
    +        .arg("--no-webserver")
             .arg("--port")
             .arg("9090")
             .kill_on_drop(true);
    @@ -71,35 +74,53 @@ async fn wait_for_server(gpu_device: Option<&str>) {
     
         loop {
             println!("Waiting for server to start...");
    -        let is_ok = reqwest::get("http://localhost:9090/v1/health")
    -            .await
    -            .is_ok();
    -        if is_ok {
    -            break;
    -        } else {
    -            sleep(Duration::from_secs(5)).await;
    +        match reqwest::get("http://127.0.0.1:9090/v1/health").await {
    +            Ok(resp) => {
    +                if resp.status().is_success() {
    +                    break;
    +                }
    +            }
    +            Err(e) => {
    +                println!("Waiting for server to start: {e:?}");
    +            }
             }
    +        sleep(Duration::from_secs(5)).await;
         }
     }
     
     async fn golden_test(body: serde_json::Value) -> String {
    -    let bytes = CLIENT
    -        .post("http://localhost:9090/v1beta/chat/completions")
    -        .json(&body)
    -        .send()
    -        .await
    -        .unwrap()
    -        .bytes()
    -        .await
    -        .unwrap();
    +    let mut es = EventSource::new(
    +        CLIENT
    +            .post("http://127.0.0.1:9090/v1/chat/completions")
    +            .json(&body),
    +    )
    +    .unwrap();
     
         let mut actual = "".to_owned();
    -    for x in bytes.lines() {
    -        let content = x.unwrap();
    -        if content.starts_with("data:") {
    -            let content = content.strip_prefix("data:").unwrap();
    -            let x: ChatCompletionChunk = serde_json::from_str(content).unwrap();
    -            actual += &x.choices[0].delta.content;
    +    while let Some(event) = es.next().await {
    +        match event {
    +            Ok(Event::Open) => {}
    +            Ok(Event::Message(message)) => {
    +                let x: ChatCompletionChunk = serde_json::from_str(&message.data).unwrap();
    +                if let Some(content) = &x.choices[0].delta.content {
    +                    actual += content
    +                }
    +            }
    +            Err(e) => {
    +                match e {
    +                    reqwest_eventsource::Error::StreamEnded => {
    +                        break;
    +                    }
    +                    reqwest_eventsource::Error::InvalidStatusCode(code, resp) => {
    +                        let resp = resp.text().await.unwrap();
    +                        println!("Error: {code} {resp:?}");
    +                    }
    +                    e => {
    +                        println!("Error: {e:?}");
    +                    }
    +                }
    +                break;
    +            }
             }
         }
     
    @@ -120,6 +141,7 @@ async fn run_chat_golden_tests() {
     
         assert_golden!(json!({
                 "seed": 0,
    +            "model": "default",
                 "messages": [
                     {
                         "role": "user",
    @@ -130,6 +152,7 @@ async fn run_chat_golden_tests() {
     
         assert_golden!(json!({
                 "seed": 0,
    +            "model": "default",
                 "messages": [
                     {
                         "role": "user",
    @@ -146,6 +169,7 @@ async fn run_chat_golden_tests_cpu() {
     
         assert_golden!(json!({
                 "seed": 0,
    +            "model": "default",
                 "messages": [
                     {
                         "role": "user",
    diff --git a/crates/tabby/tests/snapshots/goldentests__run_golden_tests-2.snap b/crates/tabby/tests/snapshots/goldentests__run_golden_tests-2.snap
    index f5806de6e2e8..ba171a4f4bab 100644
    --- a/crates/tabby/tests/snapshots/goldentests__run_golden_tests-2.snap
    +++ b/crates/tabby/tests/snapshots/goldentests__run_golden_tests-2.snap
    @@ -1,10 +1,9 @@
     ---
     source: crates/tabby/tests/goldentests.rs
    -expression: "golden_test(json!({\n                \"language\" : \"python\", \"segments\" :\n                {\n                    \"prefix\" :\n                    \"import datetime\\n\\ndef parse_expenses(expenses_string):\\n    \\\"\\\"\\\"Parse the list of expenses and return the list of triples (date, value, currency).\\n    Ignore lines starting with #.\\n    Parse the date using datetime.\\n    Example expenses_string:\\n        2016-01-02 -34.01 USD\\n        2016-01-03 2.59 DKK\\n        2016-01-03 -2.72 EUR\\n    \\\"\\\"\\\"\\n    for line in expenses_string.split('\\\\n'):\\n        \"\n                }\n            })).await"
    +expression: "golden_test(json!({\n                \"language\": \"python\", \"seed\": 0, \"segments\":\n                {\n                    \"prefix\":\n                    \"import datetime\\n\\ndef parse_expenses(expenses_string):\\n    \\\"\\\"\\\"Parse the list of expenses and return the list of triples (date, value, currency).\\n    Ignore lines starting with #.\\n    Parse the date using datetime.\\n    Example expenses_string:\\n        2016-01-02 -34.01 USD\\n        2016-01-03 2.59 DKK\\n        2016-01-03 -2.72 EUR\\n    \\\"\\\"\\\"\\n    for line in expenses_string.split('\\\\n'):\\n        \"\n                }\n            })).await"
     ---
    -id: test-id
     choices:
       - index: 0
         text: "if line.startswith('#'):\n            continue\n        date, value, currency = line.split()\n        date = datetime.datetime.strptime(date, '%Y-%m-%d')\n        yield date, float(value), currency"
     debug_data: {}
    -
    +id: test-id
    diff --git a/crates/tabby/tests/snapshots/goldentests__run_golden_tests.snap b/crates/tabby/tests/snapshots/goldentests__run_golden_tests.snap
    index 2850ac3faa6f..a454360d0528 100644
    --- a/crates/tabby/tests/snapshots/goldentests__run_golden_tests.snap
    +++ b/crates/tabby/tests/snapshots/goldentests__run_golden_tests.snap
    @@ -1,10 +1,9 @@
     ---
     source: crates/tabby/tests/goldentests.rs
    -expression: "golden_test(json!({\n                \"language\" : \"python\", \"segments\" :\n                {\n                    \"prefix\" : \"def fib(n):\\n    \", \"suffix\" :\n                    \"\\n        return fib(n - 1) + fib(n - 2)\"\n                }\n            })).await"
    +expression: "golden_test(json!({\n                \"language\": \"python\", \"seed\": 0, \"segments\":\n                {\n                    \"prefix\": \"def fib(n):\\n    \", \"suffix\":\n                    \"\\n        return fib(n - 1) + fib(n - 2)\"\n                }\n            })).await"
     ---
    -id: test-id
     choices:
       - index: 0
    -    text: "    if n <= 1:\n            return n"
    +    text: "    if n == 0:\n            return 0\n        if n == 1:\n            return 1"
     debug_data: {}
    -
    +id: test-id
    diff --git a/crates/tabby/tests/snapshots/goldentests__run_golden_tests_cpu-2.snap b/crates/tabby/tests/snapshots/goldentests__run_golden_tests_cpu-2.snap
    index d5976af93e2d..8734e6f39368 100644
    --- a/crates/tabby/tests/snapshots/goldentests__run_golden_tests_cpu-2.snap
    +++ b/crates/tabby/tests/snapshots/goldentests__run_golden_tests_cpu-2.snap
    @@ -1,10 +1,9 @@
     ---
     source: crates/tabby/tests/goldentests.rs
    -expression: "golden_test(json!({\n                \"language\": \"python\", \"segments\":\n                { \"prefix\": \"def char_frequencies(str):\\n  freqs = {}\\n  \", }\n            })).await"
    +expression: "golden_test(json!({\n                \"language\": \"python\", \"seed\": 0, \"segments\":\n                { \"prefix\": \"def char_frequencies(str):\\n  freqs = {}\\n  \", }\n            })).await"
     ---
    -id: test-id
     choices:
       - index: 0
    -    text: "for c in str:\n    if c in freqs:\n      freqs[c] += 1\n    else:\n      freqs[c] = 1\n  return freqs"
    +    text: "for c in str:\n    freqs[c] = freqs.get(c, 0) + 1\n  return freqs"
     debug_data: {}
    -
    +id: test-id
    diff --git a/crates/tabby/tests/snapshots/goldentests__run_golden_tests_cpu.snap b/crates/tabby/tests/snapshots/goldentests__run_golden_tests_cpu.snap
    index 447edc18efd9..fdaf5fbfe149 100644
    --- a/crates/tabby/tests/snapshots/goldentests__run_golden_tests_cpu.snap
    +++ b/crates/tabby/tests/snapshots/goldentests__run_golden_tests_cpu.snap
    @@ -1,10 +1,9 @@
     ---
     source: crates/tabby/tests/goldentests.rs
    -expression: "golden_test(json!({\n                \"language\": \"python\", \"segments\":\n                { \"prefix\": \"def is_prime(n):\\n\", }\n            })).await"
    +expression: "golden_test(json!({\n                \"language\": \"python\", \"seed\": 0, \"segments\":\n                { \"prefix\": \"def is_prime(n):\\n\", }\n            })).await"
     ---
    -id: test-id
     choices:
       - index: 0
         text: "    if n == 2:\n        return True\n    if n % 2 == 0:\n        return False\n    for i in range(3, int(n ** 0.5) + 1, 2):\n        if n % i == 0:\n            return False\n    return True"
     debug_data: {}
    -
    +id: test-id
    diff --git a/crates/tabby/tests/snapshots/goldentests_chat__run_chat_golden_tests-2.snap b/crates/tabby/tests/snapshots/goldentests_chat__run_chat_golden_tests-2.snap
    index 546c719879bf..b319609f0d63 100644
    --- a/crates/tabby/tests/snapshots/goldentests_chat__run_chat_golden_tests-2.snap
    +++ b/crates/tabby/tests/snapshots/goldentests_chat__run_chat_golden_tests-2.snap
    @@ -1,6 +1,5 @@
     ---
     source: crates/tabby/tests/goldentests_chat.rs
    -expression: "golden_test(json!({\n                \"messages\" :\n                [{\n                    \"role\" : \"user\", \"content\" :\n                    \"How to parse email address with regex\"\n                }]\n            })).await"
    +expression: "golden_test(json!({\n    \"seed\": 0, \"model\": \"default\", \"messages\":\n    [{ \"role\": \"user\", \"content\": \"How to parse email address with regex\" }]\n})).await"
     ---
    -" To parse an email address with regex, you can use the following pattern:\n```\n^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$\n```\nThis pattern matches email addresses in the following format:\n\n* `^`: start of the string\n* `[a-zA-Z0-9._%+-]+`: matches one or more characters that are either letters, numbers, periods, underscores, percent signs, plus signs, or hyphens\n* `@`: matches the `@` symbol\n* `[a-zA-Z0-9.-]+`: matches one or more characters that are either letters, numbers, periods, or hyphens\n* `\\.`: matches the `.` symbol\n* `[a-zA-Z]{2,}`: matches two or more characters that are letters\n* `$`: end of the string\n\nYou can use this pattern in a programming language that supports regex, such as Python, JavaScript, or Java, to extract the email address from a string. For example, in Python, you can use the `re` module to find all email addresses in a string:\n```\nimport re\n\nstring = \"Please send your feedback to john.doe@example.com or jane_doe@example.co.uk.\"\n\nemails = re.findall(r\"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}\", string)\n\nprint(emails)  # Output: ['john.doe@example.com', 'jane_doe@example.co.uk']\n```\nIn this example, the `re.findall()` function is used to find all occurrences of the email address pattern in the string. The `findall()` function returns a list of all non-overlapping matches."
    -
    +" Parsing an email address with regular expressions can be a bit tricky, but it can be done using a combination of patterns and character classes. Here's an example of a regular expression that can be used to match most email addresses:\n```\n\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b\n```\nThis regular expression uses the following patterns:\n\n* `\\b`: This is a word boundary that matches the beginning or end of a word. It ensures that the email address is matched as a whole, not just as a part of a longer string.\n* `[A-Za-z0-9._%+-]+`: This pattern matches one or more characters that are either letters (A-Z or a-z), digits (0-9), periods (.), underscores (\\_), percent signs (%), plus signs (+), or hyphens (-). This is the local part of the email address.\n* `@`: This is the character that separates the local part from the domain name.\n* `[A-Za-z0-9.-]+\\.`: This pattern matches one or more characters that are either letters (A-Z or a-z), digits (0-9), periods (.), or hyphens (-). The period is followed by a dot to indicate that it is the end of the domain name.\n* `[A-Z|a-z]{2,}`: This pattern matches two or more letters that are either uppercase (A-Z) or lowercase (a-z). This is the top-level domain of the email address.\n* `\\b`: This pattern matches the end of the email address.\n\nNote that this regular expression is not perfect and may not match all email addresses, but it should work for most common cases."
    diff --git a/crates/tabby/tests/snapshots/goldentests_chat__run_chat_golden_tests.snap b/crates/tabby/tests/snapshots/goldentests_chat__run_chat_golden_tests.snap
    index 620589e21542..0e5fb3e456d6 100644
    --- a/crates/tabby/tests/snapshots/goldentests_chat__run_chat_golden_tests.snap
    +++ b/crates/tabby/tests/snapshots/goldentests_chat__run_chat_golden_tests.snap
    @@ -1,6 +1,5 @@
     ---
     source: crates/tabby/tests/goldentests_chat.rs
    -expression: "golden_test(json!({\n                \"messages\" :\n                [{\n                    \"role\" : \"user\", \"content\" :\n                    \"How to convert a list of string to numbers in python\"\n                }]\n            })).await"
    +expression: "golden_test(json!({\n    \"seed\": 0, \"model\": \"default\", \"messages\":\n    [{\n        \"role\": \"user\", \"content\":\n        \"How to convert a list of string to numbers in python\"\n    }]\n})).await"
     ---
    -" In Python, you can convert a list of strings to numbers using the `map()` function and the `int()` function. Here's an example:\n```\nstrings = ['1', '2', '3', '4', '5']\nnumbers = list(map(int, strings))\nprint(numbers)\n```\nThis will output:\n```\n[1, 2, 3, 4, 5]\n```\nIn this example, the `map()` function applies the `int()` function to each element of the `strings` list, converting each string to an integer and returning a new list of integers. The `list()` function is used to convert the resulting iterator to a list."
    -
    +" You can convert a list of strings to numbers in Python using the built-in `list()` function to convert the list of strings to a list of numbers, and then using the `int()` function to convert each element of the list to an integer. Here's an example:\n```\n# A list of strings\nnum_strings = ['1', '2', '3']\n\n# Convert the list of strings to a list of numbers\nnum_list = list(map(int, num_strings))\n\n# Print the list of numbers\nprint(num_list)\n```\nThis will output:\n```\n[1, 2, 3]\n```\nNote that this will only work if the strings represent integers. If the strings represent a different type of number, such as a decimal number, you will need to use a different function, such as `float()`, to convert them to a float.\n\nAlso, if you want to convert the string to a specific number type, you can use the built-in `int()` function and pass the number as an argument.\n\nFor example, to convert the string '123' to a float:\n```\nnum_string = '123'\nnum_float = float(num_string)\nprint(num_float)\n```\nThis will output:\n```\n123.0\n```\nAnd to convert the string '123' to a decimal:\n```\nnum_string = '123.45'\nnum_decimal = float(num_string)\nprint(num_decimal)\n```\nThis will output:\n```\n123.45\n```"
    diff --git a/crates/tabby/tests/snapshots/goldentests_chat__run_chat_golden_tests_cpu.snap b/crates/tabby/tests/snapshots/goldentests_chat__run_chat_golden_tests_cpu.snap
    index 6aeef82f53f7..8f48826f86df 100644
    --- a/crates/tabby/tests/snapshots/goldentests_chat__run_chat_golden_tests_cpu.snap
    +++ b/crates/tabby/tests/snapshots/goldentests_chat__run_chat_golden_tests_cpu.snap
    @@ -1,6 +1,5 @@
     ---
     source: crates/tabby/tests/goldentests_chat.rs
    -expression: "golden_test(json!({\n                \"messages\": [{ \"role\": \"user\", \"content\": \"How are you?\" }]\n            })).await"
    +expression: "golden_test(json!({\n                \"seed\": 0, \"model\": \"default\", \"messages\":\n                [{ \"role\": \"user\", \"content\": \"How are you?\" }]\n            })).await"
     ---
    -" I'm just a computer program, so I don't have feelings or emotions. But I'm here to help you with any questions or problems you have! How can I assist you today?"
    -
    +" I'm an AI language model, so I don't have feelings or emotions like a human does. I'm here to help you with any questions or tasks you have. How can I assist you today?"
    diff --git a/docker/Dockerfile.cuda b/docker/Dockerfile.cuda
    new file mode 100644
    index 000000000000..ae6841bbefdf
    --- /dev/null
    +++ b/docker/Dockerfile.cuda
    @@ -0,0 +1,90 @@
    +ARG UBUNTU_VERSION=22.04
    +# This needs to generally match the container host's environment.
    +ARG CUDA_VERSION=11.7.1
    +# Target the CUDA build image
    +ARG BASE_CUDA_DEV_CONTAINER=nvidia/cuda:${CUDA_VERSION}-devel-ubuntu${UBUNTU_VERSION}
    +# Target the CUDA runtime image
    +ARG BASE_CUDA_RUN_CONTAINER=nvidia/cuda:${CUDA_VERSION}-runtime-ubuntu${UBUNTU_VERSION}
    +
    +FROM ${BASE_CUDA_DEV_CONTAINER} AS build
    +
    +# Rust toolchain version
    +ARG RUST_TOOLCHAIN=stable
    +
    +ENV DEBIAN_FRONTEND=noninteractive
    +RUN apt-get update && \
    +    apt-get install -y --no-install-recommends \
    +        curl \
    +        pkg-config \
    +        libssl-dev \
    +        protobuf-compiler \
    +        git \
    +        cmake \
    +        && \
    +    apt-get clean && \
    +    rm -rf /var/lib/apt/lists/*
    +
    +# setup rust.
    +RUN curl https://sh.rustup.rs -sSf | bash -s -- --default-toolchain ${RUST_TOOLCHAIN} -y
    +ENV PATH="/root/.cargo/bin:${PATH}"
    +ENV LD_LIBRARY_PATH="/usr/local/cuda/compat/:${LD_LIBRARY_PATH}"
    +
    +WORKDIR /root/workspace
    +
    +RUN mkdir -p /opt/tabby/bin
    +RUN mkdir -p /opt/tabby/lib
    +RUN mkdir -p target
    +
    +COPY . .
    +
    +RUN --mount=type=cache,target=/usr/local/cargo/registry \
    +    --mount=type=cache,target=/root/workspace/target \
    +    cargo build --no-default-features --features cuda,prod --release --package tabby && \
    +    cp target/release/llama-server /opt/tabby/bin/ && \
    +    cp target/release/tabby /opt/tabby/bin/
    +
    +# For compatibility with the legacy cpu build.
    +RUN cp /opt/tabby/bin/tabby /opt/tabby/bin/tabby-cpu
    +
    +FROM ${BASE_CUDA_RUN_CONTAINER} AS runtime
    +
    +RUN apt-get update && \
    +    apt-get install -y --no-install-recommends \
    +        git   \
    +        curl  \
    +        unzip \
    +        openssh-client \
    +        ca-certificates \
    +        libgomp1 \
    +        && \
    +    apt-get clean && \
    +    rm -rf /var/lib/apt/lists/*
    +
    +# Install katana
    +RUN curl -L https://github.com/projectdiscovery/katana/releases/download/v1.1.2/katana_1.1.2_linux_amd64.zip -o katana.zip \
    +  && unzip katana.zip katana \
    +  && mv katana /usr/bin/ \
    +  && rm katana.zip
    +
    +# Disable safe directory in docker
    +# Context: https://github.com/git/git/commit/8959555cee7ec045958f9b6dd62e541affb7e7d9
    +RUN git config --system --add safe.directory "*"
    +
    +# Automatic platform ARGs in the global scope
    +# https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope
    +ARG TARGETARCH
    +
    +# AMD64 only:
    +# Make link to libnvidia-ml.so (NVML) library
    +# so that we could get GPU stats.
    +RUN if [ "$TARGETARCH" = "amd64" ]; then  \
    +    ln -s /usr/lib/x86_64-linux-gnu/libnvidia-ml.so.1 \
    +        /usr/lib/x86_64-linux-gnu/libnvidia-ml.so; \
    +    fi
    +
    +COPY --from=build /opt/tabby /opt/tabby
    +
    +ENV PATH="$PATH:/opt/tabby/bin"
    +ENV TABBY_ROOT=/data
    +
    +ENTRYPOINT ["/opt/tabby/bin/tabby"]
    diff --git a/docker/Dockerfile.rocm b/docker/Dockerfile.rocm
    new file mode 100644
    index 000000000000..2d7d9bd896d1
    --- /dev/null
    +++ b/docker/Dockerfile.rocm
    @@ -0,0 +1,77 @@
    +ARG UBUNTU_VERSION=22.04
    +# This needs to generally match the container host's environment.
    +ARG ROCM_VERSION=5.7.1
    +# Target the ROCM build image
    +ARG BASE_ROCM_DEV_CONTAINER=rocm/dev-ubuntu-${UBUNTU_VERSION}:${ROCM_VERSION}-complete
    +# Target the ROCM runtime image
    +ARG BASE_ROCM_RUN_CONTAINER=rocm/dev-ubuntu-${UBUNTU_VERSION}:${ROCM_VERSION}
    +
    +FROM ${BASE_ROCM_DEV_CONTAINER} AS build
    +
    +# Rust toolchain version
    +ARG RUST_TOOLCHAIN=stable
    +
    +ENV DEBIAN_FRONTEND=noninteractive
    +RUN apt-get update && \
    +    apt-get install -y --no-install-recommends \
    +    curl \
    +    pkg-config \
    +    libssl-dev \
    +    protobuf-compiler \
    +    git \
    +    cmake \
    +    && \
    +    apt-get clean && \
    +    rm -rf /var/lib/apt/lists/*
    +
    +# setup rust.
    +RUN curl https://sh.rustup.rs -sSf | bash -s -- --default-toolchain ${RUST_TOOLCHAIN} -y
    +ENV PATH="/root/.cargo/bin:${PATH}"
    +
    +WORKDIR /root/workspace
    +
    +RUN mkdir -p /opt/tabby/bin
    +RUN mkdir -p /opt/tabby/lib
    +RUN mkdir -p target
    +
    +COPY . .
    +
    +RUN --mount=type=cache,target=/usr/local/cargo/registry \
    +    --mount=type=cache,target=/root/workspace/target \
    +    cargo build --no-default-features --features rocm,prod --release --package tabby && \
    +    cp target/release/llama-server /opt/tabby/bin/ && \
    +    cp target/release/tabby /opt/tabby/bin/
    +
    +# For compatibility with the legacy cpu build.
    +RUN cp /opt/tabby/bin/tabby /opt/tabby/bin/tabby-cpu
    +
    +FROM ${BASE_ROCM_RUN_CONTAINER} AS runtime
    +
    +RUN apt-get update && \
    +    apt-get install -y --no-install-recommends \
    +    git \
    +    curl \
    +    openssh-client \
    +    ca-certificates \
    +    libssl3 \
    +    rocblas \
    +    hipblas \
    +    libgomp1 \
    +    && \
    +    apt-get clean && \
    +    rm -rf /var/lib/apt/lists/*
    +
    +# Disable safe directory in docker
    +# Context: https://github.com/git/git/commit/8959555cee7ec045958f9b6dd62e541affb7e7d9
    +RUN git config --system --add safe.directory "*"
    +
    +# Automatic platform ARGs in the global scope
    +# https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope
    +ARG TARGETARCH
    +
    +COPY --from=build /opt/tabby /opt/tabby
    +
    +ENV PATH="$PATH:/opt/tabby/bin"
    +ENV TABBY_ROOT=/data
    +
    +ENTRYPOINT ["/opt/tabby/bin/tabby"]
    diff --git a/ee/tabby-db-macros/Cargo.toml b/ee/tabby-db-macros/Cargo.toml
    new file mode 100644
    index 000000000000..4b85500fc5de
    --- /dev/null
    +++ b/ee/tabby-db-macros/Cargo.toml
    @@ -0,0 +1,15 @@
    +[package]
    +name = "tabby-db-macros"
    +version.workspace = true
    +edition.workspace = true
    +authors.workspace = true
    +homepage.workspace = true
    +
    +[lib]
    +proc-macro = true
    +
    +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
    +
    +[dependencies]
    +quote = "1.0.35"
    +syn = { version = "2.0.52", features = ["full"] }
    diff --git a/ee/tabby-db-macros/src/lib.rs b/ee/tabby-db-macros/src/lib.rs
    new file mode 100644
    index 000000000000..7006769d9e4d
    --- /dev/null
    +++ b/ee/tabby-db-macros/src/lib.rs
    @@ -0,0 +1,121 @@
    +use proc_macro::TokenStream;
    +use quote::quote;
    +use syn::{bracketed, parse::Parse, parse_macro_input, Expr, Ident, LitStr, Token, Type};
    +
    +#[derive(Clone)]
    +struct Column {
    +    name: LitStr,
    +    non_null: bool,
    +    rename: LitStr,
    +}
    +
    +impl Parse for Column {
    +    fn parse(input: syn::parse::ParseStream) -> syn::Result {
    +        let name: LitStr = input.parse()?;
    +        let non_null = input.peek(Token![!]);
    +        if non_null {
    +            input.parse::()?;
    +        }
    +        let mut rename = None;
    +        if input.peek(Token![as]) {
    +            input.parse::()?;
    +            rename = Some(input.parse()?);
    +        }
    +        Ok(Column {
    +            rename: rename.unwrap_or(name.clone()),
    +            name,
    +            non_null,
    +        })
    +    }
    +}
    +
    +struct PaginationQueryInput {
    +    pub typ: Type,
    +    pub table_name: LitStr,
    +    pub columns: Vec,
    +    pub condition: Option,
    +    pub limit: Ident,
    +    pub skip_id: Ident,
    +    pub backwards: Ident,
    +}
    +
    +impl Parse for PaginationQueryInput {
    +    fn parse(input: syn::parse::ParseStream) -> syn::Result {
    +        let typ = input.parse()?;
    +        input.parse::()?;
    +        let table_name = input.parse()?;
    +        input.parse::()?;
    +        let mut columns = vec![];
    +
    +        let inner;
    +        bracketed!(inner in input);
    +
    +        columns.push(inner.parse()?);
    +
    +        while inner.peek(Token![,]) {
    +            inner.parse::()?;
    +            columns.push(inner.parse()?);
    +        }
    +
    +        input.parse::()?;
    +        let limit = input.parse()?;
    +        input.parse::()?;
    +        let skip_id = input.parse()?;
    +        input.parse::()?;
    +        let backwards = input.parse()?;
    +
    +        let mut condition = None;
    +        if input.peek(Token![,]) {
    +            input.parse::()?;
    +            condition = Some(input.parse()?);
    +        }
    +
    +        Ok(PaginationQueryInput {
    +            typ,
    +            table_name,
    +            columns,
    +            condition,
    +            limit,
    +            skip_id,
    +            backwards,
    +        })
    +    }
    +}
    +
    +#[proc_macro]
    +pub fn query_paged_as(input: TokenStream) -> TokenStream {
    +    let input: PaginationQueryInput = parse_macro_input!(input);
    +
    +    let typ = input.typ;
    +    let table_name = input.table_name;
    +    let columns = input
    +        .columns
    +        .iter()
    +        .map(|col| {
    +            let name = col.name.value();
    +            let rename = col.rename.value();
    +            let non_null = if col.non_null {
    +                "!"
    +            } else {
    +                Default::default()
    +            };
    +            format!("{name} AS '{rename}{non_null}'")
    +        })
    +        .collect::>()
    +        .join(", ");
    +    let column_args: Vec = input.columns.iter().map(|col| col.name.value()).collect();
    +    let limit = input.limit;
    +    let condition = match input.condition {
    +        Some(cond) => quote! {#cond},
    +        None => quote! {None},
    +    };
    +    let skip_id = input.skip_id;
    +    let backwards = input.backwards;
    +    quote! {
    +        sqlx::query_as(&crate::make_pagination_query_with_condition({
    +            let _ = sqlx::query_as!(#typ, "SELECT " + #columns + " FROM (SELECT * FROM " + #table_name + ")");
    +            &#table_name
    +        }, &[ #(#column_args),* ], #limit, #skip_id, #backwards, #condition))
    +    }
    +    .into()
    +}
    diff --git a/ee/tabby-db/.gitignore b/ee/tabby-db/.gitignore
    new file mode 100644
    index 000000000000..51a6bc637f71
    --- /dev/null
    +++ b/ee/tabby-db/.gitignore
    @@ -0,0 +1 @@
    +schema.sqlite-*
    diff --git a/ee/tabby-db/Cargo.toml b/ee/tabby-db/Cargo.toml
    index e3a8a36c74df..2b5533851acd 100644
    --- a/ee/tabby-db/Cargo.toml
    +++ b/ee/tabby-db/Cargo.toml
    @@ -7,16 +7,28 @@ homepage.workspace = true
     
     [features]
     testutils = []
    -prod-db = []
     
     [dependencies]
    +tabby-db-macros = { path = "../tabby-db-macros" }
     anyhow.workspace = true
     chrono = { workspace = true, features = ["serde"] }
    -sqlx = { version = "0.7.3", features = ["sqlite", "chrono", "runtime-tokio"] }
    -tabby-common = { path = "../../crates/tabby-common" }
    +hash-ids.workspace = true
    +lazy_static.workspace = true
    +sql_query_builder = { version = "2.1.0", features = ["sqlite"] }
     tokio = { workspace = true, features = ["fs"] }
     uuid.workspace = true
    +cached = { workspace = true, features = ["async"] }
    +serde.workspace = true
    +serde_json.workspace = true
    +sqlx = { workspace = true, features = [
    +    "sqlite",
    +    "chrono",
    +    "runtime-tokio",
    +    "macros",
    +    "json",
    +] }
    +sqlx-migrate-validate = { path = "../../crates/sqlx-migrate-validate" }
     
     [dev-dependencies]
    -assert_matches = "1.5.0"
    +assert_matches.workspace = true
     tokio = { workspace = true, features = ["macros", "process"] }
    diff --git a/ee/tabby-db/migrations/0010_server-setting.down.sql b/ee/tabby-db/migrations/0010_server-setting.down.sql
    new file mode 100644
    index 000000000000..bbd91404c0b2
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0010_server-setting.down.sql
    @@ -0,0 +1 @@
    +DROP TABLE server_setting;
    diff --git a/ee/tabby-db/migrations/0010_server-setting.up.sql b/ee/tabby-db/migrations/0010_server-setting.up.sql
    new file mode 100644
    index 000000000000..9b2d51155292
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0010_server-setting.up.sql
    @@ -0,0 +1,6 @@
    +CREATE TABLE server_setting (
    +    id INTEGER PRIMARY KEY AUTOINCREMENT,
    +    security_allowed_register_domain_list STRING,
    +    security_disable_client_side_telemetry BOOLEAN NOT NULL DEFAULT FALSE,
    +    network_external_url STRING NOT NULL DEFAULT 'http://localhost:8080'
    +);
    diff --git a/ee/tabby-db/migrations/0011_new-email-settings.down.sql b/ee/tabby-db/migrations/0011_new-email-settings.down.sql
    new file mode 100644
    index 000000000000..3e53ca9475c2
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0011_new-email-settings.down.sql
    @@ -0,0 +1,3 @@
    +ALTER TABLE email_setting DROP COLUMN from_address;
    +ALTER TABLE email_setting DROP COLUMN encryption;
    +ALTER TABLE email_setting DROP COLUMN auth_method;
    diff --git a/ee/tabby-db/migrations/0011_new-email-settings.up.sql b/ee/tabby-db/migrations/0011_new-email-settings.up.sql
    new file mode 100644
    index 000000000000..4b7a4ad4641b
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0011_new-email-settings.up.sql
    @@ -0,0 +1,10 @@
    +DROP TABLE email_setting;
    +CREATE TABLE IF NOT EXISTS email_setting(
    +    id INTEGER PRIMARY KEY AUTOINCREMENT,
    +    smtp_username VARCHAR(255) NOT NULL,
    +    smtp_password VARCHAR(255) NOT NULL,
    +    smtp_server VARCHAR(255) NOT NULL,
    +    from_address VARCHAR(255) NOT NULL,
    +    encryption VARCHAR(255) NOT NULL DEFAULT 'ssltls',
    +    auth_method VARCHAR(255) NOT NULL DEFAULT 'plain'
    +);
    diff --git a/ee/tabby-db/migrations/0012_remove-redirect-uri-field-oauth.down.sql b/ee/tabby-db/migrations/0012_remove-redirect-uri-field-oauth.down.sql
    new file mode 100644
    index 000000000000..468e1d694494
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0012_remove-redirect-uri-field-oauth.down.sql
    @@ -0,0 +1 @@
    +ALTER TABLE google_oauth_credential ADD COLUMN redirect_uri VARCHAR(256);
    \ No newline at end of file
    diff --git a/ee/tabby-db/migrations/0012_remove-redirect-uri-field-oauth.up.sql b/ee/tabby-db/migrations/0012_remove-redirect-uri-field-oauth.up.sql
    new file mode 100644
    index 000000000000..61a5a82e3f2e
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0012_remove-redirect-uri-field-oauth.up.sql
    @@ -0,0 +1 @@
    +ALTER TABLE google_oauth_credential DROP COLUMN redirect_uri;
    \ No newline at end of file
    diff --git a/ee/tabby-db/migrations/0013_add-smtp-port.down.sql b/ee/tabby-db/migrations/0013_add-smtp-port.down.sql
    new file mode 100644
    index 000000000000..b0f57073eb8c
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0013_add-smtp-port.down.sql
    @@ -0,0 +1 @@
    +ALTER TABLE email_setting DROP COLUMN smtp_port;
    diff --git a/ee/tabby-db/migrations/0013_add-smtp-port.up.sql b/ee/tabby-db/migrations/0013_add-smtp-port.up.sql
    new file mode 100644
    index 000000000000..4943927587ec
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0013_add-smtp-port.up.sql
    @@ -0,0 +1 @@
    +ALTER TABLE email_setting ADD COLUMN smtp_port INTEGER NOT NULL DEFAULT 25;
    diff --git a/ee/tabby-db/migrations/0014_password-reset.down.sql b/ee/tabby-db/migrations/0014_password-reset.down.sql
    new file mode 100644
    index 000000000000..818ffa3e6bda
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0014_password-reset.down.sql
    @@ -0,0 +1 @@
    +DROP TABLE password_reset;
    diff --git a/ee/tabby-db/migrations/0014_password-reset.up.sql b/ee/tabby-db/migrations/0014_password-reset.up.sql
    new file mode 100644
    index 000000000000..34edbe729cde
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0014_password-reset.up.sql
    @@ -0,0 +1,6 @@
    +CREATE TABLE password_reset(
    +    id INTEGER PRIMARY KEY AUTOINCREMENT,
    +    user_id INTEGER NOT NULL UNIQUE,
    +    code VARCHAR(36) NOT NULL,
    +    created_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now'))
    +);
    diff --git a/ee/tabby-db/migrations/0015_enterprise-license.down.sql b/ee/tabby-db/migrations/0015_enterprise-license.down.sql
    new file mode 100644
    index 000000000000..6d25c5ea6c40
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0015_enterprise-license.down.sql
    @@ -0,0 +1 @@
    +ALTER TABLE server_setting DROP COLUMN billing_enterprise_license;
    diff --git a/ee/tabby-db/migrations/0015_enterprise-license.up.sql b/ee/tabby-db/migrations/0015_enterprise-license.up.sql
    new file mode 100644
    index 000000000000..42914d6218d4
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0015_enterprise-license.up.sql
    @@ -0,0 +1 @@
    +ALTER TABLE server_setting ADD COLUMN billing_enterprise_license STRING;
    diff --git a/ee/tabby-db/migrations/0016_merge-oauth-tables.down.sql b/ee/tabby-db/migrations/0016_merge-oauth-tables.down.sql
    new file mode 100644
    index 000000000000..736da10e10ef
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0016_merge-oauth-tables.down.sql
    @@ -0,0 +1 @@
    +DROP TABLE oauth_credential;
    diff --git a/ee/tabby-db/migrations/0016_merge-oauth-tables.up.sql b/ee/tabby-db/migrations/0016_merge-oauth-tables.up.sql
    new file mode 100644
    index 000000000000..fc59d7bc8e31
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0016_merge-oauth-tables.up.sql
    @@ -0,0 +1,16 @@
    +CREATE TABLE oauth_credential (
    +    id            INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
    +    provider      TEXT NOT NULL,
    +    client_id     VARCHAR(256) NOT NULL,
    +    client_secret VARCHAR(64) NOT NULL,
    +    created_at    TIMESTAMP NOT NULL DEFAULT (DATETIME('now')),
    +    updated_at    TIMESTAMP NOT NULL DEFAULT (DATETIME('now')),
    +
    +    CONSTRAINT `idx_provider` UNIQUE (`provider`)
    +);
    +
    +INSERT INTO oauth_credential(provider, client_id, client_secret, created_at, updated_at)
    +    SELECT 'google', client_id, client_secret, created_at, updated_at FROM google_oauth_credential;
    +
    +INSERT INTO oauth_credential(provider, client_id, client_secret, created_at, updated_at)
    +    SELECT 'github', client_id, client_secret, created_at, updated_at FROM github_oauth_credential;
    \ No newline at end of file
    diff --git a/ee/tabby-db/migrations/0017_add-user-completions.down.sql b/ee/tabby-db/migrations/0017_add-user-completions.down.sql
    new file mode 100644
    index 000000000000..0ac90f9d8d0d
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0017_add-user-completions.down.sql
    @@ -0,0 +1,3 @@
    +DROP INDEX user_completions_completion_id_idx;
    +DROP INDEX user_completions_user_id_language_idx;
    +DROP TABLE user_completions;
    \ No newline at end of file
    diff --git a/ee/tabby-db/migrations/0017_add-user-completions.up.sql b/ee/tabby-db/migrations/0017_add-user-completions.up.sql
    new file mode 100644
    index 000000000000..64d0f5a9b9e1
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0017_add-user-completions.up.sql
    @@ -0,0 +1,18 @@
    +CREATE TABLE user_completions (
    +    id INTEGER PRIMARY KEY AUTOINCREMENT,
    +    user_id INTEGER INTEGER NOT NULL,
    +    completion_id VARCHAR(255) NOT NULL,
    +
    +    language VARCHAR(255) NOT NULL,
    +
    +    views INTEGER NOT NULL DEFAULT 0,
    +    selects INTEGER NOT NULL DEFAULT 0,
    +    dismisses INTEGER NOT NULL DEFAULT 0,
    +
    +    created_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now')),
    +    updated_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now')),
    +    FOREIGN KEY(user_id) REFERENCES users(id)
    +);
    +
    +CREATE INDEX user_completions_user_id_language_idx ON user_completions (user_id, language);
    +CREATE INDEX user_completions_completion_id_idx ON user_completions (completion_id);
    \ No newline at end of file
    diff --git a/ee/tabby-db/migrations/0018_user-password-nullable.down.sql b/ee/tabby-db/migrations/0018_user-password-nullable.down.sql
    new file mode 100644
    index 000000000000..be4a10d91180
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0018_user-password-nullable.down.sql
    @@ -0,0 +1,5 @@
    +UPDATE users SET password_encrypted =
    +    CASE
    +        WHEN password_encrypted IS NULL THEN ''
    +        ELSE password_encrypted
    +    END;
    diff --git a/ee/tabby-db/migrations/0018_user-password-nullable.up.sql b/ee/tabby-db/migrations/0018_user-password-nullable.up.sql
    new file mode 100644
    index 000000000000..e663c56a8a88
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0018_user-password-nullable.up.sql
    @@ -0,0 +1,10 @@
    +ALTER TABLE users ADD COLUMN password_encrypted_nullable VARCHAR(128);
    +
    +UPDATE users SET password_encrypted_nullable =
    +    CASE
    +        WHEN LENGTH(password_encrypted) = 0 THEN NULL
    +        ELSE password_encrypted
    +    END;
    +
    +ALTER TABLE users DROP COLUMN password_encrypted;
    +ALTER TABLE users RENAME password_encrypted_nullable TO password_encrypted;
    diff --git a/ee/tabby-db/migrations/0019_user-avatar.down.sql b/ee/tabby-db/migrations/0019_user-avatar.down.sql
    new file mode 100644
    index 000000000000..5f7c95e3f5f4
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0019_user-avatar.down.sql
    @@ -0,0 +1 @@
    +ALTER TABLE users DROP COLUMN avatar;
    diff --git a/ee/tabby-db/migrations/0019_user-avatar.up.sql b/ee/tabby-db/migrations/0019_user-avatar.up.sql
    new file mode 100644
    index 000000000000..43743becc0b9
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0019_user-avatar.up.sql
    @@ -0,0 +1 @@
    +ALTER TABLE users ADD COLUMN avatar BLOB DEFAULT NULL;
    diff --git a/ee/tabby-db/migrations/0020_job-run-index.down.sql b/ee/tabby-db/migrations/0020_job-run-index.down.sql
    new file mode 100644
    index 000000000000..3fb3c9300ebd
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0020_job-run-index.down.sql
    @@ -0,0 +1 @@
    +DROP INDEX idx_job_created_at;
    diff --git a/ee/tabby-db/migrations/0020_job-run-index.up.sql b/ee/tabby-db/migrations/0020_job-run-index.up.sql
    new file mode 100644
    index 000000000000..5bcbaa1a8701
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0020_job-run-index.up.sql
    @@ -0,0 +1 @@
    +CREATE INDEX idx_job_created_at ON job_runs(job, created_at);
    diff --git a/ee/tabby-db/migrations/0021_repository-name-index.down.sql b/ee/tabby-db/migrations/0021_repository-name-index.down.sql
    new file mode 100644
    index 000000000000..5a8a6d00e732
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0021_repository-name-index.down.sql
    @@ -0,0 +1 @@
    +DROP INDEX idx_repository_name;
    diff --git a/ee/tabby-db/migrations/0021_repository-name-index.up.sql b/ee/tabby-db/migrations/0021_repository-name-index.up.sql
    new file mode 100644
    index 000000000000..a66c545fa76a
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0021_repository-name-index.up.sql
    @@ -0,0 +1 @@
    +CREATE INDEX idx_repository_name ON repositories(name);
    diff --git a/ee/tabby-db/migrations/0022_github-provider.down.sql b/ee/tabby-db/migrations/0022_github-provider.down.sql
    new file mode 100644
    index 000000000000..2af049ced3da
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0022_github-provider.down.sql
    @@ -0,0 +1 @@
    +DROP TABLE github_repository_provider;
    diff --git a/ee/tabby-db/migrations/0022_github-provider.up.sql b/ee/tabby-db/migrations/0022_github-provider.up.sql
    new file mode 100644
    index 000000000000..0634b42cb7ac
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0022_github-provider.up.sql
    @@ -0,0 +1,8 @@
    +CREATE TABLE github_repository_provider(
    +    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    +    display_name TEXT NOT NULL,
    +    application_id TEXT NOT NULL,
    +    secret TEXT NOT NULL,
    +    access_token TEXT,
    +    CONSTRAINT `idx_application_id` UNIQUE (`application_id`)
    +);
    diff --git a/ee/tabby-db/migrations/0023_user-completions-created-at-index.down.sql b/ee/tabby-db/migrations/0023_user-completions-created-at-index.down.sql
    new file mode 100644
    index 000000000000..1db2af867680
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0023_user-completions-created-at-index.down.sql
    @@ -0,0 +1 @@
    +DROP INDEX idx_user_completion_user_id_created_at_language;
    diff --git a/ee/tabby-db/migrations/0023_user-completions-created-at-index.up.sql b/ee/tabby-db/migrations/0023_user-completions-created-at-index.up.sql
    new file mode 100644
    index 000000000000..7f1d88a84f7a
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0023_user-completions-created-at-index.up.sql
    @@ -0,0 +1,2 @@
    +DROP INDEX user_completions_user_id_language_idx;
    +CREATE INDEX idx_user_completion_user_id_created_at_language ON user_completions(user_id, created_at, language);
    diff --git a/ee/tabby-db/migrations/0024_github-provided-repos.down.sql b/ee/tabby-db/migrations/0024_github-provided-repos.down.sql
    new file mode 100644
    index 000000000000..92d711019fc3
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0024_github-provided-repos.down.sql
    @@ -0,0 +1 @@
    +DROP TABLE github_provided_repositories;
    diff --git a/ee/tabby-db/migrations/0024_github-provided-repos.up.sql b/ee/tabby-db/migrations/0024_github-provided-repos.up.sql
    new file mode 100644
    index 000000000000..d8f25f4c2179
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0024_github-provided-repos.up.sql
    @@ -0,0 +1,13 @@
    +CREATE TABLE github_provided_repositories(
    +    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    +    github_repository_provider_id INTEGER NOT NULL,
    +    -- vendor_id from https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#list-repositories-for-a-user
    +    vendor_id TEXT NOT NULL,
    +    name TEXT NOT NULL,
    +    git_url TEXT NOT NULL,
    +    active BOOLEAN NOT NULL DEFAULT FALSE,
    +    updated_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now')),
    +    FOREIGN KEY (github_repository_provider_id) REFERENCES github_repository_provider(id) ON DELETE CASCADE,
    +    CONSTRAINT `idx_vendor_id_provider_id` UNIQUE (vendor_id, github_repository_provider_id)
    +);
    +CREATE INDEX github_provided_repositories_updated_at ON github_provided_repositories(updated_at);
    diff --git a/ee/tabby-db/migrations/0025_user-events.down.sql b/ee/tabby-db/migrations/0025_user-events.down.sql
    new file mode 100644
    index 000000000000..027bc26f59ca
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0025_user-events.down.sql
    @@ -0,0 +1,3 @@
    +DROP INDEX idx_user_events_user_id;
    +DROP INDEX idx_user_events_created_at;
    +DROP TABLE user_events;
    diff --git a/ee/tabby-db/migrations/0025_user-events.up.sql b/ee/tabby-db/migrations/0025_user-events.up.sql
    new file mode 100644
    index 000000000000..bcc258e77f02
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0025_user-events.up.sql
    @@ -0,0 +1,10 @@
    +CREATE TABLE user_events (
    +    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    +    user_id INTEGER NOT NULL,
    +    kind TEXT NOT NULL,
    +    created_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now')),
    +    payload BLOB NOT NULL,
    +    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
    +);
    +CREATE INDEX idx_user_events_user_id ON user_events(user_id);
    +CREATE INDEX idx_user_events_created_at ON user_events(created_at);
    diff --git a/ee/tabby-db/migrations/0026_clean-up-tables.down.sql b/ee/tabby-db/migrations/0026_clean-up-tables.down.sql
    new file mode 100644
    index 000000000000..e69de29bb2d1
    diff --git a/ee/tabby-db/migrations/0026_clean-up-tables.up.sql b/ee/tabby-db/migrations/0026_clean-up-tables.up.sql
    new file mode 100644
    index 000000000000..fdb055e86618
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0026_clean-up-tables.up.sql
    @@ -0,0 +1,27 @@
    +DROP TABLE github_oauth_credential;
    +DROP TABLE google_oauth_credential;
    +
    +-- Drop the entire refresh_token / password_reset table to resolve the foreign key issue.
    +-- This is acceptable as it is equivalent to asking all users to sign in again for the new release.
    +DROP TABLE refresh_tokens;
    +DROP TABLE password_reset;
    +
    +CREATE TABLE refresh_tokens(
    +  id INTEGER PRIMARY KEY AUTOINCREMENT,
    +  user_id INTEGER NOT NULL,
    +  token VARCHAR(255) NOT NULL COLLATE NOCASE,
    +  expires_at TIMESTAMP NOT NULL,
    +  created_at TIMESTAMP DEFAULT(DATETIME('now')),
    +  CONSTRAINT `idx_token` UNIQUE(`token`)
    +
    +  FOREIGN KEY(user_id) REFERENCES users(id)
    +);
    +
    +CREATE TABLE password_reset(
    +  id INTEGER PRIMARY KEY AUTOINCREMENT,
    +  user_id INTEGER NOT NULL UNIQUE,
    +  code VARCHAR(36) NOT NULL,
    +  created_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +
    +  FOREIGN KEY(user_id) REFERENCES users(id)
    +);
    \ No newline at end of file
    diff --git a/ee/tabby-db/migrations/0027_remove-github-repository-columns-on-oauth.down.sql b/ee/tabby-db/migrations/0027_remove-github-repository-columns-on-oauth.down.sql
    new file mode 100644
    index 000000000000..e69de29bb2d1
    diff --git a/ee/tabby-db/migrations/0027_remove-github-repository-columns-on-oauth.up.sql b/ee/tabby-db/migrations/0027_remove-github-repository-columns-on-oauth.up.sql
    new file mode 100644
    index 000000000000..d8116958d01d
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0027_remove-github-repository-columns-on-oauth.up.sql
    @@ -0,0 +1,8 @@
    +DROP TABLE github_repository_provider;
    +
    +CREATE TABLE github_repository_provider(
    +    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    +    display_name TEXT NOT NULL,
    +    access_token TEXT,
    +    synced_at TIMESTAMP
    +);
    diff --git a/ee/tabby-db/migrations/0028_gitlab-provider.down.sql b/ee/tabby-db/migrations/0028_gitlab-provider.down.sql
    new file mode 100644
    index 000000000000..8065696f2fa8
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0028_gitlab-provider.down.sql
    @@ -0,0 +1,3 @@
    +DROP TABLE gitlab_provided_repositories;
    +DROP TABLE gitlab_repository_provider;
    +
    diff --git a/ee/tabby-db/migrations/0028_gitlab-provider.up.sql b/ee/tabby-db/migrations/0028_gitlab-provider.up.sql
    new file mode 100644
    index 000000000000..f0e387f91ca8
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0028_gitlab-provider.up.sql
    @@ -0,0 +1,20 @@
    +CREATE TABLE gitlab_repository_provider(
    +    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    +    display_name TEXT NOT NULL,
    +    access_token TEXT,
    +    synced_at TIMESTAMP
    +);
    +
    +CREATE TABLE gitlab_provided_repositories(
    +    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    +    gitlab_repository_provider_id INTEGER NOT NULL,
    +    -- vendor_id from https://docs.gitlab.com/ee/api/repositories.html
    +    vendor_id TEXT NOT NULL,
    +    name TEXT NOT NULL,
    +    git_url TEXT NOT NULL,
    +    active BOOLEAN NOT NULL DEFAULT FALSE,
    +    updated_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now')),
    +    FOREIGN KEY (gitlab_repository_provider_id) REFERENCES gitlab_repository_provider(id) ON DELETE CASCADE,
    +    CONSTRAINT `idx_vendor_id_provider_id` UNIQUE (vendor_id, gitlab_repository_provider_id)
    +);
    +CREATE INDEX gitlab_provided_repositories_updated_at ON gitlab_provided_repositories(updated_at);
    diff --git a/ee/tabby-db/migrations/0029_merged-provider-tables.down.sql b/ee/tabby-db/migrations/0029_merged-provider-tables.down.sql
    new file mode 100644
    index 000000000000..281c808ef332
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0029_merged-provider-tables.down.sql
    @@ -0,0 +1,2 @@
    +DROP TABLE integrations;
    +DROP TABLE provided_repositories;
    diff --git a/ee/tabby-db/migrations/0029_merged-provider-tables.up.sql b/ee/tabby-db/migrations/0029_merged-provider-tables.up.sql
    new file mode 100644
    index 000000000000..d2352e8359b8
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0029_merged-provider-tables.up.sql
    @@ -0,0 +1,45 @@
    +CREATE TABLE integrations(
    +    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    +    kind TEXT NOT NULL,
    +    display_name TEXT NOT NULL,
    +    access_token TEXT NOT NULL,
    +    api_base TEXT,
    +    error TEXT,
    +    created_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now')),
    +    updated_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now')),
    +    synced BOOLEAN NOT NULL DEFAULT FALSE
    +);
    +
    +CREATE TABLE provided_repositories(
    +    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    +    integration_id INTEGER NOT NULL,
    +    vendor_id TEXT NOT NULL,
    +    name TEXT NOT NULL,
    +    git_url TEXT NOT NULL,
    +    active BOOLEAN NOT NULL DEFAULT FALSE,
    +    created_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now')),
    +    updated_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now')),
    +    FOREIGN KEY (integration_id) REFERENCES integrations(id) ON DELETE CASCADE,
    +    CONSTRAINT idx_unique_integration_id_vendor_id UNIQUE (integration_id, vendor_id)
    +);
    +
    +INSERT INTO integrations(kind, display_name, access_token)
    +    SELECT 'github', display_name, access_token FROM github_repository_provider WHERE access_token IS NOT NULL;
    +
    +INSERT INTO integrations(kind, display_name, access_token)
    +    SELECT 'gitlab', display_name, access_token FROM gitlab_repository_provider WHERE access_token IS NOT NULL;
    +
    +INSERT INTO provided_repositories(integration_id, vendor_id, name, git_url, active)
    +    SELECT integrations.id, vendor_id, github_provided_repositories.name, git_url, active FROM github_provided_repositories
    +    JOIN github_repository_provider ON github_repository_provider_id = github_repository_provider.id
    +    JOIN integrations ON kind = 'github' AND github_repository_provider.access_token = integrations.access_token;
    +
    +INSERT INTO provided_repositories(integration_id, vendor_id, name, git_url, active)
    +    SELECT integrations.id, vendor_id, gitlab_provided_repositories.name, git_url, active FROM gitlab_provided_repositories
    +    JOIN gitlab_repository_provider ON gitlab_repository_provider_id = gitlab_repository_provider.id
    +    JOIN integrations ON kind = 'gitlab' AND gitlab_repository_provider.access_token = integrations.access_token;
    +
    +DROP TABLE github_provided_repositories;
    +DROP TABLE gitlab_provided_repositories;
    +DROP TABLE github_repository_provider;
    +DROP TABLE gitlab_repository_provider;
    diff --git a/ee/tabby-db/migrations/0030_user-name.down.sql b/ee/tabby-db/migrations/0030_user-name.down.sql
    new file mode 100644
    index 000000000000..1592aac27486
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0030_user-name.down.sql
    @@ -0,0 +1 @@
    +ALTER TABLE users DROP COLUMN name;
    diff --git a/ee/tabby-db/migrations/0030_user-name.up.sql b/ee/tabby-db/migrations/0030_user-name.up.sql
    new file mode 100644
    index 000000000000..6bc54f4d339d
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0030_user-name.up.sql
    @@ -0,0 +1 @@
    +ALTER TABLE users ADD COLUMN name VARCHAR(255);
    diff --git a/ee/tabby-db/migrations/0031_web-crawler-urls.down.sql b/ee/tabby-db/migrations/0031_web-crawler-urls.down.sql
    new file mode 100644
    index 000000000000..b2b59087a699
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0031_web-crawler-urls.down.sql
    @@ -0,0 +1 @@
    +DROP TABLE web_crawler_urls;
    diff --git a/ee/tabby-db/migrations/0031_web-crawler-urls.up.sql b/ee/tabby-db/migrations/0031_web-crawler-urls.up.sql
    new file mode 100644
    index 000000000000..a0fa01bcb310
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0031_web-crawler-urls.up.sql
    @@ -0,0 +1,6 @@
    +CREATE TABLE web_crawler_urls(
    +    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    +    url TEXT NOT NULL,
    +    created_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now')),
    +    CONSTRAINT `unique_url` UNIQUE(url)
    +);
    diff --git a/ee/tabby-db/migrations/0033_add-job-runs-param-column.down.sql b/ee/tabby-db/migrations/0033_add-job-runs-param-column.down.sql
    new file mode 100644
    index 000000000000..d8d9fc47e6f5
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0033_add-job-runs-param-column.down.sql
    @@ -0,0 +1,2 @@
    +DROP INDEX `idx_job_runs_command`;
    +ALTER TABLE job_runs DROP COLUMN command;
    \ No newline at end of file
    diff --git a/ee/tabby-db/migrations/0033_add-job-runs-param-column.up.sql b/ee/tabby-db/migrations/0033_add-job-runs-param-column.up.sql
    new file mode 100644
    index 000000000000..8bd7a785007d
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0033_add-job-runs-param-column.up.sql
    @@ -0,0 +1,2 @@
    +ALTER TABLE job_runs ADD COLUMN command TEXT;
    +CREATE INDEX `idx_job_runs_command` ON job_runs(command);
    diff --git a/ee/tabby-db/migrations/0034_add-started-at-column-in-job-runs.down.sql b/ee/tabby-db/migrations/0034_add-started-at-column-in-job-runs.down.sql
    new file mode 100644
    index 000000000000..949ddf5eb71c
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0034_add-started-at-column-in-job-runs.down.sql
    @@ -0,0 +1 @@
    +ALTER TABLE job_runs DROP COLUMN started_at;
    \ No newline at end of file
    diff --git a/ee/tabby-db/migrations/0034_add-started-at-column-in-job-runs.up.sql b/ee/tabby-db/migrations/0034_add-started-at-column-in-job-runs.up.sql
    new file mode 100644
    index 000000000000..d0d8a211f6d9
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0034_add-started-at-column-in-job-runs.up.sql
    @@ -0,0 +1,4 @@
    +ALTER TABLE job_runs ADD COLUMN started_at TIMESTAMP;
    +
    +-- Set started_at to created_at for all job runs that have already finished.
    +UPDATE job_runs SET started_at = created_at WHERE exit_code IS NOT NULL;
    \ No newline at end of file
    diff --git a/ee/tabby-db/migrations/0035_add-thread-message-table.down.sql b/ee/tabby-db/migrations/0035_add-thread-message-table.down.sql
    new file mode 100644
    index 000000000000..68b9a2b2e7a1
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0035_add-thread-message-table.down.sql
    @@ -0,0 +1,2 @@
    +DROP TABLE thread_messages;
    +DROP TABLE threads;
    \ No newline at end of file
    diff --git a/ee/tabby-db/migrations/0035_add-thread-message-table.up.sql b/ee/tabby-db/migrations/0035_add-thread-message-table.up.sql
    new file mode 100644
    index 000000000000..a3ed46052637
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0035_add-thread-message-table.up.sql
    @@ -0,0 +1,39 @@
    +CREATE TABLE threads(
    +    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    +
    +    -- Whether the thread is ephemeral (e.g from chat sidebar)
    +    is_ephemeral BOOLEAN NOT NULL,
    +
    +    -- The user who created the thread
    +    user_id INTEGER NOT NULL,
    +
    +    created_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now')),
    +    updated_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now')),
    +
    +    -- Array of relevant questions, in format of `String`
    +    relevant_questions BLOB,
    +
    +    FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
    +);
    +
    +CREATE TABLE thread_messages(
    +    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    +    thread_id INTEGER NOT NULL,
    +
    +    role TEXT NOT NULL,
    +    content TEXT NOT NULL,
    +
    +    -- Array of code attachments, in format of `ThreadMessageAttachmentCode`
    +    code_attachments BLOB,
    +
    +    -- Array of client code attachments, in format of `ThreadMessageAttachmentClientCode`
    +    client_code_attachments BLOB,
    +
    +    -- Array of doc attachments, in format of `ThreadMessageAttachmentDoc`
    +    doc_attachments BLOB,
    +
    +    created_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now')),
    +    updated_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now')),
    +
    +    FOREIGN KEY(thread_id) REFERENCES threads(id) ON DELETE CASCADE
    +);
    \ No newline at end of file
    diff --git a/ee/tabby-db/migrations/0036_web_document.down.sql b/ee/tabby-db/migrations/0036_web_document.down.sql
    new file mode 100644
    index 000000000000..cfdbee70bfd9
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0036_web_document.down.sql
    @@ -0,0 +1 @@
    +DROP TABLE web_documents;
    diff --git a/ee/tabby-db/migrations/0036_web_document.up.sql b/ee/tabby-db/migrations/0036_web_document.up.sql
    new file mode 100644
    index 000000000000..0a2f0d948f39
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0036_web_document.up.sql
    @@ -0,0 +1,10 @@
    +CREATE TABLE web_documents(
    +     id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    +     name VARCHAR(255) NOT NULL,
    +     url TEXT NOT NULL,
    +     is_preset BOOLEAN NOT NULL DEFAULT FALSE,
    +     created_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +     updated_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +     CONSTRAINT idx_name UNIQUE(name),
    +     CONSTRAINT idx_url UNIQUE(url)
    +);
    diff --git a/ee/tabby-db/migrations/0037_remove-webcrawler-table.down.sql b/ee/tabby-db/migrations/0037_remove-webcrawler-table.down.sql
    new file mode 100644
    index 000000000000..e69de29bb2d1
    diff --git a/ee/tabby-db/migrations/0037_remove-webcrawler-table.up.sql b/ee/tabby-db/migrations/0037_remove-webcrawler-table.up.sql
    new file mode 100644
    index 000000000000..065eac6a3eca
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0037_remove-webcrawler-table.up.sql
    @@ -0,0 +1 @@
    +DROP TABLE web_crawler_urls;
    \ No newline at end of file
    diff --git a/ee/tabby-db/migrations/0038_add-access-control-tables.down.sql b/ee/tabby-db/migrations/0038_add-access-control-tables.down.sql
    new file mode 100644
    index 000000000000..35765188e087
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0038_add-access-control-tables.down.sql
    @@ -0,0 +1,3 @@
    +DROP TABLE user_groups;
    +DROP TABLE user_group_memberships;
    +DROP TABLE source_id_read_access_policies;
    \ No newline at end of file
    diff --git a/ee/tabby-db/migrations/0038_add-access-control-tables.up.sql b/ee/tabby-db/migrations/0038_add-access-control-tables.up.sql
    new file mode 100644
    index 000000000000..e8c671b88c6d
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0038_add-access-control-tables.up.sql
    @@ -0,0 +1,43 @@
    +CREATE TABLE user_groups (
    +  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    +  name VARCHAR(255) NOT NULL,
    +
    +  created_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +  updated_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +
    +  CONSTRAINT idx_unique_name UNIQUE (name)
    +);
    +
    +CREATE TABLE user_group_memberships (
    +  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    +
    +  user_id INTEGER NOT NULL,
    +  user_group_id INTEGER NOT NULL,
    +
    +  is_group_admin BOOLEAN NOT NULL DEFAULT FALSE,
    +
    +  created_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +  updated_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +
    +  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    +  FOREIGN KEY (user_group_id) REFERENCES user_groups(id) ON DELETE CASCADE,
    +
    +  CONSTRAINT idx_unique_user_id_user_group_id UNIQUE (user_id, user_group_id)
    +);
    +
    +CREATE TABLE source_id_read_access_policies (
    +  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    +
    +  -- source_id doesn't come with DB constraint, need individual garbage collection job
    +  source_id VARCHAR(255) NOT NULL,
    +
    +  user_group_id INTEGER NOT NULL,
    +
    +  created_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +  updated_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +
    +  FOREIGN KEY (user_group_id) REFERENCES user_groups(id) ON DELETE CASCADE,
    +
    +  -- access_policy is unique per source_id and user_group_id
    +  CONSTRAINT idx_unique_source_id_user_group_id UNIQUE (source_id, user_group_id)
    +);
    \ No newline at end of file
    diff --git a/ee/tabby-db/migrations/0039_add-notification-inbox.down.sql b/ee/tabby-db/migrations/0039_add-notification-inbox.down.sql
    new file mode 100644
    index 000000000000..34e0300a477f
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0039_add-notification-inbox.down.sql
    @@ -0,0 +1,2 @@
    +DROP TABLE notifications;
    +DROP TABLE read_notifications;
    \ No newline at end of file
    diff --git a/ee/tabby-db/migrations/0039_add-notification-inbox.up.sql b/ee/tabby-db/migrations/0039_add-notification-inbox.up.sql
    new file mode 100644
    index 000000000000..3b18f122df16
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0039_add-notification-inbox.up.sql
    @@ -0,0 +1,26 @@
    +CREATE TABLE notifications (
    +  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    +
    +  created_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +  updated_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +
    +  -- enum of admin, all_user
    +  recipient VARCHAR(255) NOT NULL DEFAULT 'admin',
    +
    +  -- content of notification, in markdown format.
    +  content TEXT NOT NULL
    +);
    +
    +CREATE TABLE read_notifications (
    +  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    +  user_id INTEGER NOT NULL,
    +  notification_id INTEGER NOT NULL,
    +
    +  created_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +  updated_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +
    +  CONSTRAINT idx_unique_user_id_notification_id UNIQUE (user_id, notification_id),
    +
    +  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    +  FOREIGN KEY (notification_id) REFERENCES notifications(id) ON DELETE CASCADE
    +)
    \ No newline at end of file
    diff --git a/ee/tabby-db/migrations/0040_add-code-source-id-column.down.sql b/ee/tabby-db/migrations/0040_add-code-source-id-column.down.sql
    new file mode 100644
    index 000000000000..1eb6a274fe4a
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0040_add-code-source-id-column.down.sql
    @@ -0,0 +1,2 @@
    +ALTER TABLE thread_messages
    +  DROP code_source_id;
    diff --git a/ee/tabby-db/migrations/0040_add-code-source-id-column.up.sql b/ee/tabby-db/migrations/0040_add-code-source-id-column.up.sql
    new file mode 100644
    index 000000000000..ebc1359d8769
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0040_add-code-source-id-column.up.sql
    @@ -0,0 +1,2 @@
    +ALTER TABLE thread_messages
    +  ADD code_source_id VARCHAR(255);
    \ No newline at end of file
    diff --git a/ee/tabby-db/migrations/0041_ldap-credential.down.sql b/ee/tabby-db/migrations/0041_ldap-credential.down.sql
    new file mode 100644
    index 000000000000..ecf34c91337f
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0041_ldap-credential.down.sql
    @@ -0,0 +1 @@
    +DROP TABLE ldap_credential;
    diff --git a/ee/tabby-db/migrations/0041_ldap-credential.up.sql b/ee/tabby-db/migrations/0041_ldap-credential.up.sql
    new file mode 100644
    index 000000000000..2b5997b029cf
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0041_ldap-credential.up.sql
    @@ -0,0 +1,23 @@
    +CREATE TABLE ldap_credential(
    +  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    +
    +  host STRING NOT NULL,
    +  port INTEGER NOT NULL DEFAULT 389,
    +
    +  bind_dn STRING NOT NULL,
    +  bind_password STRING NOT NULL,
    +  base_dn STRING NOT NULL,
    +  user_filter STRING NOT NULL,
    +
    +  -- enum of none, starttls, ldaps
    +  encryption STRING NOT NULL DEFAULT 'none',
    +  skip_tls_verify BOOLEAN NOT NULL DEFAULT FALSE,
    +
    +  --- the attribute to be used as the Tabby user email address
    +  email_attribute STRING NOT NULL DEFAULT 'email',
    +  --- the attribute to be used as the Tabby user name
    +  name_attribute STRING,
    +
    +  created_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +  updated_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now'))
    +);
    diff --git a/ee/tabby-db/migrations/0042_add-file-list-attachment.down.sql b/ee/tabby-db/migrations/0042_add-file-list-attachment.down.sql
    new file mode 100644
    index 000000000000..48785f925a4e
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0042_add-file-list-attachment.down.sql
    @@ -0,0 +1,2 @@
    +ALTER TABLE thread_messages
    +  DROP attachment;
    diff --git a/ee/tabby-db/migrations/0042_add-file-list-attachment.up.sql b/ee/tabby-db/migrations/0042_add-file-list-attachment.up.sql
    new file mode 100644
    index 000000000000..b04763c8fcc3
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0042_add-file-list-attachment.up.sql
    @@ -0,0 +1,7 @@
    +ALTER TABLE thread_messages
    +  ADD attachment BLOB NOT NULL DEFAULT '{}';
    +
    +-- Migrate existing data to the new attachment column.
    +UPDATE thread_messages SET attachment = JSON_SET(attachment, '$.code', JSON(code_attachments)) WHERE code_attachments IS NOT NULL;
    +UPDATE thread_messages SET attachment = JSON_SET(attachment, '$.client_code', JSON(client_code_attachments)) WHERE client_code_attachments IS NOT NULL;
    +UPDATE thread_messages SET attachment = JSON_SET(attachment, '$.doc', JSON(doc_attachments)) WHERE doc_attachments IS NOT NULL;
    \ No newline at end of file
    diff --git a/ee/tabby-db/migrations/0043_page-section.down.sql b/ee/tabby-db/migrations/0043_page-section.down.sql
    new file mode 100644
    index 000000000000..f72a21443775
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0043_page-section.down.sql
    @@ -0,0 +1,2 @@
    +DROP TABLE page_sections;
    +DROP TABLE pages;
    diff --git a/ee/tabby-db/migrations/0043_page-section.up.sql b/ee/tabby-db/migrations/0043_page-section.up.sql
    new file mode 100644
    index 000000000000..ebfe9e2f94d8
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0043_page-section.up.sql
    @@ -0,0 +1,32 @@
    +CREATE TABLE pages(
    +    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    +
    +    -- The user who created the page
    +    author_id INTEGER NOT NULL,
    +
    +    title TEXT,
    +    content TEXT,
    +
    +    created_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now')),
    +    updated_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now')),
    +
    +    FOREIGN KEY(author_id) REFERENCES users(id) ON DELETE CASCADE
    +);
    +
    +CREATE TABLE page_sections(
    +    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    +    page_id INTEGER NOT NULL,
    +
    +    title TEXT NOT NULL,
    +    content TEXT,
    +
    +    position INTEGER NOT NULL,
    +
    +    created_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now')),
    +    updated_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now')),
    +
    +    --- Ensure that the position is unique for each page
    +    CONSTRAINT `unique_page_id_position` UNIQUE (page_id, position),
    +
    +    FOREIGN KEY(page_id) REFERENCES pages(id) ON DELETE CASCADE
    +);
    diff --git a/ee/tabby-db/migrations/0044_add-page-sources.down.sql b/ee/tabby-db/migrations/0044_add-page-sources.down.sql
    new file mode 100644
    index 000000000000..d89dba533c3c
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0044_add-page-sources.down.sql
    @@ -0,0 +1,3 @@
    +ALTER TABLE pages DROP COLUMN source_id;
    +
    +ALTER TABLE page_sections DROP COLUMN attachment;
    diff --git a/ee/tabby-db/migrations/0044_add-page-sources.up.sql b/ee/tabby-db/migrations/0044_add-page-sources.up.sql
    new file mode 100644
    index 000000000000..b12052ae6aad
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0044_add-page-sources.up.sql
    @@ -0,0 +1,3 @@
    +ALTER TABLE pages ADD code_source_id VARCHAR(255);
    +
    +ALTER TABLE page_sections ADD attachment BLOB NOT NULL DEFAULT '{}';
    diff --git a/ee/tabby-db/migrations/0045_remove-deprecated-columns.down.sql b/ee/tabby-db/migrations/0045_remove-deprecated-columns.down.sql
    new file mode 100644
    index 000000000000..e69de29bb2d1
    diff --git a/ee/tabby-db/migrations/0045_remove-deprecated-columns.up.sql b/ee/tabby-db/migrations/0045_remove-deprecated-columns.up.sql
    new file mode 100644
    index 000000000000..395fcc386dd2
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0045_remove-deprecated-columns.up.sql
    @@ -0,0 +1,3 @@
    +ALTER TABLE thread_messages DROP COLUMN code_attachments;
    +ALTER TABLE thread_messages DROP COLUMN client_code_attachments;
    +ALTER TABLE thread_messages DROP COLUMN doc_attachments;
    \ No newline at end of file
    diff --git a/ee/tabby-db/migrations/0046_add-disable-password-login.down.sql b/ee/tabby-db/migrations/0046_add-disable-password-login.down.sql
    new file mode 100644
    index 000000000000..42472e7e5b20
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0046_add-disable-password-login.down.sql
    @@ -0,0 +1 @@
    +ALTER TABLE server_setting DROP column security_disable_password_login;
    diff --git a/ee/tabby-db/migrations/0046_add-disable-password-login.up.sql b/ee/tabby-db/migrations/0046_add-disable-password-login.up.sql
    new file mode 100644
    index 000000000000..30921d565cdf
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0046_add-disable-password-login.up.sql
    @@ -0,0 +1 @@
    +ALTER TABLE server_setting ADD column security_disable_password_login BOOLEAN NOT NULL DEFAULT FALSE;
    diff --git a/ee/tabby-db/migrations/0047_add-ingestion.down.sql b/ee/tabby-db/migrations/0047_add-ingestion.down.sql
    new file mode 100644
    index 000000000000..2f12e83744a5
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0047_add-ingestion.down.sql
    @@ -0,0 +1 @@
    +DROP TABLE ingested_documents;
    diff --git a/ee/tabby-db/migrations/0047_add-ingestion.up.sql b/ee/tabby-db/migrations/0047_add-ingestion.up.sql
    new file mode 100644
    index 000000000000..f1d2bcb7095b
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0047_add-ingestion.up.sql
    @@ -0,0 +1,25 @@
    +CREATE TABLE IF NOT EXISTS ingested_documents (
    +    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    +
    +    -- User-provided document source
    +    source TEXT NOT NULL,
    +
    +    -- User-provided document ID, unique within the same source
    +    doc_id TEXT NOT NULL,
    +
    +    link TEXT,
    +    title TEXT NOT NULL,
    +    body TEXT NOT NULL,
    +
    +    -- Track progress of ingestion
    +    status TEXT NOT NULL CHECK (status IN ('pending', 'indexed', 'failed')),
    +
    +    -- Expiration time in Unix timestamp (0 means never expired, should be cleaned by API)
    +    expired_at INTEGER NOT NULL,
    +
    +    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    +    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    +
    +    -- Enforce unique constraint on (source, doc_id) to ensure document IDs are unique within the same source
    +    UNIQUE (source, doc_id)
    +);
    diff --git a/ee/tabby-db/migrations/0048_add-branding-column.down.sql b/ee/tabby-db/migrations/0048_add-branding-column.down.sql
    new file mode 100644
    index 000000000000..7978dca0e0e1
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0048_add-branding-column.down.sql
    @@ -0,0 +1,2 @@
    +ALTER TABLE server_setting DROP COLUMN branding_logo;
    +ALTER TABLE server_setting DROP COLUMN branding_icon;
    diff --git a/ee/tabby-db/migrations/0048_add-branding-column.up.sql b/ee/tabby-db/migrations/0048_add-branding-column.up.sql
    new file mode 100644
    index 000000000000..5fc051b277f8
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0048_add-branding-column.up.sql
    @@ -0,0 +1,2 @@
    +ALTER TABLE server_setting ADD COLUMN branding_logo TEXT DEFAULT NULL;
    +ALTER TABLE server_setting ADD COLUMN branding_icon TEXT DEFAULT NULL;
    diff --git a/ee/tabby-db/migrations/0049_add-oauth-columns.down.sql b/ee/tabby-db/migrations/0049_add-oauth-columns.down.sql
    new file mode 100644
    index 000000000000..49fa8aaabcfd
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0049_add-oauth-columns.down.sql
    @@ -0,0 +1,2 @@
    +ALTER TABLE oauth_credential DROP column config_url;
    +ALTER TABLE oauth_credential DROP column config_scopes;
    diff --git a/ee/tabby-db/migrations/0049_add-oauth-columns.up.sql b/ee/tabby-db/migrations/0049_add-oauth-columns.up.sql
    new file mode 100644
    index 000000000000..3915a6e8938b
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0049_add-oauth-columns.up.sql
    @@ -0,0 +1,2 @@
    +ALTER TABLE oauth_credential ADD column config_url VARCHAR(256);
    +ALTER TABLE oauth_credential ADD column config_scopes VARCHAR(256);
    diff --git a/ee/tabby-db/migrations/0050_add-repository-refs.down.sql b/ee/tabby-db/migrations/0050_add-repository-refs.down.sql
    new file mode 100644
    index 000000000000..651413cfa117
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0050_add-repository-refs.down.sql
    @@ -0,0 +1,2 @@
    +ALTER TABLE repositories DROP COLUMN refs;
    +ALTER TABLE provided_repositories DROP COLUMN refs;
    diff --git a/ee/tabby-db/migrations/0050_add-repository-refs.up.sql b/ee/tabby-db/migrations/0050_add-repository-refs.up.sql
    new file mode 100644
    index 000000000000..93cfe6bbb3df
    --- /dev/null
    +++ b/ee/tabby-db/migrations/0050_add-repository-refs.up.sql
    @@ -0,0 +1,2 @@
    +ALTER TABLE repositories ADD COLUMN refs TEXT;
    +ALTER TABLE provided_repositories ADD COLUMN refs TEXT;
    diff --git a/ee/tabby-db/schema.sqlite b/ee/tabby-db/schema.sqlite
    index 3c4b2b88c86f..782a39a0452d 100644
    Binary files a/ee/tabby-db/schema.sqlite and b/ee/tabby-db/schema.sqlite differ
    diff --git a/ee/tabby-db/schema/schema.sql b/ee/tabby-db/schema/schema.sql
    new file mode 100644
    index 000000000000..65731e5c40e8
    --- /dev/null
    +++ b/ee/tabby-db/schema/schema.sql
    @@ -0,0 +1,306 @@
    +CREATE TABLE _sqlx_migrations(
    +  version BIGINT PRIMARY KEY,
    +  description TEXT NOT NULL,
    +  installed_on TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    +  success BOOLEAN NOT NULL,
    +  checksum BLOB NOT NULL,
    +  execution_time BIGINT NOT NULL
    +);
    +CREATE TABLE registration_token(
    +  id INTEGER PRIMARY KEY AUTOINCREMENT,
    +  token VARCHAR(255) NOT NULL,
    +  created_at TIMESTAMP DEFAULT(DATETIME('now')),
    +  updated_at TIMESTAMP DEFAULT(DATETIME('now')),
    +  CONSTRAINT `idx_token` UNIQUE(`token`)
    +);
    +CREATE TABLE sqlite_sequence(name,seq);
    +CREATE TABLE users(
    +  id INTEGER PRIMARY KEY AUTOINCREMENT,
    +  email VARCHAR(150) NOT NULL COLLATE NOCASE,
    +  is_admin BOOLEAN NOT NULL DEFAULT 0,
    +  created_at TIMESTAMP DEFAULT(DATETIME('now')),
    +  updated_at TIMESTAMP DEFAULT(DATETIME('now')),
    +  auth_token VARCHAR(128) NOT NULL,
    +  active BOOLEAN NOT NULL DEFAULT 1,
    +  password_encrypted VARCHAR(128),
    +  avatar BLOB DEFAULT NULL,
    +  name VARCHAR(255),
    +  CONSTRAINT `idx_email` UNIQUE(`email`)
    +  CONSTRAINT `idx_auth_token` UNIQUE(`auth_token`)
    +);
    +CREATE TABLE invitations(
    +  id INTEGER PRIMARY KEY AUTOINCREMENT,
    +  email VARCHAR(150) NOT NULL COLLATE NOCASE,
    +  code VARCHAR(36) NOT NULL,
    +  created_at TIMESTAMP DEFAULT(DATETIME('now')),
    +  CONSTRAINT `idx_email` UNIQUE(`email`)
    +  CONSTRAINT `idx_code` UNIQUE(`code`)
    +);
    +CREATE TABLE job_runs(
    +  id INTEGER PRIMARY KEY AUTOINCREMENT,
    +  job VARCHAR(255) NOT NULL,
    +  start_ts TIMESTAMP NOT NULL,
    +  end_ts TIMESTAMP,
    +  exit_code INTEGER,
    +  stdout TEXT NOT NULL,
    +  stderr TEXT NOT NULL,
    +  created_at TIMESTAMP DEFAULT(DATETIME('now')),
    +  updated_at TIMESTAMP DEFAULT(DATETIME('now'))
    +  ,
    +  command TEXT,
    +  started_at TIMESTAMP
    +);
    +CREATE TABLE repositories(
    +  id INTEGER PRIMARY KEY AUTOINCREMENT,
    +  name VARCHAR(255) NOT NULL,
    +  git_url VARCHAR(255) NOT NULL,
    +  refs TEXT,
    +  CONSTRAINT `idx_name` UNIQUE(`name`)
    +  CONSTRAINT `idx_git_url` UNIQUE(`git_url`)
    +);
    +CREATE TABLE server_setting(
    +  id INTEGER PRIMARY KEY AUTOINCREMENT,
    +  security_allowed_register_domain_list STRING,
    +  security_disable_client_side_telemetry BOOLEAN NOT NULL DEFAULT FALSE,
    +  network_external_url STRING NOT NULL DEFAULT 'http://localhost:8080'
    +  ,
    +  billing_enterprise_license STRING,
    +  security_disable_password_login BOOLEAN NOT NULL DEFAULT FALSE,
    +  branding_logo TEXT DEFAULT NULL,
    +  branding_icon TEXT DEFAULT NULL
    +);
    +CREATE TABLE email_setting(
    +  id INTEGER PRIMARY KEY AUTOINCREMENT,
    +  smtp_username VARCHAR(255) NOT NULL,
    +  smtp_password VARCHAR(255) NOT NULL,
    +  smtp_server VARCHAR(255) NOT NULL,
    +  from_address VARCHAR(255) NOT NULL,
    +  encryption VARCHAR(255) NOT NULL DEFAULT 'ssltls',
    +  auth_method VARCHAR(255) NOT NULL DEFAULT 'plain'
    +  ,
    +  smtp_port INTEGER NOT NULL DEFAULT 25
    +);
    +CREATE TABLE oauth_credential(
    +  id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
    +  provider TEXT NOT NULL,
    +  client_id VARCHAR(256) NOT NULL,
    +  client_secret VARCHAR(64) NOT NULL,
    +  created_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +  updated_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +  config_url VARCHAR(256),
    +  config_scopes VARCHAR(256),
    +  CONSTRAINT `idx_provider` UNIQUE(`provider`)
    +);
    +CREATE TABLE user_completions(
    +  id INTEGER PRIMARY KEY AUTOINCREMENT,
    +  user_id INTEGER INTEGER NOT NULL,
    +  completion_id VARCHAR(255) NOT NULL,
    +  language VARCHAR(255) NOT NULL,
    +  views INTEGER NOT NULL DEFAULT 0,
    +  selects INTEGER NOT NULL DEFAULT 0,
    +  dismisses INTEGER NOT NULL DEFAULT 0,
    +  created_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +  updated_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +  FOREIGN KEY(user_id) REFERENCES users(id)
    +);
    +CREATE INDEX user_completions_completion_id_idx ON user_completions(
    +  completion_id
    +);
    +CREATE INDEX idx_job_created_at ON job_runs(job, created_at);
    +CREATE INDEX idx_repository_name ON repositories(name);
    +CREATE INDEX idx_user_completion_user_id_created_at_language ON user_completions(
    +  user_id,
    +  created_at,
    +  language
    +);
    +CREATE TABLE user_events(
    +  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    +  user_id INTEGER NOT NULL,
    +  kind TEXT NOT NULL,
    +  created_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +  payload BLOB NOT NULL,
    +  FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
    +);
    +CREATE INDEX idx_user_events_user_id ON user_events(user_id);
    +CREATE INDEX idx_user_events_created_at ON user_events(created_at);
    +CREATE TABLE refresh_tokens(
    +  id INTEGER PRIMARY KEY AUTOINCREMENT,
    +  user_id INTEGER NOT NULL,
    +  token VARCHAR(255) NOT NULL COLLATE NOCASE,
    +  expires_at TIMESTAMP NOT NULL,
    +  created_at TIMESTAMP DEFAULT(DATETIME('now')),
    +  CONSTRAINT `idx_token` UNIQUE(`token`)
    +  FOREIGN KEY(user_id) REFERENCES users(id)
    +);
    +CREATE TABLE password_reset(
    +  id INTEGER PRIMARY KEY AUTOINCREMENT,
    +  user_id INTEGER NOT NULL UNIQUE,
    +  code VARCHAR(36) NOT NULL,
    +  created_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +  FOREIGN KEY(user_id) REFERENCES users(id)
    +);
    +CREATE TABLE integrations(
    +  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    +  kind TEXT NOT NULL,
    +  display_name TEXT NOT NULL,
    +  access_token TEXT NOT NULL,
    +  api_base TEXT,
    +  error TEXT,
    +  created_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +  updated_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +  synced BOOLEAN NOT NULL DEFAULT FALSE
    +);
    +CREATE TABLE provided_repositories(
    +  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    +  integration_id INTEGER NOT NULL,
    +  vendor_id TEXT NOT NULL,
    +  name TEXT NOT NULL,
    +  git_url TEXT NOT NULL,
    +  active BOOLEAN NOT NULL DEFAULT FALSE,
    +  created_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +  updated_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +  refs TEXT,
    +  FOREIGN KEY(integration_id) REFERENCES integrations(id) ON DELETE CASCADE,
    +  CONSTRAINT idx_unique_integration_id_vendor_id UNIQUE(integration_id, vendor_id)
    +);
    +CREATE INDEX `idx_job_runs_command` ON job_runs(command);
    +CREATE TABLE threads(
    +  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    +  -- Whether the thread is ephemeral(e.g from chat sidebar)
    +  is_ephemeral BOOLEAN NOT NULL,
    +  -- The user who created the thread
    +  user_id INTEGER NOT NULL,
    +  created_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +  updated_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +  -- Array of relevant questions, in format of `String`
    +  relevant_questions BLOB,
    +  FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
    +);
    +CREATE TABLE thread_messages(
    +  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    +  thread_id INTEGER NOT NULL,
    +  role TEXT NOT NULL,
    +  content TEXT NOT NULL,
    +  -- Array of code attachments, in format of `ThreadMessageAttachmentCode`
    +  created_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +  updated_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +  code_source_id VARCHAR(255),
    +  attachment BLOB NOT NULL DEFAULT '{}',
    +  FOREIGN KEY(thread_id) REFERENCES threads(id) ON DELETE CASCADE
    +);
    +CREATE TABLE web_documents(
    +  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    +  name VARCHAR(255) NOT NULL,
    +  url TEXT NOT NULL,
    +  is_preset BOOLEAN NOT NULL DEFAULT FALSE,
    +  created_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +  updated_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +  CONSTRAINT idx_name UNIQUE(name),
    +  CONSTRAINT idx_url UNIQUE(url)
    +);
    +CREATE TABLE user_groups(
    +  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    +  name VARCHAR(255) NOT NULL,
    +  created_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +  updated_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +  CONSTRAINT idx_unique_name UNIQUE(name)
    +);
    +CREATE TABLE user_group_memberships(
    +  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    +  user_id INTEGER NOT NULL,
    +  user_group_id INTEGER NOT NULL,
    +  is_group_admin BOOLEAN NOT NULL DEFAULT FALSE,
    +  created_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +  updated_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +  FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
    +  FOREIGN KEY(user_group_id) REFERENCES user_groups(id) ON DELETE CASCADE,
    +  CONSTRAINT idx_unique_user_id_user_group_id UNIQUE(user_id, user_group_id)
    +);
    +CREATE TABLE source_id_read_access_policies(
    +  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    +  -- source_id doesn't come with DB constraint, need individual garbage collection job
    +source_id VARCHAR(255) NOT NULL,
    +user_group_id INTEGER NOT NULL,
    +created_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +updated_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +FOREIGN KEY(user_group_id) REFERENCES user_groups(id) ON DELETE CASCADE,
    +-- access_policy is unique per source_id and user_group_id
    +  CONSTRAINT idx_unique_source_id_user_group_id UNIQUE(source_id, user_group_id)
    +);
    +CREATE TABLE notifications(
    +  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    +  created_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +  updated_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +  -- enum of admin, all_user
    +  recipient VARCHAR(255) NOT NULL DEFAULT 'admin',
    +  -- content of notification, in markdown format.
    +  content TEXT NOT NULL
    +);
    +CREATE TABLE read_notifications(
    +  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    +  user_id INTEGER NOT NULL,
    +  notification_id INTEGER NOT NULL,
    +  created_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +  updated_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +  CONSTRAINT idx_unique_user_id_notification_id UNIQUE(user_id, notification_id),
    +  FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
    +  FOREIGN KEY(notification_id) REFERENCES notifications(id) ON DELETE CASCADE
    +);
    +CREATE TABLE ldap_credential(
    +  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    +  host STRING NOT NULL,
    +  port INTEGER NOT NULL DEFAULT 389,
    +  bind_dn STRING NOT NULL,
    +  bind_password STRING NOT NULL,
    +  base_dn STRING NOT NULL,
    +  user_filter STRING NOT NULL,
    +  -- enum of none, starttls, ldaps
    +  encryption STRING NOT NULL DEFAULT 'none',
    +  skip_tls_verify BOOLEAN NOT NULL DEFAULT FALSE,
    +  --- the attribute to be used as the Tabby user email address
    +  email_attribute STRING NOT NULL DEFAULT 'email',
    +  --- the attribute to be used as the Tabby user name
    +  name_attribute STRING,
    +  created_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')),
    +  updated_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now'))
    +);
    +CREATE TABLE IF NOT EXISTS "pages"(
    +  id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
    +  author_id integer NOT NULL,
    +  title text,
    +  content text,
    +  created_at timestamp NOT NULL DEFAULT(DATETIME('now')),
    +  updated_at timestamp NOT NULL DEFAULT(DATETIME('now')),
    +  code_source_id VARCHAR(255),
    +  FOREIGN KEY(author_id) REFERENCES "users"(id) ON DELETE CASCADE
    +);
    +CREATE TABLE IF NOT EXISTS "page_sections"(
    +  id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
    +  page_id integer NOT NULL,
    +  title text NOT NULL,
    +  content text,
    +  "position" integer NOT NULL,
    +  created_at timestamp NOT NULL DEFAULT(DATETIME('now')),
    +  updated_at timestamp NOT NULL DEFAULT(DATETIME('now')),
    +  attachment BLOB NOT NULL DEFAULT '{}',
    +  FOREIGN KEY(page_id) REFERENCES "pages"(id) ON DELETE CASCADE,
    +  UNIQUE(page_id, "position")
    +);
    +CREATE TABLE ingested_documents(
    +  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    +  -- User-provided document source
    +  source TEXT NOT NULL,
    +  -- User-provided document ID, unique within the same source
    +  doc_id TEXT NOT NULL,
    +  link TEXT,
    +  title TEXT NOT NULL,
    +  body TEXT NOT NULL,
    +  -- Track progress of ingestion
    +  status TEXT NOT NULL CHECK(status IN('pending', 'indexed', 'failed')),
    +  -- Expiration time in Unix timestamp(0 means never expired, should be cleaned by API)
    +  expired_at INTEGER NOT NULL,
    +  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    +  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    +  -- Enforce unique constraint on(source, doc_id) to ensure document IDs are unique within the same source
    +  UNIQUE(source, doc_id)
    +);
    diff --git a/ee/tabby-db/schema/schema.svg b/ee/tabby-db/schema/schema.svg
    new file mode 100644
    index 000000000000..5460aabd8fd2
    --- /dev/null
    +++ b/ee/tabby-db/schema/schema.svg
    @@ -0,0 +1,1007 @@
    +
    +
    +
    +
    +
    +
    +structs
    +
    +
    +
    +_sqlx_migrations
    +
    +_sqlx_migrations
    +
    +🔑
    +
    +version
    +
    + 
    +
    +description
    +
    + 
    +
    +installed_on
    +
    + 
    +
    +success
    +
    + 
    +
    +checksum
    +
    + 
    +
    +execution_time
    +
    +
    +
    +email_setting
    +
    +email_setting
    +
    +🔑
    +
    +id
    +
    + 
    +
    +smtp_username
    +
    + 
    +
    +smtp_password
    +
    + 
    +
    +smtp_server
    +
    + 
    +
    +from_address
    +
    + 
    +
    +encryption
    +
    + 
    +
    +auth_method
    +
    + 
    +
    +smtp_port
    +
    +
    +
    +ingested_documents
    +
    +ingested_documents
    +
    +🔑
    +
    +id
    +
    + 
    +
    +source
    +
    + 
    +
    +doc_id
    +
    + 
    +
    +link
    +
    + 
    +
    +title
    +
    + 
    +
    +body
    +
    + 
    +
    +status
    +
    + 
    +
    +expired_at
    +
    + 
    +
    +created_at
    +
    + 
    +
    +updated_at
    +
    +
    +
    +integrations
    +
    +integrations
    +
    +🔑
    +
    +id
    +
    + 
    +
    +kind
    +
    + 
    +
    +display_name
    +
    + 
    +
    +access_token
    +
    + 
    +
    +api_base
    +
    + 
    +
    +error
    +
    + 
    +
    +created_at
    +
    + 
    +
    +updated_at
    +
    + 
    +
    +synced
    +
    +
    +
    +invitations
    +
    +invitations
    +
    +🔑
    +
    +id
    +
    + 
    +
    +email
    +
    + 
    +
    +code
    +
    + 
    +
    +created_at
    +
    +
    +
    +job_runs
    +
    +job_runs
    +
    +🔑
    +
    +id
    +
    + 
    +
    +job
    +
    + 
    +
    +start_ts
    +
    + 
    +
    +end_ts
    +
    + 
    +
    +exit_code
    +
    + 
    +
    +stdout
    +
    + 
    +
    +stderr
    +
    + 
    +
    +created_at
    +
    + 
    +
    +updated_at
    +
    + 
    +
    +command
    +
    + 
    +
    +started_at
    +
    +
    +
    +ldap_credential
    +
    +ldap_credential
    +
    +🔑
    +
    +id
    +
    + 
    +
    +host
    +
    + 
    +
    +port
    +
    + 
    +
    +bind_dn
    +
    + 
    +
    +bind_password
    +
    + 
    +
    +base_dn
    +
    + 
    +
    +user_filter
    +
    + 
    +
    +encryption
    +
    + 
    +
    +skip_tls_verify
    +
    + 
    +
    +email_attribute
    +
    + 
    +
    +name_attribute
    +
    + 
    +
    +created_at
    +
    + 
    +
    +updated_at
    +
    +
    +
    +notifications
    +
    +notifications
    +
    +🔑
    +
    +id
    +
    + 
    +
    +created_at
    +
    + 
    +
    +updated_at
    +
    + 
    +
    +recipient
    +
    + 
    +
    +content
    +
    +
    +
    +oauth_credential
    +
    +oauth_credential
    +
    +🔑
    +
    +id
    +
    + 
    +
    +provider
    +
    + 
    +
    +client_id
    +
    + 
    +
    +client_secret
    +
    + 
    +
    +created_at
    +
    + 
    +
    +updated_at
    +
    + 
    +
    +config_url
    +
    + 
    +
    +config_scopes
    +
    +
    +
    +page_sections
    +
    +page_sections
    +
    +🔑
    +
    +id
    +
    + 
    +
    +page_id
    +
    + 
    +
    +title
    +
    + 
    +
    +content
    +
    + 
    +
    +position
    +
    + 
    +
    +created_at
    +
    + 
    +
    +updated_at
    +
    + 
    +
    +attachment
    +
    +
    +
    +pages
    +
    +pages
    +
    +🔑
    +
    +id
    +
    + 
    +
    +author_id
    +
    + 
    +
    +title
    +
    + 
    +
    +content
    +
    + 
    +
    +created_at
    +
    + 
    +
    +updated_at
    +
    + 
    +
    +code_source_id
    +
    +
    +
    +page_sections:e->pages:w
    +
    +
    +
    +
    +
    +users
    +
    +users
    +
    +🔑
    +
    +id
    +
    + 
    +
    +email
    +
    + 
    +
    +is_admin
    +
    + 
    +
    +created_at
    +
    + 
    +
    +updated_at
    +
    + 
    +
    +auth_token
    +
    + 
    +
    +active
    +
    + 
    +
    +password_encrypted
    +
    + 
    +
    +avatar
    +
    + 
    +
    +name
    +
    +
    +
    +pages:e->users:w
    +
    +
    +
    +
    +
    +password_reset
    +
    +password_reset
    +
    +🔑
    +
    +id
    +
    + 
    +
    +user_id
    +
    + 
    +
    +code
    +
    + 
    +
    +created_at
    +
    +
    +
    +password_reset:e->users:w
    +
    +
    +
    +
    +
    +provided_repositories
    +
    +provided_repositories
    +
    +🔑
    +
    +id
    +
    + 
    +
    +integration_id
    +
    + 
    +
    +vendor_id
    +
    + 
    +
    +name
    +
    + 
    +
    +git_url
    +
    + 
    +
    +active
    +
    + 
    +
    +created_at
    +
    + 
    +
    +updated_at
    +
    + 
    +
    +refs
    +
    +
    +
    +provided_repositories:e->integrations:w
    +
    +
    +
    +
    +
    +read_notifications
    +
    +read_notifications
    +
    +🔑
    +
    +id
    +
    + 
    +
    +user_id
    +
    + 
    +
    +notification_id
    +
    + 
    +
    +created_at
    +
    + 
    +
    +updated_at
    +
    +
    +
    +read_notifications:e->notifications:w
    +
    +
    +
    +
    +
    +read_notifications:e->users:w
    +
    +
    +
    +
    +
    +refresh_tokens
    +
    +refresh_tokens
    +
    +🔑
    +
    +id
    +
    + 
    +
    +user_id
    +
    + 
    +
    +token
    +
    + 
    +
    +expires_at
    +
    + 
    +
    +created_at
    +
    +
    +
    +refresh_tokens:e->users:w
    +
    +
    +
    +
    +
    +registration_token
    +
    +registration_token
    +
    +🔑
    +
    +id
    +
    + 
    +
    +token
    +
    + 
    +
    +created_at
    +
    + 
    +
    +updated_at
    +
    +
    +
    +repositories
    +
    +repositories
    +
    +🔑
    +
    +id
    +
    + 
    +
    +name
    +
    + 
    +
    +git_url
    +
    + 
    +
    +refs
    +
    +
    +
    +server_setting
    +
    +server_setting
    +
    +🔑
    +
    +id
    +
    + 
    +
    +security_allowed_register_domain_list
    +
    + 
    +
    +security_disable_client_side_telemetry
    +
    + 
    +
    +network_external_url
    +
    + 
    +
    +billing_enterprise_license
    +
    + 
    +
    +security_disable_password_login
    +
    + 
    +
    +branding_logo
    +
    + 
    +
    +branding_icon
    +
    +
    +
    +source_id_read_access_policies
    +
    +source_id_read_access_policies
    +
    +🔑
    +
    +id
    +
    + 
    +
    +source_id
    +
    + 
    +
    +user_group_id
    +
    + 
    +
    +created_at
    +
    + 
    +
    +updated_at
    +
    +
    +
    +user_groups
    +
    +user_groups
    +
    +🔑
    +
    +id
    +
    + 
    +
    +name
    +
    + 
    +
    +created_at
    +
    + 
    +
    +updated_at
    +
    +
    +
    +source_id_read_access_policies:e->user_groups:w
    +
    +
    +
    +
    +
    +thread_messages
    +
    +thread_messages
    +
    +🔑
    +
    +id
    +
    + 
    +
    +thread_id
    +
    + 
    +
    +role
    +
    + 
    +
    +content
    +
    + 
    +
    +created_at
    +
    + 
    +
    +updated_at
    +
    + 
    +
    +code_source_id
    +
    + 
    +
    +attachment
    +
    +
    +
    +threads
    +
    +threads
    +
    +🔑
    +
    +id
    +
    + 
    +
    +is_ephemeral
    +
    + 
    +
    +user_id
    +
    + 
    +
    +created_at
    +
    + 
    +
    +updated_at
    +
    + 
    +
    +relevant_questions
    +
    +
    +
    +thread_messages:e->threads:w
    +
    +
    +
    +
    +
    +threads:e->users:w
    +
    +
    +
    +
    +
    +user_completions
    +
    +user_completions
    +
    +🔑
    +
    +id
    +
    + 
    +
    +user_id
    +
    + 
    +
    +completion_id
    +
    + 
    +
    +language
    +
    + 
    +
    +views
    +
    + 
    +
    +selects
    +
    + 
    +
    +dismisses
    +
    + 
    +
    +created_at
    +
    + 
    +
    +updated_at
    +
    +
    +
    +user_completions:e->users:w
    +
    +
    +
    +
    +
    +user_events
    +
    +user_events
    +
    +🔑
    +
    +id
    +
    + 
    +
    +user_id
    +
    + 
    +
    +kind
    +
    + 
    +
    +created_at
    +
    + 
    +
    +payload
    +
    +
    +
    +user_events:e->users:w
    +
    +
    +
    +
    +
    +user_group_memberships
    +
    +user_group_memberships
    +
    +🔑
    +
    +id
    +
    + 
    +
    +user_id
    +
    + 
    +
    +user_group_id
    +
    + 
    +
    +is_group_admin
    +
    + 
    +
    +created_at
    +
    + 
    +
    +updated_at
    +
    +
    +
    +user_group_memberships:e->user_groups:w
    +
    +
    +
    +
    +
    +user_group_memberships:e->users:w
    +
    +
    +
    +
    +
    +web_documents
    +
    +web_documents
    +
    +🔑
    +
    +id
    +
    + 
    +
    +name
    +
    + 
    +
    +url
    +
    + 
    +
    +is_preset
    +
    + 
    +
    +created_at
    +
    + 
    +
    +updated_at
    +
    +
    +
    diff --git a/ee/tabby-db/schema/sqlite-schema-visualize.sql b/ee/tabby-db/schema/sqlite-schema-visualize.sql
    new file mode 100644
    index 000000000000..28fba17cbd71
    --- /dev/null
    +++ b/ee/tabby-db/schema/sqlite-schema-visualize.sql
    @@ -0,0 +1,172 @@
    +-- We start a GraphViz graph
    +SELECT '
    +digraph structs {
    +'
    +UNION ALL
    +
    +-- Normally, GraphViz' "dot" command lays out a hierarchical graph from
    +-- top to bottom.  However, we aren't just laying out individual nodes,
    +-- each node is a vertical list of database fields.  To prevent GraphViz
    +-- from snaking arrows all over the place, we constrain it to draw
    +-- incoming references on the left of each field, and outgoing references
    +-- on the right.  Since that's the way references flow for each database
    +-- table, we tell GraphViz to lay the whole graph out left-to-right,
    +-- which makes its job much easier and produces prettier output.
    +SELECT '
    +rankdir="LR"
    +'
    +UNION ALL
    +
    +
    +-- By default, nodes have circles around them.  We will draw our own
    +-- tables below, we do not want the circles.
    +SELECT '
    +node [shape=none]
    +'
    +UNION ALL
    +
    +-- This is the big query that renders a node complete with field names
    +-- for each table in the database.  Because we want raw GraphViz output,
    +-- our query returns rows with a single string field, whose value is a
    +-- complex calculation using SQL as a templating engine.  This is kind
    +-- of an abuse, but works nicely nevertheless.
    +SELECT
    +    CASE
    +        -- When the previous row's table name is the same as this one,
    +        -- do nothing.
    +        WHEN LAG(t.name, 1) OVER (ORDER BY t.name) = t.name THEN ''
    +
    +        -- Otherwise, this is the first row of a new table, so start
    +        -- the node markup and add a header row.  Normally in GraphViz,
    +        -- the table name would *be* the label of the node, but since
    +        -- we're using the label to represent the entire node, we have
    +        -- to make our own header.
    +        --
    +        -- GraphViz does have a "record" label shape, but it seems tricky
    +        -- to work with and I found the HTML-style label markup easier
    +        -- to get working the way I wanted.
    +        ELSE
    +            t.name || ' [label=<
    +            
    +                
    +                    
    +                
    +            '
    +
    +    -- After the header (if needed), we have rows for each field in
    +    -- the table.
    +    --
    +    -- The "pk" metadata field is zero for table fields that are not part
    +    -- of the primary key.  If the "pk" metadata field is 1 or more, that
    +    -- tells you that table field's order in the (potentially composite)
    +    -- primary key.
    +    --
    +    -- We also add ports to each of the table cells, so that we can
    +    -- later tell GraphViz to specifically connect the ports representing
    +    -- specific fields in each table, instead of connecting the tables
    +    -- generally.
    +    END || '
    +                
    +                    
    +                    
    +                
    +            ' ||
    +    CASE
    +        -- When the next row's table name is the same as this one,
    +        -- do nothing.
    +        WHEN LEAD(t.name, 1) OVER (ORDER BY t.name) = t.name THEN ''
    +
    +        -- Otherwise, this is the last row of a database table, so end
    +        -- the table markup.
    +        ELSE '
    +            
    ' || t.name || '
    ' || + CASE i.pk WHEN 0 THEN ' ' ELSE '🔑' END || + '' || i.name || '
    + >]; + ' + END + +-- This is how you get nice relational data out of SQLite's metadata +-- pragmas. +FROM pragma_table_list() AS t + JOIN pragma_table_info(t.name, t.schema) AS i + +WHERE + -- SQLite has a bunch of metadata tables in each schema, which + -- are hidden from .tables and .schema but which are reported + -- in pragma_table_list(). They're not user-created and almost + -- certainly user databases don't have foreign keys to them, so + -- let's just filter them out. + t.name NOT LIKE 'sqlite_%' + + -- Despite its name, pragma_table_list() also includes views. + -- Since those don't store any information or have any correctness + -- constraints, they're just distracting if you're trying to quickly + -- understand a database's schema, so we'll filter them out too. + AND t.type = 'table' +UNION ALL + +-- Now we have all the database tables set up, we can draw the links +-- between them. SQLite gives us the pragma_foreign_key_list() function +-- which (for a given source table) lists all the source fields that are +-- part of a foreign key reference, the target table they refer to, and +-- (if it was created with "REFERENCES table_name(column_name)" syntax, +-- the target column names too. Unfortunately, if the reference was +-- created with "REFERENCES table_name" syntax, the pragma does *not* +-- figure out what the corresponding target fields are, so we'll also need +-- pragma_table_info() to look up the primary key(s) for the target table. +-- +-- Once we have everything we need, we just do a bit more string +-- concatenation to build up the GraphViz syntax equivalent. +-- +-- Note that we use the ports we defined above, as well as the directional +-- overrides :e and :w, to force GraphViz to give us a layout that's +-- likely to be readable. +SELECT + + -- We left-join every foreign key field against pragma_table_info + -- looking for primary keys, and the target table may have a composite + -- primary key even if the foreign key does not reference the primary + -- key, so we may wind up with multiple results describing the same + -- foreign key reference. DISTINCT makes sure we only describe each + -- reference once. + DISTINCT + + t.name || ':' || f."from" || '_from:e -> ' || + + -- If the constraint was created with "REFERENCES + -- table_name(column_name)", then f.to will contain 'column_name'. + -- Otherwise, f.to is NULL, and we need to grab the corresponding + -- field from the primary key in i.name. + f."table" || ':' || COALESCE(f."to", i.name) || '_to:w' + +FROM pragma_table_list() AS t + JOIN pragma_foreign_key_list(t.name, t.schema) AS f + + -- We look up all the fields in the target table, just in case + -- pragma_foreign_key_list() doesn't tell us what the target field + -- name is. SQLite doesn't allow foreign-key references to cross + -- schemas, so it's OK to use the source table's schema name to look + -- up the target table. + -- + -- Strictly speaking, we shouldn't need to LEFT JOIN here, a basic + -- JOIN should do. This works around a bug in SQLite 3.16.0 to + -- version 3.45.1: https://sqlite.org/forum/forumpost/b1656fcb39 + LEFT JOIN pragma_table_info(f."table", t.schema) AS i + +-- f.seq represents the order of fields in a source table's composite foreign key +-- reference, starting at 0. In "FOREIGN KEY (a, b)", "a" would have +-- seq=0 and "b" would have seq=1. i.pk represents the order of fields +-- in a primary key, where "0" means "not part of the primary key". +-- In "PRIMARY KEY (a, b)", "a" would have pk=1 and "b" would have pk=2. +-- For a foreign key reference that specifies the target field name, +-- none of this matters, but if the target field name is missing, then +-- this makes sure that each field of the foreign key reference is joined +-- with the corresponding primary key field of the target table. +WHERE f.seq + 1 = i.pk + +UNION ALL + +-- Lastly, we close the GraphViz graph. +SELECT ' +}'; diff --git a/ee/tabby-db/src/access_policy.rs b/ee/tabby-db/src/access_policy.rs new file mode 100644 index 000000000000..8b61f941cb3a --- /dev/null +++ b/ee/tabby-db/src/access_policy.rs @@ -0,0 +1,136 @@ +use chrono::{DateTime, Utc}; +use sqlx::query; + +use crate::{DbConn, UserGroupDAO}; + +impl DbConn { + pub async fn allow_read_source(&self, user_id: i64, source_id: &str) -> anyhow::Result { + let is_public_source = query!( + "select id from source_id_read_access_policies where source_id = ?", + source_id + ) + .fetch_optional(&self.pool) + .await? + .is_none(); + + if is_public_source { + return Ok(true); + } + + let row = query!( + r#" +SELECT user_id +FROM user_groups + INNER JOIN user_group_memberships ON user_group_memberships.user_group_id = user_groups.id + INNER JOIN source_id_read_access_policies ON source_id_read_access_policies.user_group_id = user_groups.id +WHERE user_group_memberships.user_id = ? AND source_id = ? + "#, + user_id, + source_id, + ) + .fetch_optional(&self.pool) + .await?; + + Ok(row.is_some()) + } + + pub async fn upsert_source_id_read_access_policy( + &self, + source_id: &str, + user_group_id: i64, + ) -> anyhow::Result<()> { + query!( + r#" +INSERT INTO source_id_read_access_policies (source_id, user_group_id) VALUES (?, ?) +ON CONFLICT (source_id, user_group_id) DO UPDATE + SET updated_at = DATETIME("now") + "#, + source_id, + user_group_id + ) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn delete_source_id_read_access_policy( + &self, + source_id: &str, + user_group_id: i64, + ) -> anyhow::Result<()> { + let rows_deleted = query!( + r#" +DELETE FROM source_id_read_access_policies WHERE source_id = ? AND user_group_id = ? + "#, + source_id, + user_group_id + ) + .execute(&self.pool) + .await? + .rows_affected(); + if rows_deleted == 1 { + Ok(()) + } else { + Err(anyhow::anyhow!( + "source_id_read_access_policy doesn't exist", + )) + } + } + + pub async fn delete_unused_source_id_read_access_policy( + &self, + active_source_ids: &[String], + ) -> anyhow::Result { + let in_clause = active_source_ids + .iter() + .map(|s| format!("'{s}'")) + .collect::>() + .join(","); + + let rows_deleted = sqlx::query(&format!( + "DELETE FROM source_id_read_access_policies WHERE source_id NOT IN ({in_clause})" + )) + .execute(&self.pool) + .await? + .rows_affected(); + + Ok(rows_deleted as usize) + } + + pub async fn list_source_id_read_access_user_groups( + &self, + source_id: &str, + ) -> anyhow::Result> { + let user_groups = sqlx::query_as!( + UserGroupDAO, + r#"SELECT + user_groups.id as "id", + name, + user_groups.created_at as "created_at: DateTime", + user_groups.updated_at as "updated_at: DateTime" + FROM source_id_read_access_policies INNER JOIN user_groups ON (source_id_read_access_policies.user_group_id = user_groups.id) + WHERE source_id = ? + "#, + source_id + ) + .fetch_all(&self.pool) + .await?; + Ok(user_groups) + } +} + +#[cfg(test)] +mod tests { + use crate::DbConn; + + #[tokio::test] + async fn test_delete_unused_source_id_read_access_policy() { + let db = DbConn::new_in_memory().await.unwrap(); + let rows_deleted = db + .delete_unused_source_id_read_access_policy(&["test1".into()]) + .await + .unwrap(); + assert_eq!(rows_deleted, 0); + } +} diff --git a/ee/tabby-db/src/attachment.rs b/ee/tabby-db/src/attachment.rs new file mode 100644 index 000000000000..a0d108093858 --- /dev/null +++ b/ee/tabby-db/src/attachment.rs @@ -0,0 +1,100 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct Attachment { + pub code: Option>, + pub client_code: Option>, + pub doc: Option>, + pub code_file_list: Option, +} + +#[derive(Serialize, Deserialize)] +pub struct AttachmentCodeFileList { + pub file_list: Vec, + + #[serde(default)] + pub truncated: bool, +} + +/// AttachmentDoc represents a union of various document types. +/// For backward compatibility, it is stored untagged in the database. +/// Ensure that new document types have unique field names to avoid deserialization issues. +#[derive(Serialize, Deserialize)] +#[serde(untagged)] // Mark the serde serialization format as untagged for backward compatibility: https://serde.rs/enum-representations.html#untagged +pub enum AttachmentDoc { + Web(AttachmentWebDoc), + Issue(AttachmentIssueDoc), + Pull(AttachmentPullDoc), + Commit(AttachmentCommitDoc), + Page(AttachmentPageDoc), + Ingested(AttachmentIngestedDoc), +} + +#[derive(Serialize, Deserialize)] +pub struct AttachmentWebDoc { + pub title: String, + pub link: String, + pub content: String, +} + +#[derive(Serialize, Deserialize)] +pub struct AttachmentIssueDoc { + pub title: String, + pub link: String, + pub author_user_id: Option, + pub body: String, + pub closed: bool, +} + +#[derive(Serialize, Deserialize)] +pub struct AttachmentPullDoc { + pub title: String, + pub link: String, + pub author_user_id: Option, + pub body: String, + pub diff: String, + pub merged: bool, +} + +#[derive(Serialize, Deserialize)] +pub struct AttachmentCommitDoc { + pub sha: String, + pub message: String, + pub author_user_id: Option, + pub author_at: DateTime, +} + +#[derive(Serialize, Deserialize)] +pub struct AttachmentPageDoc { + pub page_link: String, + pub title: String, + pub content: String, +} + +#[derive(Serialize, Deserialize)] +pub struct AttachmentIngestedDoc { + pub id: String, + pub title: String, + pub body: String, + pub link: Option, +} + +#[derive(Serialize, Deserialize)] +pub struct AttachmentCode { + pub git_url: String, + pub commit: Option, + pub language: String, + pub filepath: String, + pub content: String, + + /// When start line is `None`, it represents the entire file. + pub start_line: Option, +} + +#[derive(Serialize, Deserialize)] +pub struct AttachmentClientCode { + pub filepath: Option, + pub start_line: Option, + pub content: String, +} diff --git a/ee/tabby-db/src/cache.rs b/ee/tabby-db/src/cache.rs new file mode 100644 index 000000000000..aa2a23d90db8 --- /dev/null +++ b/ee/tabby-db/src/cache.rs @@ -0,0 +1,37 @@ +use std::future::Future; + +use tokio::sync::RwLock; + +#[derive(Default)] +pub struct Cache { + value: RwLock>, +} + +impl Cache { + pub async fn new() -> Self { + Cache { + value: Default::default(), + } + } + + pub async fn invalidate(&self) { + *self.value.write().await = None; + } + + pub async fn get_or_refresh(&self, refresh: impl Fn() -> F) -> Result + where + T: Clone, + F: Future>, + { + let value = self.value.read().await; + if let Some(value) = &*value { + Ok(value.clone()) + } else { + drop(value); + let mut value = self.value.write().await; + let generated = refresh().await?; + *value = Some(generated.clone()); + Ok(generated) + } + } +} diff --git a/ee/tabby-db/src/conversions.rs b/ee/tabby-db/src/conversions.rs new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/ee/tabby-db/src/conversions.rs @@ -0,0 +1 @@ + diff --git a/ee/tabby-db/src/email_setting.rs b/ee/tabby-db/src/email_setting.rs index a763e3291455..4359fec6a3fa 100644 --- a/ee/tabby-db/src/email_setting.rs +++ b/ee/tabby-db/src/email_setting.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{anyhow, Result}; use sqlx::{query, query_as, query_scalar}; use crate::DbConn; @@ -10,13 +10,17 @@ pub struct EmailSettingDAO { pub smtp_username: String, pub smtp_password: String, pub smtp_server: String, + pub smtp_port: i64, + pub from_address: String, + pub encryption: String, + pub auth_method: String, } impl DbConn { pub async fn read_email_setting(&self) -> Result> { let setting = query_as!( EmailSettingDAO, - "SELECT smtp_username, smtp_password, smtp_server FROM email_setting WHERE id=?", + "SELECT smtp_username, smtp_password, smtp_server, smtp_port, from_address, encryption, auth_method FROM email_setting WHERE id=?", EMAIL_CREDENTIAL_ROW_ID ) .fetch_optional(&self.pool) @@ -29,25 +33,33 @@ impl DbConn { smtp_username: String, smtp_password: Option, smtp_server: String, + smtp_port: i32, + from_address: String, + encryption: String, + auth_method: String, ) -> Result<()> { let mut transaction = self.pool.begin().await?; let smtp_password = match smtp_password { Some(pass) => pass, - None => { - query_scalar!( - "SELECT smtp_password FROM email_setting WHERE id = ?", - EMAIL_CREDENTIAL_ROW_ID - ) - .fetch_one(&mut *transaction) - .await? - } + None => query_scalar!( + "SELECT smtp_password FROM email_setting WHERE id = ?", + EMAIL_CREDENTIAL_ROW_ID + ) + .fetch_one(&mut *transaction) + .await + .map_err(|_| anyhow!("smtp_password is required to enable email sending"))?, }; - query!("INSERT INTO email_setting VALUES ($1, $2, $3, $4) - ON CONFLICT(id) DO UPDATE SET smtp_username = $2, smtp_password = $3, smtp_server = $4", + query!("INSERT INTO email_setting (id, smtp_username, smtp_password, smtp_server, from_address, encryption, auth_method, smtp_port) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + ON CONFLICT(id) DO UPDATE SET smtp_username = $2, smtp_password = $3, smtp_server = $4, from_address = $5, encryption = $6, auth_method = $7, smtp_port = $8", EMAIL_CREDENTIAL_ROW_ID, smtp_username, smtp_password, - smtp_server).execute(&mut *transaction).await?; + smtp_server, + from_address, + encryption, + auth_method, + smtp_port, + ).execute(&mut *transaction).await?; transaction.commit().await?; Ok(()) } @@ -75,9 +87,17 @@ mod tests { assert_eq!(conn.read_email_setting().await.unwrap(), None); // Test insertion - conn.update_email_setting("user".into(), Some("pass".into()), "server".into()) - .await - .unwrap(); + conn.update_email_setting( + "user".into(), + Some("pass".into()), + "server".into(), + 25, + "user".into(), + "STARTTLS".into(), + "".into(), + ) + .await + .unwrap(); let creds = conn.read_email_setting().await.unwrap().unwrap(); assert_eq!(creds.smtp_username, "user"); @@ -85,9 +105,18 @@ mod tests { assert_eq!(creds.smtp_server, "server"); // Test update without password - conn.update_email_setting("user2".into(), None, "server2".into()) - .await - .unwrap(); + + conn.update_email_setting( + "user2".into(), + None, + "server2".into(), + 25, + "user2".into(), + "STARTTLS".into(), + "".into(), + ) + .await + .unwrap(); let creds = conn.read_email_setting().await.unwrap().unwrap(); assert_eq!(creds.smtp_username, "user2"); diff --git a/ee/tabby-db/src/github_oauth_credential.rs b/ee/tabby-db/src/github_oauth_credential.rs deleted file mode 100644 index 5ba84a6be1da..000000000000 --- a/ee/tabby-db/src/github_oauth_credential.rs +++ /dev/null @@ -1,94 +0,0 @@ -use anyhow::Result; -use chrono::{DateTime, Utc}; -use sqlx::{query, FromRow}; - -use super::DbConn; - -const GITHUB_OAUTH_CREDENTIAL_ROW_ID: i32 = 1; - -#[derive(FromRow)] -pub struct GithubOAuthCredentialDAO { - pub client_id: String, - pub client_secret: String, - pub created_at: DateTime, - pub updated_at: DateTime, -} - -/// db read/write operations for `github_oauth_credential` table -impl DbConn { - pub async fn update_github_oauth_credential( - &self, - client_id: &str, - client_secret: &str, - ) -> Result<()> { - let client_id = client_id.to_string(); - let client_secret = client_secret.to_string(); - query!( - r#"INSERT INTO github_oauth_credential (id, client_id, client_secret) - VALUES ($1, $2, $3) ON CONFLICT(id) DO UPDATE - SET client_id = $2, client_secret = $3, updated_at = datetime('now') - WHERE id = $1"#, - GITHUB_OAUTH_CREDENTIAL_ROW_ID, - client_id, - client_secret - ) - .execute(&self.pool) - .await?; - Ok(()) - } - - pub async fn delete_github_oauth_credential(&self) -> Result<()> { - query!( - "DELETE FROM github_oauth_credential WHERE id = ?", - GITHUB_OAUTH_CREDENTIAL_ROW_ID - ) - .execute(&self.pool) - .await?; - Ok(()) - } - - pub async fn read_github_oauth_credential(&self) -> Result> { - let token = sqlx::query_as("SELECT client_id, client_secret, created_at, updated_at FROM github_oauth_credential WHERE id = ?") - .bind(GITHUB_OAUTH_CREDENTIAL_ROW_ID) - .fetch_optional(&self.pool).await?; - Ok(token) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn test_update_github_oauth_credential() { - let conn = DbConn::new_in_memory().await.unwrap(); - - // test insert - conn.update_github_oauth_credential("client_id", "client_secret") - .await - .unwrap(); - let res = conn.read_github_oauth_credential().await.unwrap().unwrap(); - assert_eq!(res.client_id, "client_id"); - assert_eq!(res.client_secret, "client_secret"); - - // test update - conn.update_github_oauth_credential("client_id", "client_secret_2") - .await - .unwrap(); - let res = conn.read_github_oauth_credential().await.unwrap().unwrap(); - assert_eq!(res.client_id, "client_id"); - assert_eq!(res.client_secret, "client_secret_2"); - - // test delete - conn.delete_github_oauth_credential().await.unwrap(); - assert!(conn.read_github_oauth_credential().await.unwrap().is_none()); - - // test update after delete - conn.update_github_oauth_credential("client_id_2", "client_secret_2") - .await - .unwrap(); - let res = conn.read_github_oauth_credential().await.unwrap().unwrap(); - assert_eq!(res.client_id, "client_id_2"); - assert_eq!(res.client_secret, "client_secret_2"); - } -} diff --git a/ee/tabby-db/src/google_oauth_credential.rs b/ee/tabby-db/src/google_oauth_credential.rs deleted file mode 100644 index b35b7afd44c5..000000000000 --- a/ee/tabby-db/src/google_oauth_credential.rs +++ /dev/null @@ -1,133 +0,0 @@ -use anyhow::Result; -use chrono::{DateTime, Utc}; -use sqlx::{query, FromRow}; - -use super::DbConn; - -const GOOGLE_OAUTH_CREDENTIAL_ROW_ID: i32 = 1; - -#[derive(FromRow)] -pub struct GoogleOAuthCredentialDAO { - pub client_id: String, - pub client_secret: String, - pub redirect_uri: String, - pub created_at: DateTime, - pub updated_at: DateTime, -} - -/// db read/write operations for `google_oauth_credential` table -impl DbConn { - pub async fn update_google_oauth_credential( - &self, - client_id: &str, - client_secret: &str, - redirect_uri: Option<&str>, - ) -> Result<()> { - let redirect_uri = redirect_uri.unwrap_or_default().to_string(); - if !client_secret.is_empty() { - let client_id = client_id.to_string(); - let client_secret = client_secret.to_string(); - query!( - r#"INSERT INTO google_oauth_credential (id, client_id, client_secret, redirect_uri) - VALUES ($1, $2, $3, $4) ON CONFLICT(id) DO UPDATE - SET client_id = $2, client_secret = $3, redirect_uri = $4, updated_at = datetime('now') - WHERE id = $1"#, - GOOGLE_OAUTH_CREDENTIAL_ROW_ID, - client_id, - client_secret, - redirect_uri - ).execute(&self.pool).await?; - Ok(()) - } else { - let rows = query!( - "UPDATE google_oauth_credential SET redirect_uri = $2, updated_at = datetime('now') WHERE id = $1", - GOOGLE_OAUTH_CREDENTIAL_ROW_ID, - redirect_uri - ).execute(&self.pool).await?.rows_affected(); - if rows != 1 { - return Err(anyhow::anyhow!( - "failed to update: google credential not found" - )); - } - Ok(()) - } - } - - pub async fn read_google_oauth_credential(&self) -> Result> { - let token = sqlx::query_as( - r#"SELECT client_id, client_secret, redirect_uri, created_at, updated_at FROM google_oauth_credential WHERE id = ?"#, - ).bind(GOOGLE_OAUTH_CREDENTIAL_ROW_ID).fetch_optional(&self.pool).await?; - Ok(token) - } - - pub async fn delete_google_oauth_credential(&self) -> Result<()> { - query!( - "DELETE FROM google_oauth_credential WHERE id = ?", - GOOGLE_OAUTH_CREDENTIAL_ROW_ID - ) - .execute(&self.pool) - .await?; - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn test_update_google_oauth_credential() { - let conn = DbConn::new_in_memory().await.unwrap(); - - // test update failure when no record exists - let res = conn - .update_google_oauth_credential("", "", Some("http://localhost")) - .await; - assert!(res.is_err()); - - // test insert - conn.update_google_oauth_credential("client_id", "client_secret", None) - .await - .unwrap(); - let res = conn.read_google_oauth_credential().await.unwrap().unwrap(); - assert_eq!(res.client_id, "client_id"); - assert_eq!(res.client_secret, "client_secret"); - assert_eq!(res.redirect_uri, ""); - - // test delete - conn.delete_google_oauth_credential().await.unwrap(); - let res = conn.read_google_oauth_credential().await.unwrap(); - assert!(res.is_none()); - - // test insert with redirect_uri - conn.update_google_oauth_credential("client_id", "client_secret", Some("http://localhost")) - .await - .unwrap(); - let res = conn.read_google_oauth_credential().await.unwrap().unwrap(); - assert_eq!(res.client_id, "client_id"); - assert_eq!(res.client_secret, "client_secret"); - assert_eq!(res.redirect_uri, "http://localhost"); - - // test update - conn.update_google_oauth_credential( - "client_id_2", - "client_secret_2", - Some("http://127.0.0.1"), - ) - .await - .unwrap(); - let res = conn.read_google_oauth_credential().await.unwrap().unwrap(); - assert_eq!(res.client_id, "client_id_2"); - assert_eq!(res.client_secret, "client_secret_2"); - assert_eq!(res.redirect_uri, "http://127.0.0.1"); - - // test update redirect_uri - conn.update_google_oauth_credential("", "", Some("http://localhost")) - .await - .unwrap(); - let res = conn.read_google_oauth_credential().await.unwrap().unwrap(); - assert_eq!(res.client_id, "client_id_2"); - assert_eq!(res.client_secret, "client_secret_2"); - assert_eq!(res.redirect_uri, "http://localhost"); - } -} diff --git a/ee/tabby-db/src/ingestion.rs b/ee/tabby-db/src/ingestion.rs new file mode 100644 index 000000000000..b8847632fffd --- /dev/null +++ b/ee/tabby-db/src/ingestion.rs @@ -0,0 +1,277 @@ +use anyhow::Result; +use chrono::{DateTime, Utc}; +use sqlx::{query, query_as, FromRow, Row}; +use tabby_db_macros::query_paged_as; + +use super::DbConn; + +#[derive(FromRow)] +pub struct IngestedDocumentDAO { + pub id: i64, + pub source: String, + pub doc_id: String, + pub expired_at: i64, + pub link: Option, + pub title: String, + pub body: String, + pub status: IngestedDocumentStatusDAO, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +#[derive(sqlx::Type)] +#[sqlx(rename_all = "lowercase")] +pub enum IngestedDocumentStatusDAO { + Pending, + Indexed, + Failed, +} + +#[derive(FromRow)] +pub struct IngestionStatusDAO { + pub source: String, + pub pending: i32, + pub failed: i32, + pub total: i32, +} + +/// db read/write operations for `job_runs` table +impl DbConn { + pub async fn get_ingested_document( + &self, + source_id: &str, + id: &str, + ) -> Result> { + let doc = query_as!( + IngestedDocumentDAO, + r#" + SELECT + id, + source, + doc_id, + expired_at, + link, + title, + body, + status as "status: IngestedDocumentStatusDAO", + created_at as "created_at: DateTime", + updated_at as "updated_at: DateTime" + FROM ingested_documents + WHERE source = ? AND doc_id = ? + "#, + source_id, + id + ) + .fetch_optional(&self.pool) + .await?; + + Ok(doc) + } + + pub async fn list_ingested_documents( + &self, + limit: Option, + skip_id: Option, + backwards: bool, + ) -> Result> { + let docs = query_paged_as!( + IngestedDocumentDAO, + "ingested_documents", + [ + "id", + "source", + "doc_id", + "expired_at", + "link", + "title", + "body", + "status" as "status: IngestedDocumentStatusDAO", + "created_at" as "created_at: DateTime", + "updated_at" as "updated_at: DateTime" + ], + limit, + skip_id, + backwards + ) + .fetch_all(&self.pool) + .await?; + + Ok(docs) + } + + pub async fn list_ingested_document_sources( + &self, + limit: Option, + offset: Option, + ) -> Result> { + let mut query = + String::from("SELECT DISTINCT source FROM ingested_documents ORDER BY source"); + if limit.is_some() { + query.push_str(" LIMIT ?"); + } + if offset.is_some() { + query.push_str(" OFFSET ?"); + } + + let mut q = sqlx::query_scalar::<_, String>(&query); + if let Some(l) = limit { + q = q.bind(l as i64); + } + if let Some(o) = offset { + q = q.bind(o as i64); + } + + Ok(q.fetch_all(&self.pool).await?) + } + + pub async fn upsert_ingested_document( + &self, + source: &str, + doc_id: &str, + expired_at: i64, + link: Option, + title: &str, + body: &str, + ) -> Result<()> { + sqlx::query!( + r#" + INSERT INTO ingested_documents ( + source, doc_id, expired_at, link, title, body, status + ) VALUES (?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(source, doc_id) DO UPDATE SET + expired_at = excluded.expired_at, + link = excluded.link, + title = excluded.title, + body = excluded.body, + status = excluded.status + "#, + source, + doc_id, + expired_at, + link, + title, + body, + IngestedDocumentStatusDAO::Pending, + ) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn delete_ingested_document(&self, source: &str, doc_id: &str) -> Result<()> { + sqlx::query!( + r#" + DELETE FROM ingested_documents + WHERE source = ? AND doc_id = ? + "#, + source, + doc_id, + ) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn delete_ingested_document_by_source(&self, source: &str) -> Result<()> { + sqlx::query!( + r#" + DELETE FROM ingested_documents + WHERE source = ? + "#, + source, + ) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn count_pending_ingested_documents(&self) -> Result { + let count = query!( + r#" + SELECT COUNT(1) as count + FROM ingested_documents + WHERE status = ? + "#, + IngestedDocumentStatusDAO::Pending, + ) + .fetch_one(&self.pool) + .await? + .count; + + Ok(count) + } + + pub async fn mark_ingested_document_indexed(&self, source: &str, doc_id: &str) -> Result<()> { + sqlx::query!( + r#" + UPDATE ingested_documents + SET status = ? + WHERE source = ? AND doc_id = ? + "#, + IngestedDocumentStatusDAO::Indexed, + source, + doc_id, + ) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn list_ingested_document_statuses( + &self, + sources: Option>, + ) -> Result> { + let mut query = String::from( + r#" + SELECT + source, + SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending, + SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed, + COUNT(1) as total + FROM ingested_documents + "#, + ); + + if sources.is_some() { + query.push_str(" WHERE source IN ("); + query.push_str( + &sources + .as_ref() + .unwrap() + .iter() + .map(|_| "?") + .collect::>() + .join(", "), + ); + query.push(')'); + } + + query.push_str(" GROUP BY source"); + + let mut q = sqlx::query(&query); + + if let Some(sources) = sources { + for source in sources { + q = q.bind(source); + } + } + + let statuses = q + .fetch_all(&self.pool) + .await? + .into_iter() + .map(|row| IngestionStatusDAO { + source: row.get("source"), + pending: row.get("pending"), + failed: row.get("failed"), + total: row.get("total"), + }) + .collect(); + + Ok(statuses) + } +} diff --git a/ee/tabby-db/src/integrations.rs b/ee/tabby-db/src/integrations.rs new file mode 100644 index 000000000000..88d4bbbde1f6 --- /dev/null +++ b/ee/tabby-db/src/integrations.rs @@ -0,0 +1,171 @@ +use anyhow::{anyhow, Result}; +use chrono::{DateTime, Utc}; +use sqlx::{prelude::FromRow, query, query_as}; +use tabby_db_macros::query_paged_as; + +use crate::DbConn; + +#[derive(FromRow)] +pub struct IntegrationDAO { + pub id: i64, + pub kind: String, + pub error: Option, + pub display_name: String, + pub access_token: String, + pub api_base: Option, + pub created_at: DateTime, + pub updated_at: DateTime, + pub synced: bool, +} + +impl DbConn { + pub async fn create_integration( + &self, + kind: String, + name: String, + access_token: String, + api_base: Option, + ) -> Result { + let res = query!( + "INSERT INTO integrations(kind, display_name, access_token, api_base) VALUES (?, ?, ?, ?);", + kind, + name, + access_token, + api_base + ) + .execute(&self.pool) + .await?; + Ok(res.last_insert_rowid()) + } + + pub async fn get_integration(&self, id: i64) -> Result { + let provider = query_as!( + IntegrationDAO, + r#"SELECT + id, + kind, + error, + display_name, + access_token, + api_base, + updated_at as "updated_at!: DateTime", + created_at as "created_at!: DateTime", + synced + FROM integrations WHERE id = ?;"#, + id + ) + .fetch_one(&self.pool) + .await?; + Ok(provider) + } + + pub async fn delete_integration(&self, id: i64, kind: &str) -> Result<()> { + let res = query!( + "DELETE FROM integrations WHERE id = ? AND kind = ?;", + id, + kind + ) + .execute(&self.pool) + .await?; + if res.rows_affected() != 1 { + return Err(anyhow!("No integration access token to delete")); + } + Ok(()) + } + + pub async fn update_integration( + &self, + id: i64, + kind: &str, + display_name: String, + access_token: Option, + api_base: Option, + ) -> Result<()> { + let access_token = match access_token { + Some(access_token) => access_token, + None => self.get_integration(id).await?.access_token, + }; + + let res = query!( + "UPDATE integrations SET display_name = $1, access_token = $2, api_base = $3, updated_at = DATETIME('now'), + synced = IIF(access_token != $2 OR api_base != $3, false, synced), + error = IIF(access_token != $2 OR api_base != $3, NULL, error) + WHERE id = $4 AND kind = $5;", + display_name, + access_token, + api_base, + id, + kind + ) + .execute(&self.pool) + .await?; + + if res.rows_affected() != 1 { + return Err(anyhow!( + "The specified integration access token does not exist" + )); + } + + Ok(()) + } + + pub async fn update_integration_error(&self, id: i64, error: Option) -> Result<()> { + query!( + "UPDATE integrations SET synced = true, error = ? WHERE id = ?", + error, + id + ) + .execute(&self.pool) + .await?; + Ok(()) + } + + pub async fn list_integrations( + &self, + ids: Vec, + kind: Option, + limit: Option, + skip_id: Option, + backwards: bool, + ) -> Result> { + let mut conditions = vec![]; + + let id_condition = (!ids.is_empty()).then(|| { + let ids = ids + .into_iter() + .map(|id| id.to_string()) + .collect::>() + .join(", "); + format!("id in ({ids})") + }); + conditions.extend(id_condition); + + let kind_condition = kind.map(|kind| format!("kind = '{kind}'")); + conditions.extend(kind_condition); + + let condition = (!conditions.is_empty()).then(|| conditions.join(" AND ")); + + let providers = query_paged_as!( + IntegrationDAO, + "integrations", + [ + "id", + "kind", + "error", + "display_name", + "access_token", + "api_base", + "created_at" as "created_at!: DateTime", + "updated_at" as "updated_at!: DateTime", + "synced" + ], + limit, + skip_id, + backwards, + condition + ) + .fetch_all(&self.pool) + .await?; + Ok(providers) + } +} diff --git a/ee/tabby-db/src/invitations.rs b/ee/tabby-db/src/invitations.rs index d88239d00ea2..10703fb50c1d 100644 --- a/ee/tabby-db/src/invitations.rs +++ b/ee/tabby-db/src/invitations.rs @@ -1,16 +1,19 @@ use anyhow::{anyhow, Result}; +use chrono::{DateTime, Utc}; use sqlx::{prelude::FromRow, query}; +use tabby_db_macros::query_paged_as; use uuid::Uuid; use super::DbConn; +use crate::SQLXResultExt; #[derive(FromRow)] pub struct InvitationDAO { - pub id: i32, + pub id: i64, pub email: String, pub code: String, - pub created_at: String, + pub created_at: DateTime, } /// db read/write operations for `invitations` table @@ -21,34 +24,31 @@ impl DbConn { skip_id: Option, backwards: bool, ) -> Result> { - let query = Self::make_pagination_query( - "invitations", - &["id", "email", "code", "created_at"], - limit, - skip_id, - backwards, - ); - - let invitations = sqlx::query_as(&query).fetch_all(&self.pool).await?; + let invitations = query_paged_as!(InvitationDAO, "invitations", ["id", "email", "code", "created_at" as "created_at!: DateTime"], limit, skip_id, backwards) + .fetch_all(&self.pool) + .await?; Ok(invitations) } pub async fn get_invitation_by_code(&self, code: &str) -> Result> { - let token = - sqlx::query_as(r#"SELECT id, email, code, created_at FROM invitations WHERE code = ?"#) - .bind(code) - .fetch_optional(&self.pool) - .await?; + let token = sqlx::query_as!( + InvitationDAO, + r#"SELECT id as "id!", email, code, created_at as "created_at!: DateTime" FROM invitations WHERE code = ?"#, + code + ) + .fetch_optional(&self.pool) + .await?; Ok(token) } pub async fn get_invitation_by_email(&self, email: &str) -> Result> { - let token = sqlx::query_as( - r#"SELECT id, email, code, created_at FROM invitations WHERE email = ?"#, + let token = sqlx::query_as!( + InvitationDAO, + r#"SELECT id as "id!", email, code, created_at as "created_at!: DateTime" FROM invitations WHERE email = ?"#, + email ) - .bind(email) .fetch_optional(&self.pool) .await?; @@ -64,26 +64,24 @@ impl DbConn { let res = query!( "INSERT INTO invitations (email, code) VALUES (?, ?)", email, - code + code, ) .execute(&self.pool) .await; - match res { - Err(sqlx::Error::Database(db)) if db.constraint().is_some() => { - Err(anyhow!("Failed to create invitation, email already exists")) - } - Err(err) => Err(err.into()), - Ok(res) => Ok(InvitationDAO { - id: res.last_insert_rowid() as i32, - email, - code, - created_at: "".into(), - }), - } + let res = res.unique_error("Failed to create invitation, email already exists")?; + let id = res.last_insert_rowid(); + + let invitation = sqlx::query_as!( + InvitationDAO, + r#"SELECT id as "id!", email, code, created_at as "created_at!: DateTime" FROM invitations WHERE id = ?"#, + id + ).fetch_one(&self.pool).await?; + + Ok(invitation) } - pub async fn delete_invitation(&self, id: i32) -> Result { + pub async fn delete_invitation(&self, id: i64) -> Result { let res = query!("DELETE FROM invitations WHERE id = ?", id) .execute(&self.pool) .await?; diff --git a/ee/tabby-db/src/job_runs.rs b/ee/tabby-db/src/job_runs.rs index 457b48f86d00..d37ee682b922 100644 --- a/ee/tabby-db/src/job_runs.rs +++ b/ee/tabby-db/src/job_runs.rs @@ -1,37 +1,67 @@ use anyhow::Result; -use chrono::{DateTime, Utc}; +use chrono::{DateTime, Duration, Months, Utc}; use sqlx::{query, FromRow}; +use tabby_db_macros::query_paged_as; -use super::DbConn; +use super::{AsSqliteDateTimeString, DbConn}; #[derive(Default, Clone, FromRow)] pub struct JobRunDAO { - pub id: i32, - pub job_name: String, - pub start_time: DateTime, - pub finish_time: Option>, - pub exit_code: Option, + pub id: i64, + #[sqlx(rename = "job")] + pub name: String, + pub command: String, + pub exit_code: Option, pub stdout: String, - pub stderr: String, + pub created_at: DateTime, + pub updated_at: DateTime, + + /// The time when the job was started. + pub started_at: Option>, + + #[sqlx(rename = "end_ts")] + pub finished_at: Option>, +} + +impl JobRunDAO { + pub fn is_running(&self) -> bool { + self.started_at.is_some() && self.finished_at.is_none() + } + + pub fn is_pending(&self) -> bool { + self.started_at.is_none() && self.exit_code.is_none() + } +} + +#[derive(FromRow)] +pub struct JobStatsDAO { + pub success: i32, + pub failed: i32, + pub pending: i32, } /// db read/write operations for `job_runs` table impl DbConn { - pub async fn create_job_run(&self, run: JobRunDAO) -> Result { + pub async fn create_job_run(&self, job: String, command: String) -> Result { let rowid = query!( - r#"INSERT INTO job_runs (job, start_ts, end_ts, exit_code, stdout, stderr) VALUES (?, ?, ?, ?, ?, ?)"#, - run.job_name, - run.start_time, - run.finish_time, - run.exit_code, - run.stdout, - run.stderr, + r#"INSERT INTO job_runs (job, start_ts, stdout, stderr, command) VALUES (?, DATETIME('now'), '', '', ?)"#, + job, command, ).execute(&self.pool).await?.last_insert_rowid(); - Ok(rowid as i32) + Ok(rowid) } - pub async fn update_job_stdout(&self, job_id: i32, stdout: String) -> Result<()> { + pub async fn get_next_job_to_execute(&self) -> Option { + sqlx::query_as( + r#"SELECT * FROM job_runs WHERE exit_code IS NULL AND started_at is NULL ORDER BY created_at ASC LIMIT 1"#, + ) + .fetch_optional(&self.pool) + .await + .ok() + .flatten() + } + + pub async fn update_job_stdout(&self, job_id: i64, stdout: String) -> Result<()> { query!( r#"UPDATE job_runs SET stdout = stdout || ?, updated_at = datetime('now') WHERE id = ?"#, stdout, @@ -40,93 +70,146 @@ impl DbConn { Ok(()) } - pub async fn update_job_stderr(&self, job_id: i32, stderr: String) -> Result<()> { + pub async fn update_job_status(&self, job_id: i64, exit_code: i32) -> Result<()> { query!( - r#"UPDATE job_runs SET stderr = stderr || ?, updated_at = datetime('now') WHERE id = ?"#, - stderr, - job_id + r#"UPDATE job_runs SET end_ts = datetime('now'), exit_code = ?, updated_at = datetime('now') WHERE id = ?"#, + exit_code, + job_id, ).execute(&self.pool).await?; Ok(()) } - pub async fn update_job_status(&self, run: JobRunDAO) -> Result<()> { + pub async fn update_job_started(&self, job_id: i64) -> Result<()> { query!( - r#"UPDATE job_runs SET end_ts = ?, exit_code = ?, updated_at = datetime('now') WHERE id = ?"#, - run.finish_time, - run.exit_code, - run.id + r#"UPDATE job_runs SET started_at = datetime('now'), updated_at = datetime('now') WHERE id = ?"#, + job_id, ).execute(&self.pool).await?; Ok(()) } + pub async fn get_job_run(&self, id: i64) -> Option { + sqlx::query_as(r#"SELECT * FROM job_runs WHERE id = ?"#) + .bind(id) + .fetch_optional(&self.pool) + .await + .ok() + .flatten() + } + + pub async fn get_latest_job_run(&self, command: String) -> Option { + sqlx::query_as( + r#"SELECT * FROM job_runs WHERE command = ? ORDER BY created_at DESC LIMIT 1"#, + ) + .bind(command) + .fetch_optional(&self.pool) + .await + .ok() + .flatten() + } + + pub async fn delete_pending_job_run(&self, command: &str) -> Result { + // Delete jobs that are not started and doesn't have an exit code. + let num_deleted = query!("UPDATE job_runs SET exit_code = -1, end_ts = datetime('now'), updated_at = datetime('now') WHERE exit_code IS NULL AND started_at IS NULL AND command = ?", command) + .execute(&self.pool) + .await?.rows_affected(); + + Ok(num_deleted as usize) + } + + pub async fn delete_job_run_before_three_months(&self, now: DateTime) -> Result { + if let Some(three_months_ago) = now.checked_sub_months(Months::new(3)) { + let three_months_ago = three_months_ago.as_sqlite_datetime(); + let num_deleted = query!( + "delete FROM job_runs WHERE updated_at < ? AND exit_code IS NOT NULL", + three_months_ago, + ) + .execute(&self.pool) + .await? + .rows_affected(); + + Ok(num_deleted as usize) + } else { + Ok(0) + } + } + pub async fn list_job_runs_with_filter( &self, + ids: Option>, + jobs: Option>, limit: Option, skip_id: Option, backwards: bool, ) -> Result> { - let query = Self::make_pagination_query( + let mut conditions = vec![]; + + if let Some(ids) = ids { + let ids: Vec = ids.iter().map(i32::to_string).collect(); + let ids = ids.join(", "); + conditions.push(format!("id in ({ids})")); + } + + if let Some(jobs) = jobs { + let jobs: Vec = jobs.iter().map(|s| format!("{s:?}")).collect(); + let jobs = jobs.join(", "); + conditions.push(format!("job in ({jobs})")); + } + + let condition = (!conditions.is_empty()).then_some(conditions.join(" AND ")); + let job_runs: Vec = query_paged_as!( + JobRunDAO, "job_runs", - &[ + [ "id", - "job", - "start_ts", - "end_ts", + "job" as "name", "exit_code", "stdout", - "stderr", + "command"!, + "created_at" as "created_at!: DateTime", + "updated_at" as "updated_at!: DateTime", + "started_at" as "started_at: DateTime", + "end_ts" as "finished_at: DateTime" ], limit, skip_id, backwards, - ); + condition + ) + .fetch_all(&self.pool) + .await?; - let runs = sqlx::query_as(&query).fetch_all(&self.pool).await?; - Ok(runs) + Ok(job_runs) } -} -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn test_create_job_run() { - let db = DbConn::new_in_memory().await.unwrap(); - let run = JobRunDAO { - id: 0, - job_name: "test".to_string(), - start_time: chrono::Utc::now(), - finish_time: None, - exit_code: None, - stdout: "stdout".to_string(), - stderr: "stderr".to_string(), - }; - let id = db.create_job_run(run).await.unwrap(); - assert_eq!(id, 1); - - let run = JobRunDAO { - id: 0, - job_name: "test".to_string(), - start_time: chrono::Utc::now(), - finish_time: Some(chrono::Utc::now()), - exit_code: None, - stdout: "stdout".to_string(), - stderr: "stderr".to_string(), + pub async fn compute_job_stats(&self, jobs: Option>) -> Result { + let condition = match jobs { + Some(jobs) => { + let jobs: Vec<_> = jobs.into_iter().map(|s| format!("{s:?}")).collect(); + let jobs = jobs.join(", "); + format!("AND job IN ({jobs})") + } + None => "".into(), }; - let id = db.create_job_run(run).await.unwrap(); - assert_eq!(id, 2); - - let run = JobRunDAO { - id: 0, - job_name: "test".to_string(), - start_time: chrono::Utc::now(), - finish_time: Some(chrono::Utc::now()), - exit_code: Some(0), - stdout: "stdout".to_string(), - stderr: "stderr".to_string(), - }; - let id = db.create_job_run(run).await.unwrap(); - assert_eq!(id, 3); + + let cutoff = Utc::now() - Duration::days(7); + + let stats = sqlx::query_as(&format!( + r#"SELECT + SUM(exit_code == 0) AS success, + SUM(exit_code != 0 AND exit_code IS NOT NULL) AS failed, + SUM(exit_code IS NULL) AS pending FROM job_runs + WHERE created_at > ? {condition};"# + )) + .bind(cutoff.as_sqlite_datetime()) + .fetch_one(&self.pool) + .await?; + Ok(stats) + } + + pub async fn finalize_stale_job_runs(&self) -> Result<()> { + query!("UPDATE job_runs SET exit_code = -1, started_at = datetime('now'), end_ts = datetime('now'), updated_at = datetime('now') WHERE exit_code IS NULL;") + .execute(&self.pool) + .await?; + Ok(()) } } diff --git a/ee/tabby-db/src/ldap_credential.rs b/ee/tabby-db/src/ldap_credential.rs new file mode 100644 index 000000000000..3a890ee7070a --- /dev/null +++ b/ee/tabby-db/src/ldap_credential.rs @@ -0,0 +1,224 @@ +use anyhow::Result; +use chrono::{DateTime, Utc}; +use sqlx::query; + +use crate::DbConn; + +pub struct LdapCredentialDAO { + pub host: String, + pub port: i64, + + pub bind_dn: String, + pub bind_password: String, + pub base_dn: String, + pub user_filter: String, + + pub encryption: String, + pub skip_tls_verify: bool, + + pub email_attribute: String, + pub name_attribute: Option, + + pub created_at: DateTime, + pub updated_at: DateTime, +} + +impl DbConn { + pub async fn update_ldap_credential( + &self, + host: &str, + port: i32, + bind_dn: &str, + bind_password: &str, + base_dn: &str, + user_filter: &str, + encryption: &str, + skip_tls_verify: bool, + email_attribute: &str, + name_attribute: Option<&str>, + ) -> Result<()> { + // only support one ldap credential, so id is always 1 + query!( + r#"INSERT INTO ldap_credential ( + id, + host, + port, + bind_dn, + bind_password, + base_dn, + user_filter, + encryption, + skip_tls_verify, + email_attribute, + name_attribute + ) + VALUES (1, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10) + ON CONFLICT (id) DO UPDATE SET + host = excluded.host, + port = excluded.port, + bind_dn = excluded.bind_dn, + bind_password = excluded.bind_password, + base_dn = excluded.base_dn, + user_filter = excluded.user_filter, + encryption = excluded.encryption, + skip_tls_verify = excluded.skip_tls_verify, + email_attribute = excluded.email_attribute, + name_attribute = excluded.name_attribute, + updated_at = datetime('now')"#, + host, + port, + bind_dn, + bind_password, + base_dn, + user_filter, + encryption, + skip_tls_verify, + email_attribute, + name_attribute, + ) + .execute(&self.pool) + .await?; + Ok(()) + } + + pub async fn delete_ldap_credential(&self) -> Result<()> { + // only support one ldap credential, so id is always 1 + query!("DELETE FROM ldap_credential WHERE id = 1") + .execute(&self.pool) + .await?; + Ok(()) + } + + pub async fn read_ldap_credential(&self) -> Result> { + let token = sqlx::query_as!( + LdapCredentialDAO, + r#"SELECT + host as "host: String", + port, + bind_dn as "bind_dn: String", + bind_password as "bind_password: String", + base_dn as "base_dn: String", + user_filter as "user_filter: String", + encryption as "encryption: String", + skip_tls_verify, + email_attribute as "email_attribute: String", + name_attribute as "name_attribute: String", + created_at as "created_at: DateTime", + updated_at as "updated_at: DateTime" + FROM ldap_credential + WHERE id = 1"#, + ) + .fetch_optional(&self.pool) + .await?; + Ok(token) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_update_ldap_credential() { + let conn = DbConn::new_in_memory().await.unwrap(); + + // test insert + conn.update_ldap_credential( + "host", + 389, + "bind_dn", + "bind_password", + "base_dn", + "user_filter", + "encryption", + true, + "email_attribute", + Some("name_attribute"), + ) + .await + .unwrap(); + let res = conn.read_ldap_credential().await.unwrap().unwrap(); + assert_eq!(res.host, "host"); + assert_eq!(res.port, 389); + assert_eq!(res.bind_dn, "bind_dn"); + assert_eq!(res.bind_password, "bind_password"); + assert_eq!(res.base_dn, "base_dn"); + assert_eq!(res.user_filter, "user_filter"); + assert_eq!(res.encryption, "encryption"); + assert!(res.skip_tls_verify); + assert_eq!(res.email_attribute, "email_attribute"); + assert_eq!(res.name_attribute, Some("name_attribute".into())); + let created_at = res.created_at; + let updated_at = res.updated_at; + assert!(created_at > Utc::now() - chrono::Duration::seconds(2)); + assert!(updated_at > Utc::now() - chrono::Duration::seconds(2)); + + // test update + // sleep for a while to make sure updated_at is different + tokio::time::sleep(std::time::Duration::from_secs(2)).await; + + conn.update_ldap_credential( + "host_2", + 389, + "bind_dn_2", + "bind_password_2", + "base_dn_2", + "user_filter_2", + "encryption_2", + false, + "email_attribute_2", + Some("name_attribute_2"), + ) + .await + .unwrap(); + let res = conn.read_ldap_credential().await.unwrap().unwrap(); + assert_eq!(res.host, "host_2"); + assert_eq!(res.port, 389); + assert_eq!(res.bind_dn, "bind_dn_2"); + assert_eq!(res.bind_password, "bind_password_2"); + assert_eq!(res.base_dn, "base_dn_2"); + assert_eq!(res.user_filter, "user_filter_2"); + assert_eq!(res.encryption, "encryption_2"); + assert!(!res.skip_tls_verify); + assert_eq!(res.email_attribute, "email_attribute_2"); + assert_eq!(res.name_attribute, Some("name_attribute_2".into())); + assert_eq!(res.created_at, created_at); + assert!(res.updated_at > updated_at); + + // make sure only one row in the table + let res = sqlx::query_scalar!("SELECT COUNT(*) FROM ldap_credential") + .fetch_one(&conn.pool) + .await + .unwrap(); + assert_eq!(res, 1); + } + + #[tokio::test] + async fn test_delete_ldap_credential() { + let conn = DbConn::new_in_memory().await.unwrap(); + + conn.update_ldap_credential( + "host", + 389, + "bind_dn", + "bind_password", + "base_dn", + "user_filter", + "encryption", + true, + "email_attribute", + Some("name_attribute"), + ) + .await + .unwrap(); + + // make sure inserted + let res = conn.read_ldap_credential().await.unwrap(); + assert!(res.is_some()); + + // delete + conn.delete_ldap_credential().await.unwrap(); + let res = conn.read_ldap_credential().await.unwrap(); + assert!(res.is_none()); + } +} diff --git a/ee/tabby-db/src/lib.rs b/ee/tabby-db/src/lib.rs index 5835989055fb..55f7bc4f61ff 100644 --- a/ee/tabby-db/src/lib.rs +++ b/ee/tabby-db/src/lib.rs @@ -1,28 +1,119 @@ +use std::{path::Path, sync::Arc, time::Duration}; + +use anyhow::anyhow; +pub use attachment::{ + Attachment, AttachmentClientCode, AttachmentCode, AttachmentCodeFileList, AttachmentCommitDoc, + AttachmentDoc, AttachmentIngestedDoc, AttachmentIssueDoc, AttachmentPageDoc, AttachmentPullDoc, + AttachmentWebDoc, +}; +use cache::Cache; +use cached::TimedSizedCache; +use chrono::{DateTime, Utc}; pub use email_setting::EmailSettingDAO; -pub use github_oauth_credential::GithubOAuthCredentialDAO; -pub use google_oauth_credential::GoogleOAuthCredentialDAO; +pub use ingestion::{IngestedDocumentDAO, IngestedDocumentStatusDAO, IngestionStatusDAO}; +pub use integrations::IntegrationDAO; pub use invitations::InvitationDAO; pub use job_runs::JobRunDAO; +pub use ldap_credential::LdapCredentialDAO; +pub use notifications::NotificationDAO; +pub use oauth_credential::OAuthCredentialDAO; +pub use pages::{PageDAO, PageSectionDAO}; +pub use provided_repositories::ProvidedRepositoryDAO; pub use repositories::RepositoryDAO; -use sqlx::{query, query_scalar, Pool, Sqlite, SqlitePool}; +pub use server_setting::ServerSettingDAO; +use sqlx::{ + query, query_scalar, + sqlite::{SqlitePoolOptions, SqliteQueryResult}, + Pool, Sqlite, SqlitePool, +}; +pub use threads::{ThreadDAO, ThreadMessageDAO}; +use tokio::sync::Mutex; +use user_completions::UserCompletionDailyStatsDAO; +pub use user_events::UserEventDAO; +pub use user_groups::{UserGroupDAO, UserGroupMembershipDAO}; pub use users::UserDAO; +pub use web_documents::WebDocumentDAO; +mod access_policy; +mod attachment; +pub mod cache; mod email_setting; -mod github_oauth_credential; -mod google_oauth_credential; +mod ingestion; +mod integrations; mod invitations; mod job_runs; -mod path; +mod ldap_credential; +#[cfg(test)] +mod migration_tests; +mod notifications; +mod oauth_credential; +mod pages; +mod password_reset; +mod provided_repositories; mod refresh_tokens; mod repositories; +mod server_setting; +mod threads; +mod user_chats; +mod user_completions; +mod user_events; +mod user_groups; mod users; +mod web_documents; use anyhow::Result; +use sql_query_builder as sql; use sqlx::sqlite::SqliteConnectOptions; +pub struct DbCache { + pub active_user_count: Cache, + pub active_admin_count: Cache, + pub daily_stats_in_past_year: + Arc, Vec>>>, +} + #[derive(Clone)] pub struct DbConn { pool: Pool, + cache: Arc, +} + +fn make_pagination_query_with_condition( + table_name: &str, + field_names: &[&str], + limit: Option, + skip_id: Option, + backwards: bool, + condition: Option, +) -> String { + let mut source = sql::Select::new().select("*").from(table_name); + let mut select = sql::Select::new() + .select(&field_names.join(", ")) + .order_by("id ASC"); + + if let Some(condition) = condition { + source = source.where_and(&condition) + } + + if backwards { + source = source.order_by("id DESC"); + if let Some(skip_id) = skip_id { + source = source.where_and(&format!("id < {skip_id}")); + } + if let Some(limit) = limit { + source = source.limit(&limit.to_string()); + } + } else { + if let Some(skip_id) = skip_id { + select = select.where_and(&format!("id > {skip_id}")); + } + if let Some(limit) = limit { + select = select.limit(&limit.to_string()); + } + } + + select = select.from(&format!("({source})")); + select.as_string() } impl DbConn { @@ -40,13 +131,97 @@ impl DbConn { DbConn::init_db(pool).await } - pub async fn new() -> Result { - tokio::fs::create_dir_all(path::db_file().parent().unwrap()).await?; - let options = SqliteConnectOptions::new().filename(path::db_file()); - let pool = SqlitePool::connect_with(options).await?; + #[cfg(any(test, feature = "testutils"))] + pub async fn new_blank() -> Result { + use std::str::FromStr; + + use sqlx::sqlite::SqlitePoolOptions; + + let options = SqliteConnectOptions::from_str("sqlite::memory:")?; + let pool = SqlitePoolOptions::new() + .max_connections(1) + .connect_with(options) + .await?; + Ok(DbConn { + pool, + cache: Arc::new(DbCache { + active_user_count: Default::default(), + active_admin_count: Default::default(), + daily_stats_in_past_year: Arc::new(Mutex::new( + TimedSizedCache::with_size_and_lifespan(20, 3600), + )), + }), + }) + } + + /// We forked sqlx to disable support for chrono::DateTime as it's format is problematic + /// against SQLite `DATETIME("now")`. + /// + /// ```compile_fail + /// let output = sqlx::query_scalar::<_, String>("SELECT ?;").bind(chrono::Utc::now()); + /// ``` + fn _datetime_utc_shouldnt_be_bindable() {} + + pub async fn new(db_file: &Path) -> Result { + tokio::fs::create_dir_all(db_file.parent().unwrap()).await?; + + let options = SqliteConnectOptions::new() + // Reduce SQLITE_BUSY (code 5) errors. Note that the error message "database is locked" should not be confused with SQLITE_LOCKED. + // For more details, see: + // 1. https://til.simonwillison.net/sqlite/enabling-wal-mode + // 2. https://www.sqlite.org/wal.html + .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) + .filename(db_file) + .create_if_missing(true); + let pool = SqlitePoolOptions::new() + .max_connections(64) + .min_connections(2) + .acquire_timeout(Duration::from_secs(6)) + .idle_timeout(Duration::from_secs(300)) + .max_lifetime(Duration::from_secs(3600)) + .connect_with(options) + .await?; + Self::backup_db(db_file, &pool).await?; Self::init_db(pool).await } + /// Backup existing database file before opening it. + /// backup format: + /// for prod - db.backup-${date}.sqlite + /// for non-prod - dev-db.backup-${date}.sqlite + async fn backup_db(db_file: &Path, pool: &SqlitePool) -> Result<()> { + use sqlx_migrate_validate::Validate; + + let mut conn = pool.acquire().await?; + if sqlx::migrate!("./migrations") + .validate(&mut *conn) + .await + .is_ok() + { + // No migration is needed, skip the backup. + return Ok(()); + } + + if !tokio::fs::try_exists(db_file).await? { + return Ok(()); + } + let Some(db_file_name) = db_file.file_name() else { + return Err(anyhow!("failed to backup db, missing db file name")); + }; + let db_file_name = db_file_name.to_string_lossy(); + if !db_file_name.ends_with(".sqlite") { + return Err(anyhow!("failed to backup db, expect .sqlite extension")); + } + + let today = Utc::now().date_naive().format("%Y%m%d").to_string(); + let backup_file = db_file.with_file_name( + db_file_name.replace(".sqlite", format!(".backup-{today}.sqlite").as_str()), + ); + + tokio::fs::copy(db_file, &backup_file).await?; + Ok(()) + } + /// Initialize database, create tables and insert first token if not exist async fn init_db(pool: SqlitePool) -> Result { sqlx::migrate!("./migrations").run(&pool).await?; @@ -59,7 +234,16 @@ impl DbConn { .execute(&pool) .await?; - let conn = Self { pool }; + let conn = Self { + pool, + cache: Arc::new(DbCache { + active_user_count: Default::default(), + active_admin_count: Default::default(), + daily_stats_in_past_year: Arc::new(Mutex::new( + TimedSizedCache::with_size_and_lifespan(20, 3600), + )), + }), + }; conn.manual_users_active_migration().await?; Ok(conn) } @@ -81,40 +265,6 @@ impl DbConn { } Ok(()) } - - fn make_pagination_query( - table_name: &str, - field_names: &[&str], - limit: Option, - skip_id: Option, - backwards: bool, - ) -> String { - let mut source = String::new(); - let mut clause = String::new(); - if backwards { - source += &format!("SELECT * FROM {}", table_name); - if let Some(skip_id) = skip_id { - source += &format!(" WHERE id < {}", skip_id); - } - source += " ORDER BY id DESC"; - if let Some(limit) = limit { - source += &format!(" LIMIT {}", limit); - } - clause += " ORDER BY id ASC"; - } else { - source += table_name; - if let Some(skip_id) = skip_id { - clause += &format!(" WHERE id > {}", skip_id); - } - clause += " ORDER BY id ASC"; - if let Some(limit) = limit { - clause += &format!(" LIMIT {}", limit); - } - } - let fields = field_names.join(", "); - - format!(r#"SELECT {} FROM ({}) {}"#, fields, source, clause) - } } /// db read/write operations for `registration_token` table @@ -133,7 +283,7 @@ impl DbConn { pub async fn reset_registration_token(&self) -> Result { let token = uuid::Uuid::new_v4().to_string(); let result = token.clone(); - let updated_at = chrono::Utc::now(); + let updated_at = Utc::now().as_sqlite_datetime(); let res = query!( "UPDATE registration_token SET token = ?, updated_at = ? WHERE id = 1", @@ -150,8 +300,36 @@ impl DbConn { } } +trait AsSqliteDateTimeString { + fn as_sqlite_datetime(&self) -> String; +} + +impl AsSqliteDateTimeString for DateTime { + fn as_sqlite_datetime(&self) -> String { + self.format("%F %X").to_string() + } +} + +pub trait SQLXResultExt { + fn unique_error(self, msg: &'static str) -> anyhow::Result; +} + +impl SQLXResultExt for Result { + fn unique_error(self, msg: &'static str) -> anyhow::Result { + match self { + Ok(v) => Ok(v), + Err(sqlx::Error::Database(db_err)) if db_err.is_unique_violation() => { + Err(anyhow!("{msg}")) + } + Err(e) => Err(e.into()), + } + } +} + #[cfg(test)] mod tests { + use chrono::Duration; + use super::*; #[tokio::test] @@ -171,16 +349,60 @@ mod tests { assert_eq!(new_token.len(), 36); assert_ne!(old_token, new_token); } + + #[tokio::test] + async fn test_timestamp_format() { + let db = DbConn::new_in_memory().await.unwrap(); + + let time = Utc::now(); + + let time_str = time.as_sqlite_datetime(); + let sql_time: String = sqlx::query_scalar::<_, String>("SELECT ?;") + .bind(time.as_sqlite_datetime()) + .fetch_one(&db.pool) + .await + .unwrap(); + + assert_eq!(time_str, sql_time); + + let sql_time: String = sqlx::query_scalar::<_, String>("SELECT DATETIME('now');") + .fetch_one(&db.pool) + .await + .unwrap(); + assert_eq!(sql_time, Utc::now().as_sqlite_datetime()); + + // No assertions, these will fail at compiletime if adding/subtracting from these types + // yields DateTime, which could be dangerous + let time = Utc::now(); + let _added_time: DateTime = time + Duration::milliseconds(1); + let _subbed_time: DateTime = time - Duration::milliseconds(1); + } } #[cfg(any(test, feature = "testutils"))] pub mod testutils { use super::*; - pub async fn create_user(conn: &DbConn) -> i32 { + pub async fn create_user(conn: &DbConn) -> i64 { let email: &str = "test@example.com"; let password: &str = "123456789"; - conn.create_user(email.to_string(), password.to_string(), true) + conn.create_user(email.to_string(), Some(password.to_string()), true, None) + .await + .unwrap() + } + + pub async fn create_user2(conn: &DbConn) -> i64 { + let email: &str = "test2@example.com"; + let password: &str = "123456789"; + conn.create_user(email.to_string(), Some(password.to_string()), true, None) + .await + .unwrap() + } + + pub async fn create_user3(conn: &DbConn) -> i64 { + let email: &str = "test3@example.com"; + let password: &str = "123456789"; + conn.create_user(email.to_string(), Some(password.to_string()), true, None) .await .unwrap() } diff --git a/ee/tabby-db/src/migration_tests.rs b/ee/tabby-db/src/migration_tests.rs new file mode 100644 index 000000000000..f80f5c1abaa2 --- /dev/null +++ b/ee/tabby-db/src/migration_tests.rs @@ -0,0 +1,74 @@ +use std::ops::RangeBounds; + +use sqlx::{ + migrate, + migrate::{Migration, MigrationType, Migrator}, +}; + +use crate::DbConn; + +fn migrations( + migrator: &Migrator, + range: impl RangeBounds + 'static, +) -> impl Iterator { + migrator.iter().filter(move |migration| { + range.contains(&migration.version) + && matches!( + migration.migration_type, + MigrationType::Simple | MigrationType::ReversibleUp + ) + }) +} + +#[tokio::test] +async fn test_repository_migration_0029() { + let migrator = migrate!("./migrations"); + let db = DbConn::new_blank().await.unwrap(); + + for migration in migrations(&migrator, ..29) { + sqlx::query(&migration.sql).execute(&db.pool).await.unwrap(); + } + + sqlx::query("INSERT INTO github_repository_provider(display_name, access_token) VALUES ('github', 'gh-faketoken');") + .execute(&db.pool) + .await + .unwrap(); + + sqlx::query("INSERT INTO github_provided_repositories(github_repository_provider_id, vendor_id, name, git_url, active) + VALUES (1, 'vendor_id', 'tabby-gh', 'https://github.com/TabbyML/tabby', true);") + .execute(&db.pool) + .await + .unwrap(); + + sqlx::query("INSERT INTO gitlab_repository_provider(display_name, access_token) VALUES ('gitlab', 'gl-faketoken');") + .execute(&db.pool) + .await + .unwrap(); + + sqlx::query("INSERT INTO gitlab_provided_repositories(gitlab_repository_provider_id, vendor_id, name, git_url, active) + VALUES (1, 'vendor_id', 'tabby-gl', 'https://gitlab.com/TabbyML/tabby', false);") + .execute(&db.pool) + .await + .unwrap(); + + let migration = migrations(&migrator, 29..=29).next().unwrap(); + sqlx::query(&migration.sql).execute(&db.pool).await.unwrap(); + + let repos: Vec<(String, i64, String, bool)> = sqlx::query_as( + "SELECT name, integration_id, git_url, active FROM provided_repositories ORDER BY id", + ) + .fetch_all(&db.pool) + .await + .unwrap(); + + assert_eq!(2, repos.len()); + assert_eq!(repos[0].0, "tabby-gh"); + assert_eq!(repos[0].1, 1); + assert_eq!(repos[0].2, "https://github.com/TabbyML/tabby"); + assert!(repos[0].3); + + assert_eq!(repos[1].0, "tabby-gl"); + assert_eq!(repos[1].1, 2); + assert_eq!(repos[1].2, "https://gitlab.com/TabbyML/tabby"); + assert!(!repos[1].3); +} diff --git a/ee/tabby-db/src/notifications.rs b/ee/tabby-db/src/notifications.rs new file mode 100644 index 000000000000..cf654d93f1aa --- /dev/null +++ b/ee/tabby-db/src/notifications.rs @@ -0,0 +1,148 @@ +use anyhow::{Context, Result}; +use chrono::{DateTime, Duration, Utc}; +use sqlx::{prelude::*, query, query_as}; + +use crate::DbConn; + +const NOTIFICATION_RECIPIENT_ALL_USER: &str = "all_user"; +const NOTIFICATION_RECIPIENT_ADMIN: &str = "admin"; + +#[derive(FromRow)] +pub struct NotificationDAO { + pub id: i64, + + pub recipient: String, + pub content: String, + pub read: bool, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +impl DbConn { + pub async fn create_notification(&self, recipient: &str, content: &str) -> Result { + let res = query!( + "INSERT INTO notifications (recipient, content) VALUES (?, ?)", + recipient, + content + ) + .execute(&self.pool) + .await?; + + Ok(res.last_insert_rowid()) + } + + pub async fn mark_notification_read(&self, id: i64, user_id: i64) -> Result<()> { + query!( + "INSERT INTO read_notifications (notification_id, user_id) + VALUES (?, ?) + ON CONFLICT (notification_id, user_id) + DO NOTHING", + id, + user_id, + ) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn mark_all_notifications_read_by_user(&self, user_id: i64) -> Result<()> { + let user = self + .get_user(user_id) + .await? + .context("User doesn't exist")?; + let recipient_clause = if user.is_admin { + format!( + "recipient = '{NOTIFICATION_RECIPIENT_ALL_USER}' OR recipient = '{NOTIFICATION_RECIPIENT_ADMIN}'" + ) + } else { + format!("recipient = '{NOTIFICATION_RECIPIENT_ALL_USER}'") + }; + + let query = format!( + r#" +INSERT INTO read_notifications (notification_id, user_id) +SELECT + notifications.id, + ? +FROM + notifications +LEFT JOIN + read_notifications +ON + notifications.id = read_notifications.notification_id + AND read_notifications.user_id = ? +WHERE + ({recipient_clause}) + AND read_notifications.notification_id IS NULL; + "# + ); + + sqlx::query(&query) + .bind(user_id) + .bind(user_id) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn list_notifications_within_7days( + &self, + user_id: i64, + ) -> Result> { + let user = self + .get_user(user_id) + .await? + .context("User doesn't exist")?; + let recipient_clause = if user.is_admin { + format!( + "recipient = '{NOTIFICATION_RECIPIENT_ALL_USER}' OR recipient = '{NOTIFICATION_RECIPIENT_ADMIN}'" + ) + } else { + format!("recipient = '{NOTIFICATION_RECIPIENT_ALL_USER}'") + }; + let date_7days_ago = Utc::now() - Duration::days(7); + let sql = format!( + r#" +SELECT + notifications.id, + notifications.created_at, + notifications.updated_at, + notifications.recipient, + notifications.content, + CASE + WHEN read_notifications.user_id = '{user_id}' THEN 1 + ELSE 0 + END AS read +FROM + notifications +LEFT JOIN + read_notifications +ON + notifications.id = read_notifications.notification_id + AND read_notifications.user_id = '{user_id}' +WHERE + ({recipient_clause}) + AND notifications.created_at > '{date_7days_ago}' + "# + ); + let notifications = query_as(&sql).fetch_all(&self.pool).await?; + Ok(notifications) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::testutils; + + /// Smoke test to ensure sql query is valid, actual functionality test shall happens at service level. + #[tokio::test] + async fn smoketest_list_notifications() { + let db = DbConn::new_in_memory().await.unwrap(); + let user1 = testutils::create_user(&db).await; + let notifications = db.list_notifications_within_7days(user1).await.unwrap(); + assert!(notifications.is_empty()) + } +} diff --git a/ee/tabby-db/src/oauth_credential.rs b/ee/tabby-db/src/oauth_credential.rs new file mode 100644 index 000000000000..005a12031303 --- /dev/null +++ b/ee/tabby-db/src/oauth_credential.rs @@ -0,0 +1,182 @@ +use anyhow::{anyhow, Result}; +use chrono::{DateTime, Utc}; +use sqlx::{prelude::FromRow, query, query_scalar}; + +use crate::DbConn; + +#[derive(FromRow)] +pub struct OAuthCredentialDAO { + pub provider: String, + pub config_url: Option, + pub config_scopes: Option, + pub client_id: String, + pub client_secret: String, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +impl DbConn { + pub async fn update_oauth_credential( + &self, + provider: &str, + config_url: Option<&str>, + config_scopes: Option<&str>, + client_id: &str, + client_secret: Option<&str>, + ) -> Result<()> { + let client_id = client_id.to_string(); + let mut transaction = self.pool.begin().await?; + let client_secret = match client_secret { + Some(secret) => secret.to_string(), + None => { + query_scalar!( + "SELECT client_secret FROM oauth_credential WHERE provider = ?", + provider + ) + .fetch_one(&mut *transaction) + .await.map_err(|_| anyhow!("Must specify client secret when updating the OAuth credential for the first time"))? + } + }; + query!( + r#"INSERT INTO oauth_credential (provider, config_url, config_scopes, client_id, client_secret) + VALUES ($1, $2, $3, $4, $5) ON CONFLICT(provider) DO UPDATE + SET config_url = $2, config_scopes = $3, client_id = $4, client_secret = $5, updated_at = datetime('now') + WHERE provider = $1"#, + provider, + config_url, + config_scopes, + client_id, + client_secret, + ) + .execute(&mut *transaction) + .await?; + transaction.commit().await?; + Ok(()) + } + + pub async fn delete_oauth_credential(&self, provider: &str) -> Result<()> { + query!("DELETE FROM oauth_credential WHERE provider = ?", provider) + .execute(&self.pool) + .await?; + Ok(()) + } + + pub async fn read_oauth_credential( + &self, + provider: &str, + ) -> Result> { + let token = sqlx::query_as!( + OAuthCredentialDAO, + r#"SELECT provider, config_url, config_scopes, client_id, client_secret, created_at as "created_at!: DateTime", updated_at as "updated_at!: DateTime" FROM oauth_credential WHERE provider = ?"#, + provider + ) + .fetch_optional(&self.pool).await?; + Ok(token) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_update_github_oauth_credential() { + let conn = DbConn::new_in_memory().await.unwrap(); + + // test insert + conn.update_oauth_credential("github", None, None, "client_id", Some("client_secret")) + .await + .unwrap(); + let res = conn.read_oauth_credential("github").await.unwrap().unwrap(); + assert_eq!(res.client_id, "client_id"); + assert_eq!(res.client_secret, "client_secret"); + + // test update + conn.update_oauth_credential("github", None, None, "client_id", Some("client_secret_2")) + .await + .unwrap(); + let res = conn.read_oauth_credential("github").await.unwrap().unwrap(); + assert_eq!(res.client_id, "client_id"); + assert_eq!(res.client_secret, "client_secret_2"); + + conn.update_oauth_credential("github", None, None, "client_id", None) + .await + .unwrap(); + let res = conn.read_oauth_credential("github").await.unwrap().unwrap(); + assert_eq!(res.client_id, "client_id"); + assert_eq!(res.client_secret, "client_secret_2"); + + // test delete + conn.delete_oauth_credential("github").await.unwrap(); + assert!(conn + .read_oauth_credential("github") + .await + .unwrap() + .is_none()); + + // test update after delete + conn.update_oauth_credential("github", None, None, "client_id_2", Some("client_secret_2")) + .await + .unwrap(); + let res = conn.read_oauth_credential("github").await.unwrap().unwrap(); + assert_eq!(res.client_id, "client_id_2"); + assert_eq!(res.client_secret, "client_secret_2"); + } + + #[tokio::test] + async fn test_update_google_oauth_credential() { + let conn = DbConn::new_in_memory().await.unwrap(); + + // test insert + conn.update_oauth_credential("google", None, None, "client_id", Some("client_secret")) + .await + .unwrap(); + let res = conn.read_oauth_credential("google").await.unwrap().unwrap(); + assert_eq!(res.client_id, "client_id"); + assert_eq!(res.client_secret, "client_secret"); + + // test delete + conn.delete_oauth_credential("google").await.unwrap(); + let res = conn.read_oauth_credential("google").await.unwrap(); + assert!(res.is_none()); + + // test insert with redirect_uri + conn.update_oauth_credential("google", None, None, "client_id", Some("client_secret")) + .await + .unwrap(); + conn.read_oauth_credential("google").await.unwrap().unwrap(); + + conn.update_oauth_credential("google", None, None, "client_id", None) + .await + .unwrap(); + let res = conn.read_oauth_credential("google").await.unwrap().unwrap(); + assert_eq!(res.client_id, "client_id"); + assert_eq!(res.client_secret, "client_secret"); + + // test update + conn.update_oauth_credential("google", None, None, "client_id_2", Some("client_secret_2")) + .await + .unwrap(); + let res = conn.read_oauth_credential("google").await.unwrap().unwrap(); + assert_eq!(res.client_id, "client_id_2"); + assert_eq!(res.client_secret, "client_secret_2"); + } + + #[tokio::test] + async fn test_insert_two_provider() { + let conn = DbConn::new_in_memory().await.unwrap(); + + conn.update_oauth_credential("google", None, None, "client_id", Some("client_secret")) + .await + .unwrap(); + let google = conn.read_oauth_credential("google").await.unwrap().unwrap(); + assert_eq!(google.provider, "google"); + + conn.update_oauth_credential("github", None, None, "client_id", Some("client_secret")) + .await + .unwrap(); + let github = conn.read_oauth_credential("github").await.unwrap().unwrap(); + + assert_eq!(github.provider, "github"); + } +} diff --git a/ee/tabby-db/src/pages.rs b/ee/tabby-db/src/pages.rs new file mode 100644 index 000000000000..f6f7212a0f84 --- /dev/null +++ b/ee/tabby-db/src/pages.rs @@ -0,0 +1,416 @@ +use anyhow::Result; +use chrono::{DateTime, Utc}; +use sqlx::{query, query_as, types::Json, FromRow}; +use tabby_db_macros::query_paged_as; + +use crate::{Attachment, AttachmentCode, AttachmentCodeFileList, AttachmentDoc, DbConn}; + +#[derive(FromRow)] +pub struct PageDAO { + pub id: i64, + pub author_id: i64, + pub title: Option, + pub code_source_id: Option, + pub content: Option, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +#[derive(sqlx::FromRow)] +pub struct PageSectionDAO { + pub id: i64, + pub page_id: i64, + + pub title: String, + pub content: Option, + pub position: i64, + pub attachment: Option>, + + pub created_at: DateTime, + pub updated_at: DateTime, +} + +impl DbConn { + pub async fn create_page(&self, author_id: i64, code_source_id: Option<&str>) -> Result { + let res = query!( + "INSERT INTO pages(author_id, code_source_id) VALUES (?, ?)", + author_id, + code_source_id + ) + .execute(&self.pool) + .await?; + + Ok(res.last_insert_rowid()) + } + + pub async fn update_page_title(&self, page_id: i64, title: &str) -> Result<()> { + query!( + "UPDATE pages SET title = ?, updated_at = DATETIME('now') WHERE id = ?", + title, + page_id + ) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn append_page_content(&self, page_id: i64, content: &str) -> Result<()> { + query!( + "UPDATE pages SET content = COALESCE(content, '') || ?, updated_at = DATETIME('now') WHERE id = ?", + content, + page_id + ) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn update_page_content(&self, page_id: i64, content: &str) -> Result<()> { + query!( + "UPDATE pages SET content = ?, updated_at = DATETIME('now') WHERE id = ?", + content, + page_id + ) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn list_pages( + &self, + ids: Option<&[i64]>, + limit: Option, + skip_id: Option, + backwards: bool, + ) -> Result> { + let condition = match ids { + Some(ids) => format!( + "id IN ({})", + ids.iter() + .map(|id| id.to_string()) + .collect::>() + .join(",") + ), + None => "1 = 1".to_string(), + }; + + let pages = query_paged_as!( + PageDAO, + "pages", + [ + "id", + "author_id", + "title", + "code_source_id", + "content", + "created_at" as "created_at: DateTime", + "updated_at" as "updated_at: DateTime" + ], + limit, + skip_id, + backwards, + Some(condition) + ) + .fetch_all(&self.pool) + .await?; + + Ok(pages) + } + + pub async fn delete_page(&self, id: i64) -> Result<()> { + query!("DELETE FROM pages WHERE id = ?", id,) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn get_page(&self, id: i64) -> Result> { + let page = query_as!( + PageDAO, + r#"SELECT + id, + author_id, + title, + code_source_id, + content, + created_at as "created_at: DateTime", + updated_at as "updated_at: DateTime" + FROM pages + WHERE id = ?"#, + id + ) + .fetch_optional(&self.pool) + .await?; + + Ok(page) + } + + pub async fn get_page_title(&self, id: i64) -> Result { + query!( + r#"SELECT + title + FROM pages + WHERE id = ?"#, + id + ) + .fetch_optional(&self.pool) + .await? + .ok_or_else(|| anyhow::anyhow!("Page not found"))? + .title + .ok_or_else(|| anyhow::anyhow!("Title not found")) + } + + pub async fn list_page_sections( + &self, + page_id: i64, + limit: Option, + skip_id: Option, + backwards: bool, + ) -> Result> { + let condition = format!("page_id = {page_id}"); + let sections = query_paged_as!( + PageSectionDAO, + "page_sections", + [ + "id", + "page_id", + "title", + "content", + "position", + "attachment" as "attachment: Json", + "created_at" as "created_at: DateTime", + "updated_at" as "updated_at: DateTime" + ], + limit, + skip_id, + backwards, + Some(condition) + ) + .fetch_all(&self.pool) + .await?; + + Ok(sections) + } + + pub async fn get_page_section(&self, id: i64) -> Result> { + let section = query_as!( + PageSectionDAO, + r#"SELECT + id, + page_id, + title, + position, + content, + attachment as "attachment: Json", + created_at as "created_at: DateTime", + updated_at as "updated_at: DateTime" + FROM page_sections + WHERE id = ?"#, + id + ) + .fetch_optional(&self.pool) + .await?; + + Ok(section) + } + + // create_page_section creates a new section in the specified page with the given title, + // returning the id and position of the newly created section. + pub async fn create_page_section(&self, page_id: i64, title: &str) -> Result { + let res = query!( + r#" + WITH max_pos AS ( + SELECT COALESCE(MAX(position) + 1, 0) as next_pos + FROM page_sections + WHERE page_id = ?1 + ) + INSERT INTO page_sections(page_id, title, position) + SELECT + ?1, + ?2, + (SELECT next_pos FROM max_pos) + RETURNING + id, + page_id, + title, + position, + content, + created_at as "created_at: DateTime", + updated_at as "updated_at: DateTime" + "#, + page_id, + title, + ) + .fetch_one(&self.pool) + .await?; + + Ok(PageSectionDAO { + id: res.id, + page_id: res.page_id, + title: res.title, + position: res.position, + content: res.content, + created_at: res.created_at, + updated_at: res.updated_at, + attachment: None, + }) + } + + pub async fn update_page_section_code_attachments( + &self, + section_id: i64, + code_attachments: &[AttachmentCode], + ) -> Result<()> { + let code_attachments = Json(code_attachments); + query!( + "UPDATE page_sections SET attachment = JSON_SET(attachment, '$.code', JSON(?)), updated_at = DATETIME('now') WHERE id = ?", + code_attachments, + section_id + ) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn update_page_section_code_file_list( + &self, + section_id: i64, + file_list: &[String], + truncated: bool, + ) -> Result<()> { + let code_file_list_attachment = Json(AttachmentCodeFileList { + file_list: file_list.into(), + truncated, + }); + query!( + "UPDATE page_sections SET attachment = JSON_SET(attachment, '$.code_file_list', JSON(?)), updated_at = DATETIME('now') WHERE id = ?", + code_file_list_attachment, + section_id + ) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn update_page_section_doc_attachments( + &self, + section_id: i64, + doc_attachments: &[AttachmentDoc], + ) -> Result<()> { + let doc_attachments = Json(doc_attachments); + query!( + "UPDATE page_sections SET attachment = JSON_SET(attachment, '$.doc', JSON(?)), updated_at = DATETIME('now') WHERE id = ?", + doc_attachments, + section_id + ) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn update_page_section_title(&self, id: i64, title: &str) -> Result<()> { + query!( + "UPDATE page_sections SET title = ?, updated_at = DATETIME('now') WHERE id = ?", + title, + id + ) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn append_page_section_content(&self, id: i64, content: &str) -> Result<()> { + query!( + "UPDATE page_sections SET content = COALESCE(content, '') || ?, updated_at = DATETIME('now') WHERE id = ?", + content, + id + ) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn update_page_section_content(&self, id: i64, content: &str) -> Result<()> { + query!( + "UPDATE page_sections SET content = ?, updated_at = DATETIME('now') WHERE id = ?", + content, + id + ) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn delete_page_sections_without_content(&self, page_id: i64) -> Result<()> { + query!( + "DELETE FROM page_sections WHERE page_id = ? AND COALESCE(content, '') = ''", + page_id + ) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn delete_page_section(&self, id: i64) -> Result<()> { + query!("DELETE FROM page_sections WHERE id = ?", id,) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn move_page_section(&self, page_id: i64, id: i64, up: bool) -> Result<()> { + let mut tx = self.pool.begin().await?; + + let current_position = query!("SELECT position FROM page_sections WHERE id = ?", id) + .fetch_one(&mut *tx) + .await? + .position; + let new_position = if up { + current_position - 1 + } else { + current_position + 1 + }; + + let swap_section_id = query!( + "SELECT id FROM page_sections WHERE page_id = ? AND position = ?", + page_id, + new_position + ) + .fetch_one(&mut *tx) + .await? + .id; + + query!("UPDATE page_sections SET position = ? WHERE id = ?", -1, id) + .execute(&mut *tx) + .await?; + query!( + "UPDATE page_sections SET position = ? WHERE id = ?", + current_position, + swap_section_id + ) + .execute(&mut *tx) + .await?; + query!( + "UPDATE page_sections SET position = ? WHERE id = ?", + new_position, + id + ) + .execute(&mut *tx) + .await?; + + tx.commit().await?; + Ok(()) + } +} diff --git a/ee/tabby-db/src/password_reset.rs b/ee/tabby-db/src/password_reset.rs new file mode 100644 index 000000000000..3c7d3438fd30 --- /dev/null +++ b/ee/tabby-db/src/password_reset.rs @@ -0,0 +1,99 @@ +use anyhow::{anyhow, Result}; +use chrono::{DateTime, Duration, Utc}; +use sqlx::{query, query_as}; +use uuid::Uuid; + +use crate::{AsSqliteDateTimeString, DbConn}; + +pub struct PasswordResetDAO { + pub user_id: i64, + pub code: String, + pub created_at: DateTime, +} + +impl DbConn { + pub async fn create_password_reset(&self, user_id: i64) -> Result { + let code = Uuid::new_v4().to_string(); + query!( + "INSERT INTO password_reset (user_id, code) VALUES ($1, $2) + ON CONFLICT(user_id) DO UPDATE SET code = $2, created_at = DATETIME('now');", + user_id, + code, + ) + .execute(&self.pool) + .await?; + Ok(code) + } + + pub async fn delete_password_reset_by_user_id(&self, user_id: i64) -> Result<()> { + query!("DELETE FROM password_reset WHERE user_id = ?", user_id) + .execute(&self.pool) + .await?; + Ok(()) + } + + pub async fn get_password_reset_by_code(&self, code: &str) -> Result> { + let password_reset = query_as!( + PasswordResetDAO, + r#"SELECT user_id, code, created_at as "created_at!: DateTime" FROM password_reset WHERE code = ?;"#, + code + ) + .fetch_optional(&self.pool) + .await?; + Ok(password_reset) + } + + pub async fn get_password_reset_by_user_id( + &self, + user_id: i64, + ) -> Result> { + let password_reset = query_as!( + PasswordResetDAO, + r#"SELECT user_id, code, created_at as "created_at!: DateTime" FROM password_reset WHERE user_id = ?;"#, + user_id + ) + .fetch_optional(&self.pool) + .await?; + Ok(password_reset) + } + + pub async fn verify_password_reset(&self, code: &str) -> Result { + let password_reset = self + .get_password_reset_by_code(code) + .await? + .ok_or_else(|| anyhow!("Invalid code"))?; + + let user_res = self + .get_user(password_reset.user_id) + .await? + .filter(|user| user.active) + .ok_or_else(|| anyhow!("Invalid code"))?; + + if Utc::now().signed_duration_since(password_reset.created_at) > Duration::minutes(15) { + Err(anyhow!("Invalid code")) + } else { + Ok(user_res.id) + } + } + + #[cfg(any(test, feature = "testutils"))] + pub async fn mark_password_reset_expired(&self, code: &str) -> Result<()> { + let timestamp = (Utc::now() - Duration::hours(10)).as_sqlite_datetime(); + query!( + "UPDATE password_reset SET created_at = ? WHERE code = ?;", + timestamp, + code + ) + .execute(&self.pool) + .await?; + Ok(()) + } + + pub async fn delete_expired_password_resets(&self) -> Result<()> { + let time = (Utc::now() - Duration::hours(1)).as_sqlite_datetime(); + query!("DELETE FROM password_reset WHERE created_at < ?", time) + .execute(&self.pool) + .await?; + Ok(()) + } +} diff --git a/ee/tabby-db/src/path.rs b/ee/tabby-db/src/path.rs deleted file mode 100644 index cbc9da76254b..000000000000 --- a/ee/tabby-db/src/path.rs +++ /dev/null @@ -1,18 +0,0 @@ -use std::path::PathBuf; - -use tabby_common::path::tabby_root; - -fn tabby_ee_root() -> PathBuf { - tabby_root().join("ee") -} - -pub fn db_file() -> PathBuf { - #[cfg(feature = "prod-db")] - { - tabby_ee_root().join("db.sqlite") - } - #[cfg(not(feature = "prod-db"))] - { - tabby_ee_root().join("dev-db.sqlite") - } -} diff --git a/ee/tabby-db/src/provided_repositories.rs b/ee/tabby-db/src/provided_repositories.rs new file mode 100644 index 000000000000..24afd241b87d --- /dev/null +++ b/ee/tabby-db/src/provided_repositories.rs @@ -0,0 +1,197 @@ +use anyhow::{anyhow, Result}; +use chrono::{DateTime, Utc}; +use sqlx::{prelude::FromRow, query, query_as}; +use tabby_db_macros::query_paged_as; + +use crate::{AsSqliteDateTimeString, DbConn}; + +#[derive(FromRow)] +pub struct ProvidedRepositoryDAO { + pub id: i64, + pub vendor_id: String, + pub integration_id: i64, + pub name: String, + pub git_url: String, + pub refs: Option, + pub active: bool, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +impl DbConn { + pub async fn upsert_provided_repository( + &self, + integration_id: i64, + vendor_id: String, + name: String, + git_url: String, + ) -> Result { + let res = query!( + "INSERT INTO provided_repositories (integration_id, vendor_id, name, git_url) VALUES ($1, $2, $3, $4) + ON CONFLICT(integration_id, vendor_id) DO UPDATE SET name = $3, git_url = $4, updated_at = DATETIME('now')", + integration_id, + vendor_id, + name, + git_url, + ).execute(&self.pool).await?; + Ok(res.last_insert_rowid()) + } + + pub async fn delete_outdated_provided_repositories( + &self, + integration_id: i64, + cutoff_timestamp: DateTime, + ) -> Result { + let t = cutoff_timestamp.as_sqlite_datetime(); + let res = query!( + "DELETE FROM provided_repositories WHERE integration_id = ? AND updated_at < ?;", + integration_id, + t, + ) + .execute(&self.pool) + .await?; + Ok(res.rows_affected() as usize) + } + + pub async fn get_provided_repository(&self, id: i64) -> Result { + let repo = query_as!( + ProvidedRepositoryDAO, + r#"SELECT id, vendor_id, name, git_url, refs, active, integration_id, created_at as "created_at!: DateTime", updated_at as "updated_at!: DateTime" FROM provided_repositories WHERE id = ?"#, + id + ) + .fetch_one(&self.pool) + .await?; + Ok(repo) + } + + pub async fn list_provided_repositories( + &self, + integration_ids: Vec, + kind: Option, + active: Option, + limit: Option, + skip_id: Option, + backwards: bool, + ) -> Result> { + let mut conditions = vec![]; + + let integration_ids = integration_ids + .into_iter() + .map(|id| id.to_string()) + .collect::>() + .join(", "); + if !integration_ids.is_empty() { + conditions.push(format!("integration_id IN ({integration_ids})")); + } + + let active_filter = active.map(|active| format!("active = {active}")); + conditions.extend(active_filter); + + let kind_filter = kind.map(|kind| format!("kind = '{kind}'")); + conditions.extend(kind_filter); + + let condition = (!conditions.is_empty()).then(|| conditions.join(" AND ")); + + let repos = query_paged_as!( + ProvidedRepositoryDAO, + "provided_repositories JOIN integrations ON integration_id = integrations.id", + [ + "id", + "vendor_id", + "name", + "git_url", + "refs", + "active", + "integration_id", + "created_at" as "created_at!: DateTime", + "updated_at" as "updated_at!: DateTime" + ], + limit, + skip_id, + backwards, + condition + ) + .fetch_all(&self.pool) + .await?; + Ok(repos) + } + + pub async fn update_provided_repository_active( + &self, + id: i64, + active: bool, + refs: Option>, + ) -> Result<()> { + let not_active = !active; + if let Some(refs) = refs { + let refs = if refs.is_empty() { + None + } else { + serde_json::to_string(&refs).ok() + }; + let res = query!( + "UPDATE provided_repositories SET active = ?, refs = ? WHERE id = ? AND active = ?", + active, + refs, + id, + not_active + ) + .execute(&self.pool) + .await?; + + if res.rows_affected() != 1 { + return Err(anyhow!("Repository active status was not changed")); + } + } else { + let res = query!( + "UPDATE provided_repositories SET active = ? WHERE id = ? AND active = ?", + active, + id, + not_active + ) + .execute(&self.pool) + .await?; + + if res.rows_affected() != 1 { + return Err(anyhow!("Repository active status was not changed")); + } + } + + Ok(()) + } + + pub async fn update_provided_repository_refs(&self, id: i64, refs: Vec) -> Result<()> { + let refs = if refs.is_empty() { + None + } else { + serde_json::to_string(&refs).ok() + }; + let res = query!( + "UPDATE provided_repositories SET refs = ? WHERE id = ?", + refs, + id + ) + .execute(&self.pool) + .await?; + + if res.rows_affected() != 1 { + return Err(anyhow!("Repository not found")); + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::DbConn; + + #[tokio::test] + async fn test_list_provided_repositories() { + let db = DbConn::new_in_memory().await.unwrap(); + // Ensure query does not break on the join + db.list_provided_repositories(vec![], Some("github".into()), None, None, None, false) + .await + .unwrap(); + } +} diff --git a/ee/tabby-db/src/refresh_tokens.rs b/ee/tabby-db/src/refresh_tokens.rs index cea9873d1d19..d4e2165ca6cf 100644 --- a/ee/tabby-db/src/refresh_tokens.rs +++ b/ee/tabby-db/src/refresh_tokens.rs @@ -1,30 +1,34 @@ use anyhow::Result; use chrono::{DateTime, Utc}; +use hash_ids::HashIds; +use lazy_static::lazy_static; use sqlx::{query, FromRow}; +use uuid::Uuid; -use super::DbConn; +use super::{AsSqliteDateTimeString, DbConn}; #[allow(unused)] #[derive(FromRow)] pub struct RefreshTokenDAO { - id: u32, - created_at: DateTime, + pub id: i64, + pub created_at: DateTime, - pub user_id: i32, + pub user_id: i64, pub token: String, pub expires_at: DateTime, } impl RefreshTokenDAO { pub fn is_expired(&self) -> bool { - let now = chrono::Utc::now(); + let now = Utc::now(); self.expires_at < now } } /// db read/write operations for `refresh_tokens` table impl DbConn { - pub async fn create_refresh_token(&self, user_id: i32, token: &str) -> Result<()> { + pub async fn create_refresh_token(&self, user_id: i64) -> Result { + let token = generate_refresh_token(0); let res = query!( r#"INSERT INTO refresh_tokens (user_id, token, expires_at) VALUES (?, ?, datetime('now', '+7 days'))"#, user_id, @@ -35,14 +39,16 @@ impl DbConn { return Err(anyhow::anyhow!("failed to create refresh token")); } - Ok(()) + Ok(token) } - pub async fn replace_refresh_token(&self, old: &str, new: &str) -> Result<()> { + pub async fn renew_refresh_token(&self, id: i64, old: &str) -> Result { + let new = generate_refresh_token(id); let res = query!( - "UPDATE refresh_tokens SET token = $1 WHERE token = $2", + "UPDATE refresh_tokens SET token = $1, expires_at = datetime('now', '+7 days') WHERE token = $2 AND id = $3", new, - old + old, + id ) .execute(&self.pool) .await?; @@ -51,11 +57,11 @@ impl DbConn { return Err(anyhow::anyhow!("failed to replace refresh token")); } - Ok(()) + Ok(new) } pub async fn delete_expired_token(&self) -> Result { - let time = Utc::now(); + let time = Utc::now().as_sqlite_datetime(); let res = query!(r#"DELETE FROM refresh_tokens WHERE expires_at < ?"#, time) .execute(&self.pool) .await?; @@ -64,48 +70,82 @@ impl DbConn { } pub async fn get_refresh_token(&self, token: &str) -> Result> { - let token = sqlx::query_as("SELECT * FROM refresh_tokens WHERE token = ?") - .bind(token) - .fetch_optional(&self.pool) - .await?; + let token = sqlx::query_as!( + RefreshTokenDAO, + r#"SELECT id as "id!", created_at as "created_at!: DateTime", expires_at as "expires_at!: DateTime", user_id, token FROM refresh_tokens WHERE token = ?"#, + token + ) + .fetch_optional(&self.pool) + .await?; Ok(token) } + + pub async fn delete_tokens_by_user_id(&self, id: i64) -> Result<()> { + query!("DELETE FROM refresh_tokens WHERE user_id = ?", id) + .execute(&self.pool) + .await?; + Ok(()) + } +} + +lazy_static! { + static ref HASHER: HashIds = HashIds::builder() + .with_salt("tabby-refresh-token") + .with_min_length(6) + .finish(); +} + +pub fn generate_refresh_token(id: i64) -> String { + let uuid = Uuid::new_v4().to_string().replace('-', ""); + let id = HASHER.encode(&[id as u64]); + format!("{id}{uuid}") } #[cfg(test)] mod tests { - use std::ops::Add; - use super::*; #[tokio::test] async fn test_create_refresh_token() { let conn = DbConn::new_in_memory().await.unwrap(); - - conn.create_refresh_token(1, "test").await.unwrap(); - - let token = conn.get_refresh_token("test").await.unwrap().unwrap(); - - assert_eq!(token.user_id, 1); - assert_eq!(token.token, "test"); - assert!(token.expires_at > Utc::now().add(chrono::Duration::days(6))); - assert!(token.expires_at < Utc::now().add(chrono::Duration::days(7))); + let user_id = conn + .create_user("email@email".into(), None, true, None) + .await + .unwrap(); + let token = conn.create_refresh_token(user_id).await.unwrap(); + + let dao = conn.get_refresh_token(&token).await.unwrap().unwrap(); + + assert_eq!(dao.user_id, 1); + assert_eq!(dao.token, token); + assert!(dao.expires_at > Utc::now() + chrono::Duration::days(6)); + assert!(dao.expires_at < Utc::now() + chrono::Duration::days(7)); } #[tokio::test] async fn test_replace_refresh_token() { let conn = DbConn::new_in_memory().await.unwrap(); - conn.create_refresh_token(1, "test").await.unwrap(); - conn.replace_refresh_token("test", "test2").await.unwrap(); + let user_id = conn + .create_user("email@email".into(), None, true, None) + .await + .unwrap(); + let old = conn.create_refresh_token(user_id).await.unwrap(); + let new = conn.renew_refresh_token(1, &old).await.unwrap(); - let token = conn.get_refresh_token("test").await.unwrap(); + let token = conn.get_refresh_token(&old).await.unwrap(); assert!(token.is_none()); - let token = conn.get_refresh_token("test2").await.unwrap().unwrap(); + let token = conn.get_refresh_token(&new).await.unwrap().unwrap(); assert_eq!(token.user_id, 1); - assert_eq!(token.token, "test2"); + assert_eq!(token.token, new); + } + + #[tokio::test] + async fn test_delete_expired_token() { + let conn = DbConn::new_in_memory().await.unwrap(); + assert!(conn.delete_expired_token().await.is_ok()); } } diff --git a/ee/tabby-db/src/repositories.rs b/ee/tabby-db/src/repositories.rs index 1acc2067cfc2..d3604cd194e7 100644 --- a/ee/tabby-db/src/repositories.rs +++ b/ee/tabby-db/src/repositories.rs @@ -1,13 +1,15 @@ use anyhow::{anyhow, Result}; use sqlx::{prelude::FromRow, query}; +use tabby_db_macros::query_paged_as; -use crate::DbConn; +use crate::{DbConn, SQLXResultExt}; #[derive(FromRow)] pub struct RepositoryDAO { - pub id: i32, + pub id: i64, pub name: String, pub git_url: String, + pub refs: Option, } impl DbConn { @@ -17,42 +19,69 @@ impl DbConn { skip_id: Option, backwards: bool, ) -> Result> { - let query = Self::make_pagination_query( + let repos = query_paged_as!( + RepositoryDAO, "repositories", - &["id", "name", "git_url"], + ["id", "name", "git_url", "refs"], limit, skip_id, - backwards, - ); - - let repos = sqlx::query_as(&query).fetch_all(&self.pool).await?; + backwards + ) + .fetch_all(&self.pool) + .await?; Ok(repos) } - pub async fn delete_repository(&self, id: i32) -> Result { + pub async fn delete_repository(&self, id: i64) -> Result { let res = query!("DELETE FROM repositories WHERE id = ?", id) .execute(&self.pool) .await?; Ok(res.rows_affected() == 1) } - pub async fn create_repository(&self, name: String, git_url: String) -> Result { + pub async fn create_repository( + &self, + name: String, + git_url: String, + refs: Vec, + ) -> Result { + let refs = if refs.is_empty() { + None + } else { + Some(serde_json::to_string(&refs)?) + }; + let res = query!( - "INSERT INTO repositories (name, git_url) VALUES (?, ?)", + "INSERT INTO repositories (name, git_url, refs) VALUES (?, ?, ?)", name, - git_url + git_url, + refs ) .execute(&self.pool) - .await?; + .await; - Ok(res.last_insert_rowid() as i32) + res.unique_error("A repository with the same name or URL already exists") + .map(|output| output.last_insert_rowid()) } - pub async fn update_repository(&self, id: i32, name: String, git_url: String) -> Result<()> { + pub async fn update_repository( + &self, + id: i64, + name: String, + git_url: String, + refs: Vec, + ) -> Result<()> { + let refs = if refs.is_empty() { + None + } else { + Some(serde_json::to_string(&refs)?) + }; + let rows = query!( - "UPDATE repositories SET name = ?, git_url = ? WHERE id = ?", + "UPDATE repositories SET name = ?, git_url = ?, refs = ? WHERE id = ?", name, git_url, + refs, id ) .execute(&self.pool) @@ -63,6 +92,17 @@ impl DbConn { Err(anyhow!("failed to update: repository not found")) } } + + pub async fn get_repository(&self, id: i64) -> Result { + let repository = sqlx::query_as!( + RepositoryDAO, + "SELECT id as 'id!: i64', name, git_url, refs FROM repositories WHERE id = ?", + id + ) + .fetch_one(&self.pool) + .await?; + Ok(repository) + } } #[cfg(test)] @@ -74,7 +114,7 @@ mod tests { let conn = DbConn::new_in_memory().await.unwrap(); // Insert new repository - conn.create_repository("test".into(), "testurl".into()) + conn.create_repository("test".into(), "testurl".into(), vec!["main".into()]) .await .unwrap(); @@ -84,10 +124,11 @@ mod tests { .await .unwrap()[0]; assert_eq!(repository.git_url, "testurl"); + assert_eq!(repository.refs, Some("[\"main\"]".to_string())); // Update the repository let id = repository.id; - conn.update_repository(id, "test2".into(), "testurl2".into()) + conn.update_repository(id, "test2".into(), "testurl2".into(), vec!["main".into()]) .await .unwrap(); @@ -99,4 +140,34 @@ mod tests { assert_eq!(repository.git_url, "testurl2"); assert_eq!(repository.name, "test2"); } + + #[tokio::test] + async fn test_create_and_update_repository_with_empty_refs() { + let conn = DbConn::new_in_memory().await.unwrap(); + + // Insert new repository with empty refs + conn.create_repository("test".into(), "testurl".into(), vec![]) + .await + .unwrap(); + + // Test that the refs defaults to None + let repository = &conn + .list_repositories_with_filter(None, None, false) + .await + .unwrap()[0]; + assert_eq!(repository.refs, None); + + // Update the repository with empty refs + let id = repository.id; + conn.update_repository(id, "test2".into(), "testurl2".into(), vec![]) + .await + .unwrap(); + + // Check the refs is still None + let repository = &conn + .list_repositories_with_filter(None, None, false) + .await + .unwrap()[0]; + assert_eq!(repository.refs, None); + } } diff --git a/ee/tabby-db/src/server_setting.rs b/ee/tabby-db/src/server_setting.rs new file mode 100644 index 000000000000..9397fa9d7ddb --- /dev/null +++ b/ee/tabby-db/src/server_setting.rs @@ -0,0 +1,189 @@ +use anyhow::Result; +use sqlx::{prelude::FromRow, query, Sqlite, Transaction}; + +use crate::DbConn; + +#[derive(Debug, PartialEq, FromRow)] +pub struct ServerSettingDAO { + billing_enterprise_license: Option, + security_allowed_register_domain_list: Option, + pub security_disable_client_side_telemetry: bool, + pub security_disable_password_login: bool, + pub network_external_url: String, + pub branding_logo: Option, + pub branding_icon: Option, +} + +const SERVER_SETTING_ROW_ID: i32 = 1; + +impl ServerSettingDAO { + pub fn security_allowed_register_domain_list(&self) -> impl Iterator { + self.security_allowed_register_domain_list + .iter() + .flat_map(|s| s.split(',')) + .map(|x| x.trim()) + .filter(|s| !s.is_empty()) + } +} + +impl DbConn { + async fn internal_read_server_setting( + &self, + transaction: &mut Transaction<'_, Sqlite>, + ) -> Result> { + let setting: Option = sqlx::query_as( + "SELECT + security_disable_client_side_telemetry, + network_external_url, + security_allowed_register_domain_list, + billing_enterprise_license, + security_disable_password_login, + branding_logo, + branding_icon + FROM server_setting + WHERE id = ?;", + ) + .bind(SERVER_SETTING_ROW_ID) + .fetch_optional(&mut **transaction) + .await?; + Ok(setting) + } + + pub async fn read_server_setting(&self) -> Result { + let mut transaction = self.pool.begin().await?; + let setting = self.internal_read_server_setting(&mut transaction).await?; + let Some(setting) = setting else { + query!( + "INSERT INTO server_setting (id) VALUES (?);", + SERVER_SETTING_ROW_ID + ) + .execute(&mut *transaction) + .await?; + let setting = self + .internal_read_server_setting(&mut transaction) + .await? + .expect("Freshly-written row must always be present"); + transaction.commit().await?; + return Ok(setting); + }; + Ok(setting) + } + + pub async fn update_security_setting( + &self, + allowed_register_domain_list: Option, + disable_client_side_telemetry: bool, + disable_password_login: bool, + ) -> Result<()> { + query!( + "INSERT INTO server_setting ( + id, + security_allowed_register_domain_list, + security_disable_client_side_telemetry, + security_disable_password_login + ) + VALUES ($1, $2, $3, $4) + ON CONFLICT(id) DO + UPDATE SET + security_allowed_register_domain_list = $2, + security_disable_client_side_telemetry = $3, + security_disable_password_login = $4", + SERVER_SETTING_ROW_ID, + allowed_register_domain_list, + disable_client_side_telemetry, + disable_password_login, + ) + .execute(&self.pool) + .await?; + Ok(()) + } + + pub async fn update_network_setting(&self, external_url: String) -> Result<()> { + query!( + "INSERT INTO server_setting (id, network_external_url) VALUES ($1, $2) + ON CONFLICT(id) DO UPDATE SET network_external_url = $2", + SERVER_SETTING_ROW_ID, + external_url + ) + .execute(&self.pool) + .await?; + Ok(()) + } + + pub async fn update_branding_setting( + &self, + branding_logo: Option, + branding_icon: Option, + ) -> Result<()> { + sqlx::query!( + "UPDATE server_setting SET branding_logo = ?, branding_icon = ? WHERE id = ?", + branding_logo, + branding_icon, + SERVER_SETTING_ROW_ID + ) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn read_enterprise_license(&self) -> Result> { + Ok(sqlx::query_scalar( + "SELECT billing_enterprise_license FROM server_setting WHERE id = ?;", + ) + .bind(SERVER_SETTING_ROW_ID) + .fetch_one(&self.pool) + .await?) + } + + pub async fn update_enterprise_license( + &self, + enterprise_license: Option, + ) -> Result<()> { + query!( + "INSERT INTO server_setting (id, billing_enterprise_license) VALUES ($1, $2) + ON CONFLICT(id) DO UPDATE SET billing_enterprise_license = $2", + SERVER_SETTING_ROW_ID, + enterprise_license + ) + .execute(&self.pool) + .await?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_dao(security_allowed_register_domain_list: Option) -> ServerSettingDAO { + ServerSettingDAO { + billing_enterprise_license: None, + security_allowed_register_domain_list, + security_disable_client_side_telemetry: false, + security_disable_password_login: false, + network_external_url: "http://localhost:8080".into(), + branding_logo: None, + branding_icon: None, + } + } + #[test] + fn test_security_allowed_register_domain_list() { + let dao = make_dao(None); + let domains: Vec<_> = dao.security_allowed_register_domain_list().collect(); + assert!(domains.is_empty()); + + let dao = make_dao(Some("".into())); + let domains: Vec<_> = dao.security_allowed_register_domain_list().collect(); + assert!(domains.is_empty()); + + let dao = make_dao(Some(" ".into())); + let domains: Vec<_> = dao.security_allowed_register_domain_list().collect(); + assert!(domains.is_empty()); + + let dao = make_dao(Some("abc.com, def.com".into())); + let domains: Vec<_> = dao.security_allowed_register_domain_list().collect(); + assert_eq!(domains[0], "abc.com"); + assert_eq!(domains[1], "def.com"); + } +} diff --git a/ee/tabby-db/src/threads.rs b/ee/tabby-db/src/threads.rs new file mode 100644 index 000000000000..5b2ea8d42a1f --- /dev/null +++ b/ee/tabby-db/src/threads.rs @@ -0,0 +1,475 @@ +use anyhow::{bail, Result}; +use chrono::{DateTime, Duration, Utc}; +use sqlx::{query, query_as, types::Json, FromRow}; +use tabby_db_macros::query_paged_as; + +use crate::{ + attachment::{ + Attachment, AttachmentClientCode, AttachmentCode, AttachmentCodeFileList, AttachmentDoc, + }, + AsSqliteDateTimeString, DbConn, +}; + +#[derive(FromRow)] +pub struct ThreadDAO { + pub id: i64, + pub user_id: i64, + pub is_ephemeral: bool, + pub relevant_questions: Option>>, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +#[derive(sqlx::FromRow)] +pub struct ThreadMessageDAO { + pub id: i64, + pub thread_id: i64, + + pub role: String, + pub content: String, + + pub code_source_id: Option, + pub attachment: Option>, + + // Deprecated since 0.25 (not removed from db yet). + // FIXME(meng): remove these columns from db in 0.26. + // pub code_attachments: Option>>, + // pub client_code_attachments: Option>>, + // pub doc_attachments: Option>>, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +impl DbConn { + pub async fn create_thread(&self, user_id: i64, is_ephemeral: bool) -> Result { + let res = query!( + "INSERT INTO threads(user_id, is_ephemeral) VALUES (?, ?)", + user_id, + is_ephemeral + ) + .execute(&self.pool) + .await?; + + Ok(res.last_insert_rowid()) + } + + pub async fn list_threads( + &self, + ids: Option<&[i64]>, + user_id: Option, + is_ephemeral: Option, + limit: Option, + skip_id: Option, + backwards: bool, + ) -> Result> { + let mut conditions = vec![]; + + if let Some(ids) = ids { + let ids: Vec = ids.iter().map(i64::to_string).collect(); + let ids = ids.join(", "); + conditions.push(format!("id in ({ids})")); + } + + if let Some(user_id) = user_id { + conditions.push(format!("user_id = {user_id}")); + } + + if let Some(is_ephemeral) = is_ephemeral { + if is_ephemeral { + conditions.push("is_ephemeral".to_string()); + } else { + conditions.push("NOT is_ephemeral".to_string()); + } + } + + let condition = (!conditions.is_empty()).then_some(conditions.join(" AND ")); + let threads = query_paged_as!( + ThreadDAO, + "threads", + [ + "id", + "user_id", + "is_ephemeral", + "relevant_questions" as "relevant_questions: Json>", + "created_at" as "created_at: DateTime", + "updated_at" as "updated_at: DateTime" + ], + limit, + skip_id, + backwards, + condition + ) + .fetch_all(&self.pool) + .await?; + + Ok(threads) + } + + pub async fn update_thread_relevant_questions( + &self, + thread_id: i64, + relevant_questions: &[String], + ) -> Result<()> { + let relevant_questions = Json(relevant_questions.to_vec()); + query!( + "UPDATE threads SET relevant_questions = ?, updated_at = DATETIME('now') WHERE id = ?", + relevant_questions, + thread_id + ) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn update_thread_ephemeral(&self, thread_id: i64, is_ephemeral: bool) -> Result<()> { + query!( + "UPDATE threads SET is_ephemeral = ?, updated_at = DATETIME('now') WHERE id = ?", + is_ephemeral, + thread_id + ) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn create_thread_message( + &self, + thread_id: i64, + role: &str, + content: &str, + code_attachments: Option<&[AttachmentCode]>, + client_code_attachments: Option<&[AttachmentClientCode]>, + doc_attachments: Option<&[AttachmentDoc]>, + verify_last_message_role: bool, + ) -> Result { + if verify_last_message_role { + let last_message = self.get_last_thread_message(thread_id).await?; + if let Some(last_message) = last_message { + if last_message.role == role { + bail!("Cannot send two messages in a row with the same role"); + } + } + } + + let code_attachments = code_attachments.map(Json); + let client_code_attachments = client_code_attachments.map(Json); + let doc_attachments = doc_attachments.map(Json); + let res = query!( + r#"INSERT INTO thread_messages( + thread_id, + role, + content, + attachment + ) VALUES (?, ?, ?, JSON_OBJECT('code', JSON(?), 'client_code', JSON(?), 'doc', JSON(?)))"#, + thread_id, + role, + content, + code_attachments, + client_code_attachments, + doc_attachments, + ) + .execute(&self.pool) + .await?; + + Ok(res.last_insert_rowid()) + } + + pub async fn update_thread_message_code_file_list_attachment( + &self, + message_id: i64, + file_list: &[String], + truncated: bool, + ) -> Result<()> { + let code_file_list_attachment = Json(AttachmentCodeFileList { + file_list: file_list.into(), + truncated, + }); + query!( + "UPDATE thread_messages SET attachment = JSON_SET(attachment, '$.code_file_list', JSON(?)), updated_at = DATETIME('now') WHERE id = ?", + code_file_list_attachment, + message_id + ) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn update_thread_message_code_attachments( + &self, + message_id: i64, + code_attachments: &[AttachmentCode], + ) -> Result<()> { + let code_attachments = Json(code_attachments); + query!( + "UPDATE thread_messages SET attachment = JSON_SET(attachment, '$.code', JSON(?)), updated_at = DATETIME('now') WHERE id = ?", + code_attachments, + message_id + ) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn update_thread_message_code_source_id( + &self, + message_id: i64, + code_source_id: &str, + ) -> Result<()> { + query!( + "UPDATE thread_messages SET code_source_id = ?, updated_at = DATETIME('now') WHERE id = ?", + code_source_id, + message_id + ) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn update_thread_message_doc_attachments( + &self, + message_id: i64, + doc_attachments: &[AttachmentDoc], + ) -> Result<()> { + let doc_attachments = Json(doc_attachments); + query!( + "UPDATE thread_messages SET attachment = JSON_SET(attachment, '$.doc', JSON(?)), updated_at = DATETIME('now') WHERE id = ?", + doc_attachments, + message_id + ) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn update_thread_message_content( + &self, + thread_id: i64, + message_id: i64, + content: &str, + ) -> Result<()> { + query!( + "UPDATE thread_messages SET content = ?, updated_at = DATETIME('now') WHERE thread_id = ? AND id = ?", + content, + thread_id, + message_id + ) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn append_thread_message_content( + &self, + message_id: i64, + content: &str, + ) -> Result<()> { + query!( + "UPDATE thread_messages SET content = content || ?, updated_at = DATETIME('now') WHERE id = ?", + content, + message_id + ) + .execute(&self.pool) + .await?; + + Ok(()) + } + + async fn get_last_thread_message(&self, thread_id: i64) -> Result> { + let message = query_as!( + ThreadMessageDAO, + r#"SELECT + id, + thread_id, + role, + content, + code_source_id, + attachment as "attachment: Json", + created_at as "created_at: DateTime", + updated_at as "updated_at: DateTime" + FROM thread_messages + WHERE thread_id = ? + ORDER BY id DESC + LIMIT 1"#, + thread_id + ) + .fetch_optional(&self.pool) + .await?; + + Ok(message) + } + + pub async fn list_thread_messages( + &self, + thread_id: i64, + limit: Option, + skip_id: Option, + backwards: bool, + ) -> Result> { + let condition = format!("thread_id = {thread_id}"); + let messages = query_paged_as!( + ThreadMessageDAO, + "thread_messages", + [ + "id", + "thread_id", + "role", + "content", + "code_source_id", + "attachment" as "attachment: Json", + "created_at" as "created_at: DateTime", + "updated_at" as "updated_at: DateTime" + ], + limit, + skip_id, + backwards, + Some(condition) + ) + .fetch_all(&self.pool) + .await?; + + Ok(messages) + } + + pub async fn delete_thread_message_pair( + &self, + thread_id: i64, + user_message_id: i64, + assistant_message_id: i64, + ) -> Result<()> { + #[derive(FromRow)] + struct Response { + id: i64, + role: String, + } + + let message = query_as!( + Response, + "SELECT id, role FROM thread_messages WHERE thread_id = ? AND id >= ? AND id <= ?", + thread_id, + user_message_id, + assistant_message_id + ) + .fetch_all(&self.pool) + .await?; + + if message.len() != 2 { + bail!("Thread message pair is not valid") + } + + let is_valid_user_message = message[0].id == user_message_id && message[0].role == "user"; + let is_valid_assistant_message = + message[1].id == assistant_message_id && message[1].role == "assistant"; + + if !is_valid_user_message || !is_valid_assistant_message { + bail!("Invalid message pair"); + } + + query!( + "DELETE FROM thread_messages WHERE id = ? or id = ?", + user_message_id, + assistant_message_id + ) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn delete_thread(&self, id: i64) -> Result<()> { + query!("DELETE FROM threads WHERE id = ?", id,) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn delete_expired_ephemeral_threads(&self) -> Result { + let time = (Utc::now() - Duration::days(7)).as_sqlite_datetime(); + + let res = query!( + r#" + DELETE FROM threads + WHERE + id IN ( + SELECT + id + FROM + ( + SELECT + threads.id, + + --- Get the latest updated_at time from the thread and its messages using MAX aggregation + MAX( + CASE + --- If there are no messages, use the thread's updated_at time + WHEN thread_messages.updated_at IS NULL THEN threads.updated_at + + --- Otherwise, use the message's updated_at time + ELSE thread_messages.updated_at + END + ) AS last_updated_at + FROM + threads + LEFT JOIN thread_messages ON threads.id = thread_messages.thread_id + WHERE + is_ephemeral + GROUP BY + 1 + HAVING + last_updated_at < ? + ) + ); + "#, + time + ) + .execute(&self.pool) + .await?; + Ok(res.rows_affected() as usize) + } +} + +#[cfg(test)] +mod tests { + use crate::{testutils, DbConn}; + + #[tokio::test] + async fn test_delete_expired_threads() { + let db = DbConn::new_in_memory().await.unwrap(); + assert_eq!(db.delete_expired_ephemeral_threads().await.unwrap(), 0); + + let user_id = testutils::create_user(&db).await; + let _ephemeral_thread_id = db.create_thread(user_id, true).await.unwrap(); + let non_ephemeral_thread_id = db.create_thread(user_id, false).await.unwrap(); + + // Update the updated_at time to be 8 days ago for all threads + sqlx::query!("UPDATE threads SET updated_at = DATETIME('now', '-8 days')",) + .execute(&db.pool) + .await + .unwrap(); + + // Only the ephemeral thread should be deleted + assert_eq!(db.delete_expired_ephemeral_threads().await.unwrap(), 1); + + // The remaining thread should be the non-ephemeral thread + let threads = db + .list_threads(None, None, None, None, None, false) + .await + .unwrap(); + assert_eq!(threads.len(), 1); + assert_eq!(threads[0].id, non_ephemeral_thread_id); + + // No threads are ephemeral + let threads = db + .list_threads(None, None, Some(true), None, None, false) + .await + .unwrap(); + assert_eq!(threads.len(), 0); + } +} diff --git a/ee/tabby-db/src/user_chats.rs b/ee/tabby-db/src/user_chats.rs new file mode 100644 index 000000000000..f0d7ab5d9499 --- /dev/null +++ b/ee/tabby-db/src/user_chats.rs @@ -0,0 +1,51 @@ +use anyhow::Result; +use chrono::{DateTime, Utc}; +use sqlx::prelude::FromRow; + +use crate::{AsSqliteDateTimeString, DbConn}; + +#[derive(FromRow)] +pub struct UserChatCompletionDailyStatsDAO { + pub start: DateTime, + pub user_id: i64, + pub chats: i32, +} + +impl DbConn { + pub async fn compute_chat_daily_stats( + &self, + start: DateTime, + end: DateTime, + users: Vec, + ) -> Result> { + let users = users + .iter() + .map(|u| u.to_string()) + .collect::>() + .join(","); + + // Groups stats by day, round all timestamps to the beginning of the day relative to `start`. + let res = sqlx::query_as::<_, UserChatCompletionDailyStatsDAO>(&format!( + r#" + SELECT + STRFTIME('%F %T', DATE(created_at)) as start, + user_id, + COUNT(1) as chats + FROM user_events + WHERE created_at >= ?1 + AND created_at < ?2 + AND kind = 'chat_completion' + AND ({no_selected_users} OR user_id IN ({users})) + GROUP BY 1, 2 + ORDER BY 1 ASC + "#, + no_selected_users = users.is_empty(), + )) + .bind(start.as_sqlite_datetime()) + .bind(end.as_sqlite_datetime()) + .fetch_all(&self.pool) + .await?; + + Ok(res) + } +} diff --git a/ee/tabby-db/src/user_completions.rs b/ee/tabby-db/src/user_completions.rs new file mode 100644 index 000000000000..fd4cda99a3d9 --- /dev/null +++ b/ee/tabby-db/src/user_completions.rs @@ -0,0 +1,192 @@ +use std::time::Duration; + +use anyhow::{Context, Result}; +use cached::CachedAsync; +use chrono::{DateTime, Utc}; +use sqlx::{prelude::FromRow, query}; + +use crate::{AsSqliteDateTimeString, DbConn}; + +#[derive(FromRow)] +pub struct UserCompletionDAO { + pub user_id: i64, + pub completion_id: String, + pub language: String, + + pub views: i64, + pub selects: i64, + pub dismisses: i64, + + pub created_at: DateTime, + pub updated_at: DateTime, +} + +#[derive(FromRow, Clone)] +pub struct UserCompletionDailyStatsDAO { + pub start: DateTime, + pub language: String, + pub completions: i32, + pub views: i32, + pub selects: i32, +} + +impl DbConn { + pub async fn create_user_completion( + &self, + ts: u128, + user_id: i64, + completion_id: String, + language: String, + ) -> Result { + let duration = Duration::from_millis(ts as u64); + let created_at = + DateTime::::from_timestamp(duration.as_secs() as i64, duration.subsec_nanos()) + .context("Invalid created_at timestamp")? + .as_sqlite_datetime(); + let res = query!( + "INSERT INTO user_completions (user_id, completion_id, language, created_at) VALUES (?, ?, ?, ?);", + user_id, + completion_id, + language, + created_at + ) + .execute(&self.pool) + .await?; + Ok(res.last_insert_rowid() as i32) + } + + pub async fn add_to_user_completion( + &self, + ts: u128, + completion_id: &str, + views: i64, + selects: i64, + dismisses: i64, + ) -> Result<()> { + let duration = Duration::from_millis(ts as u64); + let updated_at = + DateTime::::from_timestamp(duration.as_secs() as i64, duration.subsec_nanos()) + .context("Invalid updated_at timestamp")? + .as_sqlite_datetime(); + query!("UPDATE user_completions SET views = views + ?, selects = selects + ?, dismisses = dismisses + ?, updated_at = ? WHERE completion_id = ?", + views, selects, dismisses, updated_at, completion_id).execute(&self.pool).await?; + Ok(()) + } + + pub async fn compute_daily_stats_in_past_year( + &self, + users: Vec, + ) -> Result> { + if users.len() <= 1 { + let mut cache = self.cache.daily_stats_in_past_year.lock().await; + let key = if users.is_empty() { + None + } else { + Some(users[0]) + }; + return Ok(cache + .try_get_or_set_with(key, move || async move { + self.compute_daily_stats_in_past_year_impl(users).await + }) + .await? + .clone()); + } + self.compute_daily_stats_in_past_year_impl(users).await + } + + async fn compute_daily_stats_in_past_year_impl( + &self, + users: Vec, + ) -> Result> { + let users = users + .iter() + .map(|u| u.to_string()) + .collect::>() + .join(","); + Ok(sqlx::query_as(&format!( + r#" + SELECT STRFTIME('%F %T', DATE(user_completions.created_at)) as start, + language, + SUM(1) as completions, + SUM(selects) as selects, + SUM(views) as views + FROM user_completions JOIN users ON user_id = users.id AND users.active + WHERE user_completions.created_at >= DATETIME('now', '-1 year') + AND ({users_empty} OR user_id IN ({users})) + GROUP BY 1, 2 + ORDER BY 1, 2 ASC + "#, + users_empty = users.is_empty(), + )) + .bind(users) + .fetch_all(&self.pool) + .await?) + } + + pub async fn compute_daily_stats( + &self, + start: DateTime, + end: DateTime, + users: Vec, + languages: Vec, + all_languages: Vec, + ) -> Result> { + let users = users + .iter() + .map(|u| u.to_string()) + .collect::>() + .join(","); + let languages = languages + .into_iter() + .map(|l| format!("'{l}'")) + .collect::>() + .join(","); + let all_languages = all_languages + .into_iter() + .map(|l| format!("'{l}'")) + .collect::>() + .join(","); + + // Groups stats by day, round all timestamps to the begining of the day relative to `start`. + let res = sqlx::query_as(&format!( + r#" + SELECT DATETIME((STRFTIME('%s', ?1) + days_since_start * 3600 * 24), 'unixepoch') as start, + language, + COUNT(1) as completions, + SUM(selects) as selects, + SUM(views) as views + FROM ( + SELECT user_id, + CAST((STRFTIME('%s', user_completions.created_at) - STRFTIME('%s', ?1)) / 3600 / 24 AS INT) as days_since_start, + user_completions.created_at, + selects, + views, + IIF(language IN ({all_languages}), language, 'other') as language + FROM user_completions JOIN users ON user_id = users.id AND users.active + WHERE user_completions.created_at >= ?1 AND user_completions.created_at < ?2 + ) + WHERE ({no_selected_users} OR user_id IN ({users})) + AND ({no_selected_languages} OR language IN ({languages})) + GROUP BY 1, 2 + ORDER BY 1, 2 ASC + "#, + no_selected_users = users.is_empty(), + no_selected_languages = languages.is_empty(), + )) + .bind(start.as_sqlite_datetime()) + .bind(end.as_sqlite_datetime()) + .fetch_all(&self.pool) + .await?; + Ok(res) + } + + #[cfg(any(test, feature = "testutils"))] + pub async fn fetch_one_user_completion(&self) -> Result> { + Ok( + sqlx::query_as!(UserCompletionDAO, + r#"SELECT user_id, completion_id, language, created_at as "created_at!: DateTime", updated_at as "updated_at!: DateTime", views, selects, dismisses FROM user_completions"#) + .fetch_optional(&self.pool) + .await?, + ) + } +} diff --git a/ee/tabby-db/src/user_events.rs b/ee/tabby-db/src/user_events.rs new file mode 100644 index 000000000000..2459ea54d25b --- /dev/null +++ b/ee/tabby-db/src/user_events.rs @@ -0,0 +1,96 @@ +use std::time::Duration; + +use anyhow::{Context, Result}; +use chrono::{DateTime, Months, Utc}; +use sqlx::{prelude::FromRow, query}; +use tabby_db_macros::query_paged_as; + +use crate::{AsSqliteDateTimeString, DbConn}; + +#[derive(FromRow)] +pub struct UserEventDAO { + pub id: i64, + pub user_id: i64, + pub kind: String, + pub created_at: DateTime, + pub payload: Vec, +} + +impl DbConn { + pub async fn create_user_event( + &self, + user_id: i64, + kind: String, + created_at: u128, + payload: String, + ) -> Result<()> { + let duration = Duration::from_millis(created_at as u64); + let created_at = + DateTime::::from_timestamp(duration.as_secs() as i64, duration.subsec_nanos()) + .context("Invalid created_at timestamp")? + .as_sqlite_datetime(); + query!( + r#"INSERT INTO user_events(user_id, kind, created_at, payload) VALUES (?, ?, ?, ?)"#, + user_id, + kind, + created_at, + payload + ) + .execute(&self.pool) + .await?; + Ok(()) + } + + pub async fn list_user_events( + &self, + limit: Option, + skip_id: Option, + backwards: bool, + users: Vec, + start: DateTime, + end: DateTime, + ) -> Result> { + let users = users + .iter() + .map(|u| u.to_string()) + .collect::>() + .join(","); + let no_selected_users = users.is_empty(); + let condition = Some(format!( + "created_at >= '{start}' AND created_at < '{end}' AND ({no_selected_users} OR user_id IN ({users}))" + )); + let events = query_paged_as!( + UserEventDAO, + "user_events", + ["id", "user_id", "kind", "created_at" as "created_at!: DateTime", "payload"], + limit, + skip_id, + backwards, + condition + ) + .fetch_all(&self.pool) + .await?; + + Ok(events) + } + + pub async fn delete_user_events_before_three_months( + &self, + now: DateTime, + ) -> Result { + if let Some(three_months_ago) = now.checked_sub_months(Months::new(3)) { + let three_months_ago = three_months_ago.as_sqlite_datetime(); + let num_deleted = query!( + "delete FROM user_events WHERE created_at < ?", + three_months_ago, + ) + .execute(&self.pool) + .await? + .rows_affected(); + + Ok(num_deleted as usize) + } else { + Ok(0) + } + } +} diff --git a/ee/tabby-db/src/user_groups.rs b/ee/tabby-db/src/user_groups.rs new file mode 100644 index 000000000000..2781cdc99547 --- /dev/null +++ b/ee/tabby-db/src/user_groups.rs @@ -0,0 +1,152 @@ +use anyhow::bail; +use chrono::{DateTime, Utc}; +use sqlx::{query, query_as, FromRow}; + +use crate::DbConn; + +#[derive(FromRow)] +pub struct UserGroupDAO { + pub id: i64, + pub name: String, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +#[derive(FromRow)] +pub struct UserGroupMembershipDAO { + pub id: i64, + pub user_id: i64, + pub user_group_id: i64, + pub is_group_admin: bool, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +impl DbConn { + pub async fn list_user_groups( + &self, + user_id: Option, + ) -> anyhow::Result> { + let user_groups = if let Some(user_id) = user_id { + query_as!( + UserGroupDAO, + r#"SELECT + user_groups.id as "id", + name, + user_groups.created_at as "created_at: DateTime", + user_groups.updated_at as "updated_at: DateTime" + FROM user_groups LEFT JOIN user_group_memberships ON (user_groups.id = user_group_memberships.user_group_id) + WHERE user_group_memberships.user_id = ? + "#, + user_id + ).fetch_all(&self.pool) + .await? + } else { + query_as!( + UserGroupDAO, + r#"SELECT + id, + name, + created_at as "created_at: DateTime", + updated_at as "updated_at: DateTime" + FROM user_groups + "#, + ) + .fetch_all(&self.pool) + .await? + }; + + Ok(user_groups) + } + + pub async fn list_user_group_memberships( + &self, + user_group_id: i64, + user_id: Option, + ) -> anyhow::Result> { + let memberships: Vec<_> = query_as!( + UserGroupMembershipDAO, + r#"SELECT + id, + user_id, + user_group_id, + is_group_admin, + created_at as "created_at: DateTime", + updated_at as "updated_at: DateTime" + FROM user_group_memberships + WHERE user_group_id = ?1 AND (user_id = ?2 OR ?2 IS NULL)"#, + user_group_id, + user_id + ) + .fetch_all(&self.pool) + .await?; + + Ok(memberships) + } + + pub async fn create_user_group(&self, name: &str) -> anyhow::Result { + let id = query!("INSERT INTO user_groups (name) VALUES (?)", name) + .execute(&self.pool) + .await? + .last_insert_rowid(); + + Ok(id) + } + + pub async fn delete_user_group(&self, id: i64) -> anyhow::Result<()> { + let res = query!("DELETE FROM user_groups WHERE id = ?", id) + .execute(&self.pool) + .await?; + + if res.rows_affected() != 1 { + bail!("User group not found"); + } + + Ok(()) + } + + pub async fn upsert_user_group_membership( + &self, + user_id: i64, + user_group_id: i64, + is_group_admin: bool, + ) -> anyhow::Result<()> { + let _ = query!( + r#" +INSERT INTO user_group_memberships (user_id, user_group_id, is_group_admin) VALUES (?, ?, ?) +ON CONFLICT (user_id, user_group_id) DO UPDATE + SET is_group_admin = excluded.is_group_admin, updated_at = DATETIME('now') + "#, + user_id, + user_group_id, + is_group_admin + ) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn delete_user_group_membership( + &self, + user_id: i64, + user_group_id: i64, + ) -> anyhow::Result<()> { + let rows_deleted = query!( + r#" +DELETE FROM user_group_memberships WHERE user_id = ? AND user_group_id = ? + "#, + user_id, + user_group_id + ) + .execute(&self.pool) + .await? + .rows_affected(); + + if rows_deleted == 1 { + Ok(()) + } else { + Err(anyhow::anyhow!("User group membership not found")) + } + } +} diff --git a/ee/tabby-db/src/users.rs b/ee/tabby-db/src/users.rs index 8b7eee2545dd..4b7e44c1e984 100644 --- a/ee/tabby-db/src/users.rs +++ b/ee/tabby-db/src/users.rs @@ -1,9 +1,11 @@ -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, bail, Result}; use chrono::{DateTime, Utc}; -use sqlx::{query, query_scalar, FromRow}; +use sqlx::{query, query_as, query_scalar, FromRow}; +use tabby_db_macros::query_paged_as; use uuid::Uuid; use super::DbConn; +use crate::SQLXResultExt; #[allow(unused)] #[derive(FromRow)] @@ -11,9 +13,13 @@ pub struct UserDAO { pub created_at: DateTime, pub updated_at: DateTime, - pub id: i32, + pub id: i64, pub email: String, - pub password_encrypted: String, + pub name: Option, + + // when the user is created with password, this field is set and will never be changed to None + // when the user is created with SSO, this field is None and will never be set + pub password_encrypted: Option, pub is_admin: bool, /// To authenticate IDE extensions / plugins to access code completion / chat api endpoints. @@ -21,11 +27,21 @@ pub struct UserDAO { pub active: bool, } +static OWNER_USER_ID: i64 = 1; + +macro_rules! select { + ($str:literal $(,)? $($val:expr),*) => { + query_as!( + UserDAO, + r#"SELECT id as "id!", email, name, password_encrypted, is_admin, created_at as "created_at!: DateTime", updated_at as "updated_at!: DateTime", auth_token, active FROM users WHERE "# + $str, + $($val),* + ) + } +} + impl UserDAO { - fn select(clause: &str) -> String { - r#"SELECT id, email, password_encrypted, is_admin, created_at, updated_at, auth_token, active FROM users WHERE "# - .to_owned() - + clause + pub fn is_owner(&self) -> bool { + self.id == OWNER_USER_ID } } @@ -34,31 +50,40 @@ impl DbConn { pub async fn create_user( &self, email: String, - password_encrypted: String, + password_encrypted: Option, is_admin: bool, - ) -> Result { - self.create_user_impl(email, password_encrypted, is_admin, None) + name: Option, + ) -> Result { + self.create_user_impl(email, password_encrypted, is_admin, None, name) .await } pub async fn create_user_with_invitation( &self, email: String, - password_encrypted: String, + password_encrypted: Option, is_admin: bool, - invitation_id: i32, - ) -> Result { - self.create_user_impl(email, password_encrypted, is_admin, Some(invitation_id)) - .await + invitation_id: i64, + name: Option, + ) -> Result { + self.create_user_impl( + email, + password_encrypted, + is_admin, + Some(invitation_id), + name, + ) + .await } async fn create_user_impl( &self, email: String, - password_encrypted: String, + password_encrypted: Option, is_admin: bool, - invitation_id: Option, - ) -> Result { + invitation_id: Option, + name: Option, + ) -> Result { let mut transaction = self.pool.begin().await?; if let Some(invitation_id) = invitation_id { query!("DELETE FROM invitations WHERE id = ?", invitation_id) @@ -66,26 +91,29 @@ impl DbConn { .await?; } let token = generate_auth_token(); - let res = query!("INSERT INTO users (email, password_encrypted, is_admin, auth_token) VALUES (?, ?, ?, ?)", - email, password_encrypted, is_admin, token) - .execute(&mut *transaction).await?; + let res = query!( + "INSERT INTO users (email, password_encrypted, is_admin, auth_token, name) VALUES (?, ?, ?, ?, ?)", + email, password_encrypted, is_admin, token, name) + .execute(&mut *transaction).await; + let res = res.unique_error("User already exists")?; transaction.commit().await?; - Ok(res.last_insert_rowid() as i32) + self.cache.active_user_count.invalidate().await; + if is_admin { + self.cache.active_admin_count.invalidate().await; + } + + Ok(res.last_insert_rowid()) } - pub async fn get_user(&self, id: i32) -> Result> { - let user = sqlx::query_as(&UserDAO::select("id = ?")) - .bind(id) - .fetch_optional(&self.pool) - .await?; + pub async fn get_user(&self, id: i64) -> Result> { + let user = select!("id = ?", id).fetch_optional(&self.pool).await?; Ok(user) } pub async fn get_user_by_email(&self, email: &str) -> Result> { - let user = sqlx::query_as(&UserDAO::select("email = ?")) - .bind(email) + let user = select!("email = ?", email) .fetch_optional(&self.pool) .await?; @@ -93,57 +121,72 @@ impl DbConn { } pub async fn list_admin_users(&self) -> Result> { - let users = sqlx::query_as(&UserDAO::select("is_admin")) - .fetch_all(&self.pool) - .await?; + let users = select!("is_admin").fetch_all(&self.pool).await?; Ok(users) } pub async fn list_users_with_filter( &self, + ids: Option>, limit: Option, skip_id: Option, backwards: bool, ) -> Result> { - let query = Self::make_pagination_query( + let condition = ids.map(|ids| { + let ids: Vec = ids.iter().map(i32::to_string).collect(); + let ids = ids.join(", "); + format!("id in ({ids})") + }); + + let users = query_paged_as!( + UserDAO, "users", - &[ - "id", + [ + "id"!, "email", + "name", "password_encrypted", "is_admin", - "created_at", - "updated_at", + "created_at" as "created_at!: DateTime", + "updated_at" as "updated_at!: DateTime", "auth_token", - "active", + "active" ], limit, skip_id, backwards, - ); + condition + ) + .fetch_all(&self.pool) + .await?; - let users = sqlx::query_as(&query).fetch_all(&self.pool).await?; Ok(users) } - pub async fn verify_auth_token(&self, token: &str) -> Result { + pub async fn verify_auth_token(&self, token: &str, requires_owner: bool) -> Result { let token = token.to_owned(); - let email = query_scalar!("SELECT email FROM users WHERE auth_token = ?", token) - .fetch_one(&self.pool) - .await; - email.map_err(Into::into) + let Some(id) = query_scalar!( + "SELECT id FROM users WHERE auth_token = ? AND active AND (id == ? OR NOT ?)", + token, + OWNER_USER_ID, + requires_owner + ) + .fetch_one(&self.pool) + .await? + else { + bail!("Invalid auth_token") + }; + + Ok(id) } - pub async fn reset_user_auth_token_by_email(&self, email: &str) -> Result<()> { - let email = email.to_owned(); - let updated_at = chrono::Utc::now(); + pub async fn reset_user_auth_token_by_id(&self, id: i64) -> Result<()> { let token = generate_auth_token(); query!( - r#"UPDATE users SET auth_token = ?, updated_at = ? WHERE email = ?"#, + r#"UPDATE users SET auth_token = ?, updated_at = DATETIME('now') WHERE id = ?"#, token, - updated_at, - email + id ) .execute(&self.pool) .await?; @@ -151,7 +194,7 @@ impl DbConn { Ok(()) } - pub async fn update_user_active(&self, id: i32, active: bool) -> Result<()> { + pub async fn update_user_active(&self, id: i64, active: bool) -> Result<()> { let not_active = !active; let changed = query!( "UPDATE users SET active = ? WHERE id = ? AND active = ?", @@ -163,16 +206,92 @@ impl DbConn { .await? .rows_affected(); if changed != 1 { - Err(anyhow!("user active status was not changed")) + return Err(anyhow!("user active status was not changed")); + } + self.cache.active_admin_count.invalidate().await; + self.cache.active_user_count.invalidate().await; + Ok(()) + } + + pub async fn update_user_role(&self, id: i64, is_admin: bool) -> Result<()> { + let not_admin = !is_admin; + let changed = query!( + "UPDATE users SET is_admin = ? WHERE id = ? AND is_admin = ?", + is_admin, + id, + not_admin + ) + .execute(&self.pool) + .await? + .rows_affected(); + if changed != 1 { + Err(anyhow!("user admin status was not changed")) } else { + self.cache.active_admin_count.invalidate().await; Ok(()) } } + + pub async fn update_user_password(&self, id: i64, password_encrypted: String) -> Result<()> { + query!( + "UPDATE users SET password_encrypted = ? WHERE id = ?", + password_encrypted, + id + ) + .execute(&self.pool) + .await?; + Ok(()) + } + + pub async fn update_user_avatar(&self, id: i64, avatar: Option>) -> Result<()> { + query!("UPDATE users SET avatar = ? WHERE id = ?;", avatar, id) + .execute(&self.pool) + .await?; + Ok(()) + } + + pub async fn get_user_avatar(&self, id: i64) -> Result>> { + let avatar = query_scalar!("SELECT avatar FROM users WHERE id = ?", id) + .fetch_one(&self.pool) + .await?; + Ok(avatar.map(Vec::into_boxed_slice)) + } + + pub async fn count_active_users(&self) -> Result { + self.cache + .active_user_count + .get_or_refresh(|| async { + let users = query_scalar!("SELECT COUNT(1) FROM users WHERE active;") + .fetch_one(&self.pool) + .await?; + Ok(users as usize) + }) + .await + } + + pub async fn count_active_admin_users(&self) -> Result { + self.cache + .active_admin_count + .get_or_refresh(|| async { + let users = query_scalar!("SELECT COUNT(1) FROM users WHERE active and is_admin;") + .fetch_one(&self.pool) + .await?; + Ok(users as usize) + }) + .await + } + + pub async fn update_user_name(&self, id: i64, name: String) -> Result<()> { + query!("UPDATE users SET name = ? WHERE id = ?;", name, id) + .execute(&self.pool) + .await?; + Ok(()) + } } fn generate_auth_token() -> String { let uuid = Uuid::new_v4().to_string().replace('-', ""); - format!("auth_{}", uuid) + format!("auth_{uuid}") } #[cfg(test)] @@ -189,6 +308,24 @@ mod tests { assert_eq!(user.id, 1); } + #[tokio::test] + async fn test_create_user_with_name() { + let conn = DbConn::new_in_memory().await.unwrap(); + + let id = conn + .create_user( + "use1@example.com".into(), + Some("123456".into()), + false, + Some("name1".into()), + ) + .await + .unwrap(); + let user = conn.get_user(id).await.unwrap().unwrap(); + assert_eq!(user.id, 1); + assert_eq!(user.name, Some("name1".into())); + } + #[tokio::test] async fn test_set_active() { let conn = DbConn::new_in_memory().await.unwrap(); @@ -204,6 +341,20 @@ mod tests { assert!(conn.update_user_active(id, false).await.is_err()); } + #[tokio::test] + async fn test_update_user_name() { + let conn = DbConn::new_in_memory().await.unwrap(); + let id = create_user(&conn).await; + + let user = conn.get_user(id).await.unwrap().unwrap(); + assert_eq!(user.name, None); + + conn.update_user_name(id, "test".into()).await.unwrap(); + + let user = conn.get_user(id).await.unwrap().unwrap(); + assert_eq!(user.name, Some("test".into())); + } + #[tokio::test] async fn test_get_user_by_email() { let conn = DbConn::new_in_memory().await.unwrap(); @@ -221,23 +372,37 @@ mod tests { let user = conn.get_user(id).await.unwrap().unwrap(); - assert!(conn.verify_auth_token("abcd").await.is_err()); + assert!(conn.verify_auth_token("abcd", false).await.is_err()); - assert!(conn.verify_auth_token(&user.auth_token).await.is_ok()); - - conn.reset_user_auth_token_by_email(&user.email) + assert!(conn + .verify_auth_token(&user.auth_token, false) .await - .unwrap(); + .is_ok()); + + conn.reset_user_auth_token_by_id(user.id).await.unwrap(); let new_user = conn.get_user(id).await.unwrap().unwrap(); assert_eq!(user.email, new_user.email); assert_ne!(user.auth_token, new_user.auth_token); + + // Inactive user's auth token will be rejected. + conn.update_user_active(new_user.id, false).await.unwrap(); + assert!(conn + .verify_auth_token(&new_user.auth_token, false) + .await + .is_err()); + + // Owner user should pass verification. + assert!(conn + .verify_auth_token(&new_user.auth_token, true) + .await + .is_err()); } #[tokio::test] async fn test_list_users_with_filter() { let conn = DbConn::new_in_memory().await.unwrap(); - let empty: Vec = vec![]; + let empty: Vec = vec![]; let to_ids = |users: Vec| users.into_iter().map(|u| u.id).collect::>(); // empty @@ -245,7 +410,7 @@ mod tests { assert_eq!( empty, to_ids( - conn.list_users_with_filter(None, None, false) + conn.list_users_with_filter(None, None, None, false) .await .unwrap() ) @@ -253,7 +418,7 @@ mod tests { assert_eq!( empty, to_ids( - conn.list_users_with_filter(Some(2), None, false) + conn.list_users_with_filter(None, Some(2), None, false) .await .unwrap() ) @@ -261,7 +426,7 @@ mod tests { assert_eq!( empty, to_ids( - conn.list_users_with_filter(None, Some(1), false) + conn.list_users_with_filter(None, None, Some(1), false) .await .unwrap() ) @@ -269,7 +434,7 @@ mod tests { assert_eq!( empty, to_ids( - conn.list_users_with_filter(Some(2), Some(1), false) + conn.list_users_with_filter(None, Some(2), Some(1), false) .await .unwrap() ) @@ -277,12 +442,16 @@ mod tests { // backwards assert_eq!( empty, - to_ids(conn.list_users_with_filter(None, None, true).await.unwrap()) + to_ids( + conn.list_users_with_filter(None, None, None, true) + .await + .unwrap() + ) ); assert_eq!( empty, to_ids( - conn.list_users_with_filter(Some(2), None, true) + conn.list_users_with_filter(None, Some(2), None, true) .await .unwrap() ) @@ -290,7 +459,7 @@ mod tests { assert_eq!( empty, to_ids( - conn.list_users_with_filter(None, Some(1), true) + conn.list_users_with_filter(None, None, Some(1), true) .await .unwrap() ) @@ -298,14 +467,19 @@ mod tests { assert_eq!( empty, to_ids( - conn.list_users_with_filter(Some(1), Some(1), true) + conn.list_users_with_filter(None, Some(1), Some(1), true) .await .unwrap() ) ); let id1 = conn - .create_user("use1@example.com".into(), "123456".into(), false) + .create_user( + "use1@example.com".into(), + Some("123456".into()), + false, + None, + ) .await .unwrap(); @@ -314,7 +488,7 @@ mod tests { assert_eq!( vec![id1], to_ids( - conn.list_users_with_filter(None, None, false) + conn.list_users_with_filter(None, None, None, false) .await .unwrap() ) @@ -322,7 +496,7 @@ mod tests { assert_eq!( vec![id1], to_ids( - conn.list_users_with_filter(Some(2), None, false) + conn.list_users_with_filter(None, Some(2), None, false) .await .unwrap() ) @@ -330,7 +504,7 @@ mod tests { assert_eq!( empty, to_ids( - conn.list_users_with_filter(None, Some(1), false) + conn.list_users_with_filter(None, None, Some(1), false) .await .unwrap() ) @@ -338,7 +512,7 @@ mod tests { assert_eq!( empty, to_ids( - conn.list_users_with_filter(Some(2), Some(1), false) + conn.list_users_with_filter(None, Some(2), Some(1), false) .await .unwrap() ) @@ -346,12 +520,16 @@ mod tests { // backwards assert_eq!( vec![id1], - to_ids(conn.list_users_with_filter(None, None, true).await.unwrap()) + to_ids( + conn.list_users_with_filter(None, None, None, true) + .await + .unwrap() + ) ); assert_eq!( vec![id1], to_ids( - conn.list_users_with_filter(Some(2), None, true) + conn.list_users_with_filter(None, Some(2), None, true) .await .unwrap() ) @@ -359,7 +537,7 @@ mod tests { assert_eq!( empty, to_ids( - conn.list_users_with_filter(None, Some(1), true) + conn.list_users_with_filter(None, None, Some(1), true) .await .unwrap() ) @@ -367,26 +545,46 @@ mod tests { assert_eq!( empty, to_ids( - conn.list_users_with_filter(Some(1), Some(1), true) + conn.list_users_with_filter(None, Some(1), Some(1), true) .await .unwrap() ) ); let id2 = conn - .create_user("use2@example.com".into(), "123456".into(), false) + .create_user( + "use2@example.com".into(), + Some("123456".into()), + false, + None, + ) .await .unwrap(); let id3 = conn - .create_user("use3@example.com".into(), "123456".into(), false) + .create_user( + "use3@example.com".into(), + Some("123456".into()), + false, + None, + ) .await .unwrap(); let id4 = conn - .create_user("use4@example.com".into(), "123456".into(), false) + .create_user( + "use4@example.com".into(), + Some("123456".into()), + false, + None, + ) .await .unwrap(); let id5 = conn - .create_user("use5@example.com".into(), "123456".into(), false) + .create_user( + "use5@example.com".into(), + Some("123456".into()), + false, + None, + ) .await .unwrap(); @@ -395,7 +593,7 @@ mod tests { assert_eq!( vec![id1, id2, id3, id4, id5], to_ids( - conn.list_users_with_filter(None, None, false) + conn.list_users_with_filter(None, None, None, false) .await .unwrap() ) @@ -403,7 +601,7 @@ mod tests { assert_eq!( vec![id1, id2], to_ids( - conn.list_users_with_filter(Some(2), None, false) + conn.list_users_with_filter(None, Some(2), None, false) .await .unwrap() ) @@ -411,7 +609,7 @@ mod tests { assert_eq!( vec![id3, id4, id5], to_ids( - conn.list_users_with_filter(None, Some(2), false) + conn.list_users_with_filter(None, None, Some(2), false) .await .unwrap() ) @@ -419,7 +617,7 @@ mod tests { assert_eq!( vec![id3, id4], to_ids( - conn.list_users_with_filter(Some(2), Some(2), false) + conn.list_users_with_filter(None, Some(2), Some(2), false) .await .unwrap() ) @@ -427,12 +625,16 @@ mod tests { // backwards assert_eq!( vec![id1, id2, id3, id4, id5], - to_ids(conn.list_users_with_filter(None, None, true).await.unwrap()) + to_ids( + conn.list_users_with_filter(None, None, None, true) + .await + .unwrap() + ) ); assert_eq!( vec![id4, id5], to_ids( - conn.list_users_with_filter(Some(2), None, true) + conn.list_users_with_filter(None, Some(2), None, true) .await .unwrap() ) @@ -440,7 +642,7 @@ mod tests { assert_eq!( vec![id1, id2, id3], to_ids( - conn.list_users_with_filter(None, Some(4), true) + conn.list_users_with_filter(None, None, Some(4), true) .await .unwrap() ) @@ -448,10 +650,44 @@ mod tests { assert_eq!( vec![id2, id3], to_ids( - conn.list_users_with_filter(Some(2), Some(4), true) + conn.list_users_with_filter(None, Some(2), Some(4), true) .await .unwrap() ) ); } + + #[tokio::test] + async fn test_caching() { + let db = DbConn::new_in_memory().await.unwrap(); + + db.create_user("example@example.com".into(), None, true, None) + .await + .unwrap(); + + assert_eq!(db.count_active_users().await.unwrap(), 1); + assert_eq!(db.count_active_admin_users().await.unwrap(), 1); + + let user2_id = db + .create_user("example2@example.com".into(), None, false, None) + .await + .unwrap(); + assert_eq!(db.count_active_users().await.unwrap(), 2); + assert_eq!(db.count_active_admin_users().await.unwrap(), 1); + + db.update_user_active(user2_id, false).await.unwrap(); + assert_eq!(db.count_active_users().await.unwrap(), 1); + assert_eq!(db.count_active_admin_users().await.unwrap(), 1); + + let user3_id = db + .create_user("example3@example.com".into(), None, true, None) + .await + .unwrap(); + assert_eq!(db.count_active_users().await.unwrap(), 2); + assert_eq!(db.count_active_admin_users().await.unwrap(), 2); + + db.update_user_active(user3_id, false).await.unwrap(); + assert_eq!(db.count_active_users().await.unwrap(), 1); + assert_eq!(db.count_active_admin_users().await.unwrap(), 1); + } } diff --git a/ee/tabby-db/src/web_documents.rs b/ee/tabby-db/src/web_documents.rs new file mode 100644 index 000000000000..a13017aef793 --- /dev/null +++ b/ee/tabby-db/src/web_documents.rs @@ -0,0 +1,116 @@ +use anyhow::{anyhow, Result}; +use chrono::{DateTime, Utc}; +use sqlx::{prelude::FromRow, query}; +use tabby_db_macros::query_paged_as; + +use crate::DbConn; + +#[allow(unused)] +#[derive(FromRow)] +pub struct WebDocumentDAO { + pub id: i64, + pub name: String, + pub url: String, + pub is_preset: bool, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +impl DbConn { + pub async fn list_preset_web_documents( + &self, + names: Option>, + limit: Option, + skip_id: Option, + backwards: bool, + ) -> Result> { + let mut condition = "is_preset=true".to_string(); + if let Some(names) = names { + let names = names + .into_iter() + .map(|s| "\"".to_string() + s.as_str() + "\"") + .collect::>() + .join(","); + condition += &format!(" AND name in ({names})"); + } + let condition = Some(condition); + + let urls = query_paged_as!( + WebDocumentDAO, + "web_documents", + ["id", "name", "url", "is_preset", "created_at" as "created_at!: DateTime", "updated_at" as "updated_at!: DateTime"], + limit, + skip_id, + backwards, + condition + ).fetch_all(&self.pool) + .await?; + + Ok(urls) + } + + pub async fn list_custom_web_documents( + &self, + ids: Option>, + limit: Option, + skip_id: Option, + backwards: bool, + ) -> Result> { + let mut condition = "is_preset=false".to_string(); + if let Some(ids) = ids { + let ids: Vec = ids.iter().map(i64::to_string).collect(); + let ids = ids.join(", "); + condition += &format!(" AND id in ({ids})"); + } + + let condition = Some(condition); + + let urls = query_paged_as!( + WebDocumentDAO, + "web_documents", + ["id", "name", "url", "is_preset", "created_at" as "created_at!: DateTime", "updated_at" as "updated_at!: DateTime"], + limit, + skip_id, + backwards, + condition + ).fetch_all(&self.pool) + .await?; + + Ok(urls) + } + + pub async fn create_web_document( + &self, + name: String, + url: String, + is_preset: bool, + ) -> Result { + let res = query!( + "INSERT INTO web_documents(name, url, is_preset) VALUES (?,?,?);", + name, + url, + is_preset + ) + .execute(&self.pool) + .await?; + + Ok(res.last_insert_rowid()) + } + + pub async fn deactivate_preset_web_document(&self, name: &str) -> Result<()> { + let res = query!("DELETE FROM web_documents WHERE name = ?;", name) + .execute(&self.pool) + .await?; + if res.rows_affected() != 1 { + return Err(anyhow!("No preset web document to deactivate")); + } + Ok(()) + } + + pub async fn delete_web_document(&self, id: i64) -> Result<()> { + query!("DELETE FROM web_documents WHERE id = ?;", id) + .execute(&self.pool) + .await?; + Ok(()) + } +} diff --git a/ee/tabby-email/.gitignore b/ee/tabby-email/.gitignore new file mode 100644 index 000000000000..adc158bfb710 --- /dev/null +++ b/ee/tabby-email/.gitignore @@ -0,0 +1,2 @@ +out +.react-email diff --git a/ee/tabby-email/README.md b/ee/tabby-email/README.md new file mode 100644 index 000000000000..94e08f33b2b2 --- /dev/null +++ b/ee/tabby-email/README.md @@ -0,0 +1,9 @@ +## Add new email template for Tabby + +- Navigate to `ee/tabby-email/emails` +- Run `yarn && yarn dev` +- This will start a webserver and show a link where you can preview emails +- Copy existing email templates to create new emails +- Run `make update-email-templates` to generate html files from your react email +- Go to `ee/tabby-webserver/src/service/email/templates.rs` and add a new function for your template +- Add a function in the `EmailService` trait to send your new email and implement it in `ee/tabby-webserver/src/service/email/mod.rs` diff --git a/ee/tabby-email/components/root-layout.tsx b/ee/tabby-email/components/root-layout.tsx new file mode 100644 index 000000000000..36e38ff749c9 --- /dev/null +++ b/ee/tabby-email/components/root-layout.tsx @@ -0,0 +1,44 @@ +import { + Body, + Button, + Container, + Column, + Head, + Heading, + Hr, + Html, + Img, + Link, + Preview, + Row, + Section, + Text, +} from "@react-email/components"; +import { Tailwind } from "@react-email/tailwind"; +import * as React from "react"; + +interface RootLayoutProps { + previewText: string + children: React.ReactNode +} + +export const RootLayout = ({ + previewText, + children, +}: RootLayoutProps) => { + return ( + + + {previewText} + + + + {children} + + + + + ); +}; + +export default RootLayout; diff --git a/ee/tabby-email/emails/invitation.tsx b/ee/tabby-email/emails/invitation.tsx new file mode 100644 index 000000000000..100e8b1f3173 --- /dev/null +++ b/ee/tabby-email/emails/invitation.tsx @@ -0,0 +1,60 @@ +import { + Button, Heading, + Hr, Link, Section, + Text +} from "@react-email/components"; +import * as React from "react"; +import RootLayout from "../components/root-layout"; + +interface Invitation { + email?: string; + inviteLink?: string; +} + +export const Invitation = ({ + email = "{{EMAIL}}", + inviteLink = "{{EXTERNAL_URL}}/auth/signup?invitationCode={{CODE}}&email={{EMAIL}}", +}: Invitation) => { + const title = `You've been invited to join a Tabby server!`; + + return ( + + + {title} + + + Hello, + + + You have been invited to join a Tabby Server, where you can tap into AI-driven code completions and chat assistants. + +
    + +
    + + or copy and paste this URL into your browser:{" "} + + {inviteLink} + + +
    + + This invitation was intended for{" "} + {email}. If you + were not expecting this invitation, you can ignore this email. + +
    + ); +}; + +Invitation.PreviewProps = { + email: "user@tabbyml.com", + inviteLink: "http://localhost:8080/auth/signup?invitationCode={{CODE}}&email={{EMAIL}}", +} as Invitation; + +export default Invitation; diff --git a/ee/tabby-email/emails/password_reset.tsx b/ee/tabby-email/emails/password_reset.tsx new file mode 100644 index 000000000000..755a72fb1296 --- /dev/null +++ b/ee/tabby-email/emails/password_reset.tsx @@ -0,0 +1,57 @@ +import { + Button, Heading, + Hr, Link, Section, + Text +} from "@react-email/components"; +import * as React from "react"; +import RootLayout from "../components/root-layout"; + +interface PasswordReset { + email?: string; + resetLink?: string; +} + +export const PasswordReset = ({ + email = "{{EMAIL}}", + resetLink = "{{EXTERNAL_URL}}/auth/reset-password?code={{CODE}}", +}: PasswordReset) => { + const title = `Reset your Tabby account password`; + + return ( + + + {title} + + + You recently requested a password reset for your Tabby account {email}. + +
    + +
    + + or copy and paste this URL into your browser:{" "} + + {resetLink} + + +
    + + This email was intended for{" "} + {email}. If you + were not expecting this invitation, you can ignore this email. + +
    + ); +}; + +PasswordReset.PreviewProps = { + email: "user@tabbyml.com", + resetLink: "http://localhost:8080/auth/reset-password?code={{CODE}}", +} as PasswordReset; + +export default PasswordReset; diff --git a/ee/tabby-email/emails/signup_success.tsx b/ee/tabby-email/emails/signup_success.tsx new file mode 100644 index 000000000000..6c74e798c395 --- /dev/null +++ b/ee/tabby-email/emails/signup_success.tsx @@ -0,0 +1,43 @@ +import { + Button, Heading, + Hr, Link, Section, + Text +} from "@react-email/components"; +import * as React from "react"; +import RootLayout from "../components/root-layout"; + +interface SignupSuccess { + email?: string; + link?: string; +} + +export const SignupSuccess = ({ + email = "{{EMAIL}}", + link = "{{EXTERNAL_URL}}", +}: SignupSuccess) => { + const title = `Welcome to Tabby!`; + + return ( + + + {title} + + + You have successfully signed up for Tabby using {email}. + + + You can visit the instance here: {" "} + + {link} + + + + ); +}; + +SignupSuccess.PreviewProps = { + email: "user@tabbyml.com", + link: "http://localhost:8080/", +} as SignupSuccess; + +export default SignupSuccess; diff --git a/ee/tabby-email/emails/test.tsx b/ee/tabby-email/emails/test.tsx new file mode 100644 index 000000000000..e30ad9c477bd --- /dev/null +++ b/ee/tabby-email/emails/test.tsx @@ -0,0 +1,25 @@ +import { + Heading, Text +} from "@react-email/components"; +import * as React from "react"; +import RootLayout from "../components/root-layout"; + +export const Test = () => { + const title = `Your mail server is ready to go!`; + + return ( + + + {title} + + + This is a test email from Tabby to confirm that your mail server configuration is correct. + + + If you have received this email, it means that your configuration was successful. Thank you for using Tabby! + + + ); +}; + +export default Test; diff --git a/ee/tabby-email/package.json b/ee/tabby-email/package.json new file mode 100644 index 000000000000..d0c83c0f425a --- /dev/null +++ b/ee/tabby-email/package.json @@ -0,0 +1,15 @@ +{ + "name": "emails", + "version": "0.0.19", + "private": true, + "scripts": { + "build": "email build && email export", + "dev": "email dev" + }, + "devDependencies": { + "react-email": "2.1.3", + "@react-email/components": "0.0.18", + "@react-email/tailwind": "0.0.17", + "react": "18.2.0" + } +} diff --git a/ee/tabby-schema/Cargo.toml b/ee/tabby-schema/Cargo.toml new file mode 100644 index 000000000000..1939c0426c2d --- /dev/null +++ b/ee/tabby-schema/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "tabby-schema" +version.workspace = true +edition.workspace = true +authors.workspace = true +homepage.workspace = true + +[features] +schema-language = ["juniper/schema-language"] + +[dependencies] +anyhow.workspace = true +async-openai-alt.workspace = true +async-trait.workspace = true +axum = { workspace = true } +base64 = "0.22.0" +chrono = { workspace = true, features = ["serde"] } +futures.workspace = true +juniper = { workspace = true, features = ["chrono"] } +lazy_static.workspace = true +serde.workspace = true +strum.workspace = true +tabby-db = { path = "../../ee/tabby-db" } +tabby-common = { path = "../../crates/tabby-common" } +tabby-inference = { path = "../../crates/tabby-inference" } +thiserror.workspace = true +tokio = { workspace = true, features = ["fs", "process"] } +tracing.workspace = true +validator = { version = "0.18.1", features = ["derive"] } +regex.workspace = true +hash-ids.workspace = true +url.workspace = true +ldap3.workspace = true + +[dev-dependencies] +tabby-db = { path = "../../ee/tabby-db", features = ["testutils"]} + +[[example]] +name = "update-schema" +path = "examples/update-schema.rs" +required-features = ["schema-language"] diff --git a/ee/tabby-schema/examples/update-schema.rs b/ee/tabby-schema/examples/update-schema.rs new file mode 100644 index 000000000000..bed7b61d5962 --- /dev/null +++ b/ee/tabby-schema/examples/update-schema.rs @@ -0,0 +1,8 @@ +use std::fs::write; + +use tabby_schema::create_schema; + +fn main() { + let schema = create_schema(); + write("ee/tabby-schema/graphql/schema.graphql", schema.as_sdl()).unwrap(); +} diff --git a/ee/tabby-schema/graphql/schema.graphql b/ee/tabby-schema/graphql/schema.graphql new file mode 100644 index 000000000000..8bbd28bfe7ce --- /dev/null +++ b/ee/tabby-schema/graphql/schema.graphql @@ -0,0 +1,1409 @@ +schema { + query: Query + mutation: Mutation + subscription: Subscription +} + +enum AuthMethod { + NONE + PLAIN + LOGIN +} + +enum AuthProviderKind { + OAUTH_GITHUB + OAUTH_GOOGLE + OAUTH_GITLAB + OAUTH_OIDC + LDAP +} + +"Represents the kind of context source." +enum ContextSourceKind { + GIT + GITHUB + GITLAB + DOC + WEB + PAGE + INGESTED +} + +enum Encryption { + START_TLS + SSL_TLS + NONE +} + +enum EventKind { + COMPLETION + CHAT_COMPLETION + SELECT + VIEW + DISMISS +} + +enum IntegrationKind { + GITHUB + GITLAB + GITHUB_SELF_HOSTED + GITLAB_SELF_HOSTED +} + +enum IntegrationStatus { + READY + PENDING + FAILED +} + +enum Language { + RUST + PYTHON + JAVA + KOTLIN + JAVASCRIPT + TYPESCRIPT + GO + RUBY + CSHARP + C + CPP + SOLIDITY + PHP + OTHER +} + +enum LdapEncryptionKind { + NONE + START_TLS + LDAPS +} + +enum LicenseFeature { + CUSTOM_LOGO +} + +enum LicenseStatus { + OK + EXPIRED + SEATS_EXCEEDED +} + +enum LicenseType { + COMMUNITY + TEAM + ENTERPRISE +} + +enum ModelHealthBackend { + CHAT + COMPLETION + EMBEDDING +} + +enum MoveSectionDirection { + UP + DOWN +} + +enum OAuthProvider { + GITHUB + GOOGLE + GITLAB + OIDC +} + +enum RepositoryKind { + GIT + GITHUB + GITLAB + GITHUB_SELF_HOSTED + GITLAB_SELF_HOSTED + GIT_CONFIG +} + +enum Role { + USER + ASSISTANT +} + +input BrandingSettingInput { + brandingLogo: String + brandingIcon: String +} + +input CodeQueryInput { + filepath: String + language: String + content: String! + "git_url to be included in the code search." gitUrl: String + "source_ids to be included in the code search." sourceId: String +} + +input CodeSearchParamsOverrideInput { + minEmbeddingScore: Float + minBm25Score: Float + minRrfScore: Float + numToReturn: Int + numToScore: Int +} + +input CreateCustomDocumentInput { + name: String! + url: String! +} + +input CreateIntegrationInput { + displayName: String! + accessToken: String! + kind: IntegrationKind! + apiBase: String +} + +input CreateMessageInput { + content: String! + attachments: MessageAttachmentInput +} + +input CreatePageRunInput { + titlePrompt: String! + docQuery: DocQueryInput = null + codeQuery: CodeQueryInput = null + debugOption: PageRunDebugOptionInput = null +} + +input CreatePageSectionRunInput { + pageId: ID! + titlePrompt: String! + docQuery: DocQueryInput = null + debugOption: PageSectionRunDebugOptionInput = null +} + +input CreateThreadAndRunInput { + thread: CreateThreadInput! + options: ThreadRunOptionsInput! = {codeQuery: null, debugOptions: null, docQuery: null, generateRelevantQuestions: false, modelName: null} +} + +input CreateThreadInput { + userMessage: CreateMessageInput! +} + +input CreateThreadRunInput { + threadId: ID! + additionalUserMessage: CreateMessageInput! + options: ThreadRunOptionsInput! = {codeQuery: null, debugOptions: null, docQuery: null, generateRelevantQuestions: false, modelName: null} +} + +input CreateThreadToPageRunInput { + threadId: ID! + debugOption: ThreadToPageDebugOptionInput = null +} + +input CreateUserGroupInput { + "User group name, can only start with a lowercase letter, and contain characters, numbers, and `-` or `_`" name: String! +} + +input DocQueryInput { + content: String! + "Whether to collect documents from public web." searchPublic: Boolean! + "source_ids to be included in the doc search." sourceIds: [String!] +} + +input EmailSettingInput { + smtpUsername: String! + fromAddress: String! + smtpServer: String! + smtpPort: Int! + encryption: Encryption! + authMethod: AuthMethod! + smtpPassword: String +} + +input MessageAttachmentCodeInput { + filepath: String + "When start line is `None`, it represents the entire file." startLine: Int + content: String! +} + +input MessageAttachmentInput { + code: [MessageAttachmentCodeInput!]! +} + +input NetworkSettingInput { + externalUrl: String! +} + +input PageRunDebugOptionInput { + returnChatCompletionRequest: Boolean! = false + returnQueryRequest: Boolean! = false +} + +input PageSectionRunDebugOptionInput { + returnChatCompletionRequest: Boolean! = false + returnQueryRequest: Boolean! = false +} + +input PasswordChangeInput { + oldPassword: String + newPassword1: String! + newPassword2: String! +} + +input PasswordResetInput { + code: String! + password1: String! + password2: String! +} + +input RequestInvitationInput { + email: String! +} + +input RequestPasswordResetEmailInput { + email: String! +} + +input SecuritySettingInput { + allowedRegisterDomainList: [String!]! + disableClientSideTelemetry: Boolean! + disablePasswordLogin: Boolean! +} + +input SetPresetDocumentActiveInput { + id: ID! + active: Boolean! +} + +input ThreadRunDebugOptionsInput { + codeSearchParamsOverride: CodeSearchParamsOverrideInput = null + returnChatCompletionRequest: Boolean! = false +} + +input ThreadRunOptionsInput { + modelName: String = null + docQuery: DocQueryInput = null + codeQuery: CodeQueryInput = null + generateRelevantQuestions: Boolean! = false + debugOptions: ThreadRunDebugOptionsInput = null +} + +input ThreadToPageDebugOptionInput { + returnChatCompletionRequest: Boolean! = false + returnQueryRequest: Boolean! = false +} + +input UpdateIntegrationInput { + id: ID! + displayName: String! + accessToken: String + apiBase: String + kind: IntegrationKind! +} + +input UpdateLdapCredentialInput { + host: String! + port: Int! + bindDn: String! + bindPassword: String + baseDn: String! + userFilter: String! + encryption: LdapEncryptionKind! + skipTlsVerify: Boolean! + emailAttribute: String! + nameAttribute: String +} + +input UpdateMessageInput { + id: ID! + threadId: ID! + content: String! +} + +input UpdateOAuthCredentialInput { + provider: OAuthProvider! + configUrl: String + configScopes: String + clientId: String! + clientSecret: String +} + +input UpdatePageContentInput { + id: ID! + content: String! +} + +input UpdatePageSectionContentInput { + id: ID! + content: String! +} + +input UpdatePageSectionTitleInput { + id: ID! + title: String! +} + +input UpdatePageTitleInput { + id: ID! + title: String! +} + +input UpsertUserGroupMembershipInput { + userGroupId: ID! + userId: ID! + isGroupAdmin: Boolean! +} + +interface ContextSource implements ContextSourceId { + id: ID! + sourceId: String! + sourceKind: ContextSourceKind! + "Display name of the source, used to provide a human-readable name for user selection, such as in a dropdown menu." + sourceName: String! +} + +interface ContextSourceId { + "Represents the source of the context, which is the value mapped to `source_id` in the index." + sourceId: String! +} + +interface User { + id: ID! + email: String! + name: String! + createdAt: DateTime! + isAdmin: Boolean! + isOwner: Boolean! + active: Boolean! + isSsoUser: Boolean! +} + +""" + Combined date and time (with time zone) in [RFC 3339][0] format. + + Represents a description of an exact instant on the time-line (such as the + instant that a user account was created). + + [`DateTime` scalar][1] compliant. + + See also [`chrono::DateTime`][2] for details. + + [0]: https://datatracker.ietf.org/doc/html/rfc3339#section-5 + [1]: https://graphql-scalars.dev/docs/scalars/date-time + [2]: https://docs.rs/chrono/latest/chrono/struct.DateTime.html +""" +scalar DateTime + +type AttachmentCode { + gitUrl: String! + commit: String + filepath: String! + language: String! + content: String! + "When start line is `None`, it represents the entire file." + startLine: Int +} + +type AttachmentCodeFileList { + fileList: [String!]! + truncated: Boolean! +} + +type AttachmentCodeHit { + code: AttachmentCode! + scores: AttachmentCodeScores! +} + +type AttachmentCodeQueryDebugData { + sourceId: String! + query: String! +} + +type AttachmentCodeScores { + rrf: Float! + bm25: Float! + embedding: Float! +} + +type AttachmentCommitDoc { + sha: String! + message: String! + author: User + authorAt: DateTime! +} + +type AttachmentDocHit { + doc: AttachmentDoc! + score: Float! +} + +type AttachmentDocQueryDebugData { + sourceIds: [String!]! + query: String! +} + +type AttachmentIngestedDoc { + id: String! + title: String! + body: String! + link: String +} + +type AttachmentIssueDoc { + title: String! + link: String! + author: User + body: String! + closed: Boolean! +} + +type AttachmentPageDoc { + link: String! + title: String! + content: String! +} + +type AttachmentPullDoc { + title: String! + link: String! + author: User + body: String! + diff: String! + merged: Boolean! +} + +type AttachmentWebDoc { + title: String! + link: String! + content: String! +} + +type AuthProvider { + kind: AuthProviderKind! +} + +type BrandingSetting { + brandingLogo: String + brandingIcon: String +} + +type ChatCompletionMessage { + role: String! + content: String! +} + +type ChatCompletionStats { + start: DateTime! + end: DateTime! + userId: ID! + chats: Int! +} + +type CompletionStats { + start: DateTime! + end: DateTime! + language: Language! + completions: Int! + views: Int! + selects: Int! +} + +type ContextInfo { + sources: [ContextSource!]! +} + +type CustomDocumentConnection { + edges: [CustomDocumentEdge!]! + pageInfo: PageInfo! +} + +type CustomDocumentEdge { + node: CustomWebDocument! + cursor: String! +} + +type CustomWebDocument implements ContextSourceId & ContextSource { + url: String! + name: String! + id: ID! + createdAt: DateTime! + updatedAt: DateTime! + jobInfo: JobInfo! + sourceKind: ContextSourceKind! + sourceId: String! + sourceName: String! +} + +type DiskUsage { + filepath: [String!]! + "Size in kilobytes." + sizeKb: Float! +} + +type DiskUsageStats { + events: DiskUsage! + indexedRepositories: DiskUsage! + database: DiskUsage! + models: DiskUsage! +} + +type EmailSetting { + smtpUsername: String! + smtpServer: String! + smtpPort: Int! + fromAddress: String! + encryption: Encryption! + authMethod: AuthMethod! +} + +type FileEntrySearchResult { + type: String! + path: String! + "matched indices for fuzzy search query." + indices: [Int!]! +} + +type GitReference { + name: String! + commit: String! +} + +type GitRepository implements ContextSourceId { + id: ID! + sourceId: String! + name: String! + gitUrl: String! + refs: [GitReference!]! + jobInfo: JobInfo! +} + +type GrepFile { + path: String! + lines: [GrepLine!]! +} + +type GrepLine { + "Content of the line." + line: GrepTextOrBase64! + "Byte offset in the file to the start of the line." + byteOffset: Int! + "Line number in the file, starting from 1." + lineNumber: Int! + "The matches in the line." + subMatches: [GrepSubMatch!]! +} + +type GrepSubMatch { + bytesStart: Int! + bytesEnd: Int! +} + +type GrepTextOrBase64 { + text: String + base64: String +} + +type IngestedContextSource implements ContextSourceId & ContextSource { + id: ID! + sourceKind: ContextSourceKind! + sourceId: String! + sourceName: String! +} + +type IngestionStats { + source: String! + pending: Int! + failed: Int! + total: Int! +} + +type Integration { + id: ID! + kind: IntegrationKind! + displayName: String! + accessToken: String! + apiBase: String + createdAt: DateTime! + updatedAt: DateTime! + status: IntegrationStatus! + message: String +} + +type IntegrationConnection { + edges: [IntegrationEdge!]! + pageInfo: PageInfo! +} + +type IntegrationEdge { + node: Integration! + cursor: String! +} + +type Invitation { + id: ID! + email: String! + code: String! + createdAt: DateTime! +} + +type InvitationConnection { + edges: [InvitationEdge!]! + pageInfo: PageInfo! +} + +type InvitationEdge { + node: Invitation! + cursor: String! +} + +type JobInfo { + "Last run of the job." + lastJobRun: JobRun + "The command to submit job run using triggerJobRun mutation." + command: String! +} + +type JobRun { + id: ID! + job: String! + createdAt: DateTime! + updatedAt: DateTime! + startedAt: DateTime + finishedAt: DateTime + exitCode: Int + stdout: String! +} + +type JobRunConnection { + edges: [JobRunEdge!]! + pageInfo: PageInfo! +} + +type JobRunEdge { + node: JobRun! + cursor: String! +} + +type JobStats { + success: Int! + failed: Int! + pending: Int! +} + +type LdapCredential { + host: String! + port: Int! + bindDn: String! + baseDn: String! + userFilter: String! + encryption: LdapEncryptionKind! + skipTlsVerify: Boolean! + emailAttribute: String! + nameAttribute: String + createdAt: DateTime! + updatedAt: DateTime! +} + +type LicenseInfo { + type: LicenseType! + status: LicenseStatus! + seats: Int! + seatsUsed: Int! + issuedAt: DateTime + expiresAt: DateTime + features: [LicenseFeature!] +} + +type Message { + id: ID! + threadId: ID! + codeSourceId: String + role: Role! + content: String! + attachment: MessageAttachment! + createdAt: DateTime! + updatedAt: DateTime! +} + +"Represents an attachment to a message, which can include various types of content." +type MessageAttachment { + "Code snippets retrieved from the client side." + clientCode: [MessageAttachmentClientCode!]! + "Code snippets retrieved from the server side codebase." + code: [MessageAttachmentCode!]! + "Documents retrieved from various sources, all from the server side." + doc: [MessageAttachmentDoc!]! + "File list retrieved from the server side codebase is used for generating this message." + codeFileList: MessageAttachmentCodeFileList +} + +type MessageAttachmentClientCode { + filepath: String + startLine: Int + content: String! +} + +type MessageAttachmentCode { + gitUrl: String! + commit: String + filepath: String! + language: String! + content: String! + "When start line is `None`, it represents the entire file." + startLine: Int +} + +type MessageAttachmentCodeFileList { + fileList: [String!]! + truncated: Boolean! +} + +type MessageAttachmentCodeScores { + rrf: Float! + bm25: Float! + embedding: Float! +} + +type MessageAttachmentCommitDoc { + sha: String! + message: String! + author: User + authorAt: DateTime! +} + +type MessageAttachmentIngestedDoc { + id: String! + title: String! + body: String! + link: String +} + +type MessageAttachmentIssueDoc { + title: String! + link: String! + author: User + body: String! + closed: Boolean! +} + +type MessageAttachmentPageDoc { + link: String! + title: String! + content: String! +} + +type MessageAttachmentPullDoc { + title: String! + link: String! + author: User + body: String! + patch: String! + merged: Boolean! +} + +type MessageAttachmentWebDoc { + title: String! + link: String! + content: String! +} + +type MessageCodeSearchHit { + code: MessageAttachmentCode! + scores: MessageAttachmentCodeScores! +} + +type MessageConnection { + edges: [MessageEdge!]! + pageInfo: PageInfo! +} + +type MessageDocSearchHit { + doc: MessageAttachmentDoc! + score: Float! +} + +type MessageEdge { + node: Message! + cursor: String! +} + +type ModelBackendHealthInfo { + "Latency in milliseconds." + latencyMs: Int! +} + +type Mutation { + resetRegistrationToken: String! + requestInvitationEmail(input: RequestInvitationInput!): Invitation! + generateResetPasswordUrl(userId: ID!): String! + requestPasswordResetEmail(input: RequestPasswordResetEmailInput!): Boolean! + passwordReset(input: PasswordResetInput!): Boolean! + passwordChange(input: PasswordChangeInput!): Boolean! + resetUserAuthToken: Boolean! + logoutAllSessions: Boolean! + updateUserActive(id: ID!, active: Boolean!): Boolean! + updateUserRole(id: ID!, isAdmin: Boolean!): Boolean! + uploadUserAvatarBase64(id: ID!, avatarBase64: String): Boolean! + updateUserName(id: ID!, name: String!): Boolean! + register(email: String!, password1: String!, password2: String!, invitationCode: String, name: String!): RegisterResponse! + tokenAuth(email: String!, password: String!): TokenAuthResponse! + tokenAuthLdap(userId: String!, password: String!): TokenAuthResponse! + verifyToken(token: String!): Boolean! + refreshToken(refreshToken: String!): RefreshTokenResponse! + createInvitation(email: String!): ID! + sendTestEmail(to: String!): Boolean! + markNotificationsRead(notificationId: ID): Boolean! + createGitRepository(name: String!, gitUrl: String!, refs: [String!]): ID! + deleteGitRepository(id: ID!): Boolean! + updateGitRepository(id: ID!, refs: [String!]): Boolean! + deleteInvitation(id: ID!): ID! + updateOauthCredential(input: UpdateOAuthCredentialInput!): Boolean! + deleteOauthCredential(provider: OAuthProvider!): Boolean! + testLdapConnection(input: UpdateLdapCredentialInput!): Boolean! + updateLdapCredential(input: UpdateLdapCredentialInput!): Boolean! + deleteLdapCredential: Boolean! + updateEmailSetting(input: EmailSettingInput!): Boolean! + updateSecuritySetting(input: SecuritySettingInput!): Boolean! + updateNetworkSetting(input: NetworkSettingInput!): Boolean! + updateBrandingSetting(input: BrandingSettingInput!): Boolean! + deleteEmailSetting: Boolean! + uploadLicense(license: String!): Boolean! + resetLicense: Boolean! + createIntegration(input: CreateIntegrationInput!): ID! + updateIntegration(input: UpdateIntegrationInput!): Boolean! + deleteIntegration(id: ID!, kind: IntegrationKind!): Boolean! + updateIntegratedRepositoryActive(id: ID!, active: Boolean!, refs: [String!]): Boolean! + updateIntegratedRepositoryRefs(id: ID!, refs: [String!]!): Boolean! + "Trigger a job run given its param string." + triggerJobRun(command: String!): ID! + "Delete pair of user message and bot response in a thread." + deleteThreadMessagePair(threadId: ID!, userMessageId: ID!, assistantMessageId: ID!): Boolean! + deleteThread(id: ID!): Boolean! + "Turn on persisted status for a thread." + setThreadPersisted(threadId: ID!): Boolean! + updateThreadMessage(input: UpdateMessageInput!): Boolean! + updatePageTitle(input: UpdatePageTitleInput!): Boolean! + updatePageContent(input: UpdatePageContentInput!): Boolean! + updatePageSectionTitle(input: UpdatePageSectionTitleInput!): Boolean! + updatePageSectionContent(input: UpdatePageSectionContentInput!): Boolean! + "delete a page and all its sections." + deletePage(id: ID!): Boolean! + "delete a single page section." + deletePageSection(sectionId: ID!): Boolean! + movePageSection(id: ID!, direction: MoveSectionDirection!): Boolean! + createCustomDocument(input: CreateCustomDocumentInput!): ID! + deleteCustomDocument(id: ID!): Boolean! + setPresetDocumentActive(input: SetPresetDocumentActiveInput!): Boolean! + createUserGroup(input: CreateUserGroupInput!): ID! + deleteUserGroup(id: ID!): Boolean! + upsertUserGroupMembership(input: UpsertUserGroupMembershipInput!): Boolean! + deleteUserGroupMembership(userGroupId: ID!, userId: ID!): Boolean! + grantSourceIdReadAccess(sourceId: String!, userGroupId: ID!): Boolean! + revokeSourceIdReadAccess(sourceId: String!, userGroupId: ID!): Boolean! +} + +type NetworkSetting { + externalUrl: String! +} + +type Notification { + id: ID! + content: String! + read: Boolean! + createdAt: DateTime! + updatedAt: DateTime! +} + +type OAuthCredential { + provider: OAuthProvider! + configUrl: String + configScopes: String + clientId: String! + createdAt: DateTime! + updatedAt: DateTime! +} + +type Page { + id: ID! + authorId: ID! + title: String + codeSourceId: String + content: String + createdAt: DateTime! + updatedAt: DateTime! +} + +type PageCompleted { + id: ID! +} + +type PageConnection { + edges: [PageEdge!]! + pageInfo: PageInfo! +} + +type PageContentCompleted { + id: ID! + debugData: PageContentDebugData +} + +type PageContentDebugData { + "Messages sent to LLM to generate the response." + generatePageContentMessages: [ChatCompletionMessage!]! +} + +type PageContentDelta { + delta: String! +} + +type PageContextSource implements ContextSourceId & ContextSource { + id: ID! + sourceKind: ContextSourceKind! + sourceId: String! + sourceName: String! +} + +type PageCreated { + id: ID! + authorId: ID! + title: String! + debugData: PageTitleDebugData +} + +type PageEdge { + node: Page! + cursor: String! +} + +type PageInfo { + hasPreviousPage: Boolean! + hasNextPage: Boolean! + startCursor: String + endCursor: String +} + +type PageSection { + id: ID! + pageId: ID! + title: String! + content: String! + position: Int! + attachments: SectionAttachment! + createdAt: DateTime! + updatedAt: DateTime! + debugData: PageSectionDebugData +} + +type PageSectionAttachmentCode { + id: ID! + codes: [AttachmentCodeHit!]! + debugData: AttachmentCodeQueryDebugData +} + +type PageSectionAttachmentCodeFileList { + id: ID! + codeFileList: AttachmentCodeFileList! +} + +type PageSectionAttachmentDoc { + id: ID! + doc: [AttachmentDocHit!]! + debugData: AttachmentDocQueryDebugData +} + +type PageSectionContentCompleted { + id: ID! + debugData: PageSectionContentDebugData +} + +type PageSectionContentDebugData { + "Messages sent to LLM to generate the response." + generateSectionContentMessages: [ChatCompletionMessage!]! +} + +type PageSectionContentDelta { + id: ID! + delta: String! +} + +type PageSectionCreated { + id: ID! + pageId: ID! + title: String! + position: Int! + createdAt: DateTime! + updatedAt: DateTime! + debugData: PageSectionDebugData +} + +type PageSectionDebugData { + "Messages sent to LLM to generate the page section titles." + generateSectionTitlesMessages: [ChatCompletionMessage!]! +} + +type PageSectionsCreated { + sections: [PageSectionCreated!]! + debugData: PageSectionDebugData +} + +type PageTitleDebugData { + "Messages sent to LLM to generate the page title." + generatePageTitleMessages: [ChatCompletionMessage!]! +} + +type PresetDocumentConnection { + edges: [PresetDocumentEdge!]! + pageInfo: PageInfo! +} + +type PresetDocumentEdge { + node: PresetWebDocument! + cursor: String! +} + +type PresetWebDocument implements ContextSourceId & ContextSource { + name: String! + id: ID! + "`updated_at` is only filled when the preset is active." + updatedAt: DateTime + "`job_info` is only filled when the preset is active." + jobInfo: JobInfo + isActive: Boolean! + sourceKind: ContextSourceKind! + sourceId: String! + sourceName: String! +} + +type ProvidedRepository implements ContextSourceId { + id: ID! + integrationId: ID! + active: Boolean! + displayName: String! + gitUrl: String! + vendorId: String! + createdAt: DateTime! + updatedAt: DateTime! + refs: [GitReference!]! + jobInfo: JobInfo! + sourceId: String! +} + +type ProvidedRepositoryConnection { + edges: [ProvidedRepositoryEdge!]! + pageInfo: PageInfo! +} + +type ProvidedRepositoryEdge { + node: ProvidedRepository! + cursor: String! +} + +type Query { + registrationToken: String! + me: UserSecured! + "List users, accessible for all login users." + users(ids: [ID!], after: String, before: String, first: Int, last: Int): UserConnection! + invitations(after: String, before: String, first: Int, last: Int): InvitationConnection! + jobRuns(ids: [ID!], jobs: [String!], after: String, before: String, first: Int, last: Int): JobRunConnection! + jobRunStats(jobs: [String!]): JobStats! + emailSetting: EmailSetting + networkSetting: NetworkSetting! + securitySetting: SecuritySetting! + brandingSetting: BrandingSetting! + gitRepositories(after: String, before: String, first: Int, last: Int): RepositoryConnection! + "Search files that matches the pattern in the repository." + repositorySearch(kind: RepositoryKind!, id: ID!, rev: String, pattern: String!): [FileEntrySearchResult!]! + """ + File content search with a grep-like experience. + + Syntax: + + 1. Unprefixed text will be treated as a regex pattern for file content search. + 2. 'f:' to search by file name with a regex pattern. + 3. 'lang:' to search by file language. + 4. All tokens can be negated by prefixing them with '-'. + + Examples: + * `f:schema -lang:rust fn` + * `func_name lang:go` + """ + repositoryGrep(kind: RepositoryKind!, id: ID!, rev: String, query: String!): RepositoryGrepOutput! + authProviders: [AuthProvider!]! + oauthCredential(provider: OAuthProvider!): OAuthCredential + oauthCallbackUrl(provider: OAuthProvider!): String! + ldapCredential: LdapCredential + serverInfo: ServerInfo! + license: LicenseInfo! + jobs: [String!]! + dailyStatsInPastYear(users: [ID!]): [CompletionStats!]! + dailyStats(start: DateTime!, end: DateTime!, users: [ID!], languages: [Language!]): [CompletionStats!]! + chatDailyStatsInPastYear(users: [ID!]): [ChatCompletionStats!]! + chatDailyStats(start: DateTime!, end: DateTime!, users: [ID!]): [ChatCompletionStats!]! + userEvents(after: String, before: String, first: Int, last: Int, users: [ID!], start: DateTime!, end: DateTime!): UserEventConnection! + notifications: [Notification!]! + diskUsageStats: DiskUsageStats! + repositoryList: [Repository!]! + contextInfo: ContextInfo! + integrations(ids: [ID!], kind: IntegrationKind, after: String, before: String, first: Int, last: Int): IntegrationConnection! + integratedRepositories(ids: [ID!], kind: IntegrationKind, active: Boolean, after: String, before: String, first: Int, last: Int): ProvidedRepositoryConnection! + ingestionStatus(sources: [String!]): [IngestionStats!]! + threads(ids: [ID!], isEphemeral: Boolean, after: String, before: String, first: Int, last: Int): ThreadConnection! + myThreads(after: String, before: String, first: Int, last: Int): ThreadConnection! + """ + Read thread messages by thread ID. + + Thread is public within an instance, so no need to check for ownership. + """ + threadMessages(threadId: ID!, after: String, before: String, first: Int, last: Int): MessageConnection! + "Read pages by page IDs." + pages(ids: [ID!], after: String, before: String, first: Int, last: Int): PageConnection! + pageSections(pageId: ID!, after: String, before: String, first: Int, last: Int): SectionConnection! + customWebDocuments(ids: [ID!], after: String, before: String, first: Int, last: Int): CustomDocumentConnection! + presetWebDocuments(ids: [ID!], after: String, before: String, first: Int, last: Int, isActive: Boolean): PresetDocumentConnection! + "List user groups." + userGroups: [UserGroup!]! + sourceIdAccessPolicies(sourceId: String!): SourceIdAccessPolicy! + testModelConnection(backend: ModelHealthBackend!): ModelBackendHealthInfo! + readRepositoryRelatedQuestions(sourceId: String!): [String!]! +} + +type RefreshTokenResponse { + accessToken: String! + refreshToken: String! + refreshExpiresAt: DateTime! +} + +type RegisterResponse { + accessToken: String! + refreshToken: String! +} + +type Repository implements ContextSourceId & ContextSource { + id: ID! + sourceId: String! + sourceKind: ContextSourceKind! + sourceName: String! + name: String! + kind: RepositoryKind! + gitUrl: String! + refs: [GitReference!]! +} + +type RepositoryConnection { + edges: [RepositoryEdge!]! + pageInfo: PageInfo! +} + +type RepositoryEdge { + node: GitRepository! + cursor: String! +} + +type RepositoryGrepOutput { + files: [GrepFile!]! + "Elapsed time in milliseconds for grep search." + elapsedMs: Int! +} + +type SectionAttachment { + code: [AttachmentCode!]! + codeFileList: AttachmentCodeFileList + doc: [AttachmentDoc!]! +} + +type SectionConnection { + edges: [SectionEdge!]! + pageInfo: PageInfo! +} + +type SectionEdge { + node: PageSection! + cursor: String! +} + +type SecuritySetting { + allowedRegisterDomainList: [String!]! + disableClientSideTelemetry: Boolean! + disablePasswordLogin: Boolean! +} + +type ServerInfo { + isAdminInitialized: Boolean! + isChatEnabled: Boolean! + isEmailConfigured: Boolean! + allowSelfSignup: Boolean! + disablePasswordLogin: Boolean! + isDemoMode: Boolean! +} + +type SourceIdAccessPolicy { + sourceId: String! + read: [UserGroup!]! +} + +type Subscription { + createThreadAndRun(input: CreateThreadAndRunInput!): ThreadRunItem! + createThreadRun(input: CreateThreadRunInput!): ThreadRunItem! + createPageRun(input: CreatePageRunInput!): PageRunItem! + """ + Utilize an existing thread and its messages to create a page. + This will automatically generate: + - the page title and a summary of the content. + - a few sections based on the thread messages. + """ + createThreadToPageRun(input: CreateThreadToPageRunInput!): PageRunItem! + createPageSectionRun(input: CreatePageSectionRunInput!): SectionRunItem! +} + +type Thread { + id: ID! + userId: ID! + isEphemeral: Boolean! + createdAt: DateTime! + updatedAt: DateTime! +} + +type ThreadAssistantMessageAttachmentsCode { + hits: [MessageCodeSearchHit!]! +} + +type ThreadAssistantMessageAttachmentsCodeFileList { + fileList: [String!]! + truncated: Boolean! +} + +type ThreadAssistantMessageAttachmentsDoc { + hits: [MessageDocSearchHit!]! +} + +type ThreadAssistantMessageCompleted { + "Debug data for the assistant message completion." + debugData: ThreadAssistantMessageCompletedDebugData +} + +type ThreadAssistantMessageCompletedDebugData { + chatCompletionMessages: [ChatCompletionMessage!]! +} + +type ThreadAssistantMessageContentDelta { + delta: String! +} + +type ThreadAssistantMessageCreated { + id: ID! +} + +type ThreadAssistantMessageReadingCode { + snippet: Boolean! + fileList: Boolean! +} + +type ThreadAssistantMessageReadingDoc { + sourceIds: [String!]! +} + +type ThreadConnection { + edges: [ThreadEdge!]! + pageInfo: PageInfo! +} + +type ThreadCreated { + id: ID! +} + +type ThreadEdge { + node: Thread! + cursor: String! +} + +type ThreadRelevantQuestions { + questions: [String!]! +} + +type ThreadUserMessageCreated { + id: ID! +} + +type TokenAuthResponse { + accessToken: String! + refreshToken: String! +} + +type UserConnection { + edges: [UserEdge!]! + pageInfo: PageInfo! +} + +type UserEdge { + node: User! + cursor: String! +} + +type UserEvent { + id: ID! + userId: ID! + kind: EventKind! + createdAt: DateTime! + payload: String! +} + +type UserEventConnection { + edges: [UserEventEdge!]! + pageInfo: PageInfo! +} + +type UserEventEdge { + node: UserEvent! + cursor: String! +} + +type UserGroup { + id: ID! + name: String! + createdAt: DateTime! + updatedAt: DateTime! + members: [UserGroupMembership!]! +} + +type UserGroupMembership { + user: User! + isGroupAdmin: Boolean! + createdAt: DateTime! + updatedAt: DateTime! +} + +type UserSecured implements User { + id: ID! + email: String! + name: String! + createdAt: DateTime! + isAdmin: Boolean! + isOwner: Boolean! + active: Boolean! + authToken: String! + isPasswordSet: Boolean! + isSsoUser: Boolean! +} + +type WebContextSource implements ContextSourceId & ContextSource { + id: ID! + sourceKind: ContextSourceKind! + sourceId: String! + sourceName: String! +} + +union AttachmentDoc = AttachmentWebDoc | AttachmentIssueDoc | AttachmentPullDoc | AttachmentCommitDoc | AttachmentPageDoc | AttachmentIngestedDoc + +union MessageAttachmentDoc = MessageAttachmentWebDoc | MessageAttachmentIssueDoc | MessageAttachmentPullDoc | MessageAttachmentCommitDoc | MessageAttachmentPageDoc | MessageAttachmentIngestedDoc + +"Schema of page convert stream." +union PageRunItem = PageCreated | PageContentDelta | PageContentCompleted | PageSectionsCreated | PageSectionAttachmentCodeFileList | PageSectionAttachmentCode | PageSectionAttachmentDoc | PageSectionContentDelta | PageSectionContentCompleted | PageCompleted + +"Schema of page convert stream." +union SectionRunItem = PageSectionCreated | PageSectionAttachmentCodeFileList | PageSectionAttachmentCode | PageSectionAttachmentDoc | PageSectionContentDelta | PageSectionContentCompleted + +""" + Schema of thread run stream. + + Apart from `thread_message_content_delta`, all other items will only appear once in the stream. +""" +union ThreadRunItem = ThreadCreated | ThreadRelevantQuestions | ThreadUserMessageCreated | ThreadAssistantMessageCreated | ThreadAssistantMessageReadingCode | ThreadAssistantMessageAttachmentsCodeFileList | ThreadAssistantMessageAttachmentsCode | ThreadAssistantMessageReadingDoc | ThreadAssistantMessageAttachmentsDoc | ThreadAssistantMessageContentDelta | ThreadAssistantMessageCompleted diff --git a/ee/tabby-schema/src/dao.rs b/ee/tabby-schema/src/dao.rs new file mode 100644 index 000000000000..e8bbf7937f92 --- /dev/null +++ b/ee/tabby-schema/src/dao.rs @@ -0,0 +1,751 @@ +use anyhow::bail; +use hash_ids::HashIds; +use lazy_static::lazy_static; +use tabby_db::{ + AttachmentClientCode, AttachmentCode, AttachmentCodeFileList, AttachmentCommitDoc, + AttachmentDoc, AttachmentIngestedDoc, AttachmentIssueDoc, AttachmentPageDoc, AttachmentPullDoc, + AttachmentWebDoc, EmailSettingDAO, IngestedDocumentDAO, IngestedDocumentStatusDAO, + IngestionStatusDAO, IntegrationDAO, InvitationDAO, JobRunDAO, LdapCredentialDAO, + NotificationDAO, OAuthCredentialDAO, PageDAO, ServerSettingDAO, ThreadDAO, UserEventDAO, +}; + +use crate::{ + auth::LdapEncryptionKind, + ingestion::IngestionStats, + integration::{Integration, IntegrationKind, IntegrationStatus}, + interface::UserValue, + notification::{Notification, NotificationRecipient}, + page, + repository::RepositoryKind, + retrieval, + schema::{ + auth::{self, LdapCredential, OAuthCredential, OAuthProvider}, + email::{AuthMethod, EmailSetting, Encryption}, + ingestion::{IngestedDocStatus, IngestedDocument}, + job, + repository::{ + GithubRepositoryProvider, GitlabRepositoryProvider, RepositoryProviderStatus, + }, + setting::{NetworkSetting, SecuritySetting}, + user_event::{EventKind, UserEvent}, + CoreError, + }, + setting::BrandingSetting, + thread::{self}, +}; + +impl From for auth::Invitation { + fn from(val: InvitationDAO) -> Self { + Self { + id: (val.id as i32).as_id(), + email: val.email, + code: val.code, + created_at: val.created_at, + } + } +} + +impl From for job::JobRun { + fn from(run: JobRunDAO) -> Self { + Self { + id: run.id.as_id(), + job: run.name, + created_at: run.created_at, + updated_at: run.updated_at, + started_at: run.started_at, + finished_at: run.finished_at, + exit_code: run.exit_code.map(|i| i as i32), + stdout: run.stdout, + } + } +} + +impl TryFrom for OAuthCredential { + type Error = anyhow::Error; + + fn try_from(val: OAuthCredentialDAO) -> Result { + Ok(OAuthCredential { + provider: OAuthProvider::from_enum_str(&val.provider)?, + config_url: val.config_url, + config_scopes: val.config_scopes, + client_id: val.client_id, + created_at: val.created_at, + updated_at: val.updated_at, + client_secret: val.client_secret, + }) + } +} + +impl TryFrom for LdapCredential { + type Error = anyhow::Error; + + fn try_from(val: LdapCredentialDAO) -> Result { + Ok(LdapCredential { + host: val.host, + port: val.port as i32, + bind_dn: val.bind_dn, + base_dn: val.base_dn, + user_filter: val.user_filter, + encryption: LdapEncryptionKind::from_enum_str(&val.encryption)?, + skip_tls_verify: val.skip_tls_verify, + email_attribute: val.email_attribute, + name_attribute: val.name_attribute, + created_at: val.created_at, + updated_at: val.updated_at, + }) + } +} + +impl TryFrom for EmailSetting { + type Error = anyhow::Error; + + fn try_from(value: EmailSettingDAO) -> Result { + let encryption = Encryption::from_enum_str(&value.encryption)?; + let auth_method = AuthMethod::from_enum_str(&value.auth_method)?; + + Ok(EmailSetting { + smtp_username: value.smtp_username, + smtp_server: value.smtp_server, + smtp_port: value.smtp_port as i32, + from_address: value.from_address, + encryption, + auth_method, + }) + } +} + +impl From for SecuritySetting { + fn from(value: ServerSettingDAO) -> Self { + Self { + allowed_register_domain_list: value + .security_allowed_register_domain_list() + .map(|s| s.to_owned()) + .collect(), + disable_client_side_telemetry: value.security_disable_client_side_telemetry, + disable_password_login: value.security_disable_password_login, + } + } +} + +impl From for NetworkSetting { + fn from(value: ServerSettingDAO) -> Self { + Self { + external_url: value.network_external_url, + } + } +} + +impl From for BrandingSetting { + fn from(value: ServerSettingDAO) -> Self { + Self { + branding_logo: value.branding_logo, + branding_icon: value.branding_icon, + } + } +} + +impl TryFrom for Integration { + type Error = anyhow::Error; + fn try_from(value: IntegrationDAO) -> anyhow::Result { + let status = if value.synced && value.error.is_none() { + IntegrationStatus::Ready + } else if value.error.is_some() { + IntegrationStatus::Failed + } else { + IntegrationStatus::Pending + }; + Ok(Self { + id: value.id.as_id(), + kind: IntegrationKind::from_enum_str(&value.kind)?, + display_name: value.display_name, + access_token: value.access_token, + api_base: value.api_base, + created_at: value.created_at, + updated_at: value.updated_at, + status, + message: value.error, + }) + } +} + +impl From for RepositoryKind { + fn from(value: IntegrationKind) -> Self { + match value { + IntegrationKind::Github => RepositoryKind::Github, + IntegrationKind::Gitlab => RepositoryKind::Gitlab, + IntegrationKind::GithubSelfHosted => RepositoryKind::GithubSelfHosted, + IntegrationKind::GitlabSelfHosted => RepositoryKind::GitlabSelfHosted, + } + } +} + +impl From for GithubRepositoryProvider { + fn from(value: Integration) -> Self { + Self { + id: value.id, + display_name: value.display_name, + status: value.status.into(), + access_token: Some(value.access_token), + api_base: value.api_base, + } + } +} + +impl From for GitlabRepositoryProvider { + fn from(value: Integration) -> Self { + Self { + id: value.id, + display_name: value.display_name, + status: value.status.into(), + access_token: Some(value.access_token), + api_base: value.api_base, + } + } +} + +impl From for RepositoryProviderStatus { + fn from(value: IntegrationStatus) -> Self { + match value { + IntegrationStatus::Ready => Self::Ready, + IntegrationStatus::Pending => Self::Pending, + IntegrationStatus::Failed => Self::Failed, + } + } +} + +impl TryFrom for UserEvent { + type Error = anyhow::Error; + fn try_from(value: UserEventDAO) -> Result { + Ok(Self { + id: value.id.as_id(), + user_id: value.user_id.as_id(), + kind: EventKind::from_enum_str(&value.kind)?, + created_at: value.created_at, + payload: String::from_utf8(value.payload)?, + }) + } +} + +impl From for Notification { + fn from(value: NotificationDAO) -> Self { + Self { + id: value.id.as_id(), + content: value.content, + read: value.read, + created_at: value.created_at, + updated_at: value.updated_at, + } + } +} + +impl From for thread::MessageAttachmentCode { + fn from(value: AttachmentCode) -> Self { + Self { + git_url: value.git_url, + commit: value.commit, + filepath: value.filepath, + language: value.language, + content: value.content, + start_line: value.start_line.map(|x| x as i32), + } + } +} + +impl From<&thread::MessageAttachmentCode> for AttachmentCode { + fn from(val: &thread::MessageAttachmentCode) -> Self { + AttachmentCode { + git_url: val.git_url.clone(), + commit: val.commit.clone(), + filepath: val.filepath.clone(), + language: val.language.clone(), + content: val.content.clone(), + start_line: val.start_line.map(|x| x as usize), + } + } +} + +impl From for thread::MessageAttachmentClientCode { + fn from(value: AttachmentClientCode) -> Self { + Self { + filepath: value.filepath, + content: value.content, + start_line: value.start_line.map(|x| x as i32), + } + } +} + +impl From<&thread::MessageAttachmentCodeInput> for AttachmentClientCode { + fn from(val: &thread::MessageAttachmentCodeInput) -> Self { + AttachmentClientCode { + filepath: val.filepath.clone(), + content: val.content.clone(), + start_line: val.start_line.map(|x| x as usize), + } + } +} + +impl From for thread::MessageAttachmentCodeFileList { + fn from(value: AttachmentCodeFileList) -> Self { + Self { + file_list: value.file_list, + truncated: value.truncated, + } + } +} + +pub fn from_thread_message_attachment_document( + doc: AttachmentDoc, + author: Option, +) -> thread::MessageAttachmentDoc { + match doc { + AttachmentDoc::Web(web) => { + thread::MessageAttachmentDoc::Web(thread::MessageAttachmentWebDoc { + title: web.title, + link: web.link, + content: web.content, + }) + } + AttachmentDoc::Issue(issue) => { + thread::MessageAttachmentDoc::Issue(thread::MessageAttachmentIssueDoc { + title: issue.title, + link: issue.link, + author, + body: issue.body, + closed: issue.closed, + }) + } + AttachmentDoc::Pull(pull) => { + thread::MessageAttachmentDoc::Pull(thread::MessageAttachmentPullDoc { + title: pull.title, + link: pull.link, + author, + body: pull.body, + patch: pull.diff, + merged: pull.merged, + }) + } + AttachmentDoc::Commit(commit) => { + thread::MessageAttachmentDoc::Commit(thread::MessageAttachmentCommitDoc { + sha: commit.sha, + message: commit.message, + author, + author_at: commit.author_at, + }) + } + AttachmentDoc::Page(page) => { + thread::MessageAttachmentDoc::Page(thread::MessageAttachmentPageDoc { + link: page.page_link, + title: page.title, + content: page.content, + }) + } + AttachmentDoc::Ingested(ingested) => { + thread::MessageAttachmentDoc::Ingested(thread::MessageAttachmentIngestedDoc { + id: ingested.id, + link: ingested.link, + title: ingested.title, + body: ingested.body, + }) + } + } +} + +impl From<&thread::MessageAttachmentDoc> for AttachmentDoc { + fn from(val: &thread::MessageAttachmentDoc) -> Self { + match val { + thread::MessageAttachmentDoc::Web(val) => AttachmentDoc::Web(AttachmentWebDoc { + title: val.title.clone(), + link: val.link.clone(), + content: val.content.clone(), + }), + thread::MessageAttachmentDoc::Issue(val) => AttachmentDoc::Issue(AttachmentIssueDoc { + title: val.title.clone(), + link: val.link.clone(), + author_user_id: val.author.as_ref().map(|x| match x { + UserValue::UserSecured(user) => user.id.to_string(), + }), + body: val.body.clone(), + closed: val.closed, + }), + thread::MessageAttachmentDoc::Pull(val) => AttachmentDoc::Pull(AttachmentPullDoc { + title: val.title.clone(), + link: val.link.clone(), + author_user_id: val.author.as_ref().map(|x| match x { + UserValue::UserSecured(user) => user.id.to_string(), + }), + body: val.body.clone(), + diff: val.patch.clone(), + merged: val.merged, + }), + thread::MessageAttachmentDoc::Commit(val) => { + AttachmentDoc::Commit(AttachmentCommitDoc { + sha: val.sha.clone(), + message: val.message.clone(), + author_user_id: val.author.as_ref().map(|x| match x { + UserValue::UserSecured(user) => user.id.to_string(), + }), + author_at: val.author_at, + }) + } + thread::MessageAttachmentDoc::Page(val) => AttachmentDoc::Page(AttachmentPageDoc { + page_link: val.link.clone(), + title: val.title.clone(), + content: val.content.clone(), + }), + thread::MessageAttachmentDoc::Ingested(val) => { + AttachmentDoc::Ingested(AttachmentIngestedDoc { + id: val.id.clone(), + link: val.link.clone(), + title: val.title.clone(), + body: val.body.clone(), + }) + } + } + } +} + +impl From for thread::Thread { + fn from(value: ThreadDAO) -> Self { + Self { + id: value.id.as_id(), + user_id: value.user_id.as_id(), + is_ephemeral: value.is_ephemeral, + created_at: value.created_at, + updated_at: value.updated_at, + } + } +} + +impl From for page::Page { + fn from(value: PageDAO) -> Self { + Self { + id: value.id.as_id(), + author_id: value.author_id.as_id(), + title: value.title, + code_source_id: value.code_source_id, + content: value.content, + created_at: value.created_at, + updated_at: value.updated_at, + } + } +} + +impl From<&retrieval::AttachmentCode> for AttachmentCode { + fn from(value: &retrieval::AttachmentCode) -> Self { + Self { + git_url: value.git_url.clone(), + commit: value.commit.clone(), + filepath: value.filepath.clone(), + language: value.language.clone(), + content: value.content.clone(), + start_line: value.start_line.map(|x| x as usize), + } + } +} + +impl From<&AttachmentCode> for retrieval::AttachmentCode { + fn from(value: &AttachmentCode) -> Self { + Self { + git_url: value.git_url.clone(), + commit: value.commit.clone(), + filepath: value.filepath.clone(), + language: value.language.clone(), + content: value.content.clone(), + start_line: value.start_line.map(|x| x as i32), + } + } +} + +impl From for retrieval::AttachmentCodeFileList { + fn from(value: AttachmentCodeFileList) -> Self { + Self { + file_list: value.file_list, + truncated: value.truncated, + } + } +} + +impl From<&retrieval::AttachmentDoc> for AttachmentDoc { + fn from(value: &retrieval::AttachmentDoc) -> Self { + match value { + retrieval::AttachmentDoc::Web(web) => AttachmentDoc::Web(AttachmentWebDoc { + title: web.title.clone(), + link: web.link.clone(), + content: web.content.clone(), + }), + retrieval::AttachmentDoc::Issue(issue) => AttachmentDoc::Issue(AttachmentIssueDoc { + title: issue.title.clone(), + link: issue.link.clone(), + author_user_id: issue.author.as_ref().map(|x| match x { + UserValue::UserSecured(user) => user.id.to_string(), + }), + body: issue.body.clone(), + closed: issue.closed, + }), + retrieval::AttachmentDoc::Pull(pull) => AttachmentDoc::Pull(AttachmentPullDoc { + title: pull.title.clone(), + link: pull.link.clone(), + author_user_id: pull.author.as_ref().map(|x| match x { + UserValue::UserSecured(user) => user.id.to_string(), + }), + body: pull.body.clone(), + diff: pull.diff.clone(), + merged: pull.merged, + }), + retrieval::AttachmentDoc::Commit(commit) => { + AttachmentDoc::Commit(AttachmentCommitDoc { + sha: commit.sha.clone(), + message: commit.message.clone(), + author_user_id: commit.author.as_ref().map(|x| match x { + UserValue::UserSecured(user) => user.id.to_string(), + }), + author_at: commit.author_at, + }) + } + retrieval::AttachmentDoc::Page(page) => AttachmentDoc::Page(AttachmentPageDoc { + page_link: page.link.clone(), + title: page.title.clone(), + content: page.content.clone(), + }), + retrieval::AttachmentDoc::Ingested(ingested) => { + AttachmentDoc::Ingested(AttachmentIngestedDoc { + id: ingested.id.clone(), + link: ingested.link.clone(), + title: ingested.title.clone(), + body: ingested.body.clone(), + }) + } + } + } +} + +impl From for IngestedDocStatus { + fn from(value: IngestedDocumentStatusDAO) -> Self { + match value { + IngestedDocumentStatusDAO::Pending => IngestedDocStatus::Pending, + IngestedDocumentStatusDAO::Failed => IngestedDocStatus::Failed, + IngestedDocumentStatusDAO::Indexed => IngestedDocStatus::Indexed, + } + } +} + +impl From for IngestedDocument { + fn from(value: IngestedDocumentDAO) -> Self { + Self { + id: value.doc_id, + source: value.source, + link: value.link, + title: value.title, + body: value.body, + status: value.status.into(), + } + } +} + +impl From for IngestionStats { + fn from(value: IngestionStatusDAO) -> Self { + Self { + source: value.source, + pending: value.pending, + failed: value.failed, + total: value.total, + } + } +} + +lazy_static! { + static ref HASHER: HashIds = HashIds::builder() + .with_salt("tabby-id-serializer") + .with_min_length(6) + .finish(); +} + +pub trait AsRowid { + fn as_rowid(&self) -> std::result::Result; +} + +impl AsRowid for juniper::ID { + fn as_rowid(&self) -> std::result::Result { + HASHER + .decode(self) + .and_then(|x| x.first().map(|i| *i as i64)) + .ok_or(CoreError::InvalidID) + } +} + +pub trait AsID { + fn as_id(&self) -> juniper::ID; +} + +impl AsID for i64 { + fn as_id(&self) -> juniper::ID { + juniper::ID::new(HASHER.encode(&[*self as u64])) + } +} + +impl AsID for i32 { + fn as_id(&self) -> juniper::ID { + (*self as i64).as_id() + } +} + +pub trait DbEnum: Sized { + fn as_enum_str(&self) -> &'static str; + fn from_enum_str(s: &str) -> anyhow::Result; +} + +impl DbEnum for EventKind { + fn as_enum_str(&self) -> &'static str { + match self { + EventKind::Completion => "completion", + EventKind::ChatCompletion => "chat_completion", + EventKind::Select => "select", + EventKind::View => "view", + EventKind::Dismiss => "dismiss", + } + } + + fn from_enum_str(s: &str) -> anyhow::Result { + match s { + "completion" => Ok(EventKind::Completion), + "chat_completion" => Ok(EventKind::ChatCompletion), + "select" => Ok(EventKind::Select), + "view" => Ok(EventKind::View), + "dismiss" => Ok(EventKind::Dismiss), + _ => bail!("{s} is not a valid value for EventKind"), + } + } +} + +impl DbEnum for IntegrationKind { + fn as_enum_str(&self) -> &'static str { + match self { + IntegrationKind::Github => "github", + IntegrationKind::Gitlab => "gitlab", + IntegrationKind::GithubSelfHosted => "github_self_hosted", + IntegrationKind::GitlabSelfHosted => "gitlab_self_hosted", + } + } + + fn from_enum_str(s: &str) -> anyhow::Result { + match s { + "github" => Ok(IntegrationKind::Github), + "gitlab" => Ok(IntegrationKind::Gitlab), + "github_self_hosted" => Ok(IntegrationKind::GithubSelfHosted), + "gitlab_self_hosted" => Ok(IntegrationKind::GitlabSelfHosted), + _ => bail!("{s} is not a valid value for ProviderKind"), + } + } +} + +impl DbEnum for Encryption { + fn as_enum_str(&self) -> &'static str { + match self { + Encryption::StartTls => "starttls", + Encryption::SslTls => "ssltls", + Encryption::None => "none", + } + } + + fn from_enum_str(s: &str) -> anyhow::Result { + match s { + "starttls" => Ok(Encryption::StartTls), + "ssltls" => Ok(Encryption::SslTls), + "none" => Ok(Encryption::None), + _ => bail!("{s} is not a valid value for Encryption"), + } + } +} + +impl DbEnum for OAuthProvider { + fn as_enum_str(&self) -> &'static str { + match self { + OAuthProvider::Google => "google", + OAuthProvider::Github => "github", + OAuthProvider::Gitlab => "gitlab", + OAuthProvider::Oidc => "oidc", + } + } + + fn from_enum_str(s: &str) -> anyhow::Result { + match s { + "github" => Ok(OAuthProvider::Github), + "google" => Ok(OAuthProvider::Google), + "gitlab" => Ok(OAuthProvider::Gitlab), + "oidc" => Ok(OAuthProvider::Oidc), + _ => bail!("Invalid OAuth credential type"), + } + } +} + +impl DbEnum for LdapEncryptionKind { + fn as_enum_str(&self) -> &'static str { + match self { + LdapEncryptionKind::None => "none", + LdapEncryptionKind::StartTLS => "starttls", + LdapEncryptionKind::LDAPS => "ldaps", + } + } + + fn from_enum_str(s: &str) -> anyhow::Result { + match s { + "none" => Ok(LdapEncryptionKind::None), + "starttls" => Ok(LdapEncryptionKind::StartTLS), + "ldaps" => Ok(LdapEncryptionKind::LDAPS), + _ => bail!("Invalid Ldap encryption kind"), + } + } +} + +impl DbEnum for AuthMethod { + fn as_enum_str(&self) -> &'static str { + match self { + AuthMethod::None => "none", + AuthMethod::Plain => "plain", + AuthMethod::Login => "login", + } + } + + fn from_enum_str(s: &str) -> anyhow::Result { + match s { + "none" => Ok(AuthMethod::None), + "plain" => Ok(AuthMethod::Plain), + "login" => Ok(AuthMethod::Login), + _ => bail!("{s} is not a valid value for AuthMethod"), + } + } +} + +impl DbEnum for thread::Role { + fn as_enum_str(&self) -> &'static str { + match self { + thread::Role::Assistant => "assistant", + thread::Role::User => "user", + } + } + + fn from_enum_str(s: &str) -> anyhow::Result { + match s { + "assistant" => Ok(thread::Role::Assistant), + "user" => Ok(thread::Role::User), + _ => bail!("{s} is not a valid value for thread::Role"), + } + } +} + +impl DbEnum for NotificationRecipient { + fn as_enum_str(&self) -> &'static str { + match self { + NotificationRecipient::Admin => "admin", + NotificationRecipient::AllUser => "all_user", + } + } + + fn from_enum_str(s: &str) -> anyhow::Result { + match s { + "admin" => Ok(NotificationRecipient::Admin), + "all_user" => Ok(NotificationRecipient::AllUser), + _ => bail!("{s} is not a valid value for NotificationKind"), + } + } +} diff --git a/ee/tabby-schema/src/env.rs b/ee/tabby-schema/src/env.rs new file mode 100644 index 000000000000..5195e8bb5350 --- /dev/null +++ b/ee/tabby-schema/src/env.rs @@ -0,0 +1,3 @@ +pub fn is_demo_mode() -> bool { + std::env::var("TABBY_WEBSERVER_DEMO_MODE").is_ok() +} diff --git a/ee/tabby-schema/src/juniper/mod.rs b/ee/tabby-schema/src/juniper/mod.rs new file mode 100644 index 000000000000..6193dd91b701 --- /dev/null +++ b/ee/tabby-schema/src/juniper/mod.rs @@ -0,0 +1 @@ +pub mod relay; diff --git a/crates/juniper-axum/src/relay/connection.rs b/ee/tabby-schema/src/juniper/relay/connection.rs similarity index 83% rename from crates/juniper-axum/src/relay/connection.rs rename to ee/tabby-schema/src/juniper/relay/connection.rs index 8df4c8d3806e..d71478e2f480 100644 --- a/crates/juniper-axum/src/relay/connection.rs +++ b/ee/tabby-schema/src/juniper/relay/connection.rs @@ -1,9 +1,12 @@ use juniper::{ - marker::IsOutputType, meta::MetaType, Arguments, ExecutionResult, Executor, GraphQLType, - GraphQLValue, GraphQLValueAsync, Registry, ScalarValue, + macros::reflect::{BaseSubTypes, BaseType, Type, Types, WrappedType, WrappedValue}, + marker::IsOutputType, + meta::MetaType, + Arguments, ExecutionResult, Executor, GraphQLType, GraphQLValue, GraphQLValueAsync, Registry, + ScalarValue, }; -use crate::relay::{edge::Edge, page_info::PageInfo, NodeType}; +use super::{edge::Edge, page_info::PageInfo, NodeType}; /// Connection type /// @@ -14,10 +17,23 @@ pub struct Connection { pub page_info: PageInfo, } +impl> WrappedType for Connection { + const VALUE: WrappedValue = T::VALUE * 10 + 3; +} + +impl> BaseType for Connection { + const NAME: Type = T::NAME; +} + +impl> BaseSubTypes for Connection { + const NAMES: Types = T::NAMES; +} + impl Connection where Node: NodeType, { + #[allow(unused)] /// Returns a relay relay with no elements. pub fn empty() -> Self { Self { @@ -127,7 +143,7 @@ where match field_name { "edges" => executor.resolve_with_ctx(info, &self.edges), "pageInfo" => executor.resolve_with_ctx(&(), &self.page_info), - _ => panic!("Field {} not found on type ConnectionEdge", field_name), + _ => panic!("Field {field_name} not found on type ConnectionEdge"), } } @@ -154,7 +170,7 @@ where match field_name { "edges" => executor.resolve_with_ctx_async(info, &self.edges).await, "pageInfo" => executor.resolve_with_ctx(&(), &self.page_info), - _ => panic!("Field {} not found on type ConnectionEdge", field_name), + _ => panic!("Field {field_name} not found on type ConnectionEdge"), } }; use ::juniper::futures::future; @@ -164,7 +180,8 @@ where impl IsOutputType for Connection where - Node: GraphQLType, + Node: GraphQLType + NodeType, + Node::Context: juniper::Context, S: ScalarValue, { } diff --git a/crates/juniper-axum/src/relay/edge.rs b/ee/tabby-schema/src/juniper/relay/edge.rs similarity index 91% rename from crates/juniper-axum/src/relay/edge.rs rename to ee/tabby-schema/src/juniper/relay/edge.rs index b8593ee5c546..c80578eccf86 100644 --- a/crates/juniper-axum/src/relay/edge.rs +++ b/ee/tabby-schema/src/juniper/relay/edge.rs @@ -3,7 +3,7 @@ use juniper::{ GraphQLType, GraphQLValue, GraphQLValueAsync, Registry, ScalarValue, }; -use crate::relay::NodeType; +use super::NodeType; /// An edge in a relay. pub struct Edge { @@ -66,7 +66,7 @@ where match field_name { "node" => executor.resolve_with_ctx(info, &self.node), "cursor" => executor.resolve_with_ctx(&(), &self.cursor), - _ => panic!("Field {} not found on type ConnectionEdge", field_name), + _ => panic!("Field {field_name} not found on type ConnectionEdge"), } } @@ -93,7 +93,7 @@ where match field_name { "node" => executor.resolve_with_ctx_async(info, &self.node).await, "cursor" => executor.resolve_with_ctx(&(), &self.cursor), - _ => panic!("Field {} not found on type RelayConnectionEdge", field_name), + _ => panic!("Field {field_name} not found on type RelayConnectionEdge"), } }; use ::juniper::futures::future; @@ -103,7 +103,8 @@ where impl IsOutputType for Edge where - Node: GraphQLType, + Node: GraphQLType + NodeType, + Node::Context: juniper::Context, S: ScalarValue, { } diff --git a/ee/tabby-schema/src/juniper/relay/mod.rs b/ee/tabby-schema/src/juniper/relay/mod.rs new file mode 100644 index 000000000000..dd35aee69afa --- /dev/null +++ b/ee/tabby-schema/src/juniper/relay/mod.rs @@ -0,0 +1,98 @@ +use std::future::Future; + +mod connection; +mod edge; +mod node_type; +mod page_info; + +pub use connection::Connection; +pub use node_type::NodeType; + +use crate::{bail, schema}; + +fn validate_first_last( + first: Option, + last: Option, +) -> schema::Result<(Option, Option)> { + if first.is_some() && last.is_some() { + bail!("The \"first\" and \"last\" parameters cannot exist at the same time"); + } + + let first = match first { + Some(first) if first < 0 => { + bail!("The \"first\" parameter must be a non-negative number"); + } + Some(first) => Some(first as usize), + None => None, + }; + + let last = match last { + Some(last) if last < 0 => { + bail!("The \"last\" parameter must be a non-negative number") + } + Some(last) => Some(last as usize), + None => None, + }; + Ok((first, last)) +} + +#[allow(unused)] +pub fn query( + after: Option, + before: Option, + first: Option, + last: Option, + f: F, +) -> schema::Result> +where + Node: NodeType + Sync, + F: FnOnce( + Option, + Option, + Option, + Option, + ) -> schema::Result>, +{ + let (first, last) = validate_first_last(first, last)?; + + let first_plus_one = first.map(|i| i + 1); + let last_plus_one = last.map(|i| i + 1); + let after_some = after.is_some(); + let before_some = before.is_some(); + let nodes = f(after, before, first_plus_one, last_plus_one)?; + Ok(Connection::build_connection( + nodes, + after_some, + before_some, + first, + last, + )) +} + +pub async fn query_async( + after: Option, + before: Option, + first: Option, + last: Option, + f: F, +) -> schema::Result> +where + Node: NodeType + Sync, + F: FnOnce(Option, Option, Option, Option) -> R, + R: Future>>, +{ + let (first, last) = validate_first_last(first, last)?; + + let first_plus_one = first.map(|i| i + 1); + let last_plus_one = last.map(|i| i + 1); + let after_some = after.is_some(); + let before_some = before.is_some(); + let nodes = f(after, before, first_plus_one, last_plus_one).await?; + Ok(Connection::build_connection( + nodes, + after_some, + before_some, + first, + last, + )) +} diff --git a/crates/juniper-axum/src/relay/node_type.rs b/ee/tabby-schema/src/juniper/relay/node_type.rs similarity index 100% rename from crates/juniper-axum/src/relay/node_type.rs rename to ee/tabby-schema/src/juniper/relay/node_type.rs diff --git a/crates/juniper-axum/src/relay/page_info.rs b/ee/tabby-schema/src/juniper/relay/page_info.rs similarity index 100% rename from crates/juniper-axum/src/relay/page_info.rs rename to ee/tabby-schema/src/juniper/relay/page_info.rs diff --git a/ee/tabby-schema/src/lib.rs b/ee/tabby-schema/src/lib.rs new file mode 100644 index 000000000000..c972bfd67940 --- /dev/null +++ b/ee/tabby-schema/src/lib.rs @@ -0,0 +1,23 @@ +//! Defines behavior for the tabby webserver which allows users to interact with enterprise features. +mod dao; +mod env; +mod schema; + +pub mod juniper; +pub use dao::*; +pub use env::is_demo_mode; +pub use schema::*; +pub mod policy; + +#[macro_export] +macro_rules! bail { + ($msg:literal $(,)?) => { + return std::result::Result::Err(anyhow::anyhow!($msg).into()) + }; + ($err:expr $(,)?) => { + return std::result::Result::Err(anyhow::anyhow!($err).into()) + }; + ($fmt:expr, $($arg:tt)*) => { + return std::result::Result::Err(anyhow::anyhow!($fmt, $($arg)*).into()) + }; +} diff --git a/ee/tabby-schema/src/policy.rs b/ee/tabby-schema/src/policy.rs new file mode 100644 index 000000000000..41dfcf47f923 --- /dev/null +++ b/ee/tabby-schema/src/policy.rs @@ -0,0 +1,419 @@ +use juniper::ID; +use tabby_db::DbConn; + +use crate::{user_group::UpsertUserGroupMembershipInput, AsRowid, CoreError, Result}; + +#[derive(Clone)] +pub struct AccessPolicy { + db: DbConn, + user_id: ID, + is_admin: bool, +} + +impl std::fmt::Debug for AccessPolicy { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AccessPolicy") + .field("user_id", &self.user_id) + .field("is_admin", &self.is_admin) + .finish() + } +} + +impl AccessPolicy { + pub fn new(db: DbConn, user_id: &ID, is_admin: bool) -> Self { + Self { + db, + user_id: user_id.to_owned(), + is_admin, + } + } + + /// check_read_thread verifies whether the user has permission to read a thread. + /// If the user is an admin, they have access to read any thread. + /// If the user is not an admin, they are limited to reading threads they own or those shared by others, + /// where is_ephemeral being false indicates that the thread is shared. + pub fn check_read_thread(&self, author_id: &ID, is_ephemeral: bool) -> Result<()> { + if self.is_admin { + return Ok(()); + } + + if is_ephemeral && self.user_id != *author_id { + Err(CoreError::Forbidden( + "You are unable to view a thread that you do not own unless it has been shared.", + )) + } else { + Ok(()) + } + } + + pub fn check_delete_thread(&self, user_id: &ID) -> Result<()> { + if self.user_id != *user_id { + return Err(CoreError::Forbidden( + "You cannot delete a thread that you do not own.", + )); + } + + Ok(()) + } + + pub fn check_delete_thread_messages(&self, user_id: &ID) -> Result<()> { + if self.user_id != *user_id { + return Err(CoreError::Forbidden( + "You cannot delete messages in a thread that you do not own.", + )); + } + + Ok(()) + } + + pub fn check_update_thread_persistence(&self, user_id: &ID) -> Result<()> { + if self.user_id != *user_id { + return Err(CoreError::Forbidden( + "You cannot update the persistence of a thread that you do not own.", + )); + } + + Ok(()) + } + + pub fn check_update_thread_message(&self, user_id: &ID) -> Result<()> { + if self.user_id != *user_id { + return Err(CoreError::Forbidden( + "You cannot update the message of a thread that you do not own.", + )); + } + + Ok(()) + } + + pub fn check_update_page(&self, user_id: &ID) -> Result<()> { + if self.user_id != *user_id { + return Err(CoreError::Forbidden( + "You cannot edit a page that you do not own.", + )); + } + + Ok(()) + } + + pub fn check_read_analytic(&self, users: &[ID]) -> Result<()> { + const ERROR: Result<()> = Err(CoreError::Forbidden( + "You must be admin to read other users' analytic data", + )); + + if users.is_empty() && !self.is_admin { + return ERROR; + } + + if !self.is_admin { + for id in users { + if self.user_id != *id { + return ERROR; + } + } + } + + Ok(()) + } + + pub async fn check_read_source(&self, source_id: &str) -> Result<()> { + if self.is_admin { + return Ok(()); + } + + let allow = self + .db + .allow_read_source(self.user_id.as_rowid()?, source_id) + .await?; + if !allow { + return Err(CoreError::Forbidden( + "You are not allowed to read this source", + )); + } + + Ok(()) + } + + pub async fn check_upsert_user_group_membership( + &self, + input: &UpsertUserGroupMembershipInput, + ) -> Result<()> { + if self.is_admin { + return Ok(()); + } + + // User group admin can change membership within their group + if !self + .is_user_group_admin(&input.user_group_id, &self.user_id) + .await + { + return Err(CoreError::Forbidden( + "You are not allowed to update this user group membership", + )); + } + + if input.is_group_admin { + return Err(CoreError::Forbidden( + "You are not allowed to grant group admin privileges", + )); + } + + if self + .is_user_group_admin(&input.user_group_id, &input.user_id) + .await + { + return Err(CoreError::Forbidden( + "You are not allowed to modify group admin privileges", + )); + } + + Ok(()) + } + + pub async fn check_delete_user_group_membership( + &self, + user_group_id: &ID, + user_id: &ID, + ) -> Result<()> { + if self.is_admin { + return Ok(()); + } + + let err = Err(CoreError::Forbidden( + "You are not allowed to modify group membership", + )); + + if !self.is_user_group_admin(user_group_id, &self.user_id).await { + return err; + } + + // Cannot remove admin from group + if self.is_user_group_admin(user_group_id, user_id).await { + return err; + } + + Ok(()) + } + + async fn is_user_group_admin(&self, user_group_id: &ID, user_id: &ID) -> bool { + self.is_user_group_admin_impl(user_group_id, user_id) + .await + .unwrap_or_default() + } + + async fn is_user_group_admin_impl(&self, user_group_id: &ID, user_id: &ID) -> Result { + let x = self + .db + .list_user_group_memberships(user_group_id.as_rowid()?, Some(user_id.as_rowid()?)) + .await?; + Ok(x.first().is_some_and(|x| x.is_group_admin)) + } +} + +#[cfg(test)] +mod tests { + use tabby_db::testutils; + + use super::*; + use crate::AsID; + + #[tokio::test] + async fn test_check_read_source() { + let db = DbConn::new_in_memory().await.unwrap(); + let source_id = "source_id"; + + let user_id1 = testutils::create_user(&db).await; + let user_id2 = testutils::create_user2(&db).await; + + let policy1 = AccessPolicy::new(db.clone(), &user_id1.as_id(), false); + let policy2 = AccessPolicy::new(db.clone(), &user_id2.as_id(), false); + + // 1. Setup user group + let user_group_id = db.create_user_group("test").await.unwrap(); + db.upsert_user_group_membership(user_id1, user_group_id, false) + .await + .unwrap(); + + // For source id without any access policies, it's public (readable by all users) + assert!(policy1.check_read_source(source_id).await.is_ok()); + assert!(policy2.check_read_source(source_id).await.is_ok()); + + // 2. add user_group to source id's policy, making it private + db.upsert_source_id_read_access_policy(source_id, user_group_id) + .await + .unwrap(); + + // user2 won't be able to access source, while user1 can. + assert!(policy2.check_read_source(source_id).await.is_err()); + assert!(policy1.check_read_source(source_id).await.is_ok()); + + // 3. remove user1 from user_group + db.delete_user_group_membership(user_id1, user_group_id) + .await + .unwrap(); + + // user1 won't be able to acces source either now. + assert!(policy1.check_read_source(source_id).await.is_err()); + + // 4. delete user_group from source id's policy, making it public again + db.delete_source_id_read_access_policy(source_id, user_group_id) + .await + .unwrap(); + + // user1 and user2 can access source again + assert!(policy1.check_read_source(source_id).await.is_ok()); + assert!(policy2.check_read_source(source_id).await.is_ok()); + } + + #[tokio::test] + async fn test_check_delete_thread_messages() { + let db = DbConn::new_in_memory().await.unwrap(); + let user_id1 = testutils::create_user(&db).await; + let user_id2 = testutils::create_user2(&db).await; + + let policy1 = AccessPolicy::new(db.clone(), &user_id1.as_id(), false); + + assert!(policy1 + .check_delete_thread_messages(&user_id1.as_id()) + .is_ok()); + assert!(policy1 + .check_delete_thread_messages(&user_id2.as_id()) + .is_err()); + } + + #[tokio::test] + async fn test_check_update_thread_persistence() { + let db = DbConn::new_in_memory().await.unwrap(); + let user_id1 = testutils::create_user(&db).await; + let user_id2 = testutils::create_user2(&db).await; + + let policy1 = AccessPolicy::new(db.clone(), &user_id1.as_id(), false); + + assert!(policy1 + .check_update_thread_persistence(&user_id1.as_id()) + .is_ok()); + assert!(policy1 + .check_update_thread_persistence(&user_id2.as_id()) + .is_err()); + } + + #[tokio::test] + async fn test_check_read_analytic() { + let db = DbConn::new_in_memory().await.unwrap(); + let user_id1 = testutils::create_user(&db).await; + let user_id2 = testutils::create_user2(&db).await; + + let policy_normal = AccessPolicy::new(db.clone(), &user_id1.as_id(), false); + let policy_admin = AccessPolicy::new(db.clone(), &user_id1.as_id(), true); + + assert!(policy_normal + .check_read_analytic(&[user_id1.as_id()]) + .is_ok()); + assert!(policy_normal + .check_read_analytic(&[user_id2.as_id()]) + .is_err()); + assert!(policy_normal.check_read_analytic(&[]).is_err()); + + assert!(policy_admin + .check_read_analytic(&[user_id1.as_id()]) + .is_ok()); + assert!(policy_admin + .check_read_analytic(&[user_id2.as_id()]) + .is_ok()); + assert!(policy_admin.check_read_analytic(&[]).is_ok()); + } + + #[tokio::test] + async fn test_check_upsert_user_group_membership() { + let db = DbConn::new_in_memory().await.unwrap(); + let user_id1 = testutils::create_user(&db).await; + let user_id2 = testutils::create_user2(&db).await; + let user_group_id = db.create_user_group("test").await.unwrap(); + + db.upsert_user_group_membership(user_id1, user_group_id, true) + .await + .unwrap(); + + let policy_normal = AccessPolicy::new(db.clone(), &user_id2.as_id(), false); + let policy_group_admin = AccessPolicy::new(db.clone(), &user_id1.as_id(), false); + let policy_admin = AccessPolicy::new(db.clone(), &user_id1.as_id(), true); + + let input = UpsertUserGroupMembershipInput { + user_id: user_id2.as_id(), + user_group_id: user_group_id.as_id(), + is_group_admin: false, + }; + + assert!(policy_normal + .check_upsert_user_group_membership(&input) + .await + .is_err()); + + assert!(policy_group_admin + .check_upsert_user_group_membership(&input) + .await + .is_ok()); + + let admin_input = UpsertUserGroupMembershipInput { + is_group_admin: true, + user_id: user_id2.as_id(), + user_group_id: user_group_id.as_id(), + }; + assert!(policy_group_admin + .check_upsert_user_group_membership(&admin_input) + .await + .is_err()); + + assert!(policy_admin + .check_upsert_user_group_membership(&input) + .await + .is_ok()); + assert!(policy_admin + .check_upsert_user_group_membership(&admin_input) + .await + .is_ok()); + } + #[tokio::test] + async fn test_check_delete_user_group_membership() { + let db = DbConn::new_in_memory().await.unwrap(); + let user_id1 = testutils::create_user(&db).await; + let user_id2 = testutils::create_user2(&db).await; + let user_group_id = db.create_user_group("test").await.unwrap(); + + // Make user1 a group admin and user2 a normal member + db.upsert_user_group_membership(user_id1, user_group_id, true) + .await + .unwrap(); + db.upsert_user_group_membership(user_id2, user_group_id, false) + .await + .unwrap(); + + let policy_normal = AccessPolicy::new(db.clone(), &user_id2.as_id(), false); + let policy_group_admin = AccessPolicy::new(db.clone(), &user_id1.as_id(), false); + let policy_admin = AccessPolicy::new(db.clone(), &user_id1.as_id(), true); + + assert!(policy_normal + .check_delete_user_group_membership(&user_group_id.as_id(), &user_id2.as_id()) + .await + .is_err()); + + assert!(policy_group_admin + .check_delete_user_group_membership(&user_group_id.as_id(), &user_id2.as_id()) + .await + .is_ok()); + + assert!(policy_group_admin + .check_delete_user_group_membership(&user_group_id.as_id(), &user_id1.as_id()) + .await + .is_err()); + + assert!(policy_admin + .check_delete_user_group_membership(&user_group_id.as_id(), &user_id1.as_id()) + .await + .is_ok()); + assert!(policy_admin + .check_delete_user_group_membership(&user_group_id.as_id(), &user_id2.as_id()) + .await + .is_ok()); + } +} diff --git a/ee/tabby-schema/src/schema/access_policy.rs b/ee/tabby-schema/src/schema/access_policy.rs new file mode 100644 index 000000000000..18101d1152be --- /dev/null +++ b/ee/tabby-schema/src/schema/access_policy.rs @@ -0,0 +1,19 @@ +use async_trait::async_trait; +use juniper::{GraphQLObject, ID}; + +use super::{user_group::UserGroup, Context, Result}; + +#[derive(GraphQLObject)] +#[graphql(context = Context)] +pub struct SourceIdAccessPolicy { + pub source_id: String, + pub read: Vec, +} + +#[async_trait] +pub trait AccessPolicyService: Sync + Send { + async fn list_source_id_read_access(&self, source_id: &str) -> Result>; + async fn grant_source_id_read_access(&self, source_id: &str, user_group_id: &ID) -> Result<()>; + async fn revoke_source_id_read_access(&self, source_id: &str, user_group_id: &ID) + -> Result<()>; +} diff --git a/ee/tabby-schema/src/schema/analytic.rs b/ee/tabby-schema/src/schema/analytic.rs new file mode 100644 index 000000000000..2b561ba312fc --- /dev/null +++ b/ee/tabby-schema/src/schema/analytic.rs @@ -0,0 +1,160 @@ +use std::collections::HashMap; + +use async_trait::async_trait; +use chrono::{DateTime, Utc}; +use juniper::{GraphQLEnum, GraphQLObject, ID}; +use lazy_static::lazy_static; +use strum::{EnumIter, IntoEnumIterator}; + +use crate::schema::Result; + +#[derive(GraphQLObject)] +pub struct DiskUsageStats { + pub events: DiskUsage, + pub indexed_repositories: DiskUsage, + pub database: DiskUsage, + pub models: DiskUsage, +} + +#[derive(GraphQLObject)] +pub struct DiskUsage { + pub filepath: Vec, + + /// Size in kilobytes. + pub size_kb: f64, +} + +impl DiskUsage { + pub fn combine(self, other: Self) -> Self { + DiskUsage { + size_kb: self.size_kb + other.size_kb, + filepath: self.filepath.into_iter().chain(other.filepath).collect(), + } + } +} + +#[derive(GraphQLObject, Debug, Clone)] +pub struct CompletionStats { + pub start: DateTime, + pub end: DateTime, + + pub language: Language, + pub completions: i32, + pub views: i32, + pub selects: i32, +} + +#[derive(GraphQLEnum, Clone, Debug, Eq, PartialEq, EnumIter, Hash)] +pub enum Language { + Rust, + Python, + Java, + Kotlin, + Javascript, + Typescript, + Go, + Ruby, + CSharp, + C, + Cpp, + Solidity, + PHP, + Other, +} + +lazy_static! { + static ref NAME_LANGUAGE_MAPPINGS: HashMap<&'static str, Language> = { + let mut map = HashMap::new(); + for language in Language::iter() { + for name in language.language_names() { + map.insert(name, language.clone()); + } + } + map + }; +} + +impl Language { + pub fn all_known() -> impl Iterator { + Language::iter().filter(|l| l != &Language::Other) + } + + pub fn language_names(&self) -> Vec<&'static str> { + match self { + Language::Rust => vec!["rust"], + Language::Python => vec!["python"], + Language::Java => vec!["java"], + Language::Kotlin => vec!["kotlin"], + Language::Javascript => vec!["javascript", "javascriptreact"], + Language::Typescript => vec!["typescript", "typescriptreact"], + Language::Go => vec!["go"], + Language::Ruby => vec!["ruby"], + Language::CSharp => vec!["csharp"], + Language::C => vec!["c"], + Language::Cpp => vec!["cpp", "c++"], + Language::Solidity => vec!["solidity"], + Language::PHP => vec!["php"], + Language::Other => vec!["other"], + } + } +} + +impl From for Language { + fn from(val: String) -> Self { + if let Some(lang) = NAME_LANGUAGE_MAPPINGS.get(val.as_str()) { + lang.clone() + } else { + Language::Other + } + } +} + +#[derive(GraphQLObject, Debug, Clone)] +pub struct ChatCompletionStats { + pub start: DateTime, + pub end: DateTime, + pub user_id: ID, + pub chats: i32, +} + +#[async_trait] +pub trait AnalyticService: Send + Sync { + /// Generate the completion report for past year, with daily granularity. + /// + /// `users` is a list of user IDs. If empty, the report is computed for all users. + async fn daily_stats_in_past_year(&self, users: Vec) -> Result>; + + /// Computes the completion report with daily granularity. + /// + /// 1. [`start`, `end`) define the time range for the report. + /// 2. `users` is a list of user IDs. If empty, the report is computed for all users. + /// 3. `languages` is a list of programming language identifier. If empty, the report is computed for all languages. + async fn daily_stats( + &self, + start: DateTime, + end: DateTime, + users: Vec, + languages: Vec, + ) -> Result>; + + /// Generate the chat report for past year, with daily granularity. + /// + /// `users` is a list of user IDs. If empty, the report is computed for all users. + async fn chat_daily_stats_in_past_year( + &self, + users: Vec, + ) -> Result>; + + /// Computes the chat report with daily granularity. + /// + /// 1. [`start`, `end`) define the time range for the report. + /// 2. `users` is a list of user IDs. If empty, the report is computed for all users. + async fn chat_daily_stats( + &self, + start: DateTime, + end: DateTime, + users: Vec, + ) -> Result>; + + async fn disk_usage_stats(&self) -> Result; +} diff --git a/ee/tabby-schema/src/schema/auth.rs b/ee/tabby-schema/src/schema/auth.rs new file mode 100644 index 000000000000..415faeadf0e8 --- /dev/null +++ b/ee/tabby-schema/src/schema/auth.rs @@ -0,0 +1,588 @@ +use std::{borrow::Cow, fmt::Debug}; + +use async_trait::async_trait; +use chrono::{DateTime, Utc}; +use juniper::{GraphQLEnum, GraphQLInputObject, GraphQLObject, ID}; +use serde::{Deserialize, Serialize}; +use strum::EnumIter; +use thiserror::Error; +use tokio::task::JoinHandle; +use validator::Validate; + +use super::interface::UserValue; +use crate::{ + juniper::relay, + policy::AccessPolicy, + schema::{Context, Result}, +}; + +#[derive(Debug, GraphQLObject)] +pub struct RegisterResponse { + access_token: String, + pub refresh_token: String, +} + +impl RegisterResponse { + pub fn new(access_token: String, refresh_token: String) -> Self { + Self { + access_token, + refresh_token, + } + } +} + +#[derive(Debug, GraphQLObject)] +pub struct TokenAuthResponse { + access_token: String, + pub refresh_token: String, +} + +impl TokenAuthResponse { + pub fn new(access_token: String, refresh_token: String) -> Self { + Self { + access_token, + refresh_token, + } + } +} + +/// Input parameters for token_auth mutation +/// See `RegisterInput` for `validate` attribute usage +#[derive(Validate)] +pub struct TokenAuthInput { + #[validate(email(code = "email", message = "Email is invalid"))] + #[validate(length( + max = 128, + code = "email", + message = "Email must be at most 128 characters" + ))] + pub email: String, + #[validate(length( + min = 8, + max = 20, + code = "password", + message = "Password must be between 8 and 20 characters" + ))] + pub password: String, +} + +/// Input parameters for token_auth_ldap mutation +#[derive(Validate)] +pub struct TokenAuthLdapInput<'a> { + #[validate(length(min = 1, code = "user_id", message = "User ID should not be empty"))] + pub user_id: &'a str, + #[validate(length(min = 1, code = "password", message = "Password should not be empty"))] + pub password: &'a str, +} + +/// Input parameters for register mutation +/// `validate` attribute is used to validate the input parameters +/// - `code` argument specifies which parameter causes the failure +/// - `message` argument provides client friendly error message +/// +#[derive(Validate)] +pub struct RegisterInput { + #[validate(email(code = "email", message = "Email is invalid"))] + #[validate(length( + max = 128, + code = "email", + message = "Email must be at most 128 characters" + ))] + pub email: String, + #[validate(length( + min = 8, + max = 20, + code = "password1", + message = "Password must be between 8 and 20 characters" + ))] + #[validate(custom(function = "validate_password"))] + pub password1: String, + #[validate(must_match( + code = "password2", + message = "Passwords do not match", + other = "password1" + ))] + #[validate(length( + max = 20, + code = "password2", + message = "Password must be at most 20 characters" + ))] + pub password2: String, +} + +#[derive(Default, Serialize)] +pub struct OAuthResponse { + pub access_token: String, + pub refresh_token: String, +} + +#[derive(Error, Debug)] +pub enum OAuthError { + #[error("User is not invited, please contact admin for help")] + UserNotInvited, + + #[error("User is disabled, please contact admin for help")] + UserDisabled, + + #[error("Seat limit on license would be exceeded")] + InsufficientSeats, + + #[error(transparent)] + Other(#[from] anyhow::Error), + + #[error("Unknown error")] + Unknown, +} + +#[derive(Debug, GraphQLObject)] +pub struct RefreshTokenResponse { + pub access_token: String, + pub refresh_token: String, + pub refresh_expires_at: DateTime, +} + +impl RefreshTokenResponse { + pub fn new( + access_token: String, + refresh_token: String, + refresh_expires_at: DateTime, + ) -> Self { + Self { + access_token, + refresh_token, + refresh_expires_at, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct JWTPayload { + /// Expiration time (as UTC timestamp) + exp: i64, + + /// Issued at (as UTC timestamp) + iat: i64, + + /// User id string + pub sub: ID, + + /// Whether the token is generated from auth token based authentication + #[serde(skip)] + pub is_generated_from_auth_token: bool, +} + +impl JWTPayload { + pub fn new(id: ID, iat: i64, exp: i64, is_generated_from_auth_token: bool) -> Self { + Self { + sub: id, + iat, + exp, + is_generated_from_auth_token, + } + } +} + +#[derive(Debug, GraphQLObject, Clone)] +#[graphql(context = Context, impl = [UserValue])] +pub struct UserSecured { + // === implements User === + pub id: juniper::ID, + pub email: String, + pub name: String, + pub created_at: DateTime, + pub is_admin: bool, + pub is_owner: bool, + pub active: bool, + // === end User === + pub auth_token: String, + pub is_password_set: bool, + + // is_sso_user is used to indicate if the user is created by SSO + // and should not be able to change Name and Password + // e.g. LDAP, OAuth users + pub is_sso_user: bool, + + #[graphql(skip)] + pub policy: AccessPolicy, +} + +impl relay::NodeType for UserSecured { + type Cursor = String; + + fn cursor(&self) -> Self::Cursor { + self.id.to_string() + } + + fn connection_type_name() -> &'static str { + "UserSecuredConnection" + } + + fn edge_type_name() -> &'static str { + "UserSecuredEdge" + } +} + +#[derive(Validate, GraphQLInputObject)] +pub struct RequestInvitationInput { + #[validate(email(code = "email", message = "Invalid email address"))] + pub email: String, +} + +#[derive(Validate, GraphQLInputObject)] +pub struct RequestPasswordResetEmailInput { + #[validate(email(code = "email", message = "Invalid email address"))] + pub email: String, +} + +#[derive(Validate, GraphQLInputObject)] +pub struct PasswordResetInput { + pub code: String, + #[validate(length( + min = 8, + max = 20, + code = "password1", + message = "Password must be between 8 and 20 characters" + ))] + #[validate(custom(function = "validate_password"))] + pub password1: String, + #[validate(length( + min = 8, + max = 20, + code = "password2", + message = "Password must be between 8 and 20 characters" + ))] + #[validate(must_match( + code = "password2", + message = "Passwords do not match", + other = "password1" + ))] + pub password2: String, +} + +#[derive(Validate, GraphQLInputObject)] +pub struct PasswordChangeInput { + pub old_password: Option, + + #[validate(length( + min = 8, + max = 20, + code = "newPassword1", + message = "Password must be between 8 and 20 characters" + ))] + #[validate(custom(function = "validate_new_password"))] + pub new_password1: String, + #[validate(length( + min = 8, + max = 20, + code = "newPassword2", + message = "Password must be between 8 and 20 characters" + ))] + #[validate(must_match( + code = "newPassword2", + message = "Passwords do not match", + other = "new_password1" + ))] + pub new_password2: String, +} + +#[derive(Validate)] +pub struct UpdateUserNameInput { + #[validate(length( + min = 2, + max = 20, + code = "name", + message = "Name must be between 2 and 20 characters" + ))] + #[validate(regex( + code = "name", + path = "*crate::schema::constants::USERNAME_REGEX", + message = "Invalid name, name may contain numbers or special characters which are not supported" + ))] + pub name: String, +} + +#[derive(Debug, Serialize, Deserialize, GraphQLObject)] +#[graphql(context = Context)] +pub struct Invitation { + pub id: juniper::ID, + pub email: String, + pub code: String, + + pub created_at: DateTime, +} + +impl relay::NodeType for Invitation { + type Cursor = String; + + fn cursor(&self) -> Self::Cursor { + self.id.to_string() + } + + fn connection_type_name() -> &'static str { + "InvitationConnection" + } + + fn edge_type_name() -> &'static str { + "InvitationEdge" + } +} + +#[derive(GraphQLEnum, Clone, Serialize, Deserialize, PartialEq, Debug, EnumIter)] +#[serde(rename_all = "lowercase")] +pub enum OAuthProvider { + Github, + Google, + Gitlab, + Oidc, +} + +#[derive(GraphQLEnum, Clone, Serialize, Deserialize, PartialEq, Debug)] +pub enum AuthProviderKind { + OAuthGithub, + OAuthGoogle, + OAuthGitlab, + OAuthOidc, + Ldap, +} + +impl From for AuthProvider { + fn from(provider: OAuthProvider) -> Self { + match provider { + OAuthProvider::Github => AuthProvider { + kind: AuthProviderKind::OAuthGithub, + }, + OAuthProvider::Google => AuthProvider { + kind: AuthProviderKind::OAuthGoogle, + }, + OAuthProvider::Gitlab => AuthProvider { + kind: AuthProviderKind::OAuthGitlab, + }, + OAuthProvider::Oidc => AuthProvider { + kind: AuthProviderKind::OAuthOidc, + }, + } + } +} + +#[derive(GraphQLObject)] +pub struct AuthProvider { + pub kind: AuthProviderKind, +} + +#[derive(GraphQLObject)] +pub struct OAuthCredential { + pub provider: OAuthProvider, + pub config_url: Option, + pub config_scopes: Option, + pub client_id: String, + + #[graphql(skip)] + pub client_secret: String, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +#[derive(GraphQLInputObject, Validate)] +pub struct UpdateOAuthCredentialInput { + pub provider: OAuthProvider, + + pub config_url: Option, + + pub config_scopes: Option, + + #[validate(length(min = 1, code = "clientId", message = "Client ID cannot be empty"))] + pub client_id: String, + + #[validate(length( + min = 1, + code = "clientSecret", + message = "Client secret cannot be empty" + ))] + pub client_secret: Option, +} + +#[derive(GraphQLEnum, PartialEq, Debug)] +pub enum LdapEncryptionKind { + None, + StartTLS, + LDAPS, +} + +#[derive(GraphQLInputObject, Validate)] +pub struct UpdateLdapCredentialInput { + #[validate(length( + min = 1, + code = "host", + message = "host should not be empty and should be a valid hostname or IP address" + ))] + pub host: String, + pub port: i32, + + #[validate(length(min = 1, code = "bindDn", message = "bindDn cannot be empty"))] + pub bind_dn: String, + pub bind_password: Option, + + #[validate(length(min = 1, code = "baseDn", message = "baseDn cannot be empty"))] + pub base_dn: String, + #[validate(length( + min = 1, + code = "userFilter", + message = "userFilter cannot be empty, and should be in the format of `(uid=%s)`" + ))] + pub user_filter: String, + + pub encryption: LdapEncryptionKind, + pub skip_tls_verify: bool, + + #[validate(length( + min = 1, + code = "emailAttribute", + message = "emailAttribute cannot be empty" + ))] + pub email_attribute: String, + // if name_attribute is None, we will use username as name + pub name_attribute: Option, +} + +#[derive(GraphQLObject)] +pub struct LdapCredential { + pub host: String, + pub port: i32, + pub bind_dn: String, + pub base_dn: String, + pub user_filter: String, + pub encryption: LdapEncryptionKind, + pub skip_tls_verify: bool, + pub email_attribute: String, + pub name_attribute: Option, + + pub created_at: DateTime, + pub updated_at: DateTime, +} + +#[async_trait] +pub trait AuthenticationService: Send + Sync { + async fn register( + &self, + email: String, + password1: String, + invitation_code: Option, + name: Option, + ) -> Result; + async fn allow_self_signup(&self) -> Result; + + async fn token_auth(&self, email: String, password: String) -> Result; + + async fn token_auth_ldap(&self, email: &str, password: &str) -> Result; + + async fn refresh_token(&self, refresh_token: String) -> Result; + async fn verify_access_token(&self, access_token: &str) -> Result; + async fn verify_auth_token(&self, token: &str) -> Result; + async fn is_admin_initialized(&self) -> Result; + async fn get_user_by_email(&self, email: &str) -> Result; + async fn get_user(&self, id: &ID) -> Result; + async fn logout_all_sessions(&self, id: &ID) -> Result<()>; + + async fn create_invitation(&self, email: String) -> Result; + async fn request_invitation_email(&self, input: RequestInvitationInput) -> Result; + async fn delete_invitation(&self, id: &ID) -> Result; + + async fn reset_user_auth_token(&self, id: &ID) -> Result<()>; + async fn password_reset(&self, code: &str, password: &str) -> Result<()>; + async fn generate_reset_password_url(&self, id: &ID) -> Result; + async fn request_password_reset_email(&self, email: String) -> Result>>; + async fn update_user_password( + &self, + id: &ID, + old_password: Option<&str>, + new_password: &str, + ) -> Result<()>; + + async fn list_users( + &self, + ids: Option>, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> Result>; + + async fn list_invitations( + &self, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> Result>; + + async fn oauth( + &self, + code: String, + state: Option, + provider: OAuthProvider, + ) -> std::result::Result; + + async fn oauth_callback_url(&self, provider: OAuthProvider) -> Result; + + async fn read_oauth_credential( + &self, + provider: OAuthProvider, + ) -> Result>; + + async fn update_oauth_credential(&self, input: UpdateOAuthCredentialInput) -> Result<()>; + async fn delete_oauth_credential(&self, provider: OAuthProvider) -> Result<()>; + + async fn read_ldap_credential(&self) -> Result>; + async fn test_ldap_connection(&self, input: UpdateLdapCredentialInput) -> Result<()>; + async fn update_ldap_credential(&self, input: UpdateLdapCredentialInput) -> Result<()>; + async fn delete_ldap_credential(&self) -> Result<()>; + + async fn update_user_active(&self, id: &ID, active: bool) -> Result<()>; + async fn update_user_role(&self, id: &ID, is_admin: bool) -> Result<()>; + async fn update_user_avatar(&self, id: &ID, avatar: Option>) -> Result<()>; + async fn get_user_avatar(&self, id: &ID) -> Result>>; + async fn update_user_name(&self, id: &ID, name: String) -> Result<()>; +} + +fn validate_password(value: &str) -> Result<(), validator::ValidationError> { + validate_password_impl(value, "password1") +} + +fn validate_new_password(value: &str) -> Result<(), validator::ValidationError> { + validate_password_impl(value, "newPassword1") +} + +fn validate_password_impl( + value: &str, + code: &'static str, +) -> Result<(), validator::ValidationError> { + let make_validation_error = |message: &'static str| { + let mut err = validator::ValidationError::new(code); + err.message = Some(Cow::Borrowed(message)); + Err(err) + }; + + let contains_lowercase = value.chars().any(|x| x.is_ascii_lowercase()); + if !contains_lowercase { + return make_validation_error("Password should contain at least one lowercase character"); + } + + let contains_uppercase = value.chars().any(|x| x.is_ascii_uppercase()); + if !contains_uppercase { + return make_validation_error("Password should contain at least one uppercase character"); + } + + let contains_digit = value.chars().any(|x| x.is_ascii_digit()); + if !contains_digit { + return make_validation_error("Password should contain at least one numeric character"); + } + + let contains_special_char = value.chars().any(|x| x.is_ascii_punctuation()); + if !contains_special_char { + return make_validation_error( + "Password should contain at least one special character, e.g @#$%^&{}", + ); + } + + Ok(()) +} diff --git a/ee/tabby-schema/src/schema/constants.rs b/ee/tabby-schema/src/schema/constants.rs new file mode 100644 index 000000000000..a627dc599748 --- /dev/null +++ b/ee/tabby-schema/src/schema/constants.rs @@ -0,0 +1,85 @@ +use lazy_static::lazy_static; +use regex::Regex; + +lazy_static! { + pub static ref REPOSITORY_NAME_REGEX: Regex = Regex::new("^[a-zA-Z][\\w.-]+$").unwrap(); + pub static ref USERNAME_REGEX: Regex = + Regex::new(r"^[^0-9±!@£$%^&*_+§¡€#¢¶•ªº«\\/<>?:;|=.,]{2,20}$").unwrap(); + pub static ref WEB_DOCUMENT_NAME_REGEX: Regex = + Regex::new(r"^[A-Za-z][A-Za-z0-9#]*(?:[\s.-][A-Za-z0-9]+)*$").unwrap(); + pub static ref USER_GROUP_NAME_REGEX: Regex = Regex::new(r"^[a-z][a-z0-9_-]*$").unwrap(); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_username_regex() { + let test_cases = vec![ + ("John", true), // English name + ("Müller", true), // German name + ("Jørgensen", true), // Danish name + ("李雷", true), // Chinese name + ("あきは", true), // Japanese name + ("김민수", true), // Korean name + ("Алексей", true), // Russian name + ("José", true), // Spanish names + ("علی", true), // Iranian names + // Edge cases + ("", false), // Empty string + ("JohnDoeIsAReallyLongName", false), // More than 20 characters + ("John!", false), // Invalid character '!' + ("José@", false), // Invalid character '@' + ("12345", false), // Invalid character Numbers + ("John_Doe", false), // Underscore character + ("Anna-Marie", true), // Hyphen character + ("O'Connor", true), // Apostrophe + ("李@伟", false), + ]; + + for (name, expected) in test_cases { + let result = USERNAME_REGEX.is_match(name); + assert_eq!(result, expected, "Failed for name: {name}"); + } + } + + #[test] + fn test_web_document_name_regex() { + let test_cases = vec![ + ("John", true), // English name + ("Müller", false), // German name + ("abc123", true), + ("Abc 123", true), + (" abc 123", false), + ("abc123*", false), + ("abc123_", false), + ("abc 123", false), // two space + ("ABC-abc", true), + ]; + + for (name, expected) in test_cases { + let result = WEB_DOCUMENT_NAME_REGEX.is_match(name); + assert_eq!(result, expected, "Failed for name: {name}"); + } + } + + #[test] + fn test_user_group_name_regex() { + let test_cases = vec![ + ("group", true), // Valid name + ("group123", true), // Valid name with numbers + ("group-name", true), // Valid name with hyphen + ("group_name", true), // Valid name with underscore + ("Group", false), // Invalid: starts with uppercase letter + ("1group", false), // Invalid: starts with number + ("group name", false), // Invalid: contains space + ("group*name", false), // Invalid: contains special character + ]; + + for (name, expected) in test_cases { + let result = USER_GROUP_NAME_REGEX.is_match(name); + assert_eq!(result, expected, "Failed for name: {name}"); + } + } +} diff --git a/ee/tabby-schema/src/schema/context.rs b/ee/tabby-schema/src/schema/context.rs new file mode 100644 index 000000000000..c838144d1e65 --- /dev/null +++ b/ee/tabby-schema/src/schema/context.rs @@ -0,0 +1,225 @@ +use std::collections::HashMap; + +use async_trait::async_trait; +use juniper::{ + graphql_interface, graphql_object, GraphQLEnum, GraphQLInterface, GraphQLObject, ID, +}; +use regex::{Captures, Regex}; +use tabby_common::{axum::AllowedCodeRepository, config::CodeRepository}; + +use super::{ + repository::{GitRepository, ProvidedRepository, Repository}, + web_documents::{CustomWebDocument, PresetWebDocument}, + Context, +}; +use crate::{policy::AccessPolicy, Result}; + +/// Represents the kind of context source. +#[derive(GraphQLEnum)] +pub enum ContextSourceKind { + Git, + Github, + Gitlab, + Doc, + Web, + Page, + Ingested, +} + +#[graphql_interface] +#[graphql(context = Context, for = [ProvidedRepository, GitRepository, ContextSourceValue, CustomWebDocument, PresetWebDocument, Repository, WebContextSource, PageContextSource, IngestedContextSource])] +pub trait ContextSourceId { + /// Represents the source of the context, which is the value mapped to `source_id` in the index. + fn source_id(&self) -> String; +} + +#[derive(GraphQLInterface)] +#[graphql(context = Context, impl = [ContextSourceIdValue], for = [CustomWebDocument, PresetWebDocument, Repository, WebContextSource, PageContextSource, IngestedContextSource])] +pub struct ContextSource { + pub id: ID, + + // start implements ContextSource + pub source_id: String, + // end implements ContextSource + pub source_kind: ContextSourceKind, + + /// Display name of the source, used to provide a human-readable name for user selection, such as in a dropdown menu. + pub source_name: String, +} + +impl ContextSourceValue { + pub fn source_id(&self) -> String { + match self { + ContextSourceValueEnum::Repository(x) => x.source_id().into(), + ContextSourceValueEnum::CustomWebDocument(x) => x.source_id(), + ContextSourceValueEnum::PresetWebDocument(x) => x.source_id(), + ContextSourceValueEnum::WebContextSource(x) => x.source_id().into(), + ContextSourceValueEnum::PageContextSource(x) => x.source_id().into(), + ContextSourceValueEnum::IngestedContextSource(x) => x.source_id().into(), + } + } + + pub fn source_name(&self) -> String { + match self { + ContextSourceValueEnum::Repository(x) => x.source_name().into(), + ContextSourceValueEnum::CustomWebDocument(x) => x.source_name().into(), + ContextSourceValueEnum::PresetWebDocument(x) => x.source_name().into(), + ContextSourceValueEnum::WebContextSource(x) => x.source_name().into(), + ContextSourceValueEnum::PageContextSource(x) => x.source_name().into(), + ContextSourceValueEnum::IngestedContextSource(x) => x.source_name().into(), + } + } +} + +pub struct WebContextSource; + +const PUBLIC_WEB_INTERNAL_SOURCE_ID: &str = "internal-public-web"; + +#[graphql_object(context = Context, impl = [ContextSourceIdValue, ContextSourceValue])] +impl WebContextSource { + fn id(&self) -> ID { + ID::new(PUBLIC_WEB_INTERNAL_SOURCE_ID.to_owned()) + } + + fn source_kind(&self) -> ContextSourceKind { + ContextSourceKind::Web + } + + pub fn source_id(&self) -> &'static str { + PUBLIC_WEB_INTERNAL_SOURCE_ID + } + + pub fn source_name(&self) -> &'static str { + "Web" + } +} + +pub struct PageContextSource; + +const PAGE_SOURCE_ID: &str = "page"; + +#[graphql_object(context = Context, impl = [ContextSourceIdValue, ContextSourceValue])] +impl PageContextSource { + fn id(&self) -> ID { + ID::new(PAGE_SOURCE_ID.to_owned()) + } + + fn source_kind(&self) -> ContextSourceKind { + ContextSourceKind::Page + } + + pub fn source_id(&self) -> &'static str { + PAGE_SOURCE_ID + } + + pub fn source_name(&self) -> &'static str { + "Page" + } +} + +pub struct IngestedContextSource { + pub id: String, + pub name: String, +} + +const INGESTED_SOURCE_ID_PREFIX: &str = "ingested:"; + +#[graphql_object(context = Context, impl = [ContextSourceIdValue, ContextSourceValue])] +impl IngestedContextSource { + fn id(&self) -> ID { + ID::new(self.id.clone()) + } + + fn source_kind(&self) -> ContextSourceKind { + ContextSourceKind::Ingested + } + + pub fn source_id(&self) -> &str { + &self.id + } + + pub fn source_name(&self) -> &str { + &self.name + } +} + +#[derive(GraphQLObject)] +#[graphql(context = Context)] +pub struct ContextInfo { + pub sources: Vec, +} + +impl ContextInfo { + pub fn helper(&self) -> ContextInfoHelper { + ContextInfoHelper::new(self) + } + + pub fn allowed_code_repository(&self) -> AllowedCodeRepository { + let mut repositories = vec![]; + repositories.reserve(self.sources.len()); + for x in &self.sources { + if let ContextSourceValueEnum::Repository(x) = x { + repositories.push(CodeRepository::new( + &x.git_url, + &x.source_id, + x.refs.iter().map(|r| r.name.clone()).collect(), + )); + } + } + + AllowedCodeRepository::new(repositories) + } +} + +pub struct ContextInfoHelper { + sources: HashMap, + allowed_code_repository: AllowedCodeRepository, +} + +impl ContextInfoHelper { + pub fn new(context_info: &ContextInfo) -> Self { + Self { + sources: context_info + .sources + .iter() + .map(|source| (source.source_id(), source.source_name())) + .collect(), + allowed_code_repository: context_info.allowed_code_repository(), + } + } + + /// Replace content tagged with `[[source:${id}]]` with its display name. + pub fn rewrite_tag(&self, content: &str) -> String { + let re = Regex::new(r"\[\[source:(.*?)\]\]").unwrap(); + let new_content = re.replace_all(content, |caps: &Captures| { + let source_id = caps.get(1).unwrap().as_str(); + if source_id == PUBLIC_WEB_INTERNAL_SOURCE_ID + || source_id == PAGE_SOURCE_ID + || source_id.starts_with(INGESTED_SOURCE_ID_PREFIX) + { + // For public-web source, don't include it in the content. + return "".to_owned(); + } + if let Some(display_name) = self.sources.get(source_id) { + display_name.to_string() + } else { + caps[0].to_owned() + } + }); + new_content.to_string() + } + + pub fn can_access_source_id(&self, source_id: &str) -> bool { + self.sources.contains_key(source_id) + } + + pub fn allowed_code_repository(&self) -> &AllowedCodeRepository { + &self.allowed_code_repository + } +} + +#[async_trait] +pub trait ContextService: Send + Sync { + /// Read context information from the backend. If `policy` is `None`, this retrieves all context information without applying any access policy. + async fn read(&self, policy: Option<&AccessPolicy>) -> Result; +} diff --git a/ee/tabby-schema/src/schema/email.rs b/ee/tabby-schema/src/schema/email.rs new file mode 100644 index 000000000000..de70443518ba --- /dev/null +++ b/ee/tabby-schema/src/schema/email.rs @@ -0,0 +1,55 @@ +use async_trait::async_trait; +use juniper::{GraphQLEnum, GraphQLInputObject, GraphQLObject}; +use tokio::task::JoinHandle; +use validator::Validate; + +use crate::schema::Result; + +#[derive(GraphQLEnum, Clone, Debug)] +pub enum Encryption { + StartTls, + SslTls, + None, +} + +#[derive(GraphQLEnum, Clone, Debug)] +pub enum AuthMethod { + None, + Plain, + Login, +} + +#[derive(GraphQLObject)] +pub struct EmailSetting { + pub smtp_username: String, + pub smtp_server: String, + pub smtp_port: i32, + pub from_address: String, + pub encryption: Encryption, + pub auth_method: AuthMethod, +} + +#[derive(GraphQLInputObject, Validate)] +pub struct EmailSettingInput { + pub smtp_username: String, + #[validate(email(code = "fromAddress", message = "Invalid email address"))] + pub from_address: String, + pub smtp_server: String, + #[validate(range(min = 1, max = 65535, code = "smtpPort", message = "Invalid port"))] + pub smtp_port: i32, + pub encryption: Encryption, + pub auth_method: AuthMethod, + pub smtp_password: Option, +} + +#[async_trait] +pub trait EmailService: Send + Sync { + async fn read_setting(&self) -> Result>; + async fn update_setting(&self, input: EmailSettingInput) -> Result<()>; + async fn delete_setting(&self) -> Result<()>; + + async fn send_test(&self, to: String) -> Result>; + async fn send_password_reset(&self, to: String, code: String) -> Result>; + async fn send_invitation(&self, email: String, code: String) -> Result>; + async fn send_signup(&self, email: String) -> Result>; +} diff --git a/ee/tabby-schema/src/schema/ingestion.rs b/ee/tabby-schema/src/schema/ingestion.rs new file mode 100644 index 000000000000..1fab12cda77a --- /dev/null +++ b/ee/tabby-schema/src/schema/ingestion.rs @@ -0,0 +1,57 @@ +use async_trait::async_trait; +use juniper::GraphQLObject; +use tabby_common::api::ingestion::{IngestionRequest, IngestionResponse}; + +use crate::Result; + +pub struct IngestedDocument { + pub id: String, + pub source: String, + pub link: Option, + pub title: String, + pub body: String, + pub status: IngestedDocStatus, +} + +pub enum IngestedDocStatus { + Pending, + Indexed, + Failed, +} + +#[derive(Debug, GraphQLObject)] +pub struct IngestionStats { + pub source: String, + pub pending: i32, + pub failed: i32, + pub total: i32, +} + +#[async_trait] +pub trait IngestionService: Send + Sync { + async fn get(&self, source_id: &str, id: &str) -> Result; + async fn list( + &self, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> Result>; + + async fn list_sources( + &self, + limit: Option, + offset: Option, + ) -> Result>; + + async fn ingestion(&self, ingestion: IngestionRequest) -> Result; + async fn delete(&self, source_id: String, id: String) -> Result<()>; + async fn delete_by_source_id(&self, source_id: String) -> Result<()>; + async fn stats(&self, sources: Option>) -> Result>; + + async fn should_ingest(&self) -> Result; + async fn mark_all_indexed(&self, sourced_ids: Vec<(String, String)>) -> Result<()>; + + fn source_name_from_id(&self, source_id: &str) -> String; + fn source_id_from_name(&self, source_name: &str) -> String; +} diff --git a/ee/tabby-schema/src/schema/integration.rs b/ee/tabby-schema/src/schema/integration.rs new file mode 100644 index 000000000000..ef822abb197e --- /dev/null +++ b/ee/tabby-schema/src/schema/integration.rs @@ -0,0 +1,129 @@ +use async_trait::async_trait; +use chrono::{DateTime, Utc}; +use juniper::{GraphQLEnum, GraphQLObject, ID}; +use strum::EnumIter; +use url::Url; + +use crate::{juniper::relay::NodeType, Context, CoreError, Result}; + +#[derive(Clone, EnumIter, GraphQLEnum)] +pub enum IntegrationKind { + Github, + Gitlab, + GithubSelfHosted, + GitlabSelfHosted, +} + +impl IntegrationKind { + pub fn format_authenticated_url(&self, git_url: &str, access_token: &str) -> Result { + let mut url = Url::parse(git_url).map_err(|e| CoreError::Other(e.into()))?; + match self { + IntegrationKind::Github | IntegrationKind::GithubSelfHosted => { + let _ = url.set_username(access_token); + } + IntegrationKind::Gitlab | IntegrationKind::GitlabSelfHosted => { + let _ = url.set_username("oauth2"); + let _ = url.set_password(Some(access_token)); + } + } + Ok(url.to_string()) + } + + pub fn is_self_hosted(&self) -> bool { + match self { + IntegrationKind::Github => false, + IntegrationKind::Gitlab => false, + IntegrationKind::GithubSelfHosted => true, + IntegrationKind::GitlabSelfHosted => true, + } + } +} + +#[derive(PartialEq, Eq, Debug, GraphQLEnum)] +pub enum IntegrationStatus { + Ready, + Pending, + Failed, +} + +#[derive(GraphQLObject)] +#[graphql(context = Context)] +pub struct Integration { + pub id: ID, + pub kind: IntegrationKind, + pub display_name: String, + pub access_token: String, + pub api_base: Option, + pub created_at: DateTime, + pub updated_at: DateTime, + pub status: IntegrationStatus, + pub message: Option, +} + +impl NodeType for Integration { + type Cursor = String; + + fn cursor(&self) -> Self::Cursor { + self.id.to_string() + } + + fn connection_type_name() -> &'static str { + "IntegrationConnection" + } + + fn edge_type_name() -> &'static str { + "IntegrationEdge" + } +} + +impl Integration { + pub fn api_base(&self) -> &str { + match &self.kind { + IntegrationKind::Github => "https://api.github.com", + IntegrationKind::Gitlab => "https://gitlab.com", + IntegrationKind::GithubSelfHosted => self + .api_base + .as_deref() + .expect("Self-hosted github always has a specified api_base"), + IntegrationKind::GitlabSelfHosted => self + .api_base + .as_deref() + .expect("Self-hosted gitlab always has a specified api_base"), + } + } +} + +#[async_trait] +pub trait IntegrationService: Send + Sync { + async fn create_integration( + &self, + kind: IntegrationKind, + display_name: String, + access_token: String, + api_base: Option, + ) -> Result; + + async fn delete_integration(&self, id: ID, kind: IntegrationKind) -> Result<()>; + + async fn update_integration( + &self, + id: ID, + kind: IntegrationKind, + display_name: String, + access_token: Option, + api_base: Option, + ) -> Result<()>; + + async fn list_integrations( + &self, + ids: Option>, + kind: Option, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> Result>; + + async fn get_integration(&self, id: &ID) -> Result; + async fn update_integration_sync_status(&self, id: &ID, error: Option) -> Result<()>; +} diff --git a/ee/tabby-schema/src/schema/interface.rs b/ee/tabby-schema/src/schema/interface.rs new file mode 100644 index 000000000000..92488ca8ca16 --- /dev/null +++ b/ee/tabby-schema/src/schema/interface.rs @@ -0,0 +1,36 @@ +use chrono::{DateTime, Utc}; +use juniper::GraphQLInterface; + +use super::{auth::UserSecured, Context}; +use crate::juniper::relay; + +#[derive(GraphQLInterface)] +#[graphql(for = UserSecured, context = Context)] +pub struct User { + pub id: juniper::ID, + pub email: String, + pub name: String, + pub created_at: DateTime, + pub is_admin: bool, + pub is_owner: bool, + pub active: bool, + pub is_sso_user: bool, +} + +impl relay::NodeType for UserValue { + type Cursor = String; + + fn cursor(&self) -> Self::Cursor { + match self { + UserValueEnum::UserSecured(user) => user.id.to_string(), + } + } + + fn connection_type_name() -> &'static str { + "UserConnection" + } + + fn edge_type_name() -> &'static str { + "UserEdge" + } +} diff --git a/ee/tabby-schema/src/schema/job.rs b/ee/tabby-schema/src/schema/job.rs new file mode 100644 index 000000000000..a1380d18b174 --- /dev/null +++ b/ee/tabby-schema/src/schema/job.rs @@ -0,0 +1,84 @@ +use std::{fmt::Debug, path::PathBuf}; + +use async_trait::async_trait; +use chrono::{DateTime, Utc}; +use juniper::{GraphQLObject, ID}; + +use crate::{ + juniper::relay, + schema::{Context, Result}, +}; + +#[derive(Debug, GraphQLObject)] +#[graphql(context = Context)] +pub struct JobRun { + pub id: juniper::ID, + pub job: String, + pub created_at: DateTime, + pub updated_at: DateTime, + pub started_at: Option>, + pub finished_at: Option>, + pub exit_code: Option, + + // Deprecated since v0.27.0, not yet removed from the database + // use `background_job_logs` instead + pub stdout: String, +} + +#[derive(Debug, GraphQLObject)] +#[graphql(context = Context)] +pub struct JobInfo { + /// Last run of the job. + pub last_job_run: Option, + + /// The command to submit job run using triggerJobRun mutation. + pub command: String, +} + +#[derive(Debug, GraphQLObject)] +pub struct JobStats { + pub success: i32, + pub failed: i32, + pub pending: i32, +} + +impl relay::NodeType for JobRun { + type Cursor = String; + + fn cursor(&self) -> Self::Cursor { + self.id.to_string() + } + + fn connection_type_name() -> &'static str { + "JobRunConnection" + } + + fn edge_type_name() -> &'static str { + "JobRunEdge" + } +} + +#[async_trait] +pub trait JobService: Send + Sync { + /// Trigger job run. + async fn trigger(&self, command: String) -> Result; + + /// Remove pending job run, returns number of jobs being removed. + async fn clear(&self, command: String) -> Result; + + async fn get_job_info(&self, command: String) -> Result; + + async fn list( + &self, + ids: Option>, + jobs: Option>, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> Result>; + + async fn compute_stats(&self, jobs: Option>) -> Result; + + async fn log_file_path(&self, id: &ID) -> Option; +} diff --git a/ee/tabby-schema/src/schema/license.rs b/ee/tabby-schema/src/schema/license.rs new file mode 100644 index 000000000000..6bcc5cfb23c1 --- /dev/null +++ b/ee/tabby-schema/src/schema/license.rs @@ -0,0 +1,146 @@ +use std::error::Error; + +use async_trait::async_trait; +use chrono::{DateTime, Utc}; +use juniper::{GraphQLEnum, GraphQLObject}; +use serde::Deserialize; + +use super::CoreError; +use crate::schema::Result; + +#[derive(Debug, Deserialize, GraphQLEnum, PartialEq)] +#[serde(rename_all = "UPPERCASE")] +pub enum LicenseType { + Community, + Team, + Enterprise, +} + +#[derive(GraphQLEnum, PartialEq, Debug, Clone)] +pub enum LicenseStatus { + Ok, + Expired, + SeatsExceeded, +} + +#[derive(GraphQLEnum, PartialEq, Debug, Clone, Deserialize)] +pub enum LicenseFeature { + CustomLogo, +} + +#[derive(GraphQLObject)] +pub struct LicenseInfo { + pub r#type: LicenseType, + pub status: LicenseStatus, + pub seats: i32, + pub seats_used: i32, + pub issued_at: Option>, + pub expires_at: Option>, + pub features: Option>, +} + +impl LicenseInfo { + pub fn seat_limits_for_community_license() -> usize { + 5 + } + + pub fn seat_limits_for_team_license() -> usize { + 50 + } + + pub fn guard_seat_limit(mut self) -> Self { + let seats = self.seats as usize; + self.seats = match self.r#type { + LicenseType::Community => { + std::cmp::min(seats, Self::seat_limits_for_community_license()) + } + LicenseType::Team => std::cmp::min(seats, Self::seat_limits_for_team_license()), + LicenseType::Enterprise => seats, + } as i32; + + self + } + + pub fn ensure_available_seats(&self, num_new_seats: usize) -> Result<()> { + self.ensure_valid_license()?; + if (self.seats_used as usize + num_new_seats) > self.seats as usize { + return Err(CoreError::InvalidLicense( + "No sufficient seats under current license", + )); + } + Ok(()) + } + + pub fn ensure_admin_seats(&self, num_admins: usize) -> Result<()> { + self.ensure_valid_license()?; + let num_admin_seats = match self.r#type { + LicenseType::Community => 1, + LicenseType::Team => 3, + LicenseType::Enterprise => usize::MAX, + }; + + if num_admins > num_admin_seats { + return Err(CoreError::InvalidLicense( + "No sufficient admin seats under the license", + )); + } + + Ok(()) + } + + pub fn expire_in_days(&self) -> Option { + self.expires_at.map(|expires_at| { + let now = Utc::now(); + let duration = expires_at.signed_duration_since(now); + duration.num_days() + }) + } + + pub fn ensure_available_features(&self, feature: LicenseFeature) -> Result<()> { + self.ensure_valid_license()?; + if let Some(features) = &self.features { + if features.contains(&feature) { + return Ok(()); + } + } + + Err(CoreError::InvalidLicense( + "Your plan doesn't include support for this feature.", + )) + } +} + +#[async_trait] +pub trait LicenseService: Send + Sync { + async fn read(&self) -> Result; + async fn update(&self, license: String) -> Result<()>; + async fn reset(&self) -> Result<()>; +} + +pub trait IsLicenseValid { + fn ensure_valid_license(&self) -> Result<()>; +} + +impl IsLicenseValid for LicenseInfo { + fn ensure_valid_license(&self) -> Result<()> { + match self.status { + LicenseStatus::Expired => Err(CoreError::InvalidLicense( + "Your enterprise license is expired", + )), + LicenseStatus::SeatsExceeded => Err(CoreError::InvalidLicense( + "You have more active users than seats included in your license", + )), + LicenseStatus::Ok => Ok(()), + } + } +} + +impl IsLicenseValid for std::result::Result { + fn ensure_valid_license(&self) -> Result<()> { + if let Ok(x) = self { + x.ensure_valid_license() + } else { + Err(CoreError::InvalidLicense("No valid license configured")) + } + } +} diff --git a/ee/tabby-schema/src/schema/mod.rs b/ee/tabby-schema/src/schema/mod.rs new file mode 100644 index 000000000000..cbb3ddaf98c0 --- /dev/null +++ b/ee/tabby-schema/src/schema/mod.rs @@ -0,0 +1,1973 @@ +pub mod access_policy; +pub mod analytic; +pub mod auth; +pub mod constants; +pub mod context; +pub mod email; +pub mod ingestion; +pub mod integration; +pub mod interface; +pub mod job; +pub mod license; +pub mod notification; +pub mod page; +pub mod repository; +pub mod retrieval; +pub mod setting; +pub mod thread; +pub mod user_event; +pub mod user_group; +pub mod web_documents; +pub mod worker; + +use std::{sync::Arc, time::Instant}; + +use access_policy::{AccessPolicyService, SourceIdAccessPolicy}; +use async_openai_alt::{ + error::OpenAIError, + types::{ + ChatCompletionRequestAssistantMessageContent, ChatCompletionRequestMessage, + ChatCompletionRequestSystemMessageContent, ChatCompletionRequestUserMessageArgs, + ChatCompletionRequestUserMessageContent, CreateChatCompletionRequestArgs, + }, +}; +use auth::{ + AuthProvider, AuthProviderKind, AuthenticationService, Invitation, LdapCredential, + RefreshTokenResponse, RegisterResponse, TokenAuthResponse, UpdateLdapCredentialInput, + UserSecured, +}; +use base64::Engine; +use chrono::{DateTime, Utc}; +use context::{ContextInfo, ContextService}; +use futures::StreamExt; +use interface::UserValue; +use job::{JobRun, JobService}; +use juniper::{ + graphql_object, graphql_subscription, graphql_value, FieldError, GraphQLEnum, GraphQLObject, + IntoFieldError, Object, RootNode, ScalarValue, Value, ID, +}; +use ldap3::result::LdapError; +use notification::NotificationService; +use page::{ + CreatePageRunInput, CreatePageSectionRunInput, CreateThreadToPageRunInput, PageRunStream, + SectionRunStream, ThreadToPageRunStream, UpdatePageContentInput, UpdatePageSectionContentInput, + UpdatePageSectionTitleInput, UpdatePageTitleInput, +}; +use repository::RepositoryGrepOutput; +use strum::IntoEnumIterator; +use tabby_common::{ + api::{code::CodeSearch, event::EventLogger}, + config::CompletionConfig, +}; +use tabby_inference::{ + ChatCompletionStream, CompletionOptionsBuilder, CompletionStream, Embedding as EmbeddingService, +}; +use thread::{CreateThreadAndRunInput, CreateThreadRunInput, ThreadRunStream, ThreadService}; +use tracing::warn; +use user_group::{ + CreateUserGroupInput, UpsertUserGroupMembershipInput, UserGroup, UserGroupService, +}; +use validator::{Validate, ValidationErrors}; +use worker::WorkerService; + +use self::{ + analytic::{AnalyticService, ChatCompletionStats, CompletionStats, DiskUsageStats}, + auth::{ + JWTPayload, OAuthCredential, OAuthProvider, PasswordChangeInput, PasswordResetInput, + RequestInvitationInput, RequestPasswordResetEmailInput, UpdateOAuthCredentialInput, + }, + email::{EmailService, EmailSetting, EmailSettingInput}, + ingestion::{IngestionService, IngestionStats}, + integration::{Integration, IntegrationKind, IntegrationService}, + job::JobStats, + license::{IsLicenseValid, LicenseInfo, LicenseService, LicenseType}, + page::PageService, + repository::{ + CreateIntegrationInput, FileEntrySearchResult, ProvidedRepository, Repository, + RepositoryKind, RepositoryService, UpdateIntegrationInput, + }, + setting::{ + BrandingSetting, NetworkSetting, NetworkSettingInput, SecuritySetting, + SecuritySettingInput, SettingService, + }, + user_event::{UserEvent, UserEventService}, + web_documents::{CreateCustomDocumentInput, CustomWebDocument, WebDocumentService}, +}; +use crate::{ + env, is_demo_mode, + juniper::relay::{self, query_async, Connection}, + web_documents::{PresetWebDocument, SetPresetDocumentActiveInput}, +}; + +pub trait ServiceLocator: Send + Sync { + fn auth(&self) -> Arc; + fn worker(&self) -> Arc; + fn code(&self) -> Option>; + fn chat(&self) -> Option>; + fn completion(&self) -> Option>; + fn embedding(&self) -> Option>; + fn logger(&self) -> Arc; + fn ingestion(&self) -> Arc; + fn job(&self) -> Arc; + fn repository(&self) -> Arc; + fn integration(&self) -> Arc; + fn email(&self) -> Arc; + fn setting(&self) -> Arc; + fn license(&self) -> Arc; + fn analytic(&self) -> Arc; + fn user_event(&self) -> Arc; + fn web_documents(&self) -> Arc; + fn thread(&self) -> Arc; + fn page(&self) -> Option>; + fn context(&self) -> Arc; + fn user_group(&self) -> Arc; + fn access_policy(&self) -> Arc; + fn notification(&self) -> Arc; +} + +pub struct Context { + pub claims: Option, + pub locator: Arc, +} + +// To make our context usable by Juniper, we have to implement a marker trait. +impl juniper::Context for Context {} + +pub type Result = std::result::Result; + +#[derive(thiserror::Error, Debug)] +pub enum CoreError { + #[error("{0}")] + Unauthorized(&'static str), + + #[error("{0}")] + Forbidden(&'static str), + + #[error("{0}")] + NotFound(&'static str), + + #[error("Invalid ID")] + InvalidID, + + #[error("Invalid input parameters")] + InvalidInput(#[from] ValidationErrors), + + #[error("SMTP is not configured")] + EmailNotConfigured, + + #[error("{0}")] + InvalidLicense(&'static str), + + #[error("{0}")] + Other(#[from] anyhow::Error), +} + +impl From for CoreError { + fn from(err: LdapError) -> Self { + Self::Other(err.into()) + } +} + +impl IntoFieldError for CoreError { + fn into_field_error(self) -> FieldError { + match self { + Self::Forbidden(msg) => FieldError::new(msg, graphql_value!({"code": "FORBIDDEN"})), + Self::Unauthorized(msg) => { + FieldError::new(msg, graphql_value!({"code": "UNAUTHORIZED"})) + } + Self::NotFound(msg) => FieldError::new(msg, graphql_value!({"code": "NOT_FOUND"})), + Self::InvalidInput(errors) => from_validation_errors(errors), + _ => self.into(), + } + } +} + +#[derive(thiserror::Error, Debug)] +pub enum TestModelConnectionError { + #[error("{0}")] + FailedToConnect(String), + + #[error("Model backend is not enabled")] + NotEnabled, + + #[error("{0}")] + Other(#[from] CoreError), +} + +impl From for TestModelConnectionError { + fn from(err: OpenAIError) -> Self { + match err { + OpenAIError::ApiError(e) => Self::FailedToConnect(e.message), + _ => Self::FailedToConnect(err.to_string()), + } + } +} + +impl IntoFieldError for TestModelConnectionError { + fn into_field_error(self) -> FieldError { + match self { + TestModelConnectionError::Other(err) => err.into_field_error(), + _ => self.into(), + } + } +} + +fn check_claims(ctx: &Context) -> Result<&JWTPayload, CoreError> { + ctx.claims + .as_ref() + .ok_or(CoreError::Unauthorized("You're not logged in")) +} + +async fn check_admin(ctx: &Context) -> Result<(), CoreError> { + let user = check_user(ctx).await?; + if !user.is_admin { + return Err(CoreError::Forbidden("You must be admin to proceed")); + } + + Ok(()) +} + +async fn check_user(ctx: &Context) -> Result { + check_user_and_auth_token(ctx, false).await +} + +async fn check_user_allow_auth_token(ctx: &Context) -> Result { + check_user_and_auth_token(ctx, true).await +} + +async fn check_user_and_auth_token( + ctx: &Context, + allow_auth_token: bool, +) -> Result { + let claims = check_claims(ctx)?; + if !allow_auth_token && claims.is_generated_from_auth_token { + return Err(CoreError::Forbidden( + "Invoking this API with an auth token is not allowed", + )); + } + let user = ctx.locator.auth().get_user(&claims.sub).await?; + Ok(user) +} + +async fn check_license(ctx: &Context, license_type: &[LicenseType]) -> Result<(), CoreError> { + let license = ctx.locator.license().read().await?; + + if !license_type.contains(&license.r#type) { + return Err(CoreError::InvalidLicense( + "Your plan doesn't include support for this feature.", + )); + } + + license.ensure_valid_license() +} + +#[derive(GraphQLEnum)] +enum ModelHealthBackend { + Chat, + Completion, + Embedding, +} + +#[derive(GraphQLObject, Debug, Clone)] +struct ModelBackendHealthInfo { + /// Latency in milliseconds. + latency_ms: i32, +} + +#[derive(GraphQLObject, Clone, Debug)] +pub struct ChatCompletionMessage { + pub role: String, + pub content: String, +} + +impl From for ChatCompletionMessage { + fn from(x: ChatCompletionRequestMessage) -> Self { + match x { + ChatCompletionRequestMessage::User(x) => ChatCompletionMessage { + role: "user".into(), + content: match x.content { + ChatCompletionRequestUserMessageContent::Text(x) => x, + _ => "".into(), + }, + }, + ChatCompletionRequestMessage::Assistant(x) => ChatCompletionMessage { + role: "assistant".into(), + content: match x.content { + Some(ChatCompletionRequestAssistantMessageContent::Text(x)) => x, + _ => "".into(), + }, + }, + ChatCompletionRequestMessage::Tool(_x) => ChatCompletionMessage { + role: "tool".into(), + content: "".into(), + }, + ChatCompletionRequestMessage::System(x) => ChatCompletionMessage { + role: "system".into(), + content: match x.content { + ChatCompletionRequestSystemMessageContent::Text(x) => x, + _ => "".into(), + }, + }, + ChatCompletionRequestMessage::Function(_x) => ChatCompletionMessage { + role: "function".into(), + content: "".into(), + }, + } + } +} + +#[derive(Default)] +pub struct Query; + +#[graphql_object(context = Context)] +impl Query { + async fn registration_token(ctx: &Context) -> Result { + check_admin(ctx).await?; + ctx.locator.worker().read_registration_token().await + } + + async fn me(ctx: &Context) -> Result { + check_user_allow_auth_token(ctx).await + } + + /// List users, accessible for all login users. + async fn users( + ctx: &Context, + ids: Option>, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> Result> { + check_user(ctx).await?; + relay::query_async( + after, + before, + first, + last, + |after, before, first, last| async move { + ctx.locator + .auth() + .list_users(ids, after, before, first, last) + .await + .map(|users| users.into_iter().map(UserValue::UserSecured).collect()) + }, + ) + .await + } + + async fn invitations( + ctx: &Context, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> Result> { + check_admin(ctx).await?; + relay::query_async( + after, + before, + first, + last, + |after, before, first, last| async move { + ctx.locator + .auth() + .list_invitations(after, before, first, last) + .await + }, + ) + .await + } + + async fn job_runs( + ctx: &Context, + ids: Option>, + jobs: Option>, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> Result> { + check_admin(ctx).await?; + relay::query_async( + after, + before, + first, + last, + |after, before, first, last| async move { + ctx.locator + .job() + .list(ids, jobs, after, before, first, last) + .await + }, + ) + .await + } + + async fn job_run_stats(ctx: &Context, jobs: Option>) -> Result { + ctx.locator.job().compute_stats(jobs).await + } + + async fn email_setting(ctx: &Context) -> Result> { + check_admin(ctx).await?; + ctx.locator.email().read_setting().await + } + + async fn network_setting(ctx: &Context) -> Result { + check_admin(ctx).await?; + ctx.locator.setting().read_network_setting().await + } + + async fn security_setting(ctx: &Context) -> Result { + check_admin(ctx).await?; + ctx.locator.setting().read_security_setting().await + } + + async fn branding_setting(ctx: &Context) -> Result { + let license = ctx.locator.license().read().await?; + license.ensure_available_features(license::LicenseFeature::CustomLogo)?; + ctx.locator.setting().read_branding_setting().await + } + + async fn git_repositories( + &self, + ctx: &Context, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> Result> { + check_admin(ctx).await?; + relay::query_async( + after, + before, + first, + last, + |after, before, first, last| async move { + ctx.locator + .repository() + .git() + .list(after, before, first, last) + .await + }, + ) + .await + } + + /// Search files that matches the pattern in the repository. + async fn repository_search( + ctx: &Context, + kind: RepositoryKind, + id: ID, + rev: Option, + pattern: String, + ) -> Result> { + let user = check_user(ctx).await?; + ctx.locator + .repository() + .search_files(&user.policy, &kind, &id, rev.as_deref(), &pattern, 40) + .await + } + + /// File content search with a grep-like experience. + /// + /// Syntax: + /// + /// 1. Unprefixed text will be treated as a regex pattern for file content search. + /// 2. 'f:' to search by file name with a regex pattern. + /// 3. 'lang:' to search by file language. + /// 4. All tokens can be negated by prefixing them with '-'. + /// + /// Examples: + /// * `f:schema -lang:rust fn` + /// * `func_name lang:go` + async fn repository_grep( + ctx: &Context, + kind: RepositoryKind, + id: ID, + rev: Option, + query: String, + ) -> Result { + let user = check_user(ctx).await?; + + let start_time = chrono::offset::Utc::now(); + let files = ctx + .locator + .repository() + .grep(&user.policy, &kind, &id, rev.as_deref(), &query, 40) + .await?; + let end_time = chrono::offset::Utc::now(); + let elapsed_ms = (end_time - start_time).num_milliseconds() as i32; + Ok(RepositoryGrepOutput { files, elapsed_ms }) + } + + async fn auth_providers(ctx: &Context) -> Result> { + let mut providers = vec![]; + + let auth = ctx.locator.auth(); + for x in OAuthProvider::iter() { + if auth + .read_oauth_credential(x.clone()) + .await + .is_ok_and(|x| x.is_some()) + { + providers.push(x.into()); + } + } + + if auth.read_ldap_credential().await.is_ok_and(|x| x.is_some()) { + providers.push(AuthProvider { + kind: AuthProviderKind::Ldap, + }); + } + + Ok(providers) + } + + async fn oauth_credential( + ctx: &Context, + provider: OAuthProvider, + ) -> Result> { + check_admin(ctx).await?; + ctx.locator.auth().read_oauth_credential(provider).await + } + + async fn oauth_callback_url(ctx: &Context, provider: OAuthProvider) -> Result { + check_admin(ctx).await?; + ctx.locator.auth().oauth_callback_url(provider).await + } + + async fn ldap_credential(ctx: &Context) -> Result> { + check_admin(ctx).await?; + ctx.locator.auth().read_ldap_credential().await + } + + async fn server_info(ctx: &Context) -> Result { + Ok(ServerInfo { + is_admin_initialized: ctx.locator.auth().is_admin_initialized().await?, + is_chat_enabled: ctx.locator.worker().is_chat_enabled().await?, + is_email_configured: ctx.locator.email().read_setting().await?.is_some(), + allow_self_signup: ctx.locator.auth().allow_self_signup().await?, + disable_password_login: ctx + .locator + .setting() + .read_security_setting() + .await? + .disable_password_login, + is_demo_mode: env::is_demo_mode(), + }) + } + + async fn license(ctx: &Context) -> Result { + ctx.locator.license().read().await + } + + // FIXME(meng): This is a temporary solution to expose the list of jobs, we should consider switching to a enum based approach. + async fn jobs() -> Result> { + Ok( + vec!["scheduler_git", "scheduler_github_gitlab", "web_crawler"] + .into_iter() + .map(Into::into) + .collect(), + ) + } + + async fn daily_stats_in_past_year( + ctx: &Context, + users: Option>, + ) -> Result> { + let users = users.unwrap_or_default(); + let user = check_user(ctx).await?; + user.policy.check_read_analytic(&users)?; + ctx.locator.analytic().daily_stats_in_past_year(users).await + } + + async fn daily_stats( + ctx: &Context, + start: DateTime, + end: DateTime, + users: Option>, + languages: Option>, + ) -> Result> { + let users = users.unwrap_or_default(); + let user = check_user(ctx).await?; + user.policy.check_read_analytic(&users)?; + ctx.locator + .analytic() + .daily_stats(start, end, users, languages.unwrap_or_default()) + .await + } + + async fn chat_daily_stats_in_past_year( + ctx: &Context, + users: Option>, + ) -> Result> { + let users = users.unwrap_or_default(); + let user = check_user(ctx).await?; + user.policy.check_read_analytic(&users)?; + ctx.locator + .analytic() + .chat_daily_stats_in_past_year(users) + .await + } + + async fn chat_daily_stats( + ctx: &Context, + start: DateTime, + end: DateTime, + users: Option>, + ) -> Result> { + let users = users.unwrap_or_default(); + let user = check_user(ctx).await?; + user.policy.check_read_analytic(&users)?; + ctx.locator + .analytic() + .chat_daily_stats(start, end, users) + .await + } + + async fn user_events( + ctx: &Context, + + // pagination arguments + after: Option, + before: Option, + first: Option, + last: Option, + + // filter arguments + users: Option>, + start: DateTime, + end: DateTime, + ) -> Result> { + check_admin(ctx).await?; + relay::query_async( + after, + before, + first, + last, + |after, before, first, last| async move { + ctx.locator + .user_event() + .list( + after, + before, + first, + last, + users.unwrap_or_default(), + start, + end, + ) + .await + }, + ) + .await + } + + async fn notifications(ctx: &Context) -> Result> { + let user = check_user(ctx).await?; + ctx.locator.notification().list(&user.id).await + } + + async fn disk_usage_stats(ctx: &Context) -> Result { + check_admin(ctx).await?; + ctx.locator.analytic().disk_usage_stats().await + } + + async fn repository_list(ctx: &Context) -> Result> { + let user = check_user_allow_auth_token(ctx).await?; + + ctx.locator + .repository() + .repository_list(Some(&user.policy)) + .await + } + + async fn context_info(ctx: &Context) -> Result { + let user = check_user_allow_auth_token(ctx).await?; + ctx.locator.context().read(Some(&user.policy)).await + } + + async fn integrations( + ctx: &Context, + ids: Option>, + kind: Option, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> Result> { + check_admin(ctx).await?; + query_async( + after, + before, + first, + last, + |after, before, first, last| async move { + ctx.locator + .integration() + .list_integrations(ids, kind, after, before, first, last) + .await + }, + ) + .await + } + + async fn integrated_repositories( + ctx: &Context, + ids: Option>, + kind: Option, + active: Option, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> Result> { + check_admin(ctx).await?; + query_async( + after, + before, + first, + last, + |after, before, first, last| async move { + ctx.locator + .repository() + .third_party() + .list_repositories_with_filter(ids, kind, active, after, before, first, last) + .await + }, + ) + .await + } + + async fn ingestion_status( + ctx: &Context, + sources: Option>, + ) -> Result> { + check_admin(ctx).await?; + ctx.locator.ingestion().stats(sources).await + } + + async fn threads( + ctx: &Context, + ids: Option>, + is_ephemeral: Option, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> Result> { + let user = check_user_allow_auth_token(ctx).await?; + + let threads = relay::query_async( + after, + before, + first, + last, + |after, before, first, last| async move { + ctx.locator + .thread() + .list(ids.as_deref(), is_ephemeral, after, before, first, last) + .await + }, + ) + .await?; + + for thread in threads.edges.iter() { + let thread = &thread.node; + user.policy + .check_read_thread(&thread.user_id, thread.is_ephemeral)?; + } + + Ok(threads) + } + + async fn my_threads( + ctx: &Context, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> Result> { + let user = check_user_allow_auth_token(ctx).await?; + relay::query_async( + after, + before, + first, + last, + |after, before, first, last| async move { + ctx.locator + .thread() + .list_owned(&user.id, after, before, first, last) + .await + }, + ) + .await + } + + /// Read thread messages by thread ID. + /// + /// Thread is public within an instance, so no need to check for ownership. + async fn thread_messages( + ctx: &Context, + thread_id: ID, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> Result> { + let user = check_user_allow_auth_token(ctx).await?; + + let thread = ctx + .locator + .thread() + .get(&thread_id) + .await? + .ok_or_else(|| CoreError::NotFound("thread not found"))?; + user.policy + .check_read_thread(&thread.user_id, thread.is_ephemeral)?; + + relay::query_async( + after, + before, + first, + last, + |after, before, first, last| async move { + ctx.locator + .thread() + .list_thread_messages(&thread_id, after, before, first, last) + .await + }, + ) + .await + } + + /// Read pages by page IDs. + async fn pages( + ctx: &Context, + ids: Option>, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> Result> { + check_user(ctx).await?; + + let page_service = if let Some(service) = ctx.locator.page() { + service + } else { + return Err(CoreError::Forbidden("Page service is not enabled")); + }; + + relay::query_async( + after, + before, + first, + last, + |after, before, first, last| async move { + page_service + .list(ids.as_deref(), after, before, first, last) + .await + }, + ) + .await + } + + async fn page_sections( + ctx: &Context, + page_id: ID, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> Result> { + check_user(ctx).await?; + + let page_service = if let Some(service) = ctx.locator.page() { + service + } else { + return Err(CoreError::Forbidden("Page service is not enabled")); + }; + + relay::query_async( + after, + before, + first, + last, + |after, before, first, last| async move { + page_service + .list_sections(&page_id, after, before, first, last) + .await + }, + ) + .await + } + + async fn custom_web_documents( + ctx: &Context, + ids: Option>, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> Result> { + check_admin(ctx).await?; + query_async( + after, + before, + first, + last, + |after, before, first, last| async move { + ctx.locator + .web_documents() + .list_custom_web_documents(ids, after, before, first, last) + .await + }, + ) + .await + } + async fn preset_web_documents( + ctx: &Context, + ids: Option>, + after: Option, + before: Option, + first: Option, + last: Option, + is_active: Option, + ) -> Result> { + check_admin(ctx).await?; + query_async( + after, + before, + first, + last, + |after, before, first, last| async move { + ctx.locator + .web_documents() + .list_preset_web_documents(ids, after, before, first, last, is_active) + .await + }, + ) + .await + } + + /// List user groups. + async fn user_groups(ctx: &Context) -> Result> { + check_user(ctx).await?; + ctx.locator.user_group().list().await + } + + async fn source_id_access_policies( + ctx: &Context, + source_id: String, + ) -> Result { + check_admin(ctx).await?; + let read = ctx + .locator + .access_policy() + .list_source_id_read_access(&source_id) + .await?; + + Ok(SourceIdAccessPolicy { source_id, read }) + } + + async fn test_model_connection( + ctx: &Context, + backend: ModelHealthBackend, + ) -> Result { + check_admin(ctx).await?; + + // count request time in milliseconds + let start = Instant::now(); + + match backend { + ModelHealthBackend::Completion => { + if let Some(completion) = ctx.locator.completion() { + let config = CompletionConfig::default(); + let options = CompletionOptionsBuilder::default() + .max_decoding_tokens(config.max_decoding_tokens as i32) + .sampling_temperature(0.1) + .seed(0) + .build() + .expect("Failed to build completion options"); + + let (first, _) = completion + .generate("def fib(n):\n", options) + .await + .into_future() + .await; + + if first.is_some() { + return Ok(ModelBackendHealthInfo { + latency_ms: start.elapsed().as_millis() as i32, + }); + } + + Err(TestModelConnectionError::FailedToConnect( + "Failed to connect to the completion model".into(), + )) + } else { + Err(TestModelConnectionError::NotEnabled) + } + } + + ModelHealthBackend::Chat => { + if let Some(chat) = ctx.locator.chat() { + let request = CreateChatCompletionRequestArgs::default() + .messages(vec![ChatCompletionRequestMessage::User( + ChatCompletionRequestUserMessageArgs::default() + .content("Hello, please reply in short") + .build() + .expect("Failed to build chat completion message"), + )]) + .build() + .expect("Failed to build chat completion request"); + match chat.chat(request).await { + Ok(_) => Ok(ModelBackendHealthInfo { + latency_ms: start.elapsed().as_millis() as i32, + }), + Err(e) => Err(e.into()), + } + } else { + Err(TestModelConnectionError::NotEnabled) + } + } + ModelHealthBackend::Embedding => { + if let Some(embedding) = ctx.locator.embedding() { + match embedding.embed("hello Tabby").await { + Ok(_) => Ok(ModelBackendHealthInfo { + latency_ms: start.elapsed().as_millis() as i32, + }), + Err(err) => Err(CoreError::Other(err).into()), + } + } else { + Err(TestModelConnectionError::NotEnabled) + } + } + } + } + + async fn read_repository_related_questions( + ctx: &Context, + source_id: String, + ) -> Result, CoreError> { + let user = check_user(ctx).await?; + ctx.locator + .repository() + .read_repository_related_questions( + ctx.locator + .chat() + .ok_or(CoreError::NotFound("The Chat didn't initialize yet"))?, + &user.policy, + source_id, + ) + .await + } +} + +#[derive(GraphQLObject)] +pub struct ServerInfo { + is_admin_initialized: bool, + is_chat_enabled: bool, + is_email_configured: bool, + allow_self_signup: bool, + disable_password_login: bool, + is_demo_mode: bool, +} + +#[derive(Default)] +pub struct Mutation; + +#[graphql_object(context = Context)] +impl Mutation { + async fn reset_registration_token(ctx: &Context) -> Result { + check_admin(ctx).await?; + ctx.locator.worker().reset_registration_token().await + } + + async fn request_invitation_email( + ctx: &Context, + input: RequestInvitationInput, + ) -> Result { + input.validate()?; + ctx.locator.auth().request_invitation_email(input).await + } + + async fn generate_reset_password_url(ctx: &Context, user_id: ID) -> Result { + check_admin(ctx).await?; + ctx.locator + .auth() + .generate_reset_password_url(&user_id) + .await + } + + async fn request_password_reset_email( + ctx: &Context, + input: RequestPasswordResetEmailInput, + ) -> Result { + input.validate()?; + ctx.locator + .auth() + .request_password_reset_email(input.email) + .await?; + Ok(true) + } + + async fn password_reset(ctx: &Context, input: PasswordResetInput) -> Result { + input.validate()?; + ctx.locator + .auth() + .password_reset(&input.code, &input.password1) + .await?; + Ok(true) + } + + async fn password_change(ctx: &Context, input: PasswordChangeInput) -> Result { + if is_demo_mode() { + return Err(CoreError::Forbidden( + "Changing password is disabled in Demo mode.", + )); + } + + let claims = check_claims(ctx)?; + input.validate()?; + ctx.locator + .auth() + .update_user_password( + &claims.sub, + input.old_password.as_deref(), + &input.new_password1, + ) + .await?; + Ok(true) + } + + async fn reset_user_auth_token(ctx: &Context) -> Result { + let claims = check_claims(ctx)?; + ctx.locator + .auth() + .reset_user_auth_token(&claims.sub) + .await?; + Ok(true) + } + + async fn logout_all_sessions(ctx: &Context) -> Result { + let claims = check_claims(ctx)?; + ctx.locator.auth().logout_all_sessions(&claims.sub).await?; + Ok(true) + } + + async fn update_user_active(ctx: &Context, id: ID, active: bool) -> Result { + check_admin(ctx).await?; + if ctx.claims.as_ref().is_some_and(|c| c.sub == id) { + return Err(CoreError::Forbidden( + "You cannot change your own active status", + )); + } + ctx.locator.auth().update_user_active(&id, active).await?; + Ok(true) + } + + async fn update_user_role(ctx: &Context, id: ID, is_admin: bool) -> Result { + check_admin(ctx).await?; + if ctx.claims.as_ref().is_some_and(|c| c.sub == id) { + return Err(CoreError::Forbidden("You cannot update your own role")); + } + ctx.locator.auth().update_user_role(&id, is_admin).await?; + Ok(true) + } + + async fn upload_user_avatar_base64( + ctx: &Context, + id: ID, + avatar_base64: Option, + ) -> Result { + let claims = check_claims(ctx)?; + if claims.sub != id { + return Err(CoreError::Unauthorized( + "You cannot change another user's avatar", + )); + } + // ast-grep-ignore: use-schema-result + use anyhow::Context; + let avatar = avatar_base64 + .map(|avatar| base64::prelude::BASE64_STANDARD.decode(avatar.as_bytes())) + .transpose() + .context("avatar is not valid base64 string")? + .map(Vec::into_boxed_slice); + ctx.locator.auth().update_user_avatar(&id, avatar).await?; + Ok(true) + } + + async fn update_user_name(ctx: &Context, id: ID, name: String) -> Result { + let claims = check_claims(ctx)?; + if claims.sub != id { + return Err(CoreError::Unauthorized( + "You cannot change another user's name", + )); + } + let input = auth::UpdateUserNameInput { name }; + input.validate()?; + ctx.locator.auth().update_user_name(&id, input.name).await?; + Ok(true) + } + + async fn register( + ctx: &Context, + email: String, + password1: String, + password2: String, + invitation_code: Option, + name: String, + ) -> Result { + let input = auth::RegisterInput { + email, + password1, + password2, + }; + input.validate()?; + + ctx.locator + .auth() + .register(input.email, input.password1, invitation_code, Some(name)) + .await + } + + async fn token_auth( + ctx: &Context, + email: String, + password: String, + ) -> Result { + let input = auth::TokenAuthInput { email, password }; + input.validate()?; + ctx.locator + .auth() + .token_auth(input.email, input.password) + .await + } + + async fn token_auth_ldap( + ctx: &Context, + user_id: String, + password: String, + ) -> Result { + let input = auth::TokenAuthLdapInput { + user_id: &user_id, + password: &password, + }; + input.validate()?; + ctx.locator + .auth() + .token_auth_ldap(&user_id, &password) + .await + } + + async fn verify_token(ctx: &Context, token: String) -> Result { + ctx.locator.auth().verify_access_token(&token).await?; + Ok(true) + } + + async fn refresh_token(ctx: &Context, refresh_token: String) -> Result { + ctx.locator.auth().refresh_token(refresh_token).await + } + + async fn create_invitation(ctx: &Context, email: String) -> Result { + check_admin(ctx).await?; + let invitation = ctx.locator.auth().create_invitation(email.clone()).await?; + Ok(invitation.id) + } + + async fn send_test_email(ctx: &Context, to: String) -> Result { + check_admin(ctx).await?; + ctx.locator.email().send_test(to).await?; + Ok(true) + } + + async fn mark_notifications_read(ctx: &Context, notification_id: Option) -> Result { + let user = check_user(ctx).await?; + + ctx.locator + .notification() + .mark_read(&user.id, notification_id.as_ref()) + .await?; + Ok(true) + } + + async fn create_git_repository( + ctx: &Context, + name: String, + git_url: String, + refs: Option>, + ) -> Result { + check_admin(ctx).await?; + let input = repository::CreateGitRepositoryInput { + name, + git_url, + refs, + }; + input.validate()?; + let refs = input.refs.unwrap_or_default(); + ctx.locator + .repository() + .git() + .create(input.name, input.git_url, refs) + .await + } + + async fn delete_git_repository(ctx: &Context, id: ID) -> Result { + check_admin(ctx).await?; + ctx.locator.repository().git().delete(&id).await + } + + async fn update_git_repository( + ctx: &Context, + id: ID, + refs: Option>, + ) -> Result { + check_admin(ctx).await?; + let refs = refs.unwrap_or_default(); + ctx.locator.repository().git().update(&id, refs).await + } + + async fn delete_invitation(ctx: &Context, id: ID) -> Result { + check_admin(ctx).await?; + ctx.locator.auth().delete_invitation(&id).await + } + + async fn update_oauth_credential( + ctx: &Context, + input: UpdateOAuthCredentialInput, + ) -> Result { + check_admin(ctx).await?; + check_license(ctx, &[LicenseType::Enterprise]).await?; + input.validate()?; + ctx.locator.auth().update_oauth_credential(input).await?; + Ok(true) + } + + async fn delete_oauth_credential(ctx: &Context, provider: OAuthProvider) -> Result { + check_admin(ctx).await?; + ctx.locator.auth().delete_oauth_credential(provider).await?; + Ok(true) + } + + async fn test_ldap_connection(ctx: &Context, input: UpdateLdapCredentialInput) -> Result { + check_admin(ctx).await?; + check_license(ctx, &[LicenseType::Enterprise]).await?; + ctx.locator.auth().test_ldap_connection(input).await?; + Ok(true) + } + + async fn update_ldap_credential( + ctx: &Context, + input: UpdateLdapCredentialInput, + ) -> Result { + check_admin(ctx).await?; + check_license(ctx, &[LicenseType::Enterprise]).await?; + input.validate()?; + + ctx.locator.auth().update_ldap_credential(input).await?; + Ok(true) + } + + async fn delete_ldap_credential(ctx: &Context) -> Result { + check_admin(ctx).await?; + ctx.locator.auth().delete_ldap_credential().await?; + Ok(true) + } + + async fn update_email_setting(ctx: &Context, input: EmailSettingInput) -> Result { + check_admin(ctx).await?; + input.validate()?; + ctx.locator.email().update_setting(input).await?; + Ok(true) + } + + async fn update_security_setting(ctx: &Context, input: SecuritySettingInput) -> Result { + check_admin(ctx).await?; + check_license(ctx, &[LicenseType::Enterprise]).await?; + input.validate()?; + ctx.locator.setting().update_security_setting(input).await?; + Ok(true) + } + + async fn update_network_setting(ctx: &Context, input: NetworkSettingInput) -> Result { + check_admin(ctx).await?; + input.validate()?; + ctx.locator.setting().update_network_setting(input).await?; + Ok(true) + } + + async fn update_branding_setting( + ctx: &Context, + input: setting::BrandingSettingInput, + ) -> Result { + check_admin(ctx).await?; + let license = ctx.locator.license().read().await?; + license.ensure_available_features(license::LicenseFeature::CustomLogo)?; + input.validate()?; + ctx.locator.setting().update_branding_setting(input).await?; + Ok(true) + } + + async fn delete_email_setting(ctx: &Context) -> Result { + check_admin(ctx).await?; + ctx.locator.email().delete_setting().await?; + Ok(true) + } + + async fn upload_license(ctx: &Context, license: String) -> Result { + check_admin(ctx).await?; + ctx.locator.license().update(license).await?; + Ok(true) + } + + async fn reset_license(ctx: &Context) -> Result { + check_admin(ctx).await?; + ctx.locator.license().reset().await?; + Ok(true) + } + + async fn create_integration(ctx: &Context, input: CreateIntegrationInput) -> Result { + check_admin(ctx).await?; + input.validate()?; + let id = ctx + .locator + .integration() + .create_integration( + input.kind, + input.display_name, + input.access_token, + input.api_base, + ) + .await?; + Ok(id) + } + + async fn update_integration(ctx: &Context, input: UpdateIntegrationInput) -> Result { + check_admin(ctx).await?; + input.validate()?; + ctx.locator + .integration() + .update_integration( + input.id, + input.kind, + input.display_name, + input.access_token, + input.api_base, + ) + .await?; + Ok(true) + } + + async fn delete_integration(ctx: &Context, id: ID, kind: IntegrationKind) -> Result { + check_admin(ctx).await?; + ctx.locator + .integration() + .delete_integration(id, kind) + .await?; + Ok(true) + } + + async fn update_integrated_repository_active( + ctx: &Context, + id: ID, + active: bool, + refs: Option>, + ) -> Result { + check_admin(ctx).await?; + ctx.locator + .repository() + .third_party() + .update_repository_active(id, active, refs) + .await?; + Ok(true) + } + + async fn update_integrated_repository_refs( + ctx: &Context, + id: ID, + refs: Vec, + ) -> Result { + check_admin(ctx).await?; + ctx.locator + .repository() + .third_party() + .update_repository_refs(id, refs) + .await?; + Ok(true) + } + + /// Trigger a job run given its param string. + async fn trigger_job_run(ctx: &Context, command: String) -> Result { + check_admin(ctx).await?; + ctx.locator.job().trigger(command).await + } + + /// Delete pair of user message and bot response in a thread. + async fn delete_thread_message_pair( + ctx: &Context, + thread_id: ID, + user_message_id: ID, + assistant_message_id: ID, + ) -> Result { + let user = check_user_allow_auth_token(ctx).await?; + let svc = ctx.locator.thread(); + let Some(thread) = svc.get(&thread_id).await? else { + return Err(CoreError::NotFound("Thread not found")); + }; + + user.policy.check_delete_thread_messages(&thread.user_id)?; + + ctx.locator + .thread() + .delete_thread_message_pair(&thread_id, &user_message_id, &assistant_message_id) + .await?; + Ok(true) + } + + async fn delete_thread(ctx: &Context, id: ID) -> Result { + let user = check_user_allow_auth_token(ctx).await?; + let svc = ctx.locator.thread(); + let Some(thread) = svc.get(&id).await? else { + return Err(CoreError::NotFound("Thread not found")); + }; + + user.policy.check_delete_thread(&thread.user_id)?; + + ctx.locator.thread().delete(&id).await?; + Ok(true) + } + + /// Turn on persisted status for a thread. + async fn set_thread_persisted(ctx: &Context, thread_id: ID) -> Result { + let user = check_user_allow_auth_token(ctx).await?; + let svc = ctx.locator.thread(); + let Some(thread) = svc.get(&thread_id).await? else { + return Err(CoreError::NotFound("Thread not found")); + }; + + user.policy + .check_update_thread_persistence(&thread.user_id)?; + + ctx.locator.thread().set_persisted(&thread_id).await?; + Ok(true) + } + + async fn update_thread_message( + ctx: &Context, + input: thread::UpdateMessageInput, + ) -> Result { + let user = check_user(ctx).await?; + input.validate()?; + + let svc = ctx.locator.thread(); + let Some(thread) = svc.get(&input.thread_id).await? else { + return Err(CoreError::NotFound("Thread not found")); + }; + + user.policy.check_update_thread_message(&thread.user_id)?; + + svc.update_thread_message(&input).await?; + Ok(true) + } + + // page mutations + async fn update_page_title(ctx: &Context, input: UpdatePageTitleInput) -> Result { + let user = check_user(ctx).await?; + + let page_service = if let Some(service) = ctx.locator.page() { + service + } else { + return Err(CoreError::Forbidden("Page service is not enabled")); + }; + input.validate()?; + + let page = page_service.get(&input.id).await?; + + user.policy.check_update_page(&page.author_id)?; + + page_service.update_title(&input.id, &input.title).await?; + Ok(true) + } + + async fn update_page_content(ctx: &Context, input: UpdatePageContentInput) -> Result { + let user = check_user(ctx).await?; + + let page_service = if let Some(service) = ctx.locator.page() { + service + } else { + return Err(CoreError::Forbidden("Page service is not enabled")); + }; + input.validate()?; + + let page = page_service.get(&input.id).await?; + + user.policy.check_update_page(&page.author_id)?; + + page_service + .update_content(&input.id, &input.content) + .await?; + Ok(true) + } + + async fn update_page_section_title( + ctx: &Context, + input: UpdatePageSectionTitleInput, + ) -> Result { + let user = check_user(ctx).await?; + + let page_service = if let Some(service) = ctx.locator.page() { + service + } else { + return Err(CoreError::Forbidden("Page service is not enabled")); + }; + input.validate()?; + + let section = page_service.get_section(&input.id).await?; + + let page = page_service.get(§ion.page_id).await?; + user.policy.check_update_page(&page.author_id)?; + + page_service + .update_section_title(&input.id, &input.title) + .await?; + Ok(true) + } + + async fn update_page_section_content( + ctx: &Context, + input: UpdatePageSectionContentInput, + ) -> Result { + let user = check_user(ctx).await?; + + let page_service = if let Some(service) = ctx.locator.page() { + service + } else { + return Err(CoreError::Forbidden("Page service is not enabled")); + }; + input.validate()?; + + let section = page_service.get_section(&input.id).await?; + let page = page_service.get(§ion.page_id).await?; + user.policy.check_update_page(&page.author_id)?; + page_service + .update_section_content(&input.id, &input.content) + .await?; + Ok(true) + } + + /// delete a page and all its sections. + async fn delete_page(ctx: &Context, id: ID) -> Result { + let user = check_user(ctx).await?; + + let page_service = if let Some(service) = ctx.locator.page() { + service + } else { + return Err(CoreError::Forbidden("Page service is not enabled")); + }; + + let page = page_service.get(&id).await?; + + user.policy.check_update_page(&page.author_id)?; + page_service.delete(&id).await.map(|_| true) + } + + /// delete a single page section. + async fn delete_page_section(ctx: &Context, section_id: ID) -> Result { + let user = check_user(ctx).await?; + + let page_service = if let Some(service) = ctx.locator.page() { + service + } else { + return Err(CoreError::Forbidden("Page service is not enabled")); + }; + let section = page_service.get_section(§ion_id).await?; + + let page = page_service.get(§ion.page_id).await?; + user.policy.check_update_page(&page.author_id)?; + + page_service.delete_section(§ion_id).await.map(|_| true) + } + + async fn move_page_section( + ctx: &Context, + id: ID, + direction: page::MoveSectionDirection, + ) -> Result { + let user = check_user(ctx).await?; + + let page_service = if let Some(service) = ctx.locator.page() { + service + } else { + return Err(CoreError::Forbidden("Page service is not enabled")); + }; + + let section = page_service.get_section(&id).await?; + let page = page_service.get(§ion.page_id).await?; + user.policy.check_update_page(&page.author_id)?; + + page_service + .move_section(&page.id, &id, direction) + .await + .map(|_| true) + } + + async fn create_custom_document(ctx: &Context, input: CreateCustomDocumentInput) -> Result { + check_admin(ctx).await?; + input.validate()?; + let id = ctx + .locator + .web_documents() + .create_custom_web_document(input.name, input.url) + .await?; + Ok(id) + } + + async fn delete_custom_document(ctx: &Context, id: ID) -> Result { + check_admin(ctx).await?; + ctx.locator + .web_documents() + .delete_custom_web_document(id) + .await?; + Ok(true) + } + + async fn set_preset_document_active( + ctx: &Context, + input: SetPresetDocumentActiveInput, + ) -> Result { + check_admin(ctx).await?; + input.validate()?; + ctx.locator + .web_documents() + .set_preset_web_documents_active(input.id, input.active) + .await?; + Ok(true) + } + + async fn create_user_group(ctx: &Context, input: CreateUserGroupInput) -> Result { + check_admin(ctx).await?; + input.validate()?; + let id = ctx.locator.user_group().create(&input).await?; + Ok(id) + } + + async fn delete_user_group(ctx: &Context, id: ID) -> Result { + check_admin(ctx).await?; + ctx.locator.user_group().delete(&id).await?; + Ok(true) + } + + async fn upsert_user_group_membership( + ctx: &Context, + input: UpsertUserGroupMembershipInput, + ) -> Result { + let user = check_user(ctx).await?; + user.policy + .check_upsert_user_group_membership(&input) + .await?; + + input.validate()?; + ctx.locator.user_group().upsert_membership(&input).await?; + Ok(true) + } + + async fn delete_user_group_membership( + ctx: &Context, + user_group_id: ID, + user_id: ID, + ) -> Result { + let user = check_user(ctx).await?; + user.policy + .check_delete_user_group_membership(&user_group_id, &user_id) + .await?; + + ctx.locator + .user_group() + .delete_membership(&user_group_id, &user_id) + .await?; + Ok(true) + } + + async fn grant_source_id_read_access( + ctx: &Context, + source_id: String, + user_group_id: ID, + ) -> Result { + check_admin(ctx).await?; + ctx.locator + .access_policy() + .grant_source_id_read_access(&source_id, &user_group_id) + .await?; + Ok(true) + } + + async fn revoke_source_id_read_access( + ctx: &Context, + source_id: String, + user_group_id: ID, + ) -> Result { + check_admin(ctx).await?; + ctx.locator + .access_policy() + .revoke_source_id_read_access(&source_id, &user_group_id) + .await?; + Ok(true) + } +} + +fn from_validation_errors(error: ValidationErrors) -> FieldError { + let mut errors: Vec> = vec![]; + + error.errors().iter().for_each(|(field, kind)| match kind { + validator::ValidationErrorsKind::Struct(e) => { + let mut obj = Object::with_capacity(2); + obj.add_field("path", field.to_string().into()); + obj.add_field("message", Value::scalar(e.to_string())); + errors.push(obj.into()); + } + validator::ValidationErrorsKind::List(_) => { + warn!("List errors are not handled"); + } + validator::ValidationErrorsKind::Field(e) => { + for error in e { + let mut obj = Object::with_capacity(2); + obj.add_field("path", Value::scalar(error.code.to_string())); + obj.add_field( + "message", + Value::scalar(error.message.clone().unwrap_or_default().to_string()), + ); + errors.push(obj.into()); + } + } + }); + + let mut error = Object::with_capacity(1); + error.add_field("errors", Value::list(errors)); + + let mut ext = Object::with_capacity(1); + ext.add_field("validation-errors", error.into()); + + FieldError::new("Invalid input parameters", ext.into()) +} + +#[derive(Clone, Copy, Debug)] +pub struct Subscription; + +#[graphql_subscription] +impl Subscription { + async fn create_thread_and_run( + ctx: &Context, + input: CreateThreadAndRunInput, + ) -> Result { + let user = check_user_allow_auth_token(ctx).await?; + input.validate()?; + + let thread = ctx.locator.thread(); + + let thread_id = thread.create(&user.id, &input.thread).await?; + + thread + .create_run( + &user, + &thread_id, + &input.options, + input.thread.user_message.attachments.as_ref(), + true, + true, + ) + .await + } + + async fn create_thread_run( + ctx: &Context, + input: CreateThreadRunInput, + ) -> Result { + let user = check_user_allow_auth_token(ctx).await?; + input.validate()?; + + let svc = ctx.locator.thread(); + let Some(thread) = svc.get(&input.thread_id).await? else { + return Err(CoreError::NotFound("Thread not found")); + }; + + if thread.user_id != user.id { + return Err(CoreError::Forbidden( + "You must be the thread owner to create a run", + )); + } + + svc.append_user_message(&input.thread_id, &input.additional_user_message) + .await?; + + svc.create_run( + &user, + &input.thread_id, + &input.options, + input.additional_user_message.attachments.as_ref(), + true, + false, + ) + .await + } + + async fn create_page_run(ctx: &Context, input: CreatePageRunInput) -> Result { + let user = check_user(ctx).await?; + + let page_service = if let Some(service) = ctx.locator.page() { + service + } else { + return Err(CoreError::Forbidden("Page service is not enabled")); + }; + + page_service + .create_run(&user.policy, &user.id, &input) + .await + } + + /// Utilize an existing thread and its messages to create a page. + /// This will automatically generate: + /// - the page title and a summary of the content. + /// - a few sections based on the thread messages. + async fn create_thread_to_page_run( + ctx: &Context, + input: CreateThreadToPageRunInput, + ) -> Result { + let user = check_user(ctx).await?; + + let page_service = if let Some(service) = ctx.locator.page() { + service + } else { + return Err(CoreError::Forbidden("Page service is not enabled")); + }; + + page_service + .convert_thread_to_page(&user.policy, &user.id, &input) + .await + } + + async fn create_page_section_run( + ctx: &Context, + input: CreatePageSectionRunInput, + ) -> Result { + let user = check_user(ctx).await?; + + let page_service = if let Some(service) = ctx.locator.page() { + service + } else { + return Err(CoreError::Forbidden("Page service is not enabled")); + }; + + let page = page_service.get(&input.page_id).await?; + user.policy.check_update_page(&page.author_id)?; + + page_service.append_section(&user.policy, &input).await + } +} + +pub type Schema = RootNode<'static, Query, Mutation, Subscription>; + +pub fn create_schema() -> Schema { + Schema::new(Query, Mutation, Subscription) +} diff --git a/ee/tabby-schema/src/schema/notification.rs b/ee/tabby-schema/src/schema/notification.rs new file mode 100644 index 000000000000..86f6ce30174a --- /dev/null +++ b/ee/tabby-schema/src/schema/notification.rs @@ -0,0 +1,32 @@ +use async_trait::async_trait; +use chrono::{DateTime, Utc}; +use juniper::{GraphQLEnum, GraphQLObject, ID}; + +use crate::Result; + +#[derive(GraphQLEnum, Clone, Debug)] +pub enum NotificationRecipient { + Admin, + AllUser, +} + +#[derive(GraphQLObject)] +pub struct Notification { + pub id: ID, + pub content: String, + pub read: bool, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +#[async_trait] +pub trait NotificationService: Send + Sync { + /// Create notification + async fn create(&self, recipient: NotificationRecipient, content: &str) -> Result; + + /// List notifications + async fn list(&self, user_id: &ID) -> Result>; + + /// Mark notification as read for user + async fn mark_read(&self, user_id: &ID, id: Option<&ID>) -> Result<()>; +} diff --git a/ee/tabby-schema/src/schema/page.rs b/ee/tabby-schema/src/schema/page.rs new file mode 100644 index 000000000000..63ea275b5df9 --- /dev/null +++ b/ee/tabby-schema/src/schema/page.rs @@ -0,0 +1,71 @@ +mod types; + +use async_trait::async_trait; +use futures::stream::BoxStream; +use juniper::ID; +pub use types::*; + +use crate::{policy::AccessPolicy, schema::Result}; + +pub type ThreadToPageRunStream = BoxStream<'static, Result>; +pub type PageRunStream = BoxStream<'static, Result>; +pub type SectionRunStream = BoxStream<'static, Result>; + +#[async_trait] +pub trait PageService: Send + Sync { + fn source_id(&self) -> String { + "page".into() + } + + async fn convert_thread_to_page( + &self, + policy: &AccessPolicy, + author_id: &ID, + input: &CreateThreadToPageRunInput, + ) -> Result; + + async fn create_run( + &self, + policy: &AccessPolicy, + author_id: &ID, + input: &CreatePageRunInput, + ) -> Result; + + async fn list( + &self, + ids: Option<&[ID]>, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> Result>; + async fn get(&self, id: &ID) -> Result; + async fn update_title(&self, id: &ID, title: &str) -> Result<()>; + async fn update_content(&self, id: &ID, content: &str) -> Result<()>; + async fn delete(&self, id: &ID) -> Result<()>; + + async fn list_sections( + &self, + page_id: &ID, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> Result>; + async fn get_section(&self, id: &ID) -> Result; + async fn move_section( + &self, + page_id: &ID, + id: &ID, + direction: MoveSectionDirection, + ) -> Result<()>; + async fn append_section( + &self, + policy: &AccessPolicy, + input: &CreatePageSectionRunInput, + ) -> Result; + async fn update_section_title(&self, id: &ID, title: &str) -> Result<()>; + async fn update_section_content(&self, id: &ID, content: &str) -> Result<()>; + + async fn delete_section(&self, id: &ID) -> Result<()>; +} diff --git a/ee/tabby-schema/src/schema/page/types.rs b/ee/tabby-schema/src/schema/page/types.rs new file mode 100644 index 000000000000..850eaa72a147 --- /dev/null +++ b/ee/tabby-schema/src/schema/page/types.rs @@ -0,0 +1,404 @@ +use chrono::{DateTime, Utc}; +use juniper::{GraphQLEnum, GraphQLInputObject, GraphQLObject, GraphQLUnion, ID}; +use validator::Validate; + +use crate::{ + juniper::relay::NodeType, + retrieval::{ + AttachmentCode, AttachmentCodeFileList, AttachmentCodeHit, AttachmentDoc, AttachmentDocHit, + }, + thread::{CodeQueryInput, DocQueryInput, MessageAttachment}, + ChatCompletionMessage, Context, +}; + +#[derive(GraphQLObject, Debug)] +#[graphql(context = Context)] +pub struct Page { + pub id: ID, + pub author_id: ID, + pub title: Option, + pub code_source_id: Option, + pub content: Option, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +impl NodeType for Page { + type Cursor = String; + + fn cursor(&self) -> Self::Cursor { + self.id.to_string() + } + + fn connection_type_name() -> &'static str { + "PageConnection" + } + + fn edge_type_name() -> &'static str { + "PageEdge" + } +} + +#[derive(GraphQLObject, Clone)] +#[graphql(context = Context)] +pub struct PageSection { + pub id: ID, + pub page_id: ID, + pub title: String, + pub content: String, + pub position: i32, + + pub attachments: SectionAttachment, + + pub created_at: DateTime, + pub updated_at: DateTime, + + pub debug_data: Option, +} + +impl NodeType for PageSection { + type Cursor = String; + + fn cursor(&self) -> Self::Cursor { + self.id.to_string() + } + + fn connection_type_name() -> &'static str { + "SectionConnection" + } + + fn edge_type_name() -> &'static str { + "SectionEdge" + } +} + +#[derive(GraphQLInputObject, Default)] +pub struct ThreadToPageDebugOptionInput { + #[graphql(default)] + pub return_chat_completion_request: bool, + + #[graphql(default)] + pub return_query_request: bool, +} + +#[derive(GraphQLInputObject)] +pub struct CreateThreadToPageRunInput { + pub thread_id: ID, + + #[graphql(default)] + pub debug_option: Option, +} + +#[derive(GraphQLInputObject, Validate)] +pub struct UpdatePageTitleInput { + pub id: ID, + #[validate(length(min = 1, max = 256, code = "title", message = "title can not be empty"))] + pub title: String, +} + +#[derive(GraphQLInputObject, Validate)] +pub struct UpdatePageContentInput { + pub id: ID, + #[validate(length( + min = 1, + max = 65535, + code = "content", + message = "content can not be empty" + ))] + pub content: String, +} + +#[derive(GraphQLInputObject, Validate)] +pub struct UpdatePageSectionTitleInput { + pub id: ID, + #[validate(length(min = 1, max = 256, code = "title", message = "title can not be empty"))] + pub title: String, +} + +#[derive(GraphQLInputObject, Validate)] +pub struct UpdatePageSectionContentInput { + pub id: ID, + #[validate(length( + min = 1, + max = 65535, + code = "content", + message = "content can not be empty" + ))] + pub content: String, +} + +#[derive(GraphQLInputObject, Default)] +pub struct PageRunDebugOptionInput { + #[graphql(default)] + pub return_chat_completion_request: bool, + + #[graphql(default)] + pub return_query_request: bool, +} + +#[derive(GraphQLInputObject, Validate)] +pub struct CreatePageRunInput { + pub title_prompt: String, + + #[validate(nested)] + #[graphql(default)] + pub doc_query: Option, + + #[validate(nested)] + #[graphql(default)] + pub code_query: Option, + + #[graphql(default)] + pub debug_option: Option, +} + +#[derive(GraphQLInputObject, Default, Clone)] +pub struct PageSectionRunDebugOptionInput { + #[graphql(default)] + pub return_chat_completion_request: bool, + + #[graphql(default)] + pub return_query_request: bool, +} + +#[derive(GraphQLInputObject, Validate)] +pub struct CreatePageSectionRunInput { + pub page_id: ID, + pub title_prompt: String, + + #[validate(nested)] + #[graphql(default)] + pub doc_query: Option, + + #[graphql(default)] + pub debug_option: Option, +} + +#[derive(GraphQLEnum)] +pub enum MoveSectionDirection { + Up, + Down, +} + +#[derive(GraphQLObject, Clone)] +pub struct PageTitleDebugData { + /// Messages sent to LLM to generate the page title. + pub generate_page_title_messages: Vec, +} + +#[derive(GraphQLObject)] +pub struct PageCreated { + pub id: ID, + pub author_id: ID, + pub title: String, + + pub debug_data: Option, +} + +#[derive(GraphQLObject, Clone)] +pub struct PageSectionDebugData { + /// Messages sent to LLM to generate the page section titles. + pub generate_section_titles_messages: Vec, +} + +#[derive(GraphQLObject, Clone)] +#[graphql(context = Context)] +pub struct PageSectionCreated { + pub id: ID, + pub page_id: ID, + pub title: String, + pub position: i32, + + pub created_at: DateTime, + pub updated_at: DateTime, + + pub debug_data: Option, +} + +impl From for PageSectionCreated { + fn from(value: PageSection) -> Self { + Self { + id: value.id, + page_id: value.page_id, + title: value.title, + position: value.position, + created_at: value.created_at, + updated_at: value.updated_at, + debug_data: None, + } + } +} + +#[derive(GraphQLObject, Clone)] +#[graphql(context = Context)] +pub struct PageSectionsCreated { + pub sections: Vec, + + pub debug_data: Option, +} + +#[derive(GraphQLObject)] +pub struct PageSectionAttachmentCodeFileList { + pub id: ID, + pub code_file_list: AttachmentCodeFileList, +} + +#[derive(GraphQLObject)] +pub struct PageSectionAttachmentCode { + pub id: ID, + pub codes: Vec, + + pub debug_data: Option, +} + +#[derive(GraphQLObject)] +pub struct AttachmentCodeQueryDebugData { + pub source_id: String, + pub query: String, +} + +#[derive(GraphQLObject)] +pub struct AttachmentDocQueryDebugData { + pub source_ids: Vec, + pub query: String, +} +#[derive(GraphQLObject)] +#[graphql(context = Context)] +pub struct PageSectionAttachmentDoc { + pub id: ID, + pub doc: Vec, + + pub debug_data: Option, +} + +#[derive(GraphQLObject, Clone, Default)] +#[graphql(context = Context)] +pub struct SectionAttachment { + pub code: Vec, + + // FIXME(meng): consider remove code file list from section attachment. + pub code_file_list: Option, + + pub doc: Vec, +} + +impl From for MessageAttachment { + fn from(attachment: SectionAttachment) -> MessageAttachment { + MessageAttachment { + client_code: vec![], + code: attachment.code.iter().map(Into::into).collect(), + code_file_list: attachment.code_file_list.map(Into::into), + doc: vec![], + } + } +} + +impl SectionAttachment { + pub fn from_message_attachment(attachment: &MessageAttachment) -> SectionAttachment { + SectionAttachment { + code: attachment.code.iter().map(Into::into).collect(), + code_file_list: attachment.code_file_list.as_ref().map(Into::into), + doc: attachment.doc.iter().map(Into::into).collect(), + } + } + + pub fn merge(&mut self, other: &SectionAttachment) { + for code in &other.code { + if !self.code.iter().any(|c| c != code) { + self.code.push(code.clone()); + } + } + + if let Some(code_file_list) = &other.code_file_list { + if self.code_file_list.is_none() { + self.code_file_list = Some(code_file_list.clone()); + } + } + + for doc in &other.doc { + if !self.doc.iter().any(|d| d != doc) { + self.doc.push(doc.clone()); + } + } + } +} + +#[derive(GraphQLObject)] +pub struct PageContentDelta { + pub delta: String, +} + +#[derive(GraphQLObject, Clone)] +pub struct PageContentDebugData { + /// Messages sent to LLM to generate the response. + pub generate_page_content_messages: Vec, +} + +#[derive(GraphQLObject)] +pub struct PageContentCompleted { + pub id: ID, + + pub debug_data: Option, +} + +#[derive(GraphQLObject)] +pub struct PageSectionContentDelta { + pub id: ID, + pub delta: String, +} + +#[derive(GraphQLObject, Clone)] +pub struct PageSectionContentDebugData { + /// Messages sent to LLM to generate the response. + pub generate_section_content_messages: Vec, +} + +#[derive(GraphQLObject)] +pub struct PageSectionContentCompleted { + pub id: ID, + + pub debug_data: Option, +} + +#[derive(GraphQLObject)] +pub struct PageCompleted { + pub id: ID, +} + +/// Schema of page convert stream. +#[derive(GraphQLUnion)] +#[graphql(context = Context)] +pub enum PageRunItem { + // PageCreated will return at the beginning of the stream, + // containing the page ID, author and title. + PageCreated(PageCreated), + + PageContentDelta(PageContentDelta), + PageContentCompleted(PageContentCompleted), + + // PageSectionsCreated will return the titles of all sections. + PageSectionsCreated(PageSectionsCreated), + + PageSectionAttachmentCodeFileList(PageSectionAttachmentCodeFileList), + PageSectionAttachmentCode(PageSectionAttachmentCode), + PageSectionAttachmentDoc(PageSectionAttachmentDoc), + + PageSectionContentDelta(PageSectionContentDelta), + PageSectionContentCompleted(PageSectionContentCompleted), + + PageCompleted(PageCompleted), +} + +/// Schema of page convert stream. +#[derive(GraphQLUnion)] +#[graphql(context = Context)] +pub enum SectionRunItem { + PageSectionCreated(PageSectionCreated), + + PageSectionAttachmentCodeFileList(PageSectionAttachmentCodeFileList), + PageSectionAttachmentCode(PageSectionAttachmentCode), + PageSectionAttachmentDoc(PageSectionAttachmentDoc), + + PageSectionContentDelta(PageSectionContentDelta), + PageSectionContentCompleted(PageSectionContentCompleted), +} diff --git a/ee/tabby-schema/src/schema/repository/git.rs b/ee/tabby-schema/src/schema/repository/git.rs new file mode 100644 index 000000000000..8cba319688fd --- /dev/null +++ b/ee/tabby-schema/src/schema/repository/git.rs @@ -0,0 +1,97 @@ +use async_trait::async_trait; +use juniper::{graphql_object, ID}; +use validator::Validate; + +use super::{GitReference, RepositoryProvider}; +use crate::{ + context::ContextSourceIdValue, + job::JobInfo, + juniper::relay::NodeType, + schema::{Context, Result}, +}; + +#[derive(Validate)] +pub struct CreateGitRepositoryInput { + #[validate(regex( + code = "name", + path = "*crate::schema::constants::REPOSITORY_NAME_REGEX", + message = "Invalid repository name" + ))] + pub name: String, + #[validate(url(code = "gitUrl", message = "Invalid Git URL"))] + pub git_url: String, + pub refs: Option>, +} + +pub struct GitRepository { + pub id: juniper::ID, + pub name: String, + pub git_url: String, + pub refs: Vec, + + pub job_info: JobInfo, +} + +impl GitRepository { + pub fn format_source_id(id: &ID) -> String { + format!("git:{id}") + } +} + +#[graphql_object(context = Context, impl = [ContextSourceIdValue])] +impl GitRepository { + fn id(&self) -> &ID { + &self.id + } + + pub fn source_id(&self) -> String { + Self::format_source_id(&self.id) + } + + fn name(&self) -> &String { + &self.name + } + + fn git_url(&self) -> &String { + &self.git_url + } + + fn refs(&self) -> &Vec { + &self.refs + } + + fn job_info(&self) -> &JobInfo { + &self.job_info + } +} + +impl NodeType for GitRepository { + type Cursor = String; + + fn cursor(&self) -> Self::Cursor { + self.id.to_string() + } + + fn connection_type_name() -> &'static str { + "RepositoryConnection" + } + + fn edge_type_name() -> &'static str { + "RepositoryEdge" + } +} + +#[async_trait] +pub trait GitRepositoryService: Send + Sync + RepositoryProvider { + async fn list( + &self, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> Result>; + + async fn create(&self, name: String, git_url: String, refs: Vec) -> Result; + async fn delete(&self, id: &ID) -> Result; + async fn update(&self, id: &ID, refs: Vec) -> Result; +} diff --git a/ee/tabby-schema/src/schema/repository/mod.rs b/ee/tabby-schema/src/schema/repository/mod.rs new file mode 100644 index 000000000000..cd79d0b0faca --- /dev/null +++ b/ee/tabby-schema/src/schema/repository/mod.rs @@ -0,0 +1,303 @@ +mod types; +use std::{path::PathBuf, sync::Arc}; + +use tabby_inference::ChatCompletionStream; +pub use types::*; + +mod git; +pub use git::{CreateGitRepositoryInput, GitRepository, GitRepositoryService}; + +mod third_party; +use async_trait::async_trait; +use base64::{engine::general_purpose::STANDARD, Engine}; +use juniper::{graphql_object, GraphQLEnum, GraphQLObject, ID}; +use serde::Deserialize; +use tabby_common::config::{CodeRepository, RepositoryConfig}; +pub use third_party::{ProvidedRepository, ThirdPartyRepositoryService}; + +use super::{ + context::{ContextSourceIdValue, ContextSourceKind, ContextSourceValue}, + Result, +}; +use crate::{juniper::relay::NodeType, policy::AccessPolicy, Context}; + +#[derive(GraphQLObject)] +pub struct FileEntrySearchResult { + pub r#type: &'static str, + pub path: String, + + /// matched indices for fuzzy search query. + pub indices: Vec, +} + +#[derive(GraphQLEnum, Debug, Deserialize, Clone, Copy, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum RepositoryKind { + Git, + Github, + Gitlab, + GithubSelfHosted, + GitlabSelfHosted, + GitConfig, +} + +#[derive(Debug)] +pub struct Repository { + pub id: ID, + + pub source_id: String, + + pub name: String, + pub kind: RepositoryKind, + + pub dir: PathBuf, + + pub git_url: String, + pub refs: Vec, +} + +#[graphql_object(context = Context, impl = [ContextSourceIdValue, ContextSourceValue])] +impl Repository { + fn id(&self) -> &ID { + &self.id + } + + pub fn source_id(&self) -> &str { + &self.source_id + } + + fn source_kind(&self) -> ContextSourceKind { + match self.kind { + RepositoryKind::Git | RepositoryKind::GitConfig => ContextSourceKind::Git, + RepositoryKind::Github | RepositoryKind::GithubSelfHosted => ContextSourceKind::Github, + RepositoryKind::Gitlab | RepositoryKind::GitlabSelfHosted => ContextSourceKind::Gitlab, + } + } + + pub fn source_name(&self) -> &str { + match self.kind { + RepositoryKind::Git + | RepositoryKind::GitConfig + | RepositoryKind::GithubSelfHosted + | RepositoryKind::GitlabSelfHosted => &self.git_url, + RepositoryKind::Github | RepositoryKind::Gitlab => &self.name, + } + } + + fn name(&self) -> &str { + &self.name + } + + fn kind(&self) -> RepositoryKind { + self.kind + } + + fn git_url(&self) -> &str { + &self.git_url + } + + fn refs(&self) -> &[GitReference] { + &self.refs + } +} + +#[derive(GraphQLObject, Debug)] +pub struct GitReference { + pub name: String, + pub commit: String, +} + +impl From for Repository { + fn from(value: GitRepository) -> Self { + Self { + source_id: value.source_id(), + id: ID::new(value.source_id()), + name: value.name, + kind: RepositoryKind::Git, + dir: RepositoryConfig::resolve_dir(&value.git_url), + git_url: RepositoryConfig::canonicalize_url(&value.git_url), + refs: value.refs, + } + } +} + +#[derive(GraphQLObject, Debug, PartialEq)] +#[graphql(context = Context)] +pub struct GitlabRepositoryProvider { + pub id: ID, + pub display_name: String, + + pub status: RepositoryProviderStatus, + + #[graphql(skip)] + pub access_token: Option, + + pub api_base: Option, +} + +impl NodeType for GitlabRepositoryProvider { + type Cursor = String; + + fn cursor(&self) -> Self::Cursor { + self.id.to_string() + } + + fn connection_type_name() -> &'static str { + "GitlabRepositoryProviderConnection" + } + + fn edge_type_name() -> &'static str { + "GitlabRepositoryProviderEdge" + } +} + +#[derive(GraphQLObject, Debug, PartialEq)] +#[graphql(context = Context)] +pub struct GithubRepositoryProvider { + pub id: ID, + pub display_name: String, + + pub status: RepositoryProviderStatus, + + #[graphql(skip)] + pub access_token: Option, + + pub api_base: Option, +} + +impl NodeType for GithubRepositoryProvider { + type Cursor = String; + + fn cursor(&self) -> Self::Cursor { + self.id.to_string() + } + + fn connection_type_name() -> &'static str { + "GithubRepositoryProviderConnection" + } + + fn edge_type_name() -> &'static str { + "GithubRepositoryProviderEdge" + } +} + +#[derive(GraphQLObject)] +pub struct RepositoryGrepOutput { + pub files: Vec, + + /// Elapsed time in milliseconds for grep search. + pub elapsed_ms: i32, +} + +#[derive(GraphQLObject)] +pub struct GrepFile { + pub path: String, + pub lines: Vec, +} + +#[derive(GraphQLObject)] +pub struct GrepLine { + /// Content of the line. + pub line: GrepTextOrBase64, + + /// Byte offset in the file to the start of the line. + pub byte_offset: i32, + + /// Line number in the file, starting from 1. + pub line_number: i32, + + /// The matches in the line. + pub sub_matches: Vec, +} + +pub enum GrepTextOrBase64 { + Text(String), + Base64(Vec), +} + +#[graphql_object] +impl GrepTextOrBase64 { + fn text(&self) -> Option<&str> { + match self { + GrepTextOrBase64::Text(text) => Some(text), + _ => None, + } + } + + fn base64(&self) -> Option { + match self { + GrepTextOrBase64::Base64(bytes) => Some(STANDARD.encode(bytes)), + _ => None, + } + } +} + +#[derive(GraphQLObject)] +pub struct GrepSubMatch { + // Byte offsets in the line + pub bytes_start: i32, + pub bytes_end: i32, +} + +#[async_trait] +pub trait RepositoryProvider { + async fn repository_list(&self) -> Result>; + async fn get_repository(&self, id: &ID) -> Result; +} + +#[async_trait] +pub trait RepositoryService: Send + Sync { + /// Read repositories. If `policy` is `None`, this retrieves all repositories without applying any access policy. + async fn repository_list(&self, policy: Option<&AccessPolicy>) -> Result>; + async fn resolve_repository( + &self, + policy: &AccessPolicy, + kind: &RepositoryKind, + id: &ID, + ) -> Result; + + async fn search_files( + &self, + policy: &AccessPolicy, + kind: &RepositoryKind, + id: &ID, + rev: Option<&str>, + pattern: &str, + top_n: usize, + ) -> Result>; + + /// Read files from a repository. + /// When `rev` is None, this retrieves files from the default branch. + /// When `top_n` is None, this retrieves all files. + /// + /// The file listing is in breadth first order. + async fn list_files( + &self, + policy: &AccessPolicy, + kind: &RepositoryKind, + id: &ID, + rev: Option<&str>, + top_n: Option, + ) -> Result<(Vec, bool)>; + + async fn grep( + &self, + policy: &AccessPolicy, + kind: &RepositoryKind, + id: &ID, + rev: Option<&str>, + query: &str, + top_n: usize, + ) -> Result>; + + fn git(&self) -> Arc; + fn third_party(&self) -> Arc; + + async fn list_all_code_repository(&self) -> Result>; + async fn read_repository_related_questions( + &self, + chat: Arc, + policy: &AccessPolicy, + source_id: String, + ) -> Result>; +} diff --git a/ee/tabby-schema/src/schema/repository/third_party.rs b/ee/tabby-schema/src/schema/repository/third_party.rs new file mode 100644 index 000000000000..107a6e7fa90d --- /dev/null +++ b/ee/tabby-schema/src/schema/repository/third_party.rs @@ -0,0 +1,131 @@ +use async_trait::async_trait; +use chrono::{DateTime, Utc}; +use juniper::{graphql_object, ID}; +use tabby_common::config::CodeRepository; + +use super::{GitReference, RepositoryProvider}; +use crate::{ + context::ContextSourceIdValue, integration::IntegrationKind, job::JobInfo, + juniper::relay::NodeType, schema::Result, Context, +}; + +pub struct ProvidedRepository { + pub id: ID, + pub integration_id: ID, + pub active: bool, + pub display_name: String, + pub git_url: String, + pub vendor_id: String, + pub created_at: DateTime, + pub updated_at: DateTime, + pub refs: Vec, + + pub job_info: JobInfo, +} + +impl ProvidedRepository { + pub fn format_source_id(id: &ID) -> String { + format!("provided_repository:{id}") + } +} + +#[graphql_object(context = Context, impl = [ContextSourceIdValue])] +impl ProvidedRepository { + fn id(&self) -> &ID { + &self.id + } + + fn integration_id(&self) -> &ID { + &self.integration_id + } + + fn active(&self) -> bool { + self.active + } + + fn display_name(&self) -> &String { + &self.display_name + } + + fn git_url(&self) -> &String { + &self.git_url + } + + fn vendor_id(&self) -> &String { + &self.vendor_id + } + + fn created_at(&self) -> &DateTime { + &self.created_at + } + + fn updated_at(&self) -> &DateTime { + &self.updated_at + } + + fn refs(&self) -> &Vec { + &self.refs + } + + fn job_info(&self) -> &JobInfo { + &self.job_info + } + + pub fn source_id(&self) -> String { + Self::format_source_id(&self.id) + } +} + +impl NodeType for ProvidedRepository { + type Cursor = String; + + fn cursor(&self) -> Self::Cursor { + self.id.to_string() + } + + fn connection_type_name() -> &'static str { + "ProvidedRepositoryConnection" + } + + fn edge_type_name() -> &'static str { + "ProvidedRepositoryEdge" + } +} + +#[async_trait] +pub trait ThirdPartyRepositoryService: Send + Sync + RepositoryProvider { + async fn list_repositories_with_filter( + &self, + integration_ids: Option>, + kind: Option, + active: Option, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> Result>; + + async fn get_provided_repository(&self, id: &ID) -> Result; + + async fn update_repository_active( + &self, + id: ID, + active: bool, + refs: Option>, + ) -> Result<()>; + async fn update_repository_refs(&self, id: ID, refs: Vec) -> Result<()>; + async fn upsert_repository( + &self, + integration_id: ID, + vendor_id: String, + display_name: String, + git_url: String, + ) -> Result; + async fn sync_repositories(&self, integration_id: ID) -> Result<()>; + async fn delete_outdated_repositories( + &self, + integration_id: ID, + before: DateTime, + ) -> Result; + async fn list_code_repositories(&self) -> Result>; +} diff --git a/ee/tabby-schema/src/schema/repository/types.rs b/ee/tabby-schema/src/schema/repository/types.rs new file mode 100644 index 000000000000..15399509ea62 --- /dev/null +++ b/ee/tabby-schema/src/schema/repository/types.rs @@ -0,0 +1,71 @@ +use juniper::{GraphQLEnum, GraphQLInputObject, ID}; +use validator::Validate; + +use crate::integration::IntegrationKind; + +#[derive(GraphQLInputObject, Validate)] +pub struct CreateIntegrationInput { + #[validate(regex( + code = "displayName", + path = "*crate::schema::constants::REPOSITORY_NAME_REGEX", + message = "Invalid repository provider name" + ))] + pub display_name: String, + #[validate(length(code = "accessToken", min = 10, message = "Invalid access token"))] + pub access_token: String, + pub kind: IntegrationKind, + #[validate(url(code = "apiBase", message = "Invalid URL base"))] + pub api_base: Option, +} + +#[derive(GraphQLInputObject, Validate)] +pub struct CreateSelfHostedRepositoryProviderInput { + #[validate(regex( + code = "displayName", + path = "*crate::schema::constants::REPOSITORY_NAME_REGEX", + message = "Invalid repository provider name" + ))] + pub display_name: String, + #[validate(length(code = "accessToken", min = 10, message = "Invalid access token"))] + pub access_token: String, + #[validate(url(code = "apiBase", message = "Invalid URL base"))] + pub api_base: Option, +} + +#[derive(GraphQLInputObject, Validate)] +pub struct UpdateIntegrationInput { + pub id: ID, + #[validate(regex( + code = "displayName", + path = "*crate::schema::constants::REPOSITORY_NAME_REGEX", + message = "Invalid repository provider name" + ))] + pub display_name: String, + #[validate(length(code = "accessToken", min = 10, message = "Invalid access token"))] + pub access_token: Option, + #[validate(url(code = "apiBase", message = "Invalid URL base"))] + pub api_base: Option, + pub kind: IntegrationKind, +} + +#[derive(GraphQLInputObject, Validate)] +pub struct UpdateSelfHostedRepositoryProviderInput { + pub id: ID, + #[validate(regex( + code = "displayName", + path = "*crate::schema::constants::REPOSITORY_NAME_REGEX", + message = "Invalid repository provider name" + ))] + pub display_name: String, + #[validate(length(code = "accessToken", min = 10, message = "Invalid access token"))] + pub access_token: Option, + #[validate(url(code = "apiBase", message = "Invalid URL base"))] + pub api_base: Option, +} + +#[derive(GraphQLEnum, Debug, PartialEq)] +pub enum RepositoryProviderStatus { + Ready, + Pending, + Failed, +} diff --git a/ee/tabby-schema/src/schema/retrieval.rs b/ee/tabby-schema/src/schema/retrieval.rs new file mode 100644 index 000000000000..a2f172381f78 --- /dev/null +++ b/ee/tabby-schema/src/schema/retrieval.rs @@ -0,0 +1,293 @@ +use chrono::{DateTime, Utc}; +use juniper::{GraphQLObject, GraphQLUnion}; +use tabby_common::api::code::{CodeSearchDocument, CodeSearchHit, CodeSearchScores}; + +use crate::{ + interface::UserValue, + thread::{ + MessageAttachmentCode, MessageAttachmentCodeFileList, MessageAttachmentCommitDoc, + MessageAttachmentDoc, MessageAttachmentIngestedDoc, MessageAttachmentIssueDoc, + MessageAttachmentPageDoc, MessageAttachmentPullDoc, MessageAttachmentWebDoc, + }, + Context, +}; + +#[derive(GraphQLObject)] +pub struct AttachmentCodeHits { + pub hits: Vec, +} + +#[derive(GraphQLObject)] +pub struct AttachmentCodeHit { + pub code: AttachmentCode, + pub scores: AttachmentCodeScores, +} + +#[derive(GraphQLObject, Clone)] +pub struct AttachmentCodeScores { + pub rrf: f64, + pub bm25: f64, + pub embedding: f64, +} + +#[derive(GraphQLObject, Clone, PartialEq)] +pub struct AttachmentCode { + pub git_url: String, + pub commit: Option, + pub filepath: String, + pub language: String, + pub content: String, + + /// When start line is `None`, it represents the entire file. + pub start_line: Option, +} + +#[derive(GraphQLObject, Clone)] +pub struct AttachmentCodeFileList { + pub file_list: Vec, + pub truncated: bool, +} + +#[derive(GraphQLObject, Clone)] +#[graphql(context = Context)] +pub struct AttachmentDocHit { + pub doc: AttachmentDoc, + pub score: f64, +} + +#[derive(GraphQLUnion, Clone)] +#[graphql(context = Context)] +pub enum AttachmentDoc { + Web(AttachmentWebDoc), + Issue(AttachmentIssueDoc), + Pull(AttachmentPullDoc), + Commit(AttachmentCommitDoc), + Page(AttachmentPageDoc), + Ingested(AttachmentIngestedDoc), +} + +impl PartialEq for AttachmentDoc { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (AttachmentDoc::Web(a), AttachmentDoc::Web(b)) => a.link == b.link, + (AttachmentDoc::Issue(a), AttachmentDoc::Issue(b)) => a.link == b.link, + (AttachmentDoc::Pull(a), AttachmentDoc::Pull(b)) => a.link == b.link, + (AttachmentDoc::Commit(a), AttachmentDoc::Commit(b)) => a.sha == b.sha, + (AttachmentDoc::Page(a), AttachmentDoc::Page(b)) => a.link == b.link, + (AttachmentDoc::Ingested(a), AttachmentDoc::Ingested(b)) => { + a.id == b.id && a.title == b.title + } + _ => false, + } + } +} + +#[derive(GraphQLObject, Clone)] +pub struct AttachmentWebDoc { + pub title: String, + pub link: String, + pub content: String, +} + +#[derive(GraphQLObject, Clone)] +#[graphql(context = Context)] +pub struct AttachmentIssueDoc { + pub title: String, + pub link: String, + pub author: Option, + pub body: String, + pub closed: bool, +} + +#[derive(GraphQLObject, Clone)] +#[graphql(context = Context)] +pub struct AttachmentPullDoc { + pub title: String, + pub link: String, + pub author: Option, + pub body: String, + pub diff: String, + pub merged: bool, +} + +#[derive(GraphQLObject, Clone)] +#[graphql(context = Context)] +pub struct AttachmentCommitDoc { + pub sha: String, + pub message: String, + pub author: Option, + pub author_at: DateTime, +} + +#[derive(GraphQLObject, Clone)] +#[graphql(context = Context)] +pub struct AttachmentPageDoc { + pub link: String, + pub title: String, + pub content: String, +} + +#[derive(GraphQLObject, Clone)] +#[graphql(context = Context)] +pub struct AttachmentIngestedDoc { + pub id: String, + pub title: String, + pub body: String, + pub link: Option, +} + +impl From<&MessageAttachmentCodeFileList> for AttachmentCodeFileList { + fn from(value: &MessageAttachmentCodeFileList) -> Self { + Self { + file_list: value.file_list.clone(), + truncated: value.truncated, + } + } +} + +impl From for MessageAttachmentCodeFileList { + fn from(value: AttachmentCodeFileList) -> Self { + Self { + file_list: value.file_list, + truncated: value.truncated, + } + } +} + +impl From<&MessageAttachmentCode> for AttachmentCode { + fn from(value: &MessageAttachmentCode) -> Self { + Self { + git_url: value.git_url.clone(), + commit: value.commit.clone(), + filepath: value.filepath.clone(), + language: value.language.clone(), + content: value.content.clone(), + start_line: value.start_line, + } + } +} + +impl From<&AttachmentCode> for MessageAttachmentCode { + fn from(value: &AttachmentCode) -> Self { + Self { + git_url: value.git_url.clone(), + commit: value.commit.clone(), + filepath: value.filepath.clone(), + language: value.language.clone(), + content: value.content.clone(), + start_line: value.start_line, + } + } +} + +impl From for AttachmentCodeHit { + fn from(val: CodeSearchHit) -> Self { + Self { + code: val.doc.into(), + scores: val.scores.into(), + } + } +} + +impl From for AttachmentCode { + fn from(val: CodeSearchDocument) -> Self { + Self { + git_url: val.git_url.clone(), + commit: val.commit.clone(), + filepath: val.filepath.clone(), + language: val.language.clone(), + content: val.body.clone(), + start_line: val.start_line.map(|x| x as i32), + } + } +} + +impl From for AttachmentCodeScores { + fn from(val: CodeSearchScores) -> Self { + Self { + rrf: val.rrf as f64, + bm25: val.bm25 as f64, + embedding: val.embedding as f64, + } + } +} + +impl From<&MessageAttachmentDoc> for AttachmentDoc { + fn from(val: &MessageAttachmentDoc) -> Self { + match val { + MessageAttachmentDoc::Web(doc) => AttachmentDoc::Web(doc.into()), + MessageAttachmentDoc::Issue(doc) => AttachmentDoc::Issue(doc.into()), + MessageAttachmentDoc::Pull(doc) => AttachmentDoc::Pull(doc.into()), + MessageAttachmentDoc::Commit(doc) => AttachmentDoc::Commit(doc.into()), + MessageAttachmentDoc::Page(doc) => AttachmentDoc::Page(doc.into()), + MessageAttachmentDoc::Ingested(doc) => AttachmentDoc::Ingested(doc.into()), + } + } +} + +impl From<&MessageAttachmentWebDoc> for AttachmentWebDoc { + fn from(val: &MessageAttachmentWebDoc) -> Self { + Self { + title: val.title.clone(), + link: val.link.clone(), + content: val.content.clone(), + } + } +} + +impl From<&MessageAttachmentIssueDoc> for AttachmentIssueDoc { + fn from(val: &MessageAttachmentIssueDoc) -> Self { + Self { + title: val.title.clone(), + link: val.link.clone(), + author: val.author.clone(), + body: val.body.clone(), + closed: val.closed, + } + } +} + +impl From<&MessageAttachmentPullDoc> for AttachmentPullDoc { + fn from(val: &MessageAttachmentPullDoc) -> Self { + Self { + title: val.title.clone(), + link: val.link.clone(), + author: val.author.clone(), + body: val.body.clone(), + diff: val.patch.clone(), + merged: val.merged, + } + } +} + +impl From<&MessageAttachmentCommitDoc> for AttachmentCommitDoc { + fn from(val: &MessageAttachmentCommitDoc) -> Self { + Self { + sha: val.sha.clone(), + message: val.message.clone(), + author: val.author.clone(), + author_at: val.author_at, + } + } +} + +impl From<&MessageAttachmentPageDoc> for AttachmentPageDoc { + fn from(val: &MessageAttachmentPageDoc) -> Self { + Self { + link: val.link.clone(), + title: val.title.clone(), + content: val.content.clone(), + } + } +} + +impl From<&MessageAttachmentIngestedDoc> for AttachmentIngestedDoc { + fn from(val: &MessageAttachmentIngestedDoc) -> Self { + Self { + id: val.id.clone(), + title: val.title.clone(), + body: val.body.clone(), + link: val.link.clone(), + } + } +} diff --git a/ee/tabby-schema/src/schema/setting.rs b/ee/tabby-schema/src/schema/setting.rs new file mode 100644 index 000000000000..9a58d2c2ab5b --- /dev/null +++ b/ee/tabby-schema/src/schema/setting.rs @@ -0,0 +1,205 @@ +use std::collections::{HashMap, HashSet}; + +use async_trait::async_trait; +use juniper::{GraphQLInputObject, GraphQLObject}; +use validator::{Validate, ValidateEmail, ValidationError}; + +use super::Result; + +#[async_trait] +pub trait SettingService: Send + Sync { + async fn read_security_setting(&self) -> Result; + async fn update_security_setting(&self, input: SecuritySettingInput) -> Result<()>; + + async fn read_network_setting(&self) -> Result; + async fn update_network_setting(&self, input: NetworkSettingInput) -> Result<()>; + + async fn read_branding_setting(&self) -> Result; + async fn update_branding_setting(&self, input: BrandingSettingInput) -> Result<()>; +} + +#[derive(GraphQLObject, Debug, PartialEq)] +pub struct SecuritySetting { + pub allowed_register_domain_list: Vec, + pub disable_client_side_telemetry: bool, + pub disable_password_login: bool, +} + +impl SecuritySetting { + pub fn can_register_without_invitation(&self, email: &str) -> bool { + self.allowed_register_domain_list + .iter() + .any(|domain| email.ends_with(&format!("@{domain}"))) + } +} + +#[derive(GraphQLInputObject, Validate)] +pub struct SecuritySettingInput { + #[validate(custom(function = "validate_unique_domains"))] + pub allowed_register_domain_list: Vec, + pub disable_client_side_telemetry: bool, + pub disable_password_login: bool, +} + +#[derive(GraphQLObject, Debug, PartialEq)] +pub struct NetworkSetting { + pub external_url: String, +} + +#[derive(GraphQLInputObject, Validate)] +pub struct NetworkSettingInput { + #[validate(url(code = "externalUrl", message = "URL is malformed"))] + pub external_url: String, +} + +#[derive(GraphQLObject, Debug, PartialEq)] +pub struct BrandingSetting { + pub branding_logo: Option, + pub branding_icon: Option, +} + +#[derive(GraphQLInputObject, Validate)] +pub struct BrandingSettingInput { + #[validate(custom(function = "validate_logo_image"))] + pub branding_logo: Option, + #[validate(custom(function = "validate_icon_image"))] + pub branding_icon: Option, +} + +fn first_duplicate(strings: &[impl std::hash::Hash + Eq]) -> Option { + let mut set: HashSet<_> = Default::default(); + for (i, string) in strings.iter().enumerate() { + if !set.insert(string) { + return Some(i); + } + } + None +} + +fn validate_unique_domains(domains: &[String]) -> Result<(), ValidationError> { + let duplicate_index = first_duplicate(domains); + if let Some(duplicate_index) = duplicate_index { + let err = ValidationError { + code: format!("allowedRegisterDomainList.{duplicate_index}.value").into(), + message: Some("Duplicate domain".into()), + params: HashMap::default(), + }; + return Err(err); + } + for (i, domain) in domains.iter().enumerate() { + let email = format!("noreply@{domain}"); + if !email.validate_email() { + let err = ValidationError { + code: format!("allowedRegisterDomainList.{i}.value").into(), + message: Some("Invalid domain".into()), + params: HashMap::default(), + }; + return Err(err); + } + } + Ok(()) +} + +fn validate_logo_image(logo_image: &String) -> Result<(), ValidationError> { + validate_image_impl(logo_image, "brandingLogo") +} + +fn validate_icon_image(icon_image: &String) -> Result<(), ValidationError> { + validate_image_impl(icon_image, "brandingIcon") +} + +fn validate_image_impl(image: &String, code: &'static str) -> Result<(), ValidationError> { + const MAX_IMAGE_SIZE_IN_BYTES: usize = 500 * 1024; + // Base64 is about 33% larger than original. + const MAX_BASE64_IMAGE_SIZE: usize = MAX_IMAGE_SIZE_IN_BYTES * 4 / 3 + 4; + + if image.is_empty() { + return Ok(()); + } + + if image.len() > MAX_BASE64_IMAGE_SIZE { + let mut err = ValidationError::new(code); + err.message = Some("Max file size 500KB.".into()); + return Err(err); + } + + let Some(mime_type) = image.split([',', ';']).next() else { + let mut err = ValidationError::new(code); + err.message = Some("Invalid image format".into()); + return Err(err); + }; + + if !matches!( + mime_type, + "data:image/png" | "data:image/jpeg" | "data:image/webp" | "data:image/svg+xml" + ) { + let mut err = ValidationError::new(code); + err.message = Some("Accepted file types: .png, .jpg, .webp, .svg.".into()); + return Err(err); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::SecuritySetting; + use crate::schema::setting::{first_duplicate, validate_unique_domains}; + + #[test] + fn test_validate_urls() { + assert!(validate_unique_domains(&["example.com".to_owned()]).is_ok()); + + assert!(validate_unique_domains(&["https://example.com".to_owned()]).is_err()); + + assert!(validate_unique_domains(&["domain.withmultipleparts.com".to_owned()]).is_ok()); + + assert!( + validate_unique_domains(&["example.com".to_owned(), "example.com".to_owned()]) + .unwrap_err() + .code + .contains(".1.") + ); + } + + #[test] + fn test_duplicate_index() { + assert_eq!(first_duplicate(&["a", "b", "c"]), None); + assert_eq!(first_duplicate(&["a", "b", "b"]), Some(2)); + assert_eq!(first_duplicate(&["a", "b", "c", "c", "c"]), Some(3)); + } + + #[test] + fn test_can_register_without_invitation() { + let setting = SecuritySetting { + allowed_register_domain_list: vec![], + disable_client_side_telemetry: false, + disable_password_login: false, + }; + + assert!(!setting.can_register_without_invitation("abc@abc.com")); + + let setting = SecuritySetting { + allowed_register_domain_list: vec!["".into()], + disable_client_side_telemetry: false, + disable_password_login: false, + }; + + assert!(!setting.can_register_without_invitation("abc@abc.com")); + + let setting = SecuritySetting { + allowed_register_domain_list: vec![".com".into()], + disable_client_side_telemetry: false, + disable_password_login: false, + }; + + assert!(!setting.can_register_without_invitation("abc@abc.com")); + + let setting = SecuritySetting { + allowed_register_domain_list: vec!["abc.com".into()], + disable_client_side_telemetry: false, + disable_password_login: false, + }; + + assert!(setting.can_register_without_invitation("abc@abc.com")); + } +} diff --git a/ee/tabby-schema/src/schema/thread.rs b/ee/tabby-schema/src/schema/thread.rs new file mode 100644 index 000000000000..60834a95364f --- /dev/null +++ b/ee/tabby-schema/src/schema/thread.rs @@ -0,0 +1,87 @@ +use async_trait::async_trait; +use futures::stream::BoxStream; +use juniper::ID; + +use crate::{auth::UserSecured, schema::Result}; + +mod types; +pub use types::*; + +mod inputs; +pub use inputs::*; + +pub type ThreadRunStream = BoxStream<'static, Result>; + +#[async_trait] +pub trait ThreadService: Send + Sync { + /// Create a new thread + async fn create(&self, user_id: &ID, input: &CreateThreadInput) -> Result; + + /// Get a thread by ID + async fn get(&self, id: &ID) -> Result>; + + /// Delete a thread. + async fn delete(&self, id: &ID) -> Result<()>; + + /// Converting a ephemeral thread to a persisted thread + async fn set_persisted(&self, id: &ID) -> Result<()>; + + /// List threads + async fn list( + &self, + ids: Option<&[ID]>, + is_ephemeral: Option, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> Result>; + + /// List threads owned by a user + async fn list_owned( + &self, + user_id: &ID, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> Result>; + + /// Create a new thread run + async fn create_run( + &self, + user: &UserSecured, + id: &ID, + options: &ThreadRunOptionsInput, + attachment_input: Option<&MessageAttachmentInput>, + yield_last_user_message: bool, + yield_thread_created: bool, + ) -> Result; + + /// Append message to an existing thread + async fn append_user_message(&self, id: &ID, message: &CreateMessageInput) -> Result<()>; + + /// Update a message + async fn update_thread_message(&self, message: &UpdateMessageInput) -> Result<()>; + + // /// Delete a thread by ID + // async fn delete(&self, id: ID) -> Result<()>; + + /// Query messages in a thread + async fn list_thread_messages( + &self, + thread_id: &ID, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> Result>; + + /// Delete pair of user message and bot response in a thread. + async fn delete_thread_message_pair( + &self, + thread_id: &ID, + user_message_id: &ID, + assistant_message_id: &ID, + ) -> Result<()>; +} diff --git a/ee/tabby-schema/src/schema/thread/inputs.rs b/ee/tabby-schema/src/schema/thread/inputs.rs new file mode 100644 index 000000000000..02886fddd951 --- /dev/null +++ b/ee/tabby-schema/src/schema/thread/inputs.rs @@ -0,0 +1,173 @@ +use juniper::{GraphQLInputObject, ID}; +use tabby_common::api::code::CodeSearchParams; +use validator::{Validate, ValidationError}; + +#[derive(GraphQLInputObject, Validate)] +pub struct CreateMessageInput { + #[validate(length( + min = 1, + code = "content", + message = "Message content should not be empty" + ))] + pub content: String, + + pub attachments: Option, +} + +#[derive(GraphQLInputObject, Validate)] +pub struct CreateThreadInput { + #[validate(nested)] + pub user_message: CreateMessageInput, +} + +#[derive(GraphQLInputObject, Validate)] +pub struct CreateThreadAndRunInput { + #[validate(nested)] + pub thread: CreateThreadInput, + + #[validate(nested)] + #[graphql(default)] + pub options: ThreadRunOptionsInput, +} + +#[derive(GraphQLInputObject, Validate, Clone)] +pub struct DocQueryInput { + pub content: String, + + /// Whether to collect documents from public web. + pub search_public: bool, + + /// source_ids to be included in the doc search. + pub source_ids: Option>, +} + +#[derive(GraphQLInputObject, Validate, Clone, Debug, Default)] +#[validate(schema(function = "validate_code_query_input", skip_on_field_errors = false))] +pub struct CodeQueryInput { + pub filepath: Option, + pub language: Option, + pub content: String, + + /// git_url to be included in the code search. + pub git_url: Option, + + /// source_ids to be included in the code search. + pub source_id: Option, +} + +fn validate_code_query_input(input: &CodeQueryInput) -> Result<(), ValidationError> { + if input.git_url.is_none() && input.source_id.is_none() { + return Err(ValidationError::new("gitUrl") + .with_message("Either gitUrl or sourceId must be provided".into())); + } + + if input.git_url.is_some() && input.source_id.is_some() { + return Err(ValidationError::new("gitUrl") + .with_message("Only one of gitUrl or sourceId can be provided".into())); + } + + Ok(()) +} + +#[derive(GraphQLInputObject, Validate, Default, Clone)] +pub struct ThreadRunOptionsInput { + #[graphql(default)] + pub model_name: Option, + + #[validate(nested)] + #[graphql(default)] + pub doc_query: Option, + + #[validate(nested)] + #[graphql(default)] + pub code_query: Option, + + #[graphql(default)] + pub generate_relevant_questions: bool, + + #[graphql(default)] + pub debug_options: Option, +} + +#[derive(GraphQLInputObject, Clone)] +pub struct CodeSearchParamsOverrideInput { + pub min_embedding_score: Option, + pub min_bm25_score: Option, + pub min_rrf_score: Option, + pub num_to_return: Option, + pub num_to_score: Option, +} + +#[derive(GraphQLInputObject, Clone)] +pub struct ThreadRunDebugOptionsInput { + #[graphql(default)] + pub code_search_params_override: Option, + + #[graphql(default)] + pub return_chat_completion_request: bool, +} + +impl CodeSearchParamsOverrideInput { + pub fn override_params(&self, params: &mut CodeSearchParams) { + if let Some(min_embedding_score) = self.min_embedding_score { + params.min_embedding_score = min_embedding_score as f32; + } + if let Some(min_bm25_score) = self.min_bm25_score { + params.min_bm25_score = min_bm25_score as f32; + } + if let Some(min_rrf_score) = self.min_rrf_score { + params.min_rrf_score = min_rrf_score as f32; + } + if let Some(num_to_return) = self.num_to_return { + params.num_to_return = num_to_return as usize; + } + if let Some(num_to_score) = self.num_to_score { + params.num_to_score = num_to_score as usize; + } + } +} + +#[derive(GraphQLInputObject, Validate)] +pub struct CreateThreadRunInput { + pub thread_id: ID, + + #[validate(nested)] + pub additional_user_message: CreateMessageInput, + + #[graphql(default)] + pub options: ThreadRunOptionsInput, +} + +#[derive(GraphQLInputObject, Clone)] +pub struct MessageAttachmentInput { + pub code: Vec, +} + +#[derive(GraphQLInputObject, Clone)] +pub struct MessageAttachmentCodeInput { + pub filepath: Option, + + /// When start line is `None`, it represents the entire file. + pub start_line: Option, + + pub content: String, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_create_thread_run_input_shouldnot_allow_empty_content() { + let input = CreateThreadRunInput { + thread_id: ID::from("1".to_owned()), + additional_user_message: CreateMessageInput { + content: "".into(), + attachments: None, + }, + options: Default::default(), + }; + + assert!(input.validate().is_err()) + } +} diff --git a/ee/tabby-schema/src/schema/thread/types.rs b/ee/tabby-schema/src/schema/thread/types.rs new file mode 100644 index 000000000000..d5d9c6a7be56 --- /dev/null +++ b/ee/tabby-schema/src/schema/thread/types.rs @@ -0,0 +1,399 @@ +use chrono::{DateTime, Utc}; +use juniper::{GraphQLEnum, GraphQLInputObject, GraphQLObject, GraphQLUnion, ID}; +use serde::Serialize; +use tabby_common::api::{ + code::{CodeSearchDocument, CodeSearchHit, CodeSearchScores}, + structured_doc::DocSearchDocument, +}; +use validator::Validate; + +use super::MessageAttachmentCodeInput; +use crate::{interface::UserValue, juniper::relay::NodeType, ChatCompletionMessage, Context}; + +#[derive(GraphQLEnum, Serialize, Clone, PartialEq, Eq)] +pub enum Role { + User, + Assistant, +} + +#[derive(GraphQLObject, Clone)] +#[graphql(context = Context)] +pub struct Message { + pub id: ID, + pub thread_id: ID, + pub code_source_id: Option, + pub role: Role, + pub content: String, + + pub attachment: MessageAttachment, + + pub created_at: DateTime, + pub updated_at: DateTime, +} + +impl NodeType for Message { + type Cursor = String; + + fn cursor(&self) -> Self::Cursor { + self.id.to_string() + } + + fn connection_type_name() -> &'static str { + "MessageConnection" + } + + fn edge_type_name() -> &'static str { + "MessageEdge" + } +} + +#[derive(GraphQLInputObject, Clone, Validate)] +#[graphql(context = Context)] +pub struct UpdateMessageInput { + pub id: ID, + pub thread_id: ID, + #[validate(length(min = 1, code = "content", message = "content can not be empty"))] + pub content: String, +} + +#[derive(GraphQLObject, Clone, Default)] +#[graphql(context = Context)] +/// Represents an attachment to a message, which can include various types of content. +pub struct MessageAttachment { + /// Code snippets retrieved from the client side. + pub client_code: Vec, + + /// Code snippets retrieved from the server side codebase. + pub code: Vec, + + /// Documents retrieved from various sources, all from the server side. + pub doc: Vec, + + /// File list retrieved from the server side codebase is used for generating this message. + pub code_file_list: Option, +} + +#[derive(GraphQLObject, Clone)] +pub struct MessageAttachmentCodeFileList { + pub file_list: Vec, + pub truncated: bool, +} + +#[derive(GraphQLObject, Clone)] +pub struct MessageAttachmentClientCode { + pub filepath: Option, + pub start_line: Option, + pub content: String, +} + +impl From for MessageAttachmentCodeInput { + fn from(val: MessageAttachmentClientCode) -> Self { + MessageAttachmentCodeInput { + filepath: val.filepath, + start_line: val.start_line, + content: val.content, + } + } +} + +#[derive(GraphQLObject, Clone)] +pub struct MessageAttachmentCode { + pub git_url: String, + pub commit: Option, + pub filepath: String, + pub language: String, + pub content: String, + + /// When start line is `None`, it represents the entire file. + pub start_line: Option, +} + +impl From for MessageAttachmentCode { + fn from(doc: CodeSearchDocument) -> Self { + Self { + git_url: doc.git_url, + commit: doc.commit, + filepath: doc.filepath, + language: doc.language, + content: doc.body, + start_line: doc.start_line.map(|x| x as i32), + } + } +} + +#[derive(GraphQLObject, Clone)] +pub struct MessageAttachmentCodeScores { + pub rrf: f64, + pub bm25: f64, + pub embedding: f64, +} + +impl From for MessageAttachmentCodeScores { + fn from(scores: CodeSearchScores) -> Self { + Self { + rrf: scores.rrf as f64, + bm25: scores.bm25 as f64, + embedding: scores.embedding as f64, + } + } +} + +#[derive(GraphQLObject)] +pub struct MessageCodeSearchHit { + pub code: MessageAttachmentCode, + pub scores: MessageAttachmentCodeScores, +} + +impl From for MessageCodeSearchHit { + fn from(hit: CodeSearchHit) -> Self { + Self { + code: hit.doc.into(), + scores: hit.scores.into(), + } + } +} + +#[derive(GraphQLUnion, Clone)] +#[graphql(context = Context)] +pub enum MessageAttachmentDoc { + Web(MessageAttachmentWebDoc), + Issue(MessageAttachmentIssueDoc), + Pull(MessageAttachmentPullDoc), + Commit(MessageAttachmentCommitDoc), + Page(MessageAttachmentPageDoc), + Ingested(MessageAttachmentIngestedDoc), +} + +#[derive(GraphQLObject, Clone)] +pub struct MessageAttachmentWebDoc { + pub title: String, + pub link: String, + pub content: String, +} + +#[derive(GraphQLObject, Clone)] +#[graphql(context = Context)] +pub struct MessageAttachmentIssueDoc { + pub title: String, + pub link: String, + pub author: Option, + pub body: String, + pub closed: bool, +} + +#[derive(GraphQLObject, Clone)] +#[graphql(context = Context)] +pub struct MessageAttachmentPullDoc { + pub title: String, + pub link: String, + pub author: Option, + pub body: String, + pub patch: String, + pub merged: bool, +} + +#[derive(GraphQLObject, Clone)] +#[graphql(context = Context)] +pub struct MessageAttachmentCommitDoc { + pub sha: String, + pub message: String, + pub author: Option, + pub author_at: DateTime, +} + +#[derive(GraphQLObject, Clone)] +#[graphql(context = Context)] +pub struct MessageAttachmentPageDoc { + pub link: String, + pub title: String, + pub content: String, +} + +#[derive(GraphQLObject, Clone)] +#[graphql(context = Context)] +pub struct MessageAttachmentIngestedDoc { + pub id: String, + pub title: String, + pub body: String, + pub link: Option, +} + +impl MessageAttachmentDoc { + pub fn from_doc_search_document(doc: DocSearchDocument, author: Option) -> Self { + match doc { + DocSearchDocument::Web(web) => MessageAttachmentDoc::Web(MessageAttachmentWebDoc { + title: web.title, + link: web.link, + content: web.snippet, + }), + DocSearchDocument::Issue(issue) => { + MessageAttachmentDoc::Issue(MessageAttachmentIssueDoc { + title: issue.title, + link: issue.link, + author, + body: issue.body, + closed: issue.closed, + }) + } + DocSearchDocument::Pull(pull) => MessageAttachmentDoc::Pull(MessageAttachmentPullDoc { + title: pull.title, + link: pull.link, + author, + body: pull.body, + patch: pull.diff, + merged: pull.merged, + }), + DocSearchDocument::Commit(commit) => { + MessageAttachmentDoc::Commit(MessageAttachmentCommitDoc { + sha: commit.sha, + message: commit.message, + author, + author_at: commit.author_at, + }) + } + DocSearchDocument::Page(page) => MessageAttachmentDoc::Page(MessageAttachmentPageDoc { + link: page.link, + title: page.title, + content: page.content, + }), + DocSearchDocument::Ingested(ingested) => { + MessageAttachmentDoc::Ingested(MessageAttachmentIngestedDoc { + id: ingested.id, + title: ingested.title, + body: ingested.body, + link: ingested.link, + }) + } + } + } + + pub fn content(&self) -> String { + match self { + MessageAttachmentDoc::Web(web) => web.content.to_string(), + MessageAttachmentDoc::Issue(issue) => issue.body.to_string(), + MessageAttachmentDoc::Pull(pull) => pull.body.to_string(), + MessageAttachmentDoc::Commit(commit) => commit.message.to_string(), + MessageAttachmentDoc::Page(page) => page.content.to_string(), + MessageAttachmentDoc::Ingested(ingested) => ingested.body.clone(), + } + } +} + +#[derive(GraphQLObject)] +#[graphql(context = Context)] +pub struct MessageDocSearchHit { + pub doc: MessageAttachmentDoc, + pub score: f64, +} + +#[derive(GraphQLObject)] +#[graphql(context = Context)] +pub struct Thread { + pub id: ID, + pub user_id: ID, + + pub is_ephemeral: bool, + + pub created_at: DateTime, + pub updated_at: DateTime, +} + +impl NodeType for Thread { + type Cursor = String; + + fn cursor(&self) -> Self::Cursor { + self.id.to_string() + } + + fn connection_type_name() -> &'static str { + "ThreadConnection" + } + + fn edge_type_name() -> &'static str { + "ThreadEdge" + } +} + +#[derive(GraphQLObject)] +pub struct ThreadCreated { + pub id: ID, +} + +#[derive(GraphQLObject)] +pub struct ThreadRelevantQuestions { + pub questions: Vec, +} + +#[derive(GraphQLObject)] +pub struct ThreadUserMessageCreated { + pub id: ID, +} + +#[derive(GraphQLObject)] +pub struct ThreadAssistantMessageCreated { + pub id: ID, +} + +#[derive(GraphQLObject, Clone, Debug)] +pub struct ThreadAssistantMessageReadingCode { + pub snippet: bool, + pub file_list: bool, +} + +#[derive(GraphQLObject)] +pub struct ThreadAssistantMessageAttachmentsCodeFileList { + pub file_list: Vec, + pub truncated: bool, +} + +#[derive(GraphQLObject)] +pub struct ThreadAssistantMessageAttachmentsCode { + pub hits: Vec, +} + +#[derive(GraphQLObject, Clone, Debug)] +pub struct ThreadAssistantMessageReadingDoc { + pub source_ids: Vec, +} + +#[derive(GraphQLObject)] +#[graphql(context = Context)] +pub struct ThreadAssistantMessageAttachmentsDoc { + pub hits: Vec, +} + +#[derive(GraphQLObject)] +pub struct ThreadAssistantMessageContentDelta { + pub delta: String, +} + +#[derive(GraphQLObject)] +pub struct ThreadAssistantMessageCompleted { + /// Debug data for the assistant message completion. + pub debug_data: Option, +} + +#[derive(GraphQLObject, Clone, Debug)] +pub struct ThreadAssistantMessageCompletedDebugData { + // Messages sent to LLM to generate the response. + pub chat_completion_messages: Vec, +} + +/// Schema of thread run stream. +/// +/// Apart from `thread_message_content_delta`, all other items will only appear once in the stream. +#[derive(GraphQLUnion)] +#[graphql(context = Context)] +pub enum ThreadRunItem { + ThreadCreated(ThreadCreated), + ThreadRelevantQuestions(ThreadRelevantQuestions), + ThreadUserMessageCreated(ThreadUserMessageCreated), + ThreadAssistantMessageCreated(ThreadAssistantMessageCreated), + ThreadAssistantMessageReadingCode(ThreadAssistantMessageReadingCode), + ThreadAssistantMessageAttachmentsCodeFileList(ThreadAssistantMessageAttachmentsCodeFileList), + ThreadAssistantMessageAttachmentsCode(ThreadAssistantMessageAttachmentsCode), + ThreadAssistantMessageReadingDoc(ThreadAssistantMessageReadingDoc), + ThreadAssistantMessageAttachmentsDoc(ThreadAssistantMessageAttachmentsDoc), + ThreadAssistantMessageContentDelta(ThreadAssistantMessageContentDelta), + ThreadAssistantMessageCompleted(ThreadAssistantMessageCompleted), +} diff --git a/ee/tabby-schema/src/schema/user_event.rs b/ee/tabby-schema/src/schema/user_event.rs new file mode 100644 index 000000000000..2b9d2c20ab2e --- /dev/null +++ b/ee/tabby-schema/src/schema/user_event.rs @@ -0,0 +1,55 @@ +use async_trait::async_trait; +use chrono::{DateTime, Utc}; +use juniper::{GraphQLEnum, GraphQLObject, ID}; + +use super::Context; +use crate::{juniper::relay::NodeType, schema::Result}; + +#[derive(GraphQLEnum, Debug)] +pub enum EventKind { + Completion, + ChatCompletion, + Select, + View, + Dismiss, +} + +#[derive(GraphQLObject)] +#[graphql(context = Context)] +pub struct UserEvent { + pub id: ID, + pub user_id: ID, + pub kind: EventKind, + pub created_at: DateTime, + pub payload: String, +} + +impl NodeType for UserEvent { + type Cursor = String; + + fn cursor(&self) -> Self::Cursor { + self.id.to_string() + } + + fn connection_type_name() -> &'static str { + "UserEventConnection" + } + + fn edge_type_name() -> &'static str { + "UserEventEdge" + } +} + +#[async_trait] +pub trait UserEventService: Send + Sync { + async fn list( + &self, + after: Option, + before: Option, + first: Option, + last: Option, + users: Vec, + start: DateTime, + end: DateTime, + ) -> Result>; +} diff --git a/ee/tabby-schema/src/schema/user_group.rs b/ee/tabby-schema/src/schema/user_group.rs new file mode 100644 index 000000000000..e12751f85ba3 --- /dev/null +++ b/ee/tabby-schema/src/schema/user_group.rs @@ -0,0 +1,64 @@ +use async_trait::async_trait; +use chrono::{DateTime, Utc}; +use juniper::{GraphQLInputObject, GraphQLObject, ID}; +use validator::Validate; + +use super::{interface::UserValue, Context}; +use crate::Result; + +#[derive(GraphQLObject)] +#[graphql(context = Context)] +pub struct UserGroup { + pub id: ID, + pub name: String, + pub created_at: DateTime, + pub updated_at: DateTime, + pub members: Vec, +} + +#[derive(GraphQLObject)] +#[graphql(context = Context)] +pub struct UserGroupMembership { + pub user: UserValue, + + pub is_group_admin: bool, + + pub created_at: DateTime, + pub updated_at: DateTime, +} + +#[derive(GraphQLInputObject, Validate)] +pub struct CreateUserGroupInput { + /// User group name, can only start with a lowercase letter, and contain characters, numbers, and `-` or `_` + #[validate(length( + min = 2, + max = 20, + code = "name", + message = "Name must be between 2 and 20 characters" + ))] + #[validate(regex( + code = "name", + path = "*crate::schema::constants::USER_GROUP_NAME_REGEX", + message = "Invalid name, name may contain characters which are not supported" + ))] + pub name: String, +} + +#[derive(GraphQLInputObject, Validate)] +pub struct UpsertUserGroupMembershipInput { + pub user_group_id: ID, + pub user_id: ID, + pub is_group_admin: bool, +} + +#[async_trait] +pub trait UserGroupService: Send + Sync { + // List user groups. + async fn list(&self) -> Result>; + + async fn create(&self, input: &CreateUserGroupInput) -> Result; + async fn delete(&self, user_group_id: &ID) -> Result<()>; + + async fn upsert_membership(&self, input: &UpsertUserGroupMembershipInput) -> Result<()>; + async fn delete_membership(&self, user_group_id: &ID, user_id: &ID) -> Result<()>; +} diff --git a/ee/tabby-schema/src/schema/web_documents.rs b/ee/tabby-schema/src/schema/web_documents.rs new file mode 100644 index 000000000000..b5015cb66390 --- /dev/null +++ b/ee/tabby-schema/src/schema/web_documents.rs @@ -0,0 +1,188 @@ +use async_trait::async_trait; +use chrono::{DateTime, Utc}; +use juniper::{graphql_object, GraphQLInputObject, ID}; +use validator::Validate; + +use super::context::{ContextSourceIdValue, ContextSourceKind, ContextSourceValue}; +use crate::{job::JobInfo, juniper::relay::NodeType, Context, Result}; + +pub struct CustomWebDocument { + pub url: String, + pub name: String, + pub id: ID, + pub created_at: DateTime, + pub updated_at: DateTime, + pub job_info: JobInfo, +} + +impl CustomWebDocument { + pub fn format_source_id(id: &ID) -> String { + format!("custom_web_document:{id}") + } +} + +#[graphql_object(context = Context, impl = [ContextSourceIdValue, ContextSourceValue])] +impl CustomWebDocument { + fn url(&self) -> &str { + &self.url + } + + fn name(&self) -> &str { + &self.name + } + + fn id(&self) -> &ID { + &self.id + } + + fn created_at(&self) -> &DateTime { + &self.created_at + } + + fn updated_at(&self) -> &DateTime { + &self.updated_at + } + + fn job_info(&self) -> &JobInfo { + &self.job_info + } + + fn source_kind(&self) -> ContextSourceKind { + ContextSourceKind::Doc + } + + pub fn source_id(&self) -> String { + Self::format_source_id(&self.id) + } + + pub fn source_name(&self) -> &str { + &self.name + } +} + +pub struct PresetWebDocument { + pub id: ID, + + pub name: String, + pub updated_at: Option>, + pub job_info: Option, + pub is_active: bool, +} + +impl PresetWebDocument { + pub fn format_source_id(name: &String) -> String { + format!("preset_web_document:{name}") + } +} + +#[graphql_object(context = Context, impl = [ContextSourceIdValue, ContextSourceValue])] +impl PresetWebDocument { + fn name(&self) -> &str { + &self.name + } + + fn id(&self) -> &ID { + &self.id + } + + /// `updated_at` is only filled when the preset is active. + fn updated_at(&self) -> &Option> { + &self.updated_at + } + + /// `job_info` is only filled when the preset is active. + fn job_info(&self) -> &Option { + &self.job_info + } + + fn is_active(&self) -> bool { + self.is_active + } + + fn source_kind(&self) -> ContextSourceKind { + ContextSourceKind::Doc + } + + pub fn source_id(&self) -> String { + Self::format_source_id(&self.name) + } + + pub fn source_name(&self) -> &str { + &self.name + } +} + +#[derive(Validate, GraphQLInputObject)] +pub struct CreateCustomDocumentInput { + #[validate(regex( + code = "name", + path = "*crate::schema::constants::WEB_DOCUMENT_NAME_REGEX", + message = "Invalid document name" + ))] + pub name: String, + #[validate(url(code = "url", message = "Invalid URL"))] + pub url: String, +} + +#[derive(Validate, GraphQLInputObject)] +pub struct SetPresetDocumentActiveInput { + pub id: ID, + pub active: bool, +} + +impl NodeType for CustomWebDocument { + type Cursor = String; + + fn cursor(&self) -> Self::Cursor { + self.id.to_string() + } + + fn connection_type_name() -> &'static str { + "CustomDocumentConnection" + } + + fn edge_type_name() -> &'static str { + "CustomDocumentEdge" + } +} + +impl NodeType for PresetWebDocument { + type Cursor = String; + + fn cursor(&self) -> Self::Cursor { + self.id.to_string() + } + + fn connection_type_name() -> &'static str { + "PresetDocumentConnection" + } + + fn edge_type_name() -> &'static str { + "PresetDocumentEdge" + } +} + +#[async_trait] +pub trait WebDocumentService: Send + Sync { + async fn list_custom_web_documents( + &self, + ids: Option>, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> Result>; + + async fn create_custom_web_document(&self, name: String, url: String) -> Result; + async fn delete_custom_web_document(&self, id: ID) -> Result<()>; + async fn list_preset_web_documents( + &self, + ids: Option>, + after: Option, + before: Option, + first: Option, + last: Option, + is_active: Option, + ) -> Result>; + async fn set_preset_web_documents_active(&self, id: ID, active: bool) -> Result<()>; +} diff --git a/ee/tabby-schema/src/schema/worker.rs b/ee/tabby-schema/src/schema/worker.rs new file mode 100644 index 000000000000..f40b52d53df2 --- /dev/null +++ b/ee/tabby-schema/src/schema/worker.rs @@ -0,0 +1,17 @@ +use async_trait::async_trait; +use axum::{body::Body, extract::Request, middleware::Next}; + +use crate::schema::Result; + +#[async_trait] +pub trait WorkerService: Send + Sync { + async fn read_registration_token(&self) -> Result; + async fn reset_registration_token(&self) -> Result; + + async fn dispatch_request( + &self, + request: Request, + next: Next, + ) -> axum::response::Response; + async fn is_chat_enabled(&self) -> Result; +} diff --git a/ee/tabby-ui/.env.development.example b/ee/tabby-ui/.env.development.example deleted file mode 100644 index e0d09d0a5333..000000000000 --- a/ee/tabby-ui/.env.development.example +++ /dev/null @@ -1 +0,0 @@ -NEXT_PUBLIC_TABBY_SERVER_URL=http://127.0.0.1:8080 \ No newline at end of file diff --git a/ee/tabby-ui/.eslintrc.json b/ee/tabby-ui/.eslintrc.json index c96e55738033..040f7ca3b26e 100644 --- a/ee/tabby-ui/.eslintrc.json +++ b/ee/tabby-ui/.eslintrc.json @@ -6,21 +6,48 @@ "prettier", "plugin:tailwindcss/recommended" ], - "plugins": ["tailwindcss", "unused-imports"], + "plugins": [ + "tailwindcss", + "@typescript-eslint", + "unused-imports" + ], "rules": { + "curly": ["error", "multi-line"], "unused-imports/no-unused-imports": "error", - "tailwindcss/no-custom-classname": "off" + "tailwindcss/no-custom-classname": "off", + "import/no-restricted-paths": [ + "error", + { + "zones": [ + { + "target": "./lib", + "from": "./app" + }, + { + "target": "./components", + "from": "./app" + } + ] + } + ], + "no-console": "error" }, "settings": { "tailwindcss": { - "callees": ["cn", "cva"], + "callees": [ + "cn", + "cva" + ], "config": "tailwind.config.js" } }, "overrides": [ { - "files": ["*.ts", "*.tsx"], + "files": [ + "*.ts", + "*.tsx" + ], "parser": "@typescript-eslint/parser" } ] -} +} \ No newline at end of file diff --git a/ee/tabby-ui/README.md b/ee/tabby-ui/README.md index cfed70b1812f..3a4f48aca606 100644 --- a/ee/tabby-ui/README.md +++ b/ee/tabby-ui/README.md @@ -1 +1,38 @@ # Tabby UI + +## 🤝 Contributing + +### Local Setup +Full guide at [CONTRIBUTING.md](https://github.com/TabbyML/tabby/blob/main/CONTRIBUTING.md#local-setup) + +### Running +During local development, we use Caddy to orchestrate Tabby-UI and local Tabby. We run both the Tabby-UI server and the local Tabby server simultaneously, using Caddy to forward frontend and backend requests to their respective servers, reducing the need for host and port configuration and taking advantage of the hot-reload feature of tabby-ui. +The Caddy configuration file is located [here](https://github.com/TabbyML/tabby/blob/main/ee/tabby-webserver/development/Caddyfile). + +Regarding the Tabby binary in production distribution, we do not start the tabby-ui server and Caddy server. Instead, tabby-ui is solely built and outputs static assets. Routing is configured within Tabby to distribute the static assets produced by tabby-ui. + +#### 1. Start the development frontend server + +``` +cd tabby/ee/tabby-ui +pnpm dev +``` + +#### 2. Start the development backend server + +``` +cargo run serve --port 8081 +``` + +#### 3.Start the caddy server + +``` +make caddy +``` + +#### 4. Start hacking +Now, you can open `http://localhost:8080` to see the tabby webserver! + +--- + +You might also run `make dev` directly to execute the commands above simultaneously. (requires `tmux` and `tmuxinator`). diff --git a/ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/job-detail.tsx b/ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/job-detail.tsx new file mode 100644 index 000000000000..9be48809c8dd --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/job-detail.tsx @@ -0,0 +1,293 @@ +'use client' + +import React, { useEffect, useMemo, useRef, useState } from 'react' +import { useRouter, useSearchParams } from 'next/navigation' +import Ansi from '@curvenote/ansi-to-react' +import humanizerDuration from 'humanize-duration' +import { concat, sortBy, unionWith } from 'lodash-es' +import moment from 'moment' +import useSWR from 'swr' +import { useQuery } from 'urql' + +import fetcher from '@/lib/tabby/fetcher' +import { listJobRuns } from '@/lib/tabby/query' +import { cn } from '@/lib/utils' +import { + IconChevronLeft, + IconClock, + IconHistory, + IconSpinner, + IconStopWatch +} from '@/components/ui/icons' +import { LoadMoreIndicator } from '@/components/load-more-indicator' +import { ListSkeleton } from '@/components/skeleton' + +import { getJobDisplayName, getLabelByJobRun } from '../utils' + +interface LogChunk { + startByte: number + endByte: number + logs: string +} + +const CHUNK_SIZE = 20 * 1000 + +export default function JobRunDetail() { + const router = useRouter() + const searchParams = useSearchParams() + const id = searchParams.get('id') + const [chunks, setChunks] = useState([]) + const cursor = useRef(0) + const totalBytes = useRef(0) + const [startBytes, setStartBytes] = useState(-1) + const fetchLogEndPoint = id ? `/background-jobs/${id}/logs` : null + const [{ data, fetching: fetchingJobNode }, reexecuteQuery] = useQuery({ + query: listJobRuns, + variables: { ids: [id as string] }, + pause: !id + }) + const currentNode = data?.jobRuns?.edges?.[0]?.node + const shouldFetchLogs = !!id && !fetchingJobNode && !currentNode?.stdout + const { + data: logsData, + mutate, + isLoading: isLoadingLogs, + isValidating: isValidatingLogs + } = useSWR( + fetchLogEndPoint ? [fetchLogEndPoint, startBytes + 1] : null, + ([url, start]) => { + return fetcher(url, { + headers: { + Range: `bytes=${start}-${start + CHUNK_SIZE}` + }, + responseFormatter: res => { + const contentRange = res.headers.get('Content-Range') + if (!contentRange) return null + if (res.status === 206) { + const [range, total] = contentRange + .replace(/^bytes\s/, '') + .split('/') + const [start, end] = range?.split('-') + cursor.current = parseInt(end) + return res.text().then(text => ({ + logs: text, + totalBytes: parseInt(total), + startByte: parseInt(start), + endByte: parseInt(end) + })) + } + }, + errorHandler: response => { + throw new Error(response?.statusText.toString()) + } + }) + }, + { + errorRetryCount: 1 + } + ) + + const isFetchingLogs = isLoadingLogs || isValidatingLogs + + // join logs + useEffect(() => { + if (logsData) { + const newChunk: LogChunk = { + startByte: logsData.startByte, + endByte: logsData.endByte, + logs: logsData.logs + } + setChunks(prev => { + return sortBy( + unionWith(concat([newChunk], prev), (x, y) => { + return x.startByte === y.startByte + }), + 'startByte' + ) + }) + totalBytes.current = logsData?.totalBytes ?? 0 + // setTotalBytes(logsData?.totalBytes ?? 0) + } + }, [logsData]) + + const finalLogs = useMemo(() => { + if (currentNode?.stdout) return currentNode?.stdout + const logs = chunks.reduce((sum, cur) => sum + cur.logs, '') + return processPartialLine(logs) + }, [currentNode?.stdout, chunks]) + + const stateLabel = getLabelByJobRun(currentNode) + const isPending = + (stateLabel === 'Pending' || stateLabel === 'Running') && !finalLogs + + const isLoadedCompleted = + !!totalBytes.current && + chunks[chunks.length - 1]?.endByte >= totalBytes.current - 1 + + const handleBackNavigation = () => { + if (typeof window !== 'undefined' && window.history.length <= 1) { + router.push('/jobs') + } else { + router.back() + } + } + + const handleLoadMore = () => { + if (isFetchingLogs) return + if (cursor.current && cursor.current + 1 < totalBytes.current) { + const nextCursor = cursor.current + setStartBytes(nextCursor) + } + } + + React.useEffect(() => { + let timer: number + if (currentNode?.createdAt && !currentNode?.finishedAt) { + timer = window.setTimeout(() => { + reexecuteQuery() + if (!isFetchingLogs && (isLoadedCompleted || !chunks.length)) { + mutate() + } + }, 5000) + } + + return () => { + if (timer) { + clearInterval(timer) + } + } + }, [currentNode, isLoadedCompleted, chunks?.length]) + + return ( + <> + {fetchingJobNode ? ( + + ) : ( +
    + {currentNode && ( + <> +
    + +

    + {getJobDisplayName(currentNode.job)} +

    +
    +
    +
    + +

    State: {stateLabel}

    +
    + + {currentNode.createdAt && ( +
    + +

    + Created:{' '} + {moment(currentNode.createdAt).format('YYYY-MM-DD HH:mm')} +

    +
    + )} + + {currentNode.startedAt && ( +
    + +

    + Started:{' '} + {moment(currentNode.startedAt).format('YYYY-MM-DD HH:mm')} +

    +
    + )} + + {currentNode.createdAt && currentNode.finishedAt && ( +
    + +

    + Duration:{' '} + {humanizerDuration( + moment + .duration( + moment(currentNode.finishedAt).diff( + currentNode.startedAt + ) + ) + .asMilliseconds() + )} +

    +
    + )} +
    +
    + + {shouldFetchLogs && !isLoadedCompleted && ( + +
    + +
    +
    + )} +
    +
    + + )} +
    + )} + + ) +} + +function StdoutView({ + children, + className, + value, + pending, + ...rest +}: React.HTMLAttributes & { + value?: string + pending?: boolean +}) { + return ( +
    + {pending && !value && ( +
    + +
    + )} + {!!value && ( + <> +
    +            {value}
    +          
    + {children} + + )} +
    + ) +} + +function processPartialLine(logs: string) { + if (!logs) return logs + + const lines = logs.split('\n') + const lastLine = lines[lines.length - 1] + const hasLineBreak = lastLine.endsWith('\n') + + return hasLineBreak ? lines.join('\n') : lines.slice(0, -1).join('\n') +} diff --git a/ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/job-row.tsx b/ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/job-row.tsx new file mode 100644 index 000000000000..804088d14813 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/job-row.tsx @@ -0,0 +1,220 @@ +'use client' + +import { useMemo } from 'react' +import Link from 'next/link' +import humanizerDuration from 'humanize-duration' +import { isNil } from 'lodash-es' +import moment from 'moment' +import useSWR from 'swr' +import { useQuery } from 'urql' + +import { listJobRuns, queryJobRunStats } from '@/lib/tabby/query' +import { cn } from '@/lib/utils' +import { IconSpinner } from '@/components/ui/icons' +import { TableCell, TableRow } from '@/components/ui/table' +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger +} from '@/components/ui/tooltip' +import LoadingWrapper from '@/components/loading-wrapper' +import { ListRowSkeleton } from '@/components/skeleton' + +import { getJobDisplayName } from '../utils' + +function JobAggregateState({ + count, + activeClass, + tooltip +}: { + count?: number + activeClass: string + tooltip: string +}) { + return ( + + + +
    + {count || 0} +
    +
    + +

    {tooltip}

    +
    +
    +
    + ) +} + +function JobRunState({ name }: { name: string }) { + const [{ data, fetching }] = useQuery({ + query: queryJobRunStats, + variables: { + jobs: [name] + } + }) + return ( + } + > +
    + + + +
    +
    + ) +} + +export default function JobRow({ name }: { name: string }) { + const RECENT_DISPLAYED_SIZE = 10 + + const [{ data, fetching }, reexecuteQuery] = useQuery({ + query: listJobRuns, + variables: { + last: RECENT_DISPLAYED_SIZE, + jobs: [name] + } + }) + + useSWR('refresh_jobs', () => reexecuteQuery(), { + revalidateOnFocus: true, + revalidateOnReconnect: true, + revalidateOnMount: false, + refreshInterval: 10 * 1000 + }) + + const edges = data?.jobRuns?.edges + const displayJobs = useMemo(() => { + return edges?.slice().reverse() + }, [edges]) + const lastJob = displayJobs?.[0] + const lastFinishedJob = displayJobs?.find(job => Boolean(job.node.finishedAt)) + const lastSuccessAt = lastFinishedJob + ? moment(lastFinishedJob.node.finishedAt).format('YYYY-MM-DD HH:mm') + : null + + return ( + + + + + + } + > + + {getJobDisplayName(name)} + +
    + {displayJobs?.map(job => { + const { createdAt, finishedAt, startedAt } = job.node + const isJobRunning = !finishedAt && !!startedAt + const createAt = + createdAt && moment(createdAt).format('YYYY-MM-DD HH:mm') + const duration: string | null = + (startedAt && + finishedAt && + humanizerDuration.humanizer({ + language: 'shortEn', + languages: { + shortEn: { + d: () => 'd', + h: () => 'h', + m: () => 'm', + s: () => 's' + } + } + })( + moment + .duration(moment(finishedAt).diff(startedAt)) + .asMilliseconds(), + { + units: ['d', 'h', 'm', 's'], + round: false, + largest: 1, + spacer: '', + language: 'shortEn' + } + )) ?? + null + + let displayedDuration = '' + if (duration !== null) { + const isSecond = duration.endsWith('s') + if (isSecond) { + displayedDuration = duration + } else { + const unit = duration.charAt(duration.length - 1) + const roundNumber = parseInt(duration) + 1 + displayedDuration = roundNumber + unit + } + } + + return ( + + + + + {displayedDuration} + {isJobRunning && } + + + + {createAt &&

    {createAt}

    } +
    +
    +
    + ) + })} +
    +
    + + +

    {lastSuccessAt}

    + +
    + + + +
    +
    + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/jobs.tsx b/ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/jobs.tsx new file mode 100644 index 000000000000..04650b6e9dc0 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/jobs.tsx @@ -0,0 +1,46 @@ +'use client' + +import { Metadata } from 'next' +import { useQuery } from 'urql' + +import { listJobs } from '@/lib/tabby/query' +import { + Table, + TableBody, + TableHead, + TableHeader, + TableRow +} from '@/components/ui/table' +import LoadingWrapper from '@/components/loading-wrapper' + +import JobRow from './job-row' + +export const metadata: Metadata = { + title: 'Jobs' +} + +export default function JobRunsPage() { + const [{ data, fetching }] = useQuery({ + query: listJobs + }) + + return ( + + + + + Job + Recent Tasks + Last Run + Job Runs + + + + {data?.jobs.map(jobName => { + return + })} + +
    +
    + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/(logs)/jobs/detail/page.tsx b/ee/tabby-ui/app/(dashboard)/(logs)/jobs/detail/page.tsx new file mode 100644 index 000000000000..1e2b5186366a --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/(logs)/jobs/detail/page.tsx @@ -0,0 +1,5 @@ +import JobRunDetail from '../components/job-detail' + +export default function JobDetailPage() { + return +} diff --git a/ee/tabby-ui/app/(dashboard)/(logs)/jobs/page.tsx b/ee/tabby-ui/app/(dashboard)/(logs)/jobs/page.tsx new file mode 100644 index 000000000000..44f73066e34b --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/(logs)/jobs/page.tsx @@ -0,0 +1,11 @@ +import { Metadata } from 'next' + +import JobRunsPage from './components/jobs' + +export const metadata: Metadata = { + title: 'Jobs' +} + +export default function IndexPage() { + return +} diff --git a/ee/tabby-ui/app/(dashboard)/(logs)/jobs/utils.ts b/ee/tabby-ui/app/(dashboard)/(logs)/jobs/utils.ts new file mode 100644 index 000000000000..85fd994b7b60 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/(logs)/jobs/utils.ts @@ -0,0 +1,30 @@ +import { isNil } from 'lodash-es' + +import { JobRun } from '@/lib/gql/generates/graphql' + +const JOB_DISPLAY_NAME_MAPPINGS = { + scheduler_git: 'Git', + scheduler_github_gitlab: 'Github / Gitlab', + web_crawler: 'Docs' +} + +export function getJobDisplayName(name: string): string { + if (name in JOB_DISPLAY_NAME_MAPPINGS) { + return JOB_DISPLAY_NAME_MAPPINGS[ + name as keyof typeof JOB_DISPLAY_NAME_MAPPINGS + ] + } else { + return name + } +} + +// status: pending, running, success, failed +export function getLabelByJobRun( + info?: Pick +) { + if (!info) return 'Pending' + if (isNil(info.exitCode)) { + return info.startedAt ? 'Running' : 'Pending' + } + return info.exitCode === 0 ? 'Success' : 'Failed' +} diff --git a/ee/tabby-ui/app/(dashboard)/(logs)/layout.tsx b/ee/tabby-ui/app/(dashboard)/(logs)/layout.tsx new file mode 100644 index 000000000000..3f6da82da9ee --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/(logs)/layout.tsx @@ -0,0 +1,7 @@ +export default function LogsLayout({ + children +}: { + children: React.ReactNode +}) { + return
    {children}
    +} diff --git a/ee/tabby-ui/app/(dashboard)/activities/components/activity.tsx b/ee/tabby-ui/app/(dashboard)/activities/components/activity.tsx new file mode 100644 index 000000000000..5e66a8771557 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/activities/components/activity.tsx @@ -0,0 +1,408 @@ +'use client' + +import React from 'react' +import dynamic from 'next/dynamic' +import moment from 'moment' +import momentTimezone from 'moment-timezone' +import { DateRange } from 'react-day-picker' +import { toast } from 'sonner' +import { useQuery } from 'urql' + +import { DEFAULT_PAGE_SIZE } from '@/lib/constants' +import { graphql } from '@/lib/gql/generates' +import { EventKind, ListUserEventsQuery } from '@/lib/gql/generates/graphql' +import { Member, useAllMembers } from '@/lib/hooks/use-all-members' +import { useCurrentTheme } from '@/lib/hooks/use-current-theme' +import { QueryVariables } from '@/lib/tabby/gql' +import { Button } from '@/components/ui/button' +import { Card, CardContent } from '@/components/ui/card' +import { + IconChevronLeft, + IconChevronRight, + IconChevronsDownUp, + IconChevronUpDown, + IconFileSearch +} from '@/components/ui/icons' +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue +} from '@/components/ui/select' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from '@/components/ui/table' +import { + Tooltip, + TooltipContent, + TooltipTrigger +} from '@/components/ui/tooltip' +import DateRangePicker from '@/components/date-range-picker' +import LoadingWrapper from '@/components/loading-wrapper' + +const ReactJson = dynamic(() => import('react-json-view'), { ssr: false }) + +const DEFAULT_DATE_RANGE = '-24h' +const KEY_SELECT_ALL = 'all' + +export const listUserEvents = graphql(/* GraphQL */ ` + query ListUserEvents( + $after: String + $before: String + $first: Int + $last: Int + $start: DateTime! + $end: DateTime! + $users: [ID!] + ) { + userEvents( + after: $after + before: $before + first: $first + last: $last + start: $start + end: $end + users: $users + ) { + edges { + node { + id + userId + createdAt + kind + payload + } + cursor + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } + } +`) + +export default function Activity() { + const defaultFromDate = moment().add(parseInt(DEFAULT_DATE_RANGE, 10), 'hour') + const defaultToDate = moment() + + const [members] = useAllMembers() + const [dateRange, setDateRange] = React.useState({ + from: defaultFromDate.toDate(), + to: defaultToDate.toDate() + }) + const [page, setPage] = React.useState(1) + const [userEvents, setUserEvents] = + React.useState() + const [selectedMember, setSelectedMember] = React.useState(KEY_SELECT_ALL) + + const [queryVariables, setQueryVariables] = React.useState< + Omit, 'start' | 'end'> + >({ + last: DEFAULT_PAGE_SIZE + }) + + const [{ data, error, fetching }] = useQuery({ + query: listUserEvents, + variables: { + start: moment(dateRange.from!).utc().format(), + end: dateRange.to + ? moment(dateRange.to).utc().format() + : moment(dateRange.from!).utc().format(), + users: selectedMember === KEY_SELECT_ALL ? undefined : [selectedMember], + ...queryVariables + } + }) + + React.useEffect(() => { + if (data?.userEvents.edges.length) { + setUserEvents(data.userEvents) + } + }, [data]) + + React.useEffect(() => { + if (error?.message) { + toast.error(error.message) + } + }, [error]) + + const updateDateRange = (range: DateRange) => { + setDateRange(range) + setPage(1) + setQueryVariables({ last: DEFAULT_PAGE_SIZE }) + } + return ( + <> +
    +
    +
    +
    +

    {`View raw events generated by team members' activities while interacting with Tabby.`}

    + + {members.length > 0 && ( +
    + + + +
    + )} +
    + + <> + + {(!data?.userEvents.edges || + data?.userEvents.edges.length === 0) && ( + + +

    + No data available for the chosen dates +

    +

    + Please try a different date range +

    +
    + )} + + {data?.userEvents.edges && + data?.userEvents.edges.length > 0 && ( + <> + + + + + + + Event + + + User + + + Time + + + + + {userEvents?.edges + .sort( + (a, b) => + new Date(b.node.createdAt).getTime() - + new Date(a.node.createdAt).getTime() + ) + .map(userEvent => ( + + ))} + +
    +
    + + )} +
    + + {(data?.userEvents.pageInfo?.hasNextPage || + data?.userEvents.pageInfo?.hasPreviousPage) && ( +
    +
    + {' '} + Page {page} +
    +
    + + +
    +
    + )} + +
    +
    +
    +
    + + ) +} + +function ActivityRow({ + activity, + members +}: { + activity: ListUserEventsQuery['userEvents']['edges'][0]['node'] + members: Member[] +}) { + const { theme } = useCurrentTheme() + const [isExpanded, setIsExpanded] = React.useState(false) + + let payloadJson + try { + payloadJson = JSON.parse(activity.payload) as { + [key: string]: { language?: string } + } + } catch (error: any) { + if (error?.message) { + toast.error(error.message) + } + } + + if (!payloadJson) return null + + let tooltip = '' + switch (activity.kind) { + case EventKind.Completion: { + tooltip = 'Code completion supplied' + break + } + + case EventKind.Dismiss: { + tooltip = 'Code completion viewed but not used' + break + } + case EventKind.Select: { + tooltip = 'Code completion accepted and inserted' + break + } + case EventKind.View: { + tooltip = 'Code completion shown in editor' + break + } + case EventKind.ChatCompletion: { + tooltip = 'Chat completion supplied' + break + } + } + + let displayUser = activity.userId + const user = members.find(user => user.id === activity.userId) + if (user) displayUser = user.name || user.email + return ( + <> + setIsExpanded(!isExpanded)} + > + +
    + {isExpanded && } + {!isExpanded && } +
    +
    + + + {activity.kind} + +

    {tooltip}

    +
    +
    +
    + {displayUser} + + + + {moment(activity.createdAt).isBefore(moment().subtract(1, 'days')) + ? moment(activity.createdAt).format('YYYY-MM-DD HH:mm') + : moment(activity.createdAt).fromNow()} + + +

    + UTC: + {moment.utc(activity.createdAt).format('YYYY-MM-DD HH:mm:ss')} +

    +

    + + {momentTimezone.tz(momentTimezone.tz.guess()).format('z')}: + + {moment(activity.createdAt).format('YYYY-MM-DD HH:mm:ss')} +

    +
    +
    +
    +
    + + {isExpanded && ( + + + + + + )} + + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/activities/page.tsx b/ee/tabby-ui/app/(dashboard)/activities/page.tsx new file mode 100644 index 000000000000..06aca3cc11ea --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/activities/page.tsx @@ -0,0 +1,10 @@ +import { Metadata } from 'next' + +import Activity from './components/activity' + +export const metadata: Metadata = { + title: 'Activities' +} +export default function Page() { + return +} diff --git a/ee/tabby-ui/app/(dashboard)/cluster/components/cluster.tsx b/ee/tabby-ui/app/(dashboard)/cluster/components/cluster.tsx deleted file mode 100644 index 5848a6f0f08b..000000000000 --- a/ee/tabby-ui/app/(dashboard)/cluster/components/cluster.tsx +++ /dev/null @@ -1,117 +0,0 @@ -'use client' - -import { noop } from 'lodash-es' -import { useQuery } from 'urql' - -import { graphql } from '@/lib/gql/generates' -import { WorkerKind } from '@/lib/gql/generates/graphql' -import { useHealth } from '@/lib/hooks/use-health' -import { useWorkers } from '@/lib/hooks/use-workers' -import { useMutation } from '@/lib/tabby/gql' -import { Button } from '@/components/ui/button' -import { IconRotate } from '@/components/ui/icons' -import { Input } from '@/components/ui/input' -import { Separator } from '@/components/ui/separator' -import { CopyButton } from '@/components/copy-button' - -import WorkerCard from './worker-card' - -const getRegistrationTokenDocument = graphql(/* GraphQL */ ` - query GetRegistrationToken { - registrationToken - } -`) - -const resetRegistrationTokenDocument = graphql(/* GraphQL */ ` - mutation ResetRegistrationToken { - resetRegistrationToken - } -`) - -function toBadgeString(str: string) { - return encodeURIComponent(str.replaceAll('-', '--')) -} - -export default function Workers() { - const { data: healthInfo } = useHealth() - const workers = useWorkers() - const [{ data: registrationTokenRes }, reexecuteQuery] = useQuery({ - query: getRegistrationTokenDocument - }) - - const resetRegistrationToken = useMutation(resetRegistrationTokenDocument, { - onCompleted() { - reexecuteQuery() - } - }) - - if (!healthInfo) return - - return ( -
    -

    - Congratulations, your tabby instance - is up! -

    - - - - - - - {!!registrationTokenRes?.registrationToken && ( -
    - Registration token: - - - -
    - )} - -
    - {!!workers?.[WorkerKind.Completion] && ( - <> - {workers[WorkerKind.Completion].map((worker, i) => { - return - })} - - )} - {!!workers?.[WorkerKind.Chat] && ( - <> - {workers[WorkerKind.Chat].map((worker, i) => { - return - })} - - )} - -
    -
    - ) -} diff --git a/ee/tabby-ui/app/(dashboard)/cluster/components/worker-card.tsx b/ee/tabby-ui/app/(dashboard)/cluster/components/worker-card.tsx deleted file mode 100644 index 8928ea7df398..000000000000 --- a/ee/tabby-ui/app/(dashboard)/cluster/components/worker-card.tsx +++ /dev/null @@ -1,198 +0,0 @@ -import { Worker, WorkerKind } from '@/lib/gql/generates/graphql' -import { cn } from '@/lib/utils' -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' - -type RunnerType = WorkerKind | 'INDEX' - -interface RunnerCardProps extends Partial> { - kind: RunnerType -} - -export default function RunnerCard({ - addr, - name, - kind, - device, - cudaDevices, - cpuCount, - cpuInfo -}: RunnerCardProps) { - const textClass = cn( - 'ml-2', - 'whitespace-nowrap', - 'overflow-hidden', - 'text-ellipsis' - ) - const cpuMessage = `${cpuInfo} (${cpuCount} cores)` - return ( - - - - -

    - {name} -

    -
    -
    - - - - - - - - - -

    - {addr} -

    -
    - - - - - - - - - - - - - -

    - {cpuMessage} -

    -
    - {device == 'cuda' && - cudaDevices?.length && - cudaDevices.map((x, i) => ( - - - - - - - - - - - - -

    - {x} -

    -
    - ))} -
    -
    - ) -} - -interface InfoProps { - children: React.ReactNode -} - -function Info({ children }: InfoProps) { - return ( -
    - {children} -
    - ) -} - -function ModelIcon({ type }: { type: RunnerType }) { - const className = 'h-5 w-5' - if (type == WorkerKind.Completion) { - return ( - - - - - - - ) - } else if (type == WorkerKind.Chat) { - return ( - - - - - ) - } else if (type == 'INDEX') { - return ( - - - - - - - ) - } -} diff --git a/ee/tabby-ui/app/(dashboard)/cluster/page.tsx b/ee/tabby-ui/app/(dashboard)/cluster/page.tsx deleted file mode 100644 index 09548c6a4508..000000000000 --- a/ee/tabby-ui/app/(dashboard)/cluster/page.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Metadata } from 'next' - -import ClusterInfo from './components/cluster' - -export const metadata: Metadata = { - title: 'Cluster Information' -} - -export default function IndexPage() { - return ( -
    - -
    - ) -} diff --git a/ee/tabby-ui/app/(dashboard)/components/dashboard-layout.tsx b/ee/tabby-ui/app/(dashboard)/components/dashboard-layout.tsx new file mode 100644 index 000000000000..e97e42da78ee --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/components/dashboard-layout.tsx @@ -0,0 +1,33 @@ +'use client' + +import { useHydrated } from '@/lib/hooks/use-hydration' +import { + toggleSidebar, + useUserPreferencesStore +} from '@/lib/stores/user-preferences-store' +import { SidebarProvider } from '@/components/ui/sidebar' + +import MainContent from './dashboard-main' +import AppSidebar from './dashboard-sidebar' + +export default function Layout({ children }: { children: React.ReactNode }) { + const hydrated = useHydrated() + const isSidebarExpanded = useUserPreferencesStore( + state => state.isSidebarExpanded + ) + + if (!hydrated) return null + + return ( + <> + toggleSidebar(open)} + > + + {children} + + + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/components/dashboard-main.tsx b/ee/tabby-ui/app/(dashboard)/components/dashboard-main.tsx new file mode 100644 index 000000000000..daa5d016250e --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/components/dashboard-main.tsx @@ -0,0 +1,37 @@ +'use client' + +import { useRef } from 'react' + +import { ScrollArea } from '@/components/ui/scroll-area' +import { SidebarInset } from '@/components/ui/sidebar' +import { BANNER_HEIGHT, useShowDemoBanner } from '@/components/demo-banner' +import { Header } from '@/components/header' +import { useShowLicenseBanner } from '@/components/license-banner' + +export default function MainContent({ + children +}: { + children: React.ReactNode +}) { + const scroller = useRef(null) + const [isShowDemoBanner] = useShowDemoBanner() + const [isShowLicenseBanner] = useShowLicenseBanner() + const style = + isShowDemoBanner || isShowLicenseBanner + ? { + height: `calc(100vh - ${ + isShowDemoBanner ? BANNER_HEIGHT : '0rem' + } - ${isShowLicenseBanner ? BANNER_HEIGHT : '0rem'})` + } + : { height: '100vh' } + + return ( + + {/* Wraps right hand side into ScrollArea, making scroll bar consistent across all browsers */} + +
    +
    {children}
    + + + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/components/dashboard-sidebar.tsx b/ee/tabby-ui/app/(dashboard)/components/dashboard-sidebar.tsx new file mode 100644 index 000000000000..31ce5d19dde1 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/components/dashboard-sidebar.tsx @@ -0,0 +1,295 @@ +'use client' + +import React, { FunctionComponent } from 'react' +import Link from 'next/link' +import { usePathname } from 'next/navigation' +import logoDarkUrl from '@/assets/logo-dark.png' +import logoUrl from '@/assets/logo.png' +import tabbyLogo from '@/assets/tabby.png' +import { HoverCardPortal } from '@radix-ui/react-hover-card' + +import { useMe } from '@/lib/hooks/use-me' +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger +} from '@/components/ui/collapsible' +import { + HoverCard, + HoverCardContent, + HoverCardTrigger +} from '@/components/ui/hover-card' +import { + IconBookOpenText, + IconChevronRight, + IconGear, + IconLightingBolt, + IconUser +} from '@/components/ui/icons' +import { + Sidebar, + SidebarContent, + SidebarGroup, + SidebarHeader, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, + useSidebar +} from '@/components/ui/sidebar' +import { BrandingIcon, BrandingLogo } from '@/components/branding-logo' +import LoadingWrapper from '@/components/loading-wrapper' + +export interface SidebarProps { + children?: React.ReactNode + className?: string +} + +type SubMenu = { + title: string + href: string + allowUser?: boolean +} + +type Menu = + | { + title: string + icon: FunctionComponent + allowUser?: boolean + items: SubMenu[] + } + | { + title: string + href: string + icon: FunctionComponent + allowUser?: boolean + items?: never + } + +const menus: Menu[] = [ + { + title: 'Profile', + icon: IconUser, + href: '/profile', + allowUser: true + }, + { + title: 'Information', + icon: IconBookOpenText, + items: [ + { + title: 'System', + href: '/system' + }, + { + title: 'Jobs', + href: '/jobs' + }, + { + title: 'Reports', + href: '/reports' + }, + { + title: 'Activities', + href: '/activities' + } + ] + }, + { + title: 'Settings', + icon: IconGear, + allowUser: true, + items: [ + { + title: 'General', + href: '/settings/general' + }, + { + title: 'Users & Groups', + href: '/settings/team', + allowUser: true + }, + { + title: 'Subscription', + href: '/settings/subscription' + } + ] + }, + { + title: 'Integrations', + icon: IconLightingBolt, + items: [ + { + title: 'Context Providers', + href: '/settings/providers/git' + }, + { + title: 'SSO', + href: '/settings/sso' + }, + { + title: 'Mail Delivery', + href: '/settings/mail' + } + ] + } +] + +export default function AppSidebar() { + const pathname = usePathname() + const [{ data, fetching: fetchingMe }] = useMe() + const isAdmin = data?.me.isAdmin + const { isMobile, state } = useSidebar() + + return ( + + + + +
    + + +
    + +
    + + + + {menus.map(menu => { + if (isAdmin || menu.allowUser) { + if (menu.items) { + return ( + + + + + + + {!!menu.icon && } + {menu.title} + + + + + + + + + + + {menu.items.map(item => { + if (isAdmin || item.allowUser) { + return ( + + + + {item.title} + + + + ) + } + })} + + + + + ) + } else { + return ( + + + {menu.title} + + ) + }} + > + + {!!menu.icon && } + {menu.title} + + + + ) + } + } + return null + })} + + + +
    + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/components/sidebar.tsx b/ee/tabby-ui/app/(dashboard)/components/sidebar.tsx index f950e95a1617..481978f9b9a9 100644 --- a/ee/tabby-ui/app/(dashboard)/components/sidebar.tsx +++ b/ee/tabby-ui/app/(dashboard)/components/sidebar.tsx @@ -1,95 +1,297 @@ 'use client' +import React, { FunctionComponent } from 'react' import Image from 'next/image' import Link from 'next/link' import { usePathname } from 'next/navigation' import logoDarkUrl from '@/assets/logo-dark.png' import logoUrl from '@/assets/logo.png' -import { cva } from 'class-variance-authority' +import tabbyLogo from '@/assets/tabby.png' +import { HoverCardPortal } from '@radix-ui/react-hover-card' -import { useSession } from '@/lib/tabby/auth' -import { cn } from '@/lib/utils' -import { IconHome, IconNetwork, IconUsers } from '@/components/ui/icons' +import { useMe } from '@/lib/hooks/use-me' +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger +} from '@/components/ui/collapsible' +import { + HoverCard, + HoverCardContent, + HoverCardTrigger +} from '@/components/ui/hover-card' +import { + IconBookOpenText, + IconChevronRight, + IconGear, + IconLightingBolt, + IconUser +} from '@/components/ui/icons' +import { + Sidebar, + SidebarContent, + SidebarGroup, + SidebarHeader, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, + useSidebar +} from '@/components/ui/sidebar' +import LoadingWrapper from '@/components/loading-wrapper' export interface SidebarProps { - children: React.ReactNode + children?: React.ReactNode className?: string } -export default function Sidebar({ children, className }: SidebarProps) { - const { data: session } = useSession() - const isAdmin = session?.isAdmin || false - return ( -
    -
    -
    -
    -
    - -
    -
    -
    -
    {children}
    -
    - ) -} - -interface SidebarButtonProps { +type SubMenu = { + title: string href: string - children: React.ReactNode + allowUser?: boolean } -const linkVariants = cva( - 'flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:bg-accent', +type Menu = + | { + title: string + icon: FunctionComponent + allowUser?: boolean + items: SubMenu[] + } + | { + title: string + href: string + icon: FunctionComponent + allowUser?: boolean + items?: never + } + +const menus: Menu[] = [ { - variants: { - state: { - selected: 'bg-accent', - 'not-selected': '' + title: 'Profile', + icon: IconUser, + href: '/profile', + allowUser: true + }, + { + title: 'Information', + icon: IconBookOpenText, + items: [ + { + title: 'System', + href: '/system' + }, + { + title: 'Jobs', + href: '/jobs' + }, + { + title: 'Reports', + href: '/reports' + }, + { + title: 'Activities', + href: '/activities' } - }, - defaultVariants: { - state: 'not-selected' - } + ] + }, + { + title: 'Settings', + icon: IconGear, + allowUser: true, + items: [ + { + title: 'General', + href: '/settings/general' + }, + { + title: 'Users & Groups', + href: '/settings/team', + allowUser: true + }, + { + title: 'Subscription', + href: '/settings/subscription' + } + ] + }, + { + title: 'Integrations', + icon: IconLightingBolt, + items: [ + { + title: 'Context Providers', + href: '/settings/providers/git' + }, + { + title: 'SSO', + href: '/settings/sso' + }, + { + title: 'Mail Delivery', + href: '/settings/mail' + } + ] } -) +] -function SidebarButton({ href, children }: SidebarButtonProps) { +export default function AppSidebar() { const pathname = usePathname() - const state = pathname == href ? 'selected' : 'not-selected' + const [{ data, fetching: fetchingMe }] = useMe() + const isAdmin = data?.me.isAdmin + const { isMobile, state } = useSidebar() + return ( - - {children} - + + + + <> + logo +
    + logo + logo +
    + + +
    + + + + {menus.map(menu => { + if (isAdmin || menu.allowUser) { + if (menu.items) { + return ( + + + + + + + {!!menu.icon && } + {menu.title} + + + + + + + + + + + {menu.items.map(item => { + if (isAdmin || item.allowUser) { + return ( + + + + {item.title} + + + + ) + } + })} + + + + + ) + } else { + return ( + + + {menu.title} + + ) + }} + > + + {!!menu.icon && } + {menu.title} + + + + ) + } + } + return null + })} + + + +
    ) } diff --git a/ee/tabby-ui/app/(dashboard)/experiments/components/feature-list.tsx b/ee/tabby-ui/app/(dashboard)/experiments/components/feature-list.tsx new file mode 100644 index 000000000000..a35dc502901f --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/experiments/components/feature-list.tsx @@ -0,0 +1,67 @@ +'use client' + +import { ENABLE_CHAT } from '@/lib/constants' +import { + useEnableDeveloperMode, + useEnablePage, + useEnableSearchPages +} from '@/lib/experiment-flags' +import { Switch } from '@/components/ui/switch' + +export default function FeatureList() { + const [developerMode, toggleDeveloperMode] = useEnableDeveloperMode() + const [enablePage, toggleEnablePage] = useEnablePage() + const [enableSearchPages, toggleEnableSearchPages] = useEnableSearchPages() + return ( + <> + {!developerMode.loading && ( +
    +
    +

    + {developerMode.title} +

    +

    + {developerMode.description} +

    +
    + +
    + )} + {ENABLE_CHAT && !enablePage.loading && ( +
    +
    +

    + {enablePage.title} +

    +

    + {enablePage.description} +

    +
    + +
    + )} + {ENABLE_CHAT && !enableSearchPages.loading && ( +
    +
    +

    + {enableSearchPages.title} +

    +

    + {enableSearchPages.description} +

    +
    + +
    + )} + + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/experiments/page.tsx b/ee/tabby-ui/app/(dashboard)/experiments/page.tsx new file mode 100644 index 000000000000..2071520705af --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/experiments/page.tsx @@ -0,0 +1,18 @@ +import { Metadata } from 'next' + +import FeatureList from './components/feature-list' + +export const metadata: Metadata = { + title: 'Experiment Flags' +} + +export default function IndexPage() { + return ( +
    +

    + Experiment Flags +

    + +
    + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/layout.tsx b/ee/tabby-ui/app/(dashboard)/layout.tsx index 12be28345be1..382c10b55d47 100644 --- a/ee/tabby-ui/app/(dashboard)/layout.tsx +++ b/ee/tabby-ui/app/(dashboard)/layout.tsx @@ -1,32 +1,25 @@ import { Metadata } from 'next' -import { ScrollArea } from '@/components/ui/scroll-area' -import { Header } from '@/components/header' +import { LicenseBanner } from '@/components/license-banner' -import Sidebar from './components/sidebar' +import Layout from './components/dashboard-layout' export const metadata: Metadata = { title: { - default: 'Home', + default: 'Dashboard', template: `Tabby - %s` } } -interface DashboardLayoutProps { +export default function DashboardLayout({ + children +}: { children: React.ReactNode -} - -export default function RootLayout({ children }: DashboardLayoutProps) { +}) { return ( <> -
    - - -
    -
    {children}
    - - -
    + + {children} ) } diff --git a/ee/tabby-ui/app/(dashboard)/page.tsx b/ee/tabby-ui/app/(dashboard)/page.tsx deleted file mode 100644 index 4d12bea35359..000000000000 --- a/ee/tabby-ui/app/(dashboard)/page.tsx +++ /dev/null @@ -1,105 +0,0 @@ -'use client' - -import { useEffect, useState } from 'react' -import { noop } from 'lodash-es' -import { useQuery } from 'urql' - -import { graphql } from '@/lib/gql/generates' -import { useHealth } from '@/lib/hooks/use-health' -import { useMutation } from '@/lib/tabby/gql' -import { Button } from '@/components/ui/button' -import { - CardContent, - CardFooter, - CardHeader, - CardTitle -} from '@/components/ui/card' -import { IconRotate } from '@/components/ui/icons' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { CopyButton } from '@/components/copy-button' -import SlackDialog from '@/components/slack-dialog' - -export default function Home() { - return ( -
    - - -
    - ) -} - -const meQuery = graphql(/* GraphQL */ ` - query MeQuery { - me { - authToken - } - } -`) - -const resetUserAuthTokenDocument = graphql(/* GraphQL */ ` - mutation ResetUserAuthToken { - resetUserAuthToken - } -`) - -function MainPanel() { - const { data: healthInfo } = useHealth() - const [{ data }, reexecuteQuery] = useQuery({ query: meQuery }) - const [origin, setOrigin] = useState('') - useEffect(() => { - setOrigin(new URL(window.location.href).origin) - }, []) - - const resetUserAuthToken = useMutation(resetUserAuthTokenDocument, { - onCompleted: () => reexecuteQuery() - }) - - if (!healthInfo || !data) return - - return ( -
    - - Getting Started - - - - - - - - - - - - - - - - - - Use informations above for IDE extensions / plugins configuration, see{' '} - - documentation website - {' '} - for details - - -
    - ) -} diff --git a/ee/tabby-ui/app/(dashboard)/profile/components/avatar.tsx b/ee/tabby-ui/app/(dashboard)/profile/components/avatar.tsx new file mode 100644 index 000000000000..2a46f0475151 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/profile/components/avatar.tsx @@ -0,0 +1,128 @@ +'use client' + +import { ChangeEvent, useState } from 'react' +import { toast } from 'sonner' + +import { graphql } from '@/lib/gql/generates' +import { useMe } from '@/lib/hooks/use-me' +import { useMutation } from '@/lib/tabby/gql' +import { cn, delay } from '@/lib/utils' +import { Button } from '@/components/ui/button' +import { IconCloudUpload, IconSpinner } from '@/components/ui/icons' +import { Separator } from '@/components/ui/separator' +import { mutateAvatar, MyAvatar } from '@/components/user-avatar' + +const uploadUserAvatarMutation = graphql(/* GraphQL */ ` + mutation uploadUserAvatarBase64($id: ID!, $avatarBase64: String!) { + uploadUserAvatarBase64(id: $id, avatarBase64: $avatarBase64) + } +`) + +const MAX_UPLOAD_SIZE_KB = 500 + +export const Avatar = () => { + const [isSubmitting, setIsSubmitting] = useState(false) + const [uploadedImgString, setUploadedImgString] = useState('') + const [{ data }] = useMe() + const uploadUserAvatar = useMutation(uploadUserAvatarMutation, { + onError(err) { + toast.error(err.message) + } + }) + if (!data?.me?.email) return null + + const onPreviewAvatar = (e: ChangeEvent) => { + const file = e.target.files ? e.target.files[0] : null + + if (file) { + const fileSizeInKB = parseFloat((file.size / 1024).toFixed(2)) + if (fileSizeInKB > MAX_UPLOAD_SIZE_KB) { + return toast.error( + `The image you are attempting to upload is too large. Please ensure the file size is under ${MAX_UPLOAD_SIZE_KB}KB and try again.` + ) + } + + const reader = new FileReader() + + reader.onloadend = () => { + const imageString = reader.result as string + setUploadedImgString(imageString) + } + + reader.readAsDataURL(file) + } + } + + const onUploadAvatar = async () => { + setIsSubmitting(true) + + const response = await uploadUserAvatar({ + avatarBase64: uploadedImgString.split(',')[1], + id: data.me.id + }) + + if (response?.data?.uploadUserAvatarBase64 === true) { + await delay(1000) + mutateAvatar(data.me.id) + toast.success('Successfully updated your profile picture!') + await delay(200) + } + + setUploadedImgString('') + setIsSubmitting(false) + } + + return ( +
    +
    + + + {uploadedImgString && ( + avatar to be uploaded + )} + +
    + + + +
    + + +
    +

    + {`Square image recommended. Accepted file types: .png, .jpg. Max file size: ${MAX_UPLOAD_SIZE_KB}KB.`} +

    +
    +
    +
    + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/profile/components/change-name.tsx b/ee/tabby-ui/app/(dashboard)/profile/components/change-name.tsx new file mode 100644 index 000000000000..1f5fb1741051 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/profile/components/change-name.tsx @@ -0,0 +1,140 @@ +'use client' + +import React from 'react' +import { zodResolver } from '@hookform/resolvers/zod' +import { useForm } from 'react-hook-form' +import { toast } from 'sonner' +import * as z from 'zod' + +import { graphql } from '@/lib/gql/generates' +import { useMe } from '@/lib/hooks/use-me' +import { useMutation } from '@/lib/tabby/gql' +import { Button } from '@/components/ui/button' +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage +} from '@/components/ui/form' +import { IconSpinner } from '@/components/ui/icons' +import { Input } from '@/components/ui/input' +import { Separator } from '@/components/ui/separator' +import { + Tooltip, + TooltipContent, + TooltipTrigger +} from '@/components/ui/tooltip' +import { ListSkeleton } from '@/components/skeleton' + +const updateNameMutation = graphql(/* GraphQL */ ` + mutation UpdateUserName($id: ID!, $name: String!) { + updateUserName(id: $id, name: $name) + } +`) + +interface ChangeNameFormProps { + showOldPassword?: boolean + onSuccess?: () => void + defaultValues: { + name?: string + } +} + +const ChangeNameForm: React.FC = ({ + onSuccess, + defaultValues +}) => { + const [{ data, fetching }] = useMe() + const isSsoUser = data?.me?.isSsoUser + const formSchema = z.object({ + name: z.string() + }) + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues + }) + const { isSubmitting } = form.formState + const { name } = form.watch() + + const updateName = useMutation(updateNameMutation, { + form, + onCompleted(values) { + if (values?.updateUserName) { + onSuccess?.() + } + } + }) + + const onSubmit = async (values: z.infer) => { + await updateName({ + id: data!.me.id, + name: values.name + }) + } + + const isNameModified = name !== defaultValues.name + return ( +
    + + ( + + Name + + + + + + + + + + + )} + /> + + +
    + +
    + + + ) +} + +export const ChangeName = () => { + const [{ data }, reexecuteQuery] = useMe() + const onSuccess = () => { + toast.success('Name is updated') + reexecuteQuery() + } + + return data ? ( + + ) : ( + + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/profile/components/change-password.tsx b/ee/tabby-ui/app/(dashboard)/profile/components/change-password.tsx new file mode 100644 index 000000000000..f39070aadfa7 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/profile/components/change-password.tsx @@ -0,0 +1,190 @@ +'use client' + +import React from 'react' +import { zodResolver } from '@hookform/resolvers/zod' +import { useForm } from 'react-hook-form' +import { toast } from 'sonner' +import * as z from 'zod' + +import { graphql } from '@/lib/gql/generates' +import { useMe } from '@/lib/hooks/use-me' +import { useMutation } from '@/lib/tabby/gql' +import { Button } from '@/components/ui/button' +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage +} from '@/components/ui/form' +import { IconSpinner } from '@/components/ui/icons' +import { Input } from '@/components/ui/input' +import { Separator } from '@/components/ui/separator' +import { + PASSWORD_ERRORCODE, + PasswordCheckList, + usePasswordErrors +} from '@/components/password-check-list' +import { ListSkeleton } from '@/components/skeleton' + +const passwordChangeMutation = graphql(/* GraphQL */ ` + mutation PasswordChange($input: PasswordChangeInput!) { + passwordChange(input: $input) + } +`) + +interface ChangePasswordFormProps { + showOldPassword?: boolean + onSuccess?: () => void +} + +const ChangePasswordForm: React.FC = ({ + onSuccess, + showOldPassword +}) => { + const [showPasswordSchema, setShowPasswordSchema] = React.useState(false) + const [showPasswordError, setShowPasswordError] = React.useState(false) + const formSchema = z.object({ + oldPassword: showOldPassword ? z.string() : z.string().optional(), + newPassword1: z.string(), + newPassword2: z.string() + }) + + const form = useForm>({ + resolver: zodResolver(formSchema) + }) + const { isSubmitting } = form.formState + const { newPassword1: password } = form.watch() + const [passworErrors] = usePasswordErrors(password) + + const passwordChange = useMutation(passwordChangeMutation, { + form, + onCompleted(values) { + if (values?.passwordChange) { + onSuccess?.() + form.reset({ + newPassword1: '', + newPassword2: '', + oldPassword: '' + }) + } + } + }) + + const onSubmit = async (values: z.infer) => { + await passwordChange({ + input: values + }) + } + + const onPasswordBlur = () => { + if (passworErrors.length === 0) return setShowPasswordSchema(false) + setShowPasswordError(true) + } + + return ( +
    + + {showOldPassword && ( + ( + + Old password + + + + + + )} + /> + )} +
    + ( + + New password + + setShowPasswordSchema(true)} + onBlur={onPasswordBlur} + /> + + + )} + /> + +
    + ( + + Confirm new password + + + + + + )} + /> + + +
    + +
    + + + ) +} + +export const ChangePassword = () => { + const [{ data }, reexecuteQuery] = useMe() + const onSuccess = () => { + toast.success('Password is updated') + reexecuteQuery() + } + + return data ? ( + + ) : ( + + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/profile/components/email.tsx b/ee/tabby-ui/app/(dashboard)/profile/components/email.tsx new file mode 100644 index 000000000000..08bfa1101462 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/profile/components/email.tsx @@ -0,0 +1,19 @@ +import { noop } from 'lodash-es' + +import { useMe } from '@/lib/hooks/use-me' +import { Input } from '@/components/ui/input' + +export const Email = () => { + const [{ data }] = useMe() + + return ( +
    + +
    + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/profile/components/profile-card.tsx b/ee/tabby-ui/app/(dashboard)/profile/components/profile-card.tsx new file mode 100644 index 000000000000..32c66a69d025 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/profile/components/profile-card.tsx @@ -0,0 +1,63 @@ +import React from 'react' + +import { useMe } from '@/lib/hooks/use-me' +import { cn } from '@/lib/utils' +import { CardContent, CardTitle } from '@/components/ui/card' +import { Separator } from '@/components/ui/separator' + +interface ProfileCardProps extends React.HTMLAttributes { + title: string + description?: string + footer?: React.ReactNode + footerClassname?: string + hideForSsoUser?: boolean +} + +const ProfileCard: React.FC = ({ + title, + description, + footer, + footerClassname, + className, + hideForSsoUser, + children, + ...props +}) => { + const [{ data }] = useMe() + const isSsoUser = data?.me?.isSsoUser + + if (isSsoUser && hideForSsoUser) { + return null + } + + return ( +
    +
    + {title} + {description && ( +
    + {description} +
    + )} +
    + {children} +
    + {!!footer && } + {footer} +
    +
    + ) +} + +export { ProfileCard } diff --git a/ee/tabby-ui/app/(dashboard)/profile/components/profile.tsx b/ee/tabby-ui/app/(dashboard)/profile/components/profile.tsx new file mode 100644 index 000000000000..b0fb838f38b3 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/profile/components/profile.tsx @@ -0,0 +1,40 @@ +'use client' + +import React from 'react' + +import { Avatar } from './avatar' +import { ChangeName } from './change-name' +import { ChangePassword } from './change-password' +import { Email } from './email' +import { ProfileCard } from './profile-card' + +export default function Profile() { + return ( +
    + + + + + + + + + + + + +
    + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/profile/page.tsx b/ee/tabby-ui/app/(dashboard)/profile/page.tsx new file mode 100644 index 000000000000..c1d44dc91ff9 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/profile/page.tsx @@ -0,0 +1,11 @@ +import { Metadata } from 'next' + +import Profile from './components/profile' + +export const metadata: Metadata = { + title: 'Profile' +} + +export default function Page() { + return +} diff --git a/ee/tabby-ui/app/(dashboard)/reports/components/annual-activity.tsx b/ee/tabby-ui/app/(dashboard)/reports/components/annual-activity.tsx new file mode 100644 index 000000000000..2349127d285b --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/reports/components/annual-activity.tsx @@ -0,0 +1,58 @@ +'use client' + +import { useWindowSize } from '@uidotdev/usehooks' +import ReactActivityCalendar from 'react-activity-calendar' + +import { useCurrentTheme } from '@/lib/hooks/use-current-theme' + +function ActivityCalendar({ + data +}: { + data: { + date: string + count: number + level: number + }[] +}) { + const { theme } = useCurrentTheme() + const size = useWindowSize() + const width = size.width || 0 + const blockSize = + width >= 1300 ? 13 : width >= 1100 ? 9 : width >= 900 ? 6 : 5 + + return ( + + ) +} + +export function AnnualActivity({ + totalCount, + dailyData +}: { + totalCount: number + dailyData: Array<{ + date: string + count: number + level: number + }> +}) { + return ( +
    +

    + {totalCount} activities in the last year +

    +
    + +
    +
    + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/reports/components/chat-daily-activity.tsx b/ee/tabby-ui/app/(dashboard)/reports/components/chat-daily-activity.tsx new file mode 100644 index 000000000000..fe5fc91122b0 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/reports/components/chat-daily-activity.tsx @@ -0,0 +1,160 @@ +'use client' + +import { useState } from 'react' +import moment from 'moment' +import numeral from 'numeral' +import { DateRange } from 'react-day-picker' +import { + Bar, + BarChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis +} from 'recharts' + +import { useCurrentTheme } from '@/lib/hooks/use-current-theme' +import { useChatDailyStats } from '@/lib/hooks/use-statistics' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { IconMessageSquare } from '@/components/ui/icons' +import { Skeleton } from '@/components/ui/skeleton' +import DateRangePicker from '@/components/date-range-picker' +import LoadingWrapper from '@/components/loading-wrapper' + +import { DEFAULT_DATE_RANGE } from './constants' + +export function ChatDailyActivity({ + selectedMember, + sample +}: { + selectedMember: string + sample?: boolean +}) { + const { theme } = useCurrentTheme() + const [dateRange, setDateRange] = useState({ + from: moment().add(parseInt(DEFAULT_DATE_RANGE, 10), 'day').toDate(), + to: moment().toDate() + }) + + const from = dateRange.from || new Date() + const to = dateRange.to || from + + const { fetchingChatDailyStats, chatChartData, chatDailyStats, totalCount } = + useChatDailyStats({ + sample, + selectedMember, + dateRange: { + from, + to + } + }) + return ( + +
    + + + +
    + + + } + > +
    +
    +

    Chats

    +
    + +
    +
    +
    + + + Total Chats + + + +
    + {numeral(totalCount).format('0,0')} +
    +
    +
    +
    +
    +

    + Daily Statistics +

    + + + + + + } + /> + + +
    +
    +
    + ) +} + +function BarTooltip({ + active, + payload, + label +}: { + active?: boolean + label?: string + payload?: { + name: string + payload: { + chats: number + } + }[] +}) { + if (active && payload && payload.length) { + const { chats } = payload[0].payload + if (!chats) return null + return ( + + +

    + Chat: + {chats} +

    +

    {label}

    +
    +
    + ) + } + + return null +} diff --git a/ee/tabby-ui/app/(dashboard)/reports/components/completion-daily-activity.tsx b/ee/tabby-ui/app/(dashboard)/reports/components/completion-daily-activity.tsx new file mode 100644 index 000000000000..7116526a3bff --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/reports/components/completion-daily-activity.tsx @@ -0,0 +1,326 @@ +'use client' + +import { useState } from 'react' +import { sum } from 'lodash-es' +import moment from 'moment' +import numeral from 'numeral' +import type { DateRange } from 'react-day-picker' +import { + Bar, + BarChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis +} from 'recharts' + +import { DailyStatsQuery, Language } from '@/lib/gql/generates/graphql' +import { useCurrentTheme } from '@/lib/hooks/use-current-theme' +import { useCompletionDailyStats } from '@/lib/hooks/use-statistics' +import { getLanguageDisplayName } from '@/lib/language-utils' +import { cn } from '@/lib/utils' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator +} from '@/components/ui/command' +import { + IconActivity, + IconCheck, + IconChevronUpDown, + IconCode +} from '@/components/ui/icons' +import { + Popover, + PopoverContent, + PopoverTrigger +} from '@/components/ui/popover' +import { Skeleton } from '@/components/ui/skeleton' +import DateRangePicker from '@/components/date-range-picker' +import LoadingWrapper from '@/components/loading-wrapper' + +import { DEFAULT_DATE_RANGE } from './constants' + +function BarTooltip({ + active, + payload, + label +}: { + active?: boolean + label?: string + payload?: { + name: string + payload: { + views: number + selects: number + pendings: number + } + }[] +}) { + if (active && payload && payload.length) { + const { views, selects } = payload[0].payload + if (!views) return null + return ( + + +

    + Completion: + {views} +

    +

    + Acceptance: + {selects} +

    +

    {label}

    +
    +
    + ) + } + + return null +} + +export function CompletionDailyActivity({ + selectedMember, + sample +}: { + selectedMember: string + sample?: boolean +}) { + const { theme } = useCurrentTheme() + + const [dateRange, setDateRange] = useState({ + from: moment().add(parseInt(DEFAULT_DATE_RANGE, 10), 'day').toDate(), + to: moment().toDate() + }) + const [selectedLanguage, setSelectedLanguage] = useState([]) + // Query stats of selected date range + const { + completionChartData, + completionDailyStats, + fetchingCompletionDailyStats + } = useCompletionDailyStats({ + selectedMember, + dateRange, + sample, + languages: selectedLanguage + }) + + return ( + +
    + + + +
    + + + } + > +
    +

    Usage

    +
    +
    +

    Completions

    +
    + + +
    +
    + {selectedLanguage.length === 0 && ( +

    + All languages +

    + )} + {selectedLanguage.length === 1 && ( +

    + {getLanguageDisplayName(selectedLanguage[0])} +

    + )} + {selectedLanguage.length > 1 && ( + + {selectedLanguage.length} selected + + )} +
    + +
    +
    + + + + + No results found. + + + {Object.entries(Language) + .sort((_, b) => (b[1] === Language.Other ? -1 : 0)) + .map(([_, value]) => { + const isSelected = selectedLanguage.includes(value) + return ( + { + const newSelect = [...selectedLanguage] + if (isSelected) { + const idx = newSelect.findIndex( + item => item === value + ) + if (idx !== -1) newSelect.splice(idx, 1) + } else { + newSelect.push(value) + } + setSelectedLanguage(newSelect) + }} + className="!pointer-events-auto cursor-pointer !opacity-100" + > +
    + +
    + {getLanguageDisplayName(value)} +
    + ) + })} +
    + {selectedLanguage.length > 0 && ( + <> + + + setSelectedLanguage([])} + className="!pointer-events-auto cursor-pointer justify-center text-center !opacity-100" + > + Clear filters + + + + )} +
    +
    +
    +
    + + +
    +
    + +
    +

    + Daily Statistics +

    + + + + + + + } + /> + + +
    +
    +
    +
    + ) +} + +function StatsSummary({ + dailyStats +}: { + dailyStats?: DailyStatsQuery['dailyStats'] +}) { + const totalViews = sum(dailyStats?.map(stats => stats.views)) + const totalAcceptances = sum(dailyStats?.map(stats => stats.selects)) + const acceptRate = + totalAcceptances === 0 + ? 0 + : ((totalAcceptances / totalViews) * 100).toFixed(2) + return ( +
    + + + Acceptance Rate + + + +
    {acceptRate}%
    +
    +
    + + + + + Total Completions + + + + +
    + {numeral(totalViews).format('0,0')} +
    +
    +
    + + + + + Total Acceptances + + + + +
    {totalAcceptances}
    +
    +
    +
    + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/reports/components/constants.ts b/ee/tabby-ui/app/(dashboard)/reports/components/constants.ts new file mode 100644 index 000000000000..ec273ab930e0 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/reports/components/constants.ts @@ -0,0 +1,2 @@ +export const KEY_SELECT_ALL = 'all' +export const DEFAULT_DATE_RANGE = '-14d' diff --git a/ee/tabby-ui/app/(dashboard)/reports/components/report.tsx b/ee/tabby-ui/app/(dashboard)/reports/components/report.tsx new file mode 100644 index 000000000000..71aaec3c2f11 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/reports/components/report.tsx @@ -0,0 +1,106 @@ +'use client' + +import { useState } from 'react' +import { useSearchParams } from 'next/navigation' + +import { ENABLE_CHAT } from '@/lib/constants' +import { useAllMembers } from '@/lib/hooks/use-all-members' +import { useIsDemoMode } from '@/lib/hooks/use-server-info' +import { useYearlyStats } from '@/lib/hooks/use-statistics' +import { IconUsers } from '@/components/ui/icons' +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue +} from '@/components/ui/select' +import { Skeleton } from '@/components/ui/skeleton' +import LoadingWrapper from '@/components/loading-wrapper' +import { SubHeader } from '@/components/sub-header' + +import { AnnualActivity } from './annual-activity' +import { ChatDailyActivity } from './chat-daily-activity' +import { CompletionDailyActivity } from './completion-daily-activity' +import { KEY_SELECT_ALL } from './constants' + +export function Report() { + const searchParams = useSearchParams() + const [members, fetchingMembers] = useAllMembers() + const isDemoMode = useIsDemoMode() + const [selectedMember, setSelectedMember] = useState(KEY_SELECT_ALL) + const sample = isDemoMode || searchParams.get('sample') === 'true' + + const { + dailyData, + fetching: fetchingYearlyStats, + totalCount + } = useYearlyStats({ + selectedMember, + sample + }) + + return ( +
    +
    + + Statistics around Tabby IDE / Extensions + + + } + > + + +
    + + {/* Yearly */} + } + > +
    +

    + Activity +

    + +
    +
    + + {/* completions */} + + + {/* chats */} + {ENABLE_CHAT && ( + + )} +
    + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/reports/page.tsx b/ee/tabby-ui/app/(dashboard)/reports/page.tsx new file mode 100644 index 000000000000..ba6d8d8083c9 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/reports/page.tsx @@ -0,0 +1,11 @@ +import { Metadata } from 'next' + +import { Report } from './components/report' + +export const metadata: Metadata = { + title: 'Reports' +} + +export default function Page() { + return +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/mail/components/header.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/mail/components/header.tsx new file mode 100644 index 000000000000..a8cc6c111f1e --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/mail/components/header.tsx @@ -0,0 +1,10 @@ +import { SubHeader } from '@/components/sub-header' + +export const MailDeliveryHeader = ({ className }: { className?: string }) => { + return ( + + Configuring SMTP information will enable users to receive database reports + via email, such as slow query weekly reports. + + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/mail/components/mail-form.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/mail/components/mail-form.tsx new file mode 100644 index 000000000000..0eb6c6c3b356 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/mail/components/mail-form.tsx @@ -0,0 +1,353 @@ +'use client' + +import React from 'react' +import { zodResolver } from '@hookform/resolvers/zod' +import { isEmpty } from 'lodash-es' +import { useForm, UseFormReturn } from 'react-hook-form' +import { toast } from 'sonner' +import * as z from 'zod' + +import { graphql } from '@/lib/gql/generates' +import { AuthMethod, Encryption } from '@/lib/gql/generates/graphql' +import { useMutation } from '@/lib/tabby/gql' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger +} from '@/components/ui/alert-dialog' +import { Button, buttonVariants } from '@/components/ui/button' +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage +} from '@/components/ui/form' +import { Input } from '@/components/ui/input' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from '@/components/ui/select' + +const updateEmailSettingMutation = graphql(/* GraphQL */ ` + mutation updateEmailSetting($input: EmailSettingInput!) { + updateEmailSetting(input: $input) + } +`) + +const deleteEmailSettingMutation = graphql(/* GraphQL */ ` + mutation deleteEmailSetting { + deleteEmailSetting + } +`) + +const formSchema = z.object({ + smtpUsername: z.string(), + smtpPassword: z.string(), + smtpServer: z.string(), + smtpPort: z.coerce.number({ + invalid_type_error: 'Invalid port', + required_error: 'Required' + }), + fromAddress: z.string(), + encryption: z.nativeEnum(Encryption), + authMethod: z.nativeEnum(AuthMethod) +}) + +type MailFormValues = z.infer + +interface MailFormProps { + isNew?: boolean + defaultValues?: Partial + onSuccess?: () => void + onDelete?: () => void +} + +interface MailFormRef { + form: UseFormReturn +} + +const MailForm = React.forwardRef((props, ref) => { + const { + isNew, + onSuccess, + onDelete, + defaultValues: propsDefaultValues + } = props + const defaultValues = React.useMemo(() => { + return { + encryption: Encryption.None, + authMethod: AuthMethod.None, + ...(propsDefaultValues || {}) + } + }, [propsDefaultValues]) + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues + }) + const isDirty = !isEmpty(form.formState.dirtyFields) + const [deleteAlertVisible, setDeleteAlertVisible] = React.useState(false) + + const updateEmailSetting = useMutation(updateEmailSettingMutation, { + form, + onCompleted(data) { + if (data?.updateEmailSetting) { + onSuccess?.() + toast.success('Email configuration is updated.') + } + } + }) + + const deleteEmailSetting = useMutation(deleteEmailSettingMutation, { + onCompleted(data) { + if (data?.deleteEmailSetting) { + onDelete?.() + } + }, + onError(err) { + toast.error(err.message) + } + }) + + const handleDelete: React.MouseEventHandler = async e => { + e.preventDefault() + await deleteEmailSetting() + } + + const onSubmit = async (input: MailFormValues) => { + await updateEmailSetting({ + input: { + ...input, + smtpPassword: + input.smtpPassword !== propsDefaultValues?.smtpPassword + ? input.smtpPassword + : undefined + } + }) + } + + React.useImperativeHandle( + ref, + () => ({ + form + }), + [form] + ) + + return ( +
    +
    + +
    + ( + + SMTP Server Host + + + + + + )} + /> + ( + + SMTP Server Port + + + + + + )} + /> +
    + ( + + From + + + + + + )} + /> + ( + + Authentication Method + + + + )} + /> +
    + ( + + SMTP Username + + + + + + )} + /> + ( + + SMTP Password + + + + + + )} + /> +
    + ( + + Encryption + + + + )} + /> +
    + {!isNew && ( + + + + + + + + Are you absolutely sure? + + + This action cannot be undone. It will permanently delete + the current setting. + + + + Cancel + + Yes, delete it + + + + + )} + +
    + + +
    + + ) +}) + +MailForm.displayName = 'MailForm' + +export { MailForm } +export type { MailFormRef } diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/mail/components/mail-testing-form.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/mail/components/mail-testing-form.tsx new file mode 100644 index 000000000000..e58391e00e2e --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/mail/components/mail-testing-form.tsx @@ -0,0 +1,100 @@ +'use client' + +import * as React from 'react' +import { zodResolver } from '@hookform/resolvers/zod' +import { useForm } from 'react-hook-form' +import { toast } from 'sonner' +import * as z from 'zod' + +import { PLACEHOLDER_EMAIL_FORM } from '@/lib/constants' +import { graphql } from '@/lib/gql/generates/gql' +import { useMutation } from '@/lib/tabby/gql' +import { Button } from '@/components/ui/button' +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage +} from '@/components/ui/form' +import { IconSpinner } from '@/components/ui/icons' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' + +const sendTestEmailMutation = graphql(/* GraphQL */ ` + mutation SendTestEmail($to: String!) { + sendTestEmail(to: $to) + } +`) + +const formSchema = z.object({ + to: z.string().email('Invalid email address') +}) + +type FormValues = z.infer + +export default function MailTestingForm({ + onSuccess +}: { + onSuccess?: () => Promise +}) { + const form = useForm({ + resolver: zodResolver(formSchema) + }) + + const { isSubmitting } = form.formState + const sendTestEmail = useMutation(sendTestEmailMutation, { form }) + const onSubmit = (values: FormValues) => { + return sendTestEmail(values).then(res => { + if (res?.data?.sendTestEmail) { + toast.info( + 'A test email has been sent. If your configuration is correct, you should receive an email shortly.' + ) + onSuccess?.() + } + }) + } + + return ( +
    +
    + + +
    + ( + + + + + + + )} + /> + +
    + + +
    + + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/mail/components/mail.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/mail/components/mail.tsx new file mode 100644 index 000000000000..64901bac07d5 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/mail/components/mail.tsx @@ -0,0 +1,101 @@ +'use client' + +import React from 'react' +import { OperationResult } from 'urql' + +import { graphql } from '@/lib/gql/generates' +import { EmailSettingQuery } from '@/lib/gql/generates/graphql' +import { client } from '@/lib/tabby/gql' +import { ListSkeleton } from '@/components/skeleton' + +import { MailDeliveryHeader } from './header' +import { MailForm } from './mail-form' +import type { MailFormRef } from './mail-form' +import MailTestingForm from './mail-testing-form' + +const emailSetting = graphql(/* GraphQL */ ` + query emailSetting { + emailSetting { + smtpUsername + smtpServer + fromAddress + encryption + authMethod + smtpPort + } + } +`) + +const ENCODE_PASSWORD = '********************************' + +export const Mail = () => { + const [queryResult, setQueryResult] = + React.useState>() + const [initialized, setInitialized] = React.useState(false) + const mailFormRef = React.useRef(null) + + const queryEmailSettings = () => { + return client + .query(emailSetting, {}) + .toPromise() + .then(res => { + setQueryResult(res) + setInitialized(true) + return res + }) + } + + const isNew = !queryResult?.data?.emailSetting + + const handleMailFormSuccess = () => { + queryEmailSettings().then(res => { + const newEmailSettings = res?.data?.emailSetting + if (newEmailSettings) { + // reset latest settings + mailFormRef.current?.form?.reset({ + ...newEmailSettings, + smtpPassword: ENCODE_PASSWORD + }) + } + }) + } + + const handleMailFormDelete = () => { + // MailForm re-render + setInitialized(false) + queryEmailSettings() + } + + const defaultValues = isNew + ? {} + : { + ...queryResult?.data?.emailSetting, + smtpPassword: ENCODE_PASSWORD + } + + React.useEffect(() => { + queryEmailSettings() + }, []) + + return ( + <> + + {initialized ? ( +
    +
    + +
    + +
    + ) : ( + + )} + + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/mail/page.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/mail/page.tsx new file mode 100644 index 000000000000..18b06fdae4f0 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/mail/page.tsx @@ -0,0 +1,11 @@ +import { Metadata } from 'next' + +import { Mail } from './components/mail' + +export const metadata: Metadata = { + title: 'Mail Delivery' +} + +export default function MailPage() { + return +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/[kind]/components/common-provider-form.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/[kind]/components/common-provider-form.tsx new file mode 100644 index 000000000000..3de753f59afb --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/[kind]/components/common-provider-form.tsx @@ -0,0 +1,392 @@ +'use client' + +import * as React from 'react' +import Link from 'next/link' +import { useRouter } from 'next/navigation' +import { zodResolver } from '@hookform/resolvers/zod' +import { isEmpty } from 'lodash-es' +import { DefaultValues, useForm, UseFormReturn } from 'react-hook-form' +import { toast } from 'sonner' +import * as z from 'zod' + +import { IntegrationKind } from '@/lib/gql/generates/graphql' +import { cn } from '@/lib/utils' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger +} from '@/components/ui/alert-dialog' +import { Button, buttonVariants } from '@/components/ui/button' +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from '@/components/ui/form' +import { IconExternalLink, IconSpinner } from '@/components/ui/icons' +import { Input } from '@/components/ui/input' + +import { useIntegrationKind } from '../hooks/use-repository-kind' + +export const createRepositoryProviderFormSchema = z.object({ + displayName: z.string().trim(), + accessToken: z.string(), + apiBase: z.string().url().optional().nullable() +}) + +const createSelfHostedRepositoryProviderFormSchema = + createRepositoryProviderFormSchema.extend({ + // for githubSelfHosted & gitlabSelfHosted, apiBase is required + apiBase: z.string().url() + }) + +export const updateRepositoryProviderFormSchema = + createRepositoryProviderFormSchema.extend({ + accessToken: z.string().optional() + }) + +export const updateSelfHostedRepositoryProviderFormSchema = + createSelfHostedRepositoryProviderFormSchema.extend({ + accessToken: z.string().optional() + }) + +export type CreateRepositoryProviderFormValues = z.infer< + typeof createRepositoryProviderFormSchema +> + +export type UpdateRepositoryProviderFormValues = z.infer< + typeof updateRepositoryProviderFormSchema +> + +type FormValues = T extends true + ? CreateRepositoryProviderFormValues + : UpdateRepositoryProviderFormValues + +interface GithubProviderFormProps { + isNew: T + form: UseFormReturn + onSubmit: (values: any) => Promise + onDelete?: () => Promise + cancleable?: boolean + deletable?: boolean +} + +export function CommonProviderForm({ + isNew, + form, + onSubmit, + onDelete, + cancleable = true, + deletable +}: GithubProviderFormProps) { + const kind = useIntegrationKind() + const router = useRouter() + + const [deleteAlertVisible, setDeleteAlertVisible] = React.useState(false) + const [isDeleting, setIsDeleting] = React.useState(false) + + const { isSubmitting, dirtyFields } = form.formState + const isDirty = !isEmpty(dirtyFields) + + const handleDeleteRepositoryProvider: React.MouseEventHandler< + HTMLButtonElement + > = async e => { + e.preventDefault() + if (!onDelete) return + + setIsDeleting(true) + try { + await onDelete() + } catch (error) { + toast.error('Failed to delete GitHub repository provider') + } finally { + setIsDeleting(false) + } + } + + const displayNamePlaceholder = React.useMemo(() => { + switch (kind) { + case IntegrationKind.Github: + return 'e.g. GitHub' + case IntegrationKind.GithubSelfHosted: + return 'e.g. GitHub-Self-Hosted' + case IntegrationKind.Gitlab: + return 'e.g. GitLab' + case IntegrationKind.GitlabSelfHosted: + return 'e.g. GitLab-Self-Hosted' + default: + return '' + } + }, [kind]) + + const accessTokenPlaceholder = React.useMemo(() => { + if (!isNew) return new Array(36).fill('*').join('') + switch (kind) { + case IntegrationKind.Github: + case IntegrationKind.GithubSelfHosted: + return 'e.g. github_pat_1ABCD1234ABCD1234ABCD1234ABCD1234ABCD1234' + case IntegrationKind.Gitlab: + case IntegrationKind.GitlabSelfHosted: + return 'e.g. glpat_1ABCD1234ABCD1234ABCD1234ABCD1234' + default: + return '' + } + }, [kind, isNew]) + + const apiBasePlaceholder = React.useMemo(() => { + switch (kind) { + case IntegrationKind.GithubSelfHosted: + return 'e.g. https://api.github.yourcompany.com' + case IntegrationKind.GitlabSelfHosted: + return 'e.g. https://gitlab.yourcompany.com' + default: + return '' + } + }, [kind]) + + const showApiBase = [ + IntegrationKind.GithubSelfHosted, + IntegrationKind.GitlabSelfHosted + ].includes(kind) + + return ( +
    +
    + + ( + + Display name + + A display name to help identifying different providers. + + + + + + + )} + /> + {showApiBase && ( + ( + + Instance URL + + The VCS instance URL. Make sure this instance and Tabby are + network reachable from each other. + + + field.onChange(e.target.value)} + /> + + + + )} + /> + )} + ( + + Personal Access Token + + + + + + + + + )} + /> +
    +
    + +
    +
    + {cancleable && ( + + )} + {deletable && ( + + + + + + + + Are you absolutely sure? + + + This will delete the provider and remove any + repositories that have already been added to the + provider. + + + + Cancel + + {isDeleting && } + Yes, delete it + + + + + )} + +
    +
    + +
    + + ) +} + +export function useRepositoryProviderForm( + isNew: T, + kind: IntegrationKind, + defaultValues?: Partial> +): UseFormReturn> { + let isSelfHostedIntegration = [ + IntegrationKind.GithubSelfHosted, + IntegrationKind.GitlabSelfHosted + ].includes(kind) + const schema = isNew + ? isSelfHostedIntegration + ? createSelfHostedRepositoryProviderFormSchema + : createRepositoryProviderFormSchema + : isSelfHostedIntegration + ? updateSelfHostedRepositoryProviderFormSchema + : updateRepositoryProviderFormSchema + + return useForm>({ + resolver: zodResolver(schema), + defaultValues: defaultValues as DefaultValues> + }) +} + +function AccessTokenDescription() { + const kind = useIntegrationKind() + + if ( + kind === IntegrationKind.Github || + kind === IntegrationKind.GithubSelfHosted + ) { + return ( + <> +
    + Create a dedicated service user and generate a{' '} + + fine-grained personal access + {' '} + token with the member role for the organization or all projects to be + managed. +
    +
    • Contents (Read-only)
    +
    • Pull requests (Read-only)
    +
    • Issues (Read-only)
    + + ) + } + + if ( + kind === IntegrationKind.Gitlab || + kind === IntegrationKind.GitlabSelfHosted + ) { + return ( + <> +
    + Create a dedicated service user and generate a{' '} + + personal access token + {' '} + with the maintainer role and at least following permissions for the + group or projects to be managed. You can generate a project access + token for managing a single project, or generate a group access token + to manage all projects within the group. +
    +
    • api
    + + ) + } + + return null +} + +function ExternalLink({ + href, + children +}: { + href: string + children: React.ReactNode +}) { + return ( + + {children} + + + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/[kind]/components/provider-list.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/[kind]/components/provider-list.tsx new file mode 100644 index 000000000000..38414c67f40d --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/[kind]/components/provider-list.tsx @@ -0,0 +1,165 @@ +'use client' + +import React from 'react' +import Link from 'next/link' +import { useParams } from 'next/navigation' +import { useQuery } from 'urql' + +import { DEFAULT_PAGE_SIZE } from '@/lib/constants' +import { + IntegrationStatus, + ListIntegrationsQuery +} from '@/lib/gql/generates/graphql' +import { listIntegrations } from '@/lib/tabby/query' +import { buttonVariants } from '@/components/ui/button' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Skeleton } from '@/components/ui/skeleton' +import { LoadMoreIndicator } from '@/components/load-more-indicator' +import LoadingWrapper from '@/components/loading-wrapper' + +import { useIntegrationKind } from '../hooks/use-repository-kind' + +const PAGE_SIZE = DEFAULT_PAGE_SIZE + +export default function RepositoryProvidersPage() { + const kind = useIntegrationKind() + const params = useParams() + const [lastCursor, setLastCursor] = React.useState( + undefined + ) + const [{ data, fetching }] = useQuery({ + query: listIntegrations, + variables: { kind, last: PAGE_SIZE, before: lastCursor } + }) + + const edges = React.useMemo(() => { + return data?.integrations?.edges?.slice().reverse() + }, [data?.integrations?.edges]) + const pageInfo = data?.integrations?.pageInfo + + const loadMore = () => { + if (pageInfo?.startCursor) { + setLastCursor(pageInfo.startCursor) + } + } + + return ( + }> + {edges?.length ? ( + <> + +
    + {edges?.map(item => { + return ( + + +
    + +
    + {item.node.displayName} +
    +
    + + View + +
    +
    + +
    + + Status + + {toStatusMessage(item.node)} +
    +
    +
    + ) + })} + {!!pageInfo?.hasPreviousPage && ( + + + + )} +
    + + ) : ( + + )} +
    + ) +} + +function CreateRepositoryProvider() { + const params = useParams() + return ( +
    + + Create + +
    + ) +} + +function toStatusMessage( + node: ListIntegrationsQuery['integrations']['edges'][0]['node'] +) { + switch (node.status) { + case IntegrationStatus.Ready: + return 'Ready' + case IntegrationStatus.Failed: + return ( + node.message || + 'Processing error. Please check if the access token is still valid' + ) + case IntegrationStatus.Pending: + return 'Awaiting the next data synchronization' + } +} + +function ProvidersPlaceholder() { + const params = useParams() + return ( +
    +
    No Data
    +
    + + Create + +
    +
    + ) +} + +function CardSkeleton() { + return ( + + + + + + + + + + + ) +} + +function FetchingSkeletion() { + return ( +
    + + +
    + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/[kind]/detail/components/add-repository-form.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/[kind]/detail/components/add-repository-form.tsx new file mode 100644 index 000000000000..5019e8fb84c0 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/[kind]/detail/components/add-repository-form.tsx @@ -0,0 +1,248 @@ +'use client' + +import * as React from 'react' +import { zodResolver } from '@hookform/resolvers/zod' +import { useForm } from 'react-hook-form' +import * as z from 'zod' + +import { + IntegrationKind, + IntegrationStatus, + ListIntegratedRepositoriesQuery +} from '@/lib/gql/generates/graphql' +import { useMutation } from '@/lib/tabby/gql' +import { cn } from '@/lib/utils' +import { Button } from '@/components/ui/button' +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList +} from '@/components/ui/command' +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from '@/components/ui/form' +import { + IconCheck, + IconChevronUpDown, + IconSpinner +} from '@/components/ui/icons' +import { + Popover, + PopoverContent, + PopoverTrigger +} from '@/components/ui/popover' +import { TagInput } from '@/components/ui/tag-input' + +import { updateIntegratedRepositoryActiveMutation } from '../query' + +const formSchema = z.object({ + id: z.string(), + refs: z.array(z.string()).optional() +}) + +type ActivateRepositoryFormValues = z.infer + +interface ActivateRepositoryFormProps { + kind: IntegrationKind + onCreated?: (id: string) => void + onCancel: () => void + providerStatus: IntegrationStatus | undefined + repositories: + | ListIntegratedRepositoriesQuery['integratedRepositories']['edges'] + | undefined + fetchingRepos: boolean +} + +export default function AddRepositoryForm({ + onCreated, + onCancel, + repositories, + providerStatus, + fetchingRepos +}: ActivateRepositoryFormProps) { + const [open, setOpen] = React.useState(false) + const form = useForm({ + resolver: zodResolver(formSchema) + }) + const commandListRef = React.useRef(null) + + const { isSubmitting } = form.formState + + const emptyText = React.useMemo(() => { + switch (providerStatus) { + case IntegrationStatus.Pending: + return 'Awaiting the next data synchronization' + case IntegrationStatus.Failed: + return 'Synchronizing error. Please check if the access token is still valid' + default: + return 'No repository found' + } + }, [providerStatus]) + + const updateProvidedRepositoryActive = useMutation( + updateIntegratedRepositoryActiveMutation, + { + form + } + ) + + const onSubmit = async (values: ActivateRepositoryFormValues) => { + const id = values.id + const refs = values.refs?.length ? values.refs : [] + + const activeRes = await updateProvidedRepositoryActive({ + id, + active: true, + refs + }) + + if (activeRes?.data?.updateIntegratedRepositoryActive) { + form.reset({ id: undefined, refs: [] }) + onCreated?.(id) + } + } + + const scrollCommandListToTop = () => { + requestAnimationFrame(() => { + if (commandListRef.current) { + commandListRef.current.scrollTop = 0 + } + }) + } + + const onSearchChange = () => { + scrollCommandListToTop() + } + + return ( +
    +
    + + ( + + + + + + + + + + + + + {fetchingRepos ? ( +
    + +
    + ) : ( + emptyText + )} +
    + + {providerStatus !== IntegrationStatus.Pending && + repositories?.map(repo => ( + { + form.setValue('id', repo.node.id) + setOpen(false) + }} + > + + {repo.node.gitUrl} + + ))} + +
    +
    +
    +
    + +
    + )} + /> + ( + + Branches + + Branches to index (press Enter to select, leave empty for + default branch) + + + + + + + )} + /> +
    + + +
    + + +
    + + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/[kind]/detail/components/provider-detail.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/[kind]/detail/components/provider-detail.tsx new file mode 100644 index 000000000000..f7689c83ef5a --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/[kind]/detail/components/provider-detail.tsx @@ -0,0 +1,764 @@ +'use client' + +import React, { useEffect, useState } from 'react' +import { useRouter, useSearchParams } from 'next/navigation' +import { zodResolver } from '@hookform/resolvers/zod' +import { useForm } from 'react-hook-form' +import { toast } from 'sonner' +import useSWR from 'swr' +import { useQuery } from 'urql' +import * as z from 'zod' + +import { DEFAULT_PAGE_SIZE } from '@/lib/constants' +import { + IntegrationKind, + IntegrationStatus, + ListIntegratedRepositoriesQuery, + ListIntegrationsQuery +} from '@/lib/gql/generates/graphql' +import { useDebounceCallback } from '@/lib/hooks/use-debounce' +import { + client, + QueryResponseData, + QueryVariables, + useMutation +} from '@/lib/tabby/gql' +import { + listIntegratedRepositories, + listIntegrations, + userGroupsQuery +} from '@/lib/tabby/query' +import { Badge } from '@/components/ui/badge' +import { Button, buttonVariants } from '@/components/ui/button' +import { CardContent, CardTitle } from '@/components/ui/card' +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle +} from '@/components/ui/dialog' +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from '@/components/ui/form' +import { + IconChevronLeft, + IconChevronRight, + IconPencil, + IconPlus, + IconSpinner, + IconTrash +} from '@/components/ui/icons' +import { Input } from '@/components/ui/input' +import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from '@/components/ui/table' +import { TagInput } from '@/components/ui/tag-input' +import { + Tooltip, + TooltipContent, + TooltipTrigger +} from '@/components/ui/tooltip' +import LoadingWrapper from '@/components/loading-wrapper' +import { ListSkeleton } from '@/components/skeleton' + +import { AccessPolicyView } from '../../../components/access-policy-view' +import { JobInfoView } from '../../../components/job-trigger' +import { triggerJobRunMutation } from '../../../query' +import { useIntegrationKind } from '../../hooks/use-repository-kind' +import { + updateIntegratedRepositoryActiveMutation, + updateIntegratedRepositoryRefsMutation +} from '../query' +import AddRepositoryForm from './add-repository-form' +import { UpdateProviderForm } from './update-provider-form' + +const PAGE_SIZE = DEFAULT_PAGE_SIZE + +type IntegratedRepositories = + ListIntegratedRepositoriesQuery['integratedRepositories']['edges'] + +const ProviderDetail: React.FC = () => { + const searchParams = useSearchParams() + const kind = useIntegrationKind() + const router = useRouter() + const id = searchParams.get('id')?.toString() ?? '' + + const [{ data, fetching }, reexecuteQuery] = useQuery({ + query: listIntegrations, + variables: { ids: [id], kind }, + pause: !id || !kind + }) + const provider = data?.integrations?.edges?.[0]?.node + const shouldRefreshProvider = provider?.status === IntegrationStatus.Pending + + useSWR( + shouldRefreshProvider ? 'refresh' : null, + () => { + reexecuteQuery() + }, + { + revalidateOnFocus: true, + revalidateOnReconnect: true, + revalidateOnMount: false, + refreshInterval: 5 * 1000 + } + ) + + const onDeleteProvider = () => { + router.back() + } + + const onUpdateProvider = () => { + reexecuteQuery() + } + + if (!id || (!!id && !fetching && !provider)) { + return ( +
    + Provider not found +
    + ) + } + + return ( + + +
    + + {provider?.displayName} +
    +
    +
    {provider && toStatusBadge(provider)}
    +
    +
    + + }> + + + + + + + + +
    + ) +} + +const ActiveRepoTable: React.FC<{ + providerId: string + providerStatus: IntegrationStatus | undefined + kind: IntegrationKind +}> = ({ providerStatus, providerId, kind }) => { + const [page, setPage] = React.useState(1) + const [{ data: userGroupData, fetching: fetchingUserGroups }] = useQuery({ + query: userGroupsQuery + }) + const { + repositories: inactiveRepositories, + setRepositories: setInactiveRepositories, + isAllLoaded: isInactiveRepositoriesLoaded + } = useAllInactiveRepositories(providerId, kind) + + const fetchRepositories = ( + variables: QueryVariables + ) => { + return client.query(listIntegratedRepositories, variables).toPromise() + } + + const fetchRepositoriesSequentially = async ( + page: number, + cursor?: string + ): Promise< + QueryResponseData | undefined + > => { + const res = await fetchRepositories({ + ids: [providerId], + first: PAGE_SIZE, + after: cursor, + active: true, + kind + }) + const responseData = res?.data?.integratedRepositories + const _pageInfo = responseData?.pageInfo + if (page - 1 > 0 && _pageInfo?.hasNextPage && _pageInfo?.endCursor) { + return fetchRepositoriesSequentially(page - 1, _pageInfo.endCursor) + } else { + return res?.data + } + } + + useSWR( + ['refresh_repos', page], + ([, p]) => { + fetchRepositoriesSequentially(p).then(res => + setActiveRepositoriesResult(res) + ) + }, + { + revalidateOnFocus: true, + revalidateOnReconnect: true, + revalidateOnMount: false, + refreshInterval: 10 * 1000 + } + ) + + const [activeRepositoriesResult, setActiveRepositoriesResult] = + React.useState>() + const [fetching, setFetching] = React.useState(true) + const [recentlyActivatedRepositories, setRecentlyActivatedRepositories] = + React.useState([]) + const activeRepos = activeRepositoriesResult?.integratedRepositories?.edges + const pageInfo = activeRepositoriesResult?.integratedRepositories?.pageInfo + const [editingRepo, setEditingRepo] = React.useState<{ + id: string + displayName: string + gitUrl: string + refs: string[] + } | null>(null) + + const updateProvidedRepositoryActive = useMutation( + updateIntegratedRepositoryActiveMutation, + { + onError(error) { + toast.error(error.message || 'Failed to delete') + } + } + ) + + const updateProvidedRepositoryRefs = useMutation( + updateIntegratedRepositoryRefsMutation, + { + onError(error) { + toast.error(error.message || 'Failed to update') + } + } + ) + + const handleUpdateRepository = (values: { refs?: string[] }) => { + if (!editingRepo) return + + updateProvidedRepositoryRefs({ + id: editingRepo.id, + refs: values.refs && values.refs.length > 0 ? values.refs : [] + }).then(res => { + if (res?.data?.updateIntegratedRepositoryRefs) { + toast.success('Repository updated successfully') + setEditingRepo(null) + loadPage(page) + } + }) + } + + const handleEditRepository = (repo: { + id: string + displayName: string + gitUrl: string + refs: Array<{ name: string }> + }) => { + setEditingRepo({ + id: repo.id, + displayName: repo.displayName, + gitUrl: repo.gitUrl, + refs: repo.refs.map(r => { + // Extract branch name from refs/heads/xxx or refs/tags/xxx + if (r.name.startsWith('refs/heads/')) { + return r.name.substring('refs/heads/'.length) + } else if (r.name.startsWith('refs/tags/')) { + return r.name.substring('refs/tags/'.length) + } + return r.name + }) + }) + } + + const triggerJobRun = useMutation(triggerJobRunMutation) + + const handleDelete = async ( + repo: IntegratedRepositories[0], + isLastItem?: boolean + ) => { + updateProvidedRepositoryActive({ + id: repo.node.id, + active: false + }).then(res => { + if (res?.data?.updateIntegratedRepositoryActive) { + setInactiveRepositories(sortRepos([...inactiveRepositories, repo])) + const nextPage = isLastItem ? page - 1 : page + loadPage(nextPage || 1) + } + }) + } + + const loadPage = async (pageNo: number) => { + try { + setFetching(true) + const res = await fetchRepositoriesSequentially(pageNo) + setActiveRepositoriesResult(res) + setPage(pageNo) + } catch (e) { + } finally { + setFetching(false) + } + } + + const clearRecentlyActivated = useDebounceCallback((page: number) => { + setRecentlyActivatedRepositories([]) + loadPage(page) + }, 3000) + + const [open, setOpen] = React.useState(false) + + const sortRepos = (repos: IntegratedRepositories) => { + if (!repos?.length) return repos + return repos.sort((a, b) => + a.node.displayName?.localeCompare(b.node.displayName) + ) + } + + const onCreated = (id: string) => { + const activedRepo = inactiveRepositories?.find(o => o?.node?.id === id) + if (activedRepo) { + setRecentlyActivatedRepositories([ + activedRepo, + ...recentlyActivatedRepositories + ]) + setInactiveRepositories(repos => + sortRepos(repos.filter(o => o.node.id !== id)) + ) + clearRecentlyActivated.run(page) + } + setOpen(false) + } + + const handleLoadPage = (page: number) => { + clearRecentlyActivated.cancel() + setRecentlyActivatedRepositories([]) + loadPage(page) + } + + const handleTriggerJobRun = (command: string) => { + return triggerJobRun({ command }).then(res => { + if (res?.data?.triggerJobRun) { + toast.success( + 'The job has been triggered successfully, it may take a few minutes to process.' + ) + handleLoadPage(page) + } else { + toast.error(res?.error?.message || 'Failed to trigger job') + } + }) + } + + React.useEffect(() => { + loadPage(1) + + return () => clearRecentlyActivated.cancel() + }, []) + + return ( + <> + + + + + + Name + URL + Access + Job + + + + + + + {activeRepos?.length || recentlyActivatedRepositories?.length ? ( + <> + {recentlyActivatedRepositories?.map(x => { + return ( + + + {x.node.displayName} + + + {x.node.gitUrl} + + + + +
    + +
    +
    +
    + ) + })} + {activeRepos?.map(x => { + return ( + + + {x.node.displayName} + + + {x.node.gitUrl} + + + + + + + handleTriggerJobRun(x.node.jobInfo.command) + } + /> + + +
    + + +
    +
    +
    + ) + })} + + ) : ( + + +
    + No repositories + +
    +
    +
    + )} +
    +
    + +
    + {(page > 1 || pageInfo?.hasNextPage) && ( +
    +
    + {' '} + Page {page} +
    +
    + + +
    +
    + )} +
    + + + + Add new repository + + Add new repository from this provider + + + setOpen(false)} + onCreated={onCreated} + repositories={inactiveRepositories} + kind={kind} + providerStatus={providerStatus} + fetchingRepos={!isInactiveRepositoriesLoaded} + /> + + + { + if (!open) setEditingRepo(null) + }} + onSubmit={handleUpdateRepository} + /> + + ) +} + +const editFormSchema = z.object({ + refs: z.array(z.string()).optional() +}) + +type EditFormValues = z.infer + +function EditRepositoryDialog({ + repo, + open, + onOpenChange, + onSubmit +}: { + repo: { + id: string + displayName: string + gitUrl: string + refs: string[] + } | null + open: boolean + onOpenChange: (open: boolean) => void + onSubmit: (values: EditFormValues) => void +}) { + const form = useForm({ + resolver: zodResolver(editFormSchema) + }) + + React.useEffect(() => { + if (repo) { + form.reset({ + refs: repo.refs + }) + } + }, [repo, form]) + + const { isSubmitting } = form.formState + + return ( + + + + Edit Repository + + Update the repository branches to index + + +
    + + + Name + + + + + + + Git URL + Remote or local Git URL + + + + + + ( + + Branches + + Branches to index (press Enter to select, leave empty for + default branch) + + + + + + + )} + /> +
    + + +
    + + +
    +
    + ) +} + +function toStatusBadge( + node: ListIntegrationsQuery['integrations']['edges'][0]['node'] +) { + switch (node.status) { + case IntegrationStatus.Ready: + return Ready + case IntegrationStatus.Failed: { + return ( + + + Error + + + {node.message ? ( +
    +

    {node.message}

    + Please verify your context provider settings to resolve the + issue +
    + ) : ( +

    + Processing error. Please check if the access token is still + valid +

    + )} +
    +
    + ) + } + case IntegrationStatus.Pending: + return Pending + } +} + +function useAllInactiveRepositories(id: string, kind: IntegrationKind) { + const [queryVariables, setQueryVariables] = useState< + QueryVariables + >({ ids: [id], first: PAGE_SIZE, active: false, kind }) + const [repositories, setRepositories] = useState([]) + const [isAllLoaded, setIsAllLoaded] = useState(!id) + + const [{ data, fetching }] = useQuery({ + query: listIntegratedRepositories, + variables: queryVariables, + pause: !id + }) + + useEffect(() => { + if (isAllLoaded) return + if (!fetching && data) { + const pageInfo = data?.integratedRepositories?.pageInfo + const currentList = [...repositories] + setRepositories(currentList.concat(data?.integratedRepositories?.edges)) + + if (pageInfo?.hasNextPage) { + setQueryVariables({ + ids: [id], + first: PAGE_SIZE, + after: pageInfo.endCursor, + active: false + }) + } else { + setIsAllLoaded(true) + } + } + }, [fetching, data]) + + return { + repositories, + setRepositories, + isAllLoaded + } +} + +export default ProviderDetail diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/[kind]/detail/components/update-provider-form.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/[kind]/detail/components/update-provider-form.tsx new file mode 100644 index 000000000000..a58567266193 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/[kind]/detail/components/update-provider-form.tsx @@ -0,0 +1,88 @@ +'use client' + +import React from 'react' +import { toast } from 'sonner' + +import { graphql } from '@/lib/gql/generates' +import { IntegrationKind } from '@/lib/gql/generates/graphql' +import { useMutation } from '@/lib/tabby/gql' + +import { + CommonProviderForm, + CreateRepositoryProviderFormValues, + useRepositoryProviderForm +} from '../../components/common-provider-form' + +const updateIntegrationMutation = graphql(/* GraphQL */ ` + mutation UpdateIntegration($input: UpdateIntegrationInput!) { + updateIntegration(input: $input) + } +`) + +const deleteIntegrationMutation = graphql(/* GraphQL */ ` + mutation DeleteIntegration($id: ID!, $kind: IntegrationKind!) { + deleteIntegration(id: $id, kind: $kind) + } +`) + +interface UpdateProviderFormProps { + id: string + kind: IntegrationKind + defaultValues?: Partial + onSuccess?: () => void + onDelete: () => void + onUpdate: () => void +} + +export const UpdateProviderForm: React.FC = ({ + defaultValues, + onSuccess, + onDelete, + onUpdate, + id, + kind +}) => { + const form = useRepositoryProviderForm(false, kind, defaultValues) + + const deleteRepositoryProvider = useMutation(deleteIntegrationMutation) + + const updateRepositoryProvider = useMutation(updateIntegrationMutation, { + form + }) + + const onSubmit = async (values: CreateRepositoryProviderFormValues) => { + const res = await updateRepositoryProvider({ + input: { + id, + ...values, + kind + } + }) + if (res?.data?.updateIntegration) { + toast.success('Updated provider successfully') + form?.reset(form?.getValues()) + onSuccess?.() + onUpdate?.() + } + } + + const handleDeleteRepositoryProvider = async () => { + const res = await deleteRepositoryProvider({ id, kind }) + if (res?.data?.deleteIntegration) { + onDelete?.() + } else { + toast.error(res?.error?.message || 'Failed to delete provider') + } + } + + return ( + + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/[kind]/detail/page.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/[kind]/detail/page.tsx new file mode 100644 index 000000000000..c2ed906949c1 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/[kind]/detail/page.tsx @@ -0,0 +1,5 @@ +import ProviderDetail from './components/provider-detail' + +export default function IndexPage() { + return +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/[kind]/detail/query.ts b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/[kind]/detail/query.ts new file mode 100644 index 000000000000..d98b035c3af1 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/[kind]/detail/query.ts @@ -0,0 +1,17 @@ +import { graphql } from '@/lib/gql/generates' + +export const updateIntegratedRepositoryActiveMutation = graphql(/* GraphQL */ ` + mutation UpdateIntegratedRepositoryActive( + $id: ID! + $active: Boolean! + $refs: [String!] + ) { + updateIntegratedRepositoryActive(id: $id, active: $active, refs: $refs) + } +`) + +export const updateIntegratedRepositoryRefsMutation = graphql(/* GraphQL */ ` + mutation UpdateIntegratedRepositoryRefs($id: ID!, $refs: [String!]!) { + updateIntegratedRepositoryRefs(id: $id, refs: $refs) + } +`) diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/[kind]/hooks/use-repository-kind.ts b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/[kind]/hooks/use-repository-kind.ts new file mode 100644 index 000000000000..b93e40ffd376 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/[kind]/hooks/use-repository-kind.ts @@ -0,0 +1,17 @@ +import { useParams } from 'next/navigation' +import { findIndex } from 'lodash-es' + +import { PROVIDER_KIND_METAS } from '../../constants' + +export function useIntegrationKind() { + const params = useParams<{ kind?: string }>() + const kindIndex = findIndex( + PROVIDER_KIND_METAS, + item => item.name === params.kind?.toLowerCase() + ) + const kind = + kindIndex > -1 + ? PROVIDER_KIND_METAS[kindIndex].enum + : PROVIDER_KIND_METAS[0].enum + return kind +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/[kind]/layout.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/[kind]/layout.tsx new file mode 100644 index 000000000000..fc1a0cd32800 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/[kind]/layout.tsx @@ -0,0 +1,65 @@ +import { findIndex } from 'lodash-es' + +import { IntegrationKind } from '@/lib/gql/generates/graphql' +import { SubHeader } from '@/components/sub-header' + +import { PROVIDER_KIND_METAS } from '../constants' + +type Params = { kind: string } + +export function generateStaticParams() { + return PROVIDER_KIND_METAS.map(item => ({ kind: item.name })) +} + +export default function IntegrationProviderLayout({ + children, + params +}: { + children: React.ReactNode + params: Params +}) { + const kindIndex = findIndex( + PROVIDER_KIND_METAS, + item => item.name === params.kind?.toLocaleLowerCase() + ) + const kind = + kindIndex > -1 + ? PROVIDER_KIND_METAS[kindIndex].enum + : PROVIDER_KIND_METAS[0].enum + + return ( + <> + + {children} + + ) +} + +function IntegrationHeader({ kind }: { kind: IntegrationKind }) { + let text = '' + + switch (kind) { + case IntegrationKind.Github: + text = + 'Connect to GitHub as a provider, and select repositories from this provider to serve as context, thereby improving the performance of large language models' + break + case IntegrationKind.GithubSelfHosted: + text = + 'Connect to Self-Hosted GitHub as a provider, and select repositories from this provider to serve as context, thereby improving the performance of large language models' + break + case IntegrationKind.Gitlab: + text = + 'Connect to GitLab as a provider, and select repositories from this provider to serve as context, thereby improving the performance of large language models' + break + case IntegrationKind.GitlabSelfHosted: + text = + 'Connect to Self-Hosted GitLab as a provider, and select repositories from this provider to serve as context, thereby improving the performance of large language models' + break + } + + return ( + + {text} + + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/[kind]/new/components/new-page.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/[kind]/new/components/new-page.tsx new file mode 100644 index 000000000000..a09b431135a7 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/[kind]/new/components/new-page.tsx @@ -0,0 +1,56 @@ +'use client' + +import React from 'react' +import { useRouter } from 'next/navigation' +import { UseFormReturn } from 'react-hook-form' + +import { graphql } from '@/lib/gql/generates' +import { useMutation } from '@/lib/tabby/gql' + +import { + CommonProviderForm, + CreateRepositoryProviderFormValues, + UpdateRepositoryProviderFormValues, + useRepositoryProviderForm +} from '../../components/common-provider-form' +import { useIntegrationKind } from '../../hooks/use-repository-kind' + +const createIntegration = graphql(/* GraphQL */ ` + mutation CreateIntegration($input: CreateIntegrationInput!) { + createIntegration(input: $input) + } +`) + +export const NewProvider = () => { + const kind = useIntegrationKind() + const router = useRouter() + const form = useRepositoryProviderForm(true, kind) + + const createRepositoryProviderMutation = useMutation(createIntegration, { + onCompleted(data) { + if (data?.createIntegration) { + router.back() + } + }, + form + }) + + const handleSubmit = async (values: CreateRepositoryProviderFormValues) => { + return createRepositoryProviderMutation({ + input: { + ...values, + kind + } + }) + } + + return ( +
    + } + onSubmit={handleSubmit} + /> +
    + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/[kind]/new/page.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/[kind]/new/page.tsx new file mode 100644 index 000000000000..22c998b7c6d7 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/[kind]/new/page.tsx @@ -0,0 +1,5 @@ +import { NewProvider } from './components/new-page' + +export default function IndexPage() { + return +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/[kind]/page.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/[kind]/page.tsx new file mode 100644 index 000000000000..e961c7713314 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/[kind]/page.tsx @@ -0,0 +1,5 @@ +import RepositoryProvidersPage from './components/provider-list' + +export default function IntegrateGitPage() { + return +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/components/access-policy-view.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/components/access-policy-view.tsx new file mode 100644 index 000000000000..2c4fb4d4d707 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/components/access-policy-view.tsx @@ -0,0 +1,225 @@ +import { HTMLAttributes, useMemo, useState } from 'react' +import { CheckIcon } from '@radix-ui/react-icons' +import { toast } from 'sonner' +import { useQuery } from 'urql' + +import { graphql } from '@/lib/gql/generates' +import { UserGroupsQuery } from '@/lib/gql/generates/graphql' +import { useMutation } from '@/lib/tabby/gql' +import { listSourceIdAccessPolicies } from '@/lib/tabby/query' +import { cn } from '@/lib/utils' +import { Button } from '@/components/ui/button' +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList +} from '@/components/ui/command' +import { IconEdit, IconSpinner } from '@/components/ui/icons' +import { + Popover, + PopoverContent, + PopoverTrigger +} from '@/components/ui/popover' +import { Skeleton } from '@/components/ui/skeleton' +import LoadingWrapper from '@/components/loading-wrapper' + +const grantSourceIdReadAccessMutation = graphql(/* GraphQL */ ` + mutation grantSourceIdReadAccess($sourceId: String!, $userGroupId: ID!) { + grantSourceIdReadAccess(sourceId: $sourceId, userGroupId: $userGroupId) + } +`) + +const revokeSourceIdReadAccessMutation = graphql(/* GraphQL */ ` + mutation revokeSourceIdReadAccess($sourceId: String!, $userGroupId: ID!) { + revokeSourceIdReadAccess(sourceId: $sourceId, userGroupId: $userGroupId) + } +`) + +interface AccessPolicyViewProps extends HTMLAttributes { + sourceId: string + sourceName: string + editable: boolean + userGroups: UserGroupsQuery['userGroups'] | undefined + fetchingUserGroups: boolean +} + +export function AccessPolicyView({ + sourceId, + sourceName, + className, + editable, + userGroups, + fetchingUserGroups, + ...rest +}: AccessPolicyViewProps) { + const [open, setOpen] = useState(false) + const [{ data, fetching }] = useQuery({ + query: listSourceIdAccessPolicies, + variables: { + sourceId + } + }) + + const grantSourceIdReadAccess = useMutation(grantSourceIdReadAccessMutation) + const revokeSourceIdReadAccess = useMutation(revokeSourceIdReadAccessMutation) + + const sourceIdAccessPolicies = data?.sourceIdAccessPolicies?.read + const policiesLen = sourceIdAccessPolicies?.length || 0 + + const selectedIdSet = useMemo(() => { + if (!sourceIdAccessPolicies?.length) { + return new Set() + } + + return new Set(sourceIdAccessPolicies.map(policy => policy.id)) + }, [sourceIdAccessPolicies]) + + const handleSelectGroup = ( + userGroupId: string, + userGroupName: string, + grant: boolean + ) => { + if (grant) { + onGrantSourceIdReadAccess(userGroupId, userGroupName) + } else { + onRevokeSourceIdReadAccess(userGroupId, userGroupName) + } + } + + const onGrantSourceIdReadAccess = ( + userGroupId: string, + userGroupName: string + ) => { + const defaultErrorMessage = `Failed to grant ${userGroupName}` + return grantSourceIdReadAccess( + { + sourceId, + userGroupId + }, + { + extraParams: { + userGroupName + } + } + ) + .then(res => { + if (!res?.data?.grantSourceIdReadAccess) { + const errorMessage = res?.error?.message || defaultErrorMessage + toast.error(errorMessage) + return + } + }) + .catch(error => { + const errorMessage = error?.message || defaultErrorMessage + toast.error(errorMessage) + }) + } + + const onRevokeSourceIdReadAccess = ( + userGroupId: string, + userGroupName: string + ) => { + return revokeSourceIdReadAccess( + { + sourceId, + userGroupId + }, + { + extraParams: { + userGroupName + } + } + ) + .then(res => { + if (!res?.data?.revokeSourceIdReadAccess) { + const errorMessage = + res?.error?.message || `Failed to revoke '${userGroupName}'` + toast.error(errorMessage) + return + } + }) + .catch(error => { + const errorMessage = + error?.message || `Failed to revoke '${userGroupName}'` + toast.error(errorMessage) + }) + } + + let accessText = + policiesLen === 0 + ? 'Everyone' + : `${policiesLen} ${policiesLen <= 1 ? 'group' : 'groups'}` + + return ( + } + > +
    + {accessText} + {editable && ( + + + + + + + + + + {fetchingUserGroups ? ( +
    + +
    + ) : userGroups?.length ? ( + 'No matches results' + ) : ( + 'No groups found' + )} +
    + + {userGroups?.map(group => { + const isSelected = selectedIdSet.has(group.id) + const memberLen = group.members.length + return ( + + handleSelectGroup(group.id, group.name, !isSelected) + } + > +
    + +
    + + {group.name} + {`(${memberLen} member${ + memberLen > 1 ? 's' : '' + })`} + +
    + ) + })} +
    +
    +
    +
    +
    + )} +
    +
    + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/components/job-trigger.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/components/job-trigger.tsx new file mode 100644 index 000000000000..ad1656273ec0 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/components/job-trigger.tsx @@ -0,0 +1,121 @@ +import React from 'react' +import Link from 'next/link' +import { useRouter } from 'next/navigation' +import moment from 'moment' + +import { cn } from '@/lib/utils' +import { Button } from '@/components/ui/button' +import { IconCirclePlay, IconSpinner } from '@/components/ui/icons' +import { + Tooltip, + TooltipContent, + TooltipTrigger +} from '@/components/ui/tooltip' + +interface JobInfoProps { + jobInfo: + | { + command: string + lastJobRun?: { + id: string + job: string + createdAt: any + finishedAt?: any | null + exitCode?: number | null + } | null + } + | undefined + | null + onTrigger: () => Promise + className?: string +} + +function JobTrigger({ + onTrigger, + isPending, + jobLink +}: Pick & { + isPending?: boolean + jobLink?: string +}) { + const router = useRouter() + const [loading, setLoading] = React.useState(false) + const handleClick = () => { + if (isPending) { + if (jobLink) { + router.push(jobLink) + } + return + } + + const res = onTrigger() + + if (res && res instanceof Promise) { + setLoading(true) + res.finally(() => setLoading(false)) + } + + return res + } + + return ( + + + + + +

    Run

    +
    +
    + ) +} + +function LastJobRunInfo({ + jobInfo, + className +}: Pick & { className?: string }) { + if (!jobInfo?.lastJobRun) return null + + return ( + + {moment(jobInfo.lastJobRun.createdAt).format('YYYY-MM-DD HH:mm')} + + ) +} + +export function JobInfoView(props: JobInfoProps) { + const { jobInfo, onTrigger, className } = props + const isJobPending = + !!jobInfo?.lastJobRun && jobInfo.lastJobRun.exitCode === null + const jobLink = jobInfo?.lastJobRun?.id + ? `/jobs/detail?id=${jobInfo.lastJobRun.id}` + : undefined + + return ( +
    + + +
    + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/components/nav-bar.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/components/nav-bar.tsx new file mode 100644 index 000000000000..1ee26ebdcc07 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/components/nav-bar.tsx @@ -0,0 +1,95 @@ +'use client' + +import React from 'react' +import Link from 'next/link' +import { usePathname } from 'next/navigation' +import { cva } from 'class-variance-authority' + +import { cn } from '@/lib/utils' +import { Badge } from '@/components/ui/badge' +import { useShowDemoBanner } from '@/components/demo-banner' +import { useShowLicenseBanner } from '@/components/license-banner' + +import { PROVIDER_KIND_METAS } from '../constants' + +const linkVariants = cva( + 'flex items-center gap-1 rounded-lg px-3 py-2 transition-all hover:bg-accent', + { + variants: { + state: { + selected: 'bg-accent', + 'not-selected': '' + } + }, + defaultVariants: { + state: 'not-selected' + } + } +) + +interface SidebarButtonProps { + href: string + children: React.ReactNode +} + +export default function NavBar({ className }: { className?: string }) { + const [isShowDemoBanner] = useShowDemoBanner() + const [isShowLicenseBanner] = useShowLicenseBanner() + const showBanner = isShowDemoBanner || isShowLicenseBanner + const bannerHeight = + isShowDemoBanner && isShowLicenseBanner ? '7rem' : '3.5rem' + const style = showBanner + ? { height: `calc(100vh - ${bannerHeight} - 4rem)` } + : { height: 'calc(100vh - 4rem)' } + + return ( +
    + Git + {PROVIDER_KIND_METAS.map(provider => { + return ( + + {provider.meta.displayName} + + ) + })} + + Developer Docs + + Beta + + +
    + ) +} + +function SidebarButton({ href, children }: SidebarButtonProps) { + const pathname = usePathname() + const isSelected = React.useMemo(() => { + const docPathname = '/settings/providers/doc' + if (pathname?.startsWith(docPathname)) { + return href.startsWith(docPathname) + } + + const matcher = pathname.match(/^(\/settings\/providers\/[\w-]+)/)?.[1] + return matcher === href + }, [pathname, href]) + + const state = isSelected ? 'selected' : 'not-selected' + return ( + + {children} + + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/constants.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/constants.tsx new file mode 100644 index 000000000000..75fec71ce2be --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/constants.tsx @@ -0,0 +1,38 @@ +import { IntegrationKind } from '@/lib/gql/generates/graphql' + +export const PROVIDER_KIND_METAS: Array<{ + name: string + enum: IntegrationKind + meta: { + displayName: string + } +}> = [ + { + name: 'github', + enum: IntegrationKind.Github, + meta: { + displayName: 'GitHub' + } + }, + { + name: 'github-self-hosted', + enum: IntegrationKind.GithubSelfHosted, + meta: { + displayName: 'GitHub Self-Hosted' + } + }, + { + name: 'gitlab', + enum: IntegrationKind.Gitlab, + meta: { + displayName: 'GitLab' + } + }, + { + name: 'gitlab-self-hosted', + enum: IntegrationKind.GitlabSelfHosted, + meta: { + displayName: 'GitLab Self-Hosted' + } + } +] diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/doc/components/create-custom-doc.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/doc/components/create-custom-doc.tsx new file mode 100644 index 000000000000..596d97939cce --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/doc/components/create-custom-doc.tsx @@ -0,0 +1,120 @@ +'use client' + +import * as React from 'react' +import { useRouter } from 'next/navigation' +import { zodResolver } from '@hookform/resolvers/zod' +import { useForm } from 'react-hook-form' +import * as z from 'zod' + +import { graphql } from '@/lib/gql/generates' +import { useMutation } from '@/lib/tabby/gql' +import { Button } from '@/components/ui/button' +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage +} from '@/components/ui/form' +import { Input } from '@/components/ui/input' + +const createCustomDocumentMutation = graphql(/* GraphQL */ ` + mutation CreateCustomDocument($input: CreateCustomDocumentInput!) { + createCustomDocument(input: $input) + } +`) + +const formSchema = z.object({ + name: z.string().trim(), + url: z.string().url().trim() +}) + +type FormValues = z.infer + +export default function CreateCustomDocument() { + const router = useRouter() + const form = useForm>({ + resolver: zodResolver(formSchema) + }) + + const onCreated = () => { + router.push('./') + } + + const { isSubmitting } = form.formState + const createCustomDocument = useMutation(createCustomDocumentMutation, { + onCompleted() { + form.reset({ url: undefined }) + onCreated() + }, + form + }) + + const onSubmit = (values: FormValues) => { + return createCustomDocument({ + input: values + }) + } + + return ( + <> +
    +
    + + ( + + Name + + + + + + )} + /> + ( + + URL + + + + + + )} + /> +
    + + +
    + + +
    + + + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/doc/components/custom-doc.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/doc/components/custom-doc.tsx new file mode 100644 index 000000000000..6dc566df5f00 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/doc/components/custom-doc.tsx @@ -0,0 +1,381 @@ +'use client' + +import React, { useEffect, useMemo, useRef, useState } from 'react' +import Link from 'next/link' +import { toast } from 'sonner' +import useSWR from 'swr' +import { useQuery } from 'urql' + +import { graphql } from '@/lib/gql/generates' +import { CustomWebDocumentsQuery } from '@/lib/gql/generates/graphql' +import { useDebounceValue } from '@/lib/hooks/use-debounce' +import { client, useMutation } from '@/lib/tabby/gql' +import { userGroupsQuery } from '@/lib/tabby/query' +import { ArrayElementType } from '@/lib/types' +import { Button, buttonVariants } from '@/components/ui/button' +import { + IconClose, + IconListFilter, + IconPlus, + IconSearch, + IconTrash +} from '@/components/ui/icons' +import { Input } from '@/components/ui/input' +import { + Popover, + PopoverContent, + PopoverTrigger +} from '@/components/ui/popover' +import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from '@/components/ui/table' +import LoadingWrapper from '@/components/loading-wrapper' +import { QuickNavPagination } from '@/components/quick-nav-pagination' + +import { AccessPolicyView } from '../../components/access-policy-view' +import { JobInfoView } from '../../components/job-trigger' +import { triggerJobRunMutation } from '../../query' + +const listCustomWebDocuments = graphql(/* GraphQL */ ` + query CustomWebDocuments( + $ids: [ID!] + $after: String + $before: String + $first: Int + $last: Int + ) { + customWebDocuments( + ids: $ids + after: $after + before: $before + first: $first + last: $last + ) { + edges { + node { + url + name + id + sourceId + jobInfo { + lastJobRun { + id + job + createdAt + finishedAt + exitCode + } + command + } + } + cursor + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } + } +`) + +const deleteCustomWebDocumentMutation = graphql(/* GraphQL */ ` + mutation DeleteCustomDocument($id: ID!) { + deleteCustomDocument(id: $id) + } +`) + +type ListItem = ArrayElementType< + CustomWebDocumentsQuery['customWebDocuments']['edges'] +> + +export default function CustomDocument() { + const [page, setPage] = useState(1) + const [pageSize, setPageSize] = useState(8) + const [filterPattern, setFilterPattern] = useState() + const [debouncedFilterPattern] = useDebounceValue(filterPattern, 200) + const [list, setList] = useState() + const inputRef = useRef(null) + const [filterOpen, setFilterOpen] = useState(false) + + const [{ data: userGroupData, fetching: fetchingUserGroups }] = useQuery({ + query: userGroupsQuery + }) + + const [{ fetching, data, stale }, reexecuteQuery] = useQuery({ + query: listCustomWebDocuments + }) + + useSWR( + ['refresh_docs'], + () => { + reexecuteQuery() + }, + { + revalidateOnFocus: true, + revalidateOnReconnect: true, + revalidateOnMount: false, + refreshInterval: 10 * 1000 + } + ) + + const clearFilter = () => { + setFilterPattern('') + inputRef.current?.focus() + } + + const deleteCustomWebDocument = useMutation(deleteCustomWebDocumentMutation) + + const handleDeleteCustomDoc = (id: string) => { + deleteCustomWebDocument({ + id + }) + .then(res => { + if (!res?.data?.deleteCustomDocument) { + const errorMessage = res?.error?.message || 'Failed to delete' + toast.error(errorMessage) + } else { + setList(l => l?.filter(o => o.node.id !== id)) + } + }) + .catch(e => { + const errorMessage = e?.message || 'Failed to delete' + toast.error(errorMessage) + }) + } + + const getDocumentById = async (id: string) => { + if (!id) return undefined + try { + const res = await client + .query(listCustomWebDocuments, { ids: [id] }) + .toPromise() + const record = res?.data?.customWebDocuments?.edges?.[0] + return record + } catch (e) { + return undefined + } + } + + const updateDocumentItemById = async (id: string) => { + try { + const docItem = await getDocumentById(id) + if (!docItem?.node?.id || !list?.length) return + + const targetIdx = list.findIndex(o => o.node?.id === docItem.node.id) + if (targetIdx > -1) { + setList(prev => + prev?.map(o => { + if (o.node.id === docItem.node.id) { + return docItem + } else { + return o + } + }) + ) + } + } catch (e) {} + } + + const triggerJobRun = useMutation(triggerJobRunMutation) + const handleTriggerJobRun = (id: string, command: string) => { + return triggerJobRun({ command }).then(res => { + if (res?.data?.triggerJobRun) { + toast.success( + 'The job has been triggered successfully, it may take a few minutes to process.' + ) + + updateDocumentItemById(id) + } else { + toast.error(res?.error?.message || 'Failed to trigger job') + } + }) + } + + useEffect(() => { + setList(data?.customWebDocuments?.edges) + }, [data]) + + const onInputKeyDown = ( + event: React.KeyboardEvent + ): void => { + if (event.key === 'Enter' && !event.nativeEvent.isComposing) { + setFilterOpen(false) + } + } + + const filteredList = useMemo(() => { + if (!debouncedFilterPattern) return list + return ( + list?.filter(item => + item.node.name.toLowerCase().includes(debouncedFilterPattern) + ) ?? [] + ) + }, [debouncedFilterPattern, list]) + + const currentList = useMemo(() => { + return filteredList?.slice((page - 1) * pageSize, page * pageSize) + }, [filteredList, page, pageSize]) + + // reset pageNo + useEffect(() => { + setPage(1) + }, [debouncedFilterPattern]) + + return ( + <> + + + + + + + Name + + + + + +
    + inputRef.current?.focus()} + /> + setFilterPattern(e.target.value)} + ref={inputRef} + placeholder="Search..." + onKeyDown={onInputKeyDown} + /> + {filterPattern ? ( + + ) : null} +
    +
    +
    +
    + + + +
    +
    + Access + Job + +
    +
    + + {!currentList?.length && !fetching ? ( + + + {!list?.length ? ( +
    + No data + + + Add + +
    + ) : ( + 'No matches data' + )} +
    +
    + ) : ( + <> + {currentList?.map(x => { + return ( + + +

    {x.node.name}

    +

    + {x.node.url} +

    +
    + + + + + { + if (x.node?.jobInfo?.command) { + handleTriggerJobRun( + x.node.id, + x.node?.jobInfo.command + ) + } + }} + /> + + + + +
    + ) + })} + + )} +
    +
    + +
    + { + setPage(page) + setPageSize(pageSize) + }} + /> +
    + + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/doc/components/preset-doc.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/doc/components/preset-doc.tsx new file mode 100644 index 000000000000..50fd739b0afa --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/doc/components/preset-doc.tsx @@ -0,0 +1,378 @@ +'use client' + +import React, { useEffect, useMemo, useRef, useState } from 'react' +import { go as fuzzy } from 'fuzzysort' +import { toast } from 'sonner' +import useSWR from 'swr' +import { useQuery } from 'urql' + +import { graphql } from '@/lib/gql/generates' +import { PresetWebDocumentsQuery } from '@/lib/gql/generates/graphql' +import { useDebounceValue } from '@/lib/hooks/use-debounce' +import { client, useMutation } from '@/lib/tabby/gql' +import { ArrayElementType } from '@/lib/types' +import { Button } from '@/components/ui/button' +import { IconClose, IconListFilter, IconSearch } from '@/components/ui/icons' +import { Input } from '@/components/ui/input' +import { + Popover, + PopoverContent, + PopoverTrigger +} from '@/components/ui/popover' +import { Switch } from '@/components/ui/switch' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from '@/components/ui/table' +import LoadingWrapper from '@/components/loading-wrapper' +import { QuickNavPagination } from '@/components/quick-nav-pagination' + +import { JobInfoView } from '../../components/job-trigger' +import { triggerJobRunMutation } from '../../query' + +const listPresetWebDocuments = graphql(/* GraphQL */ ` + query PresetWebDocuments( + $ids: [ID!] + $after: String + $before: String + $first: Int + $last: Int + $isActive: Boolean + ) { + presetWebDocuments( + ids: $ids + after: $after + before: $before + first: $first + last: $last + isActive: $isActive + ) { + edges { + node { + id + name + isActive + sourceId + jobInfo { + lastJobRun { + id + job + createdAt + finishedAt + exitCode + } + command + } + } + cursor + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } + } +`) + +const setPresetDocumentActiveMutation = graphql(/* GraphQL */ ` + mutation SetPresetDocumentActive($input: SetPresetDocumentActiveInput!) { + setPresetDocumentActive(input: $input) + } +`) + +type ListItem = ArrayElementType< + PresetWebDocumentsQuery['presetWebDocuments']['edges'] +> + +export default function PresetDocument() { + const [page, setPage] = useState(1) + const [pageSize, setPageSize] = useState(8) + const [filterPattern, setFilterPattern] = useState() + const [debouncedFilterPattern] = useDebounceValue(filterPattern, 200) + const [list, setList] = useState() + const [processingIds, setProcessingIds] = useState>(new Set()) + const inputRef = useRef(null) + const [filterOpen, setFilterOpen] = useState(false) + const [{ data, stale }, reexecuteQuery] = useQuery({ + query: listPresetWebDocuments + }) + + useSWR( + ['refresh_docs'], + () => { + reexecuteQuery() + }, + { + revalidateOnFocus: true, + revalidateOnReconnect: true, + revalidateOnMount: false, + refreshInterval: 10 * 1000 + } + ) + + const setPresetDocumentActive = useMutation(setPresetDocumentActiveMutation) + + const getDocumentById = async (id: string) => { + if (!id) return undefined + try { + const res = await client + .query(listPresetWebDocuments, { ids: [id] }) + .toPromise() + const record = res?.data?.presetWebDocuments?.edges?.[0] + return record + } catch (e) { + return undefined + } + } + + const updateDocumentItemById = async (id: string) => { + try { + const docItem = await getDocumentById(id) + if (!docItem?.node?.id || !list?.length) return + + const targetIdx = list.findIndex(o => o.node?.id === docItem.node.id) + if (targetIdx > -1) { + setList(prev => + prev?.map(o => { + if (o.node.id === docItem.node.id) { + return docItem + } else { + return o + } + }) + ) + } + } catch (e) {} + } + + const triggerJobRun = useMutation(triggerJobRunMutation) + const handleTriggerJobRun = (id: string, command: string) => { + return triggerJobRun({ command }).then(res => { + if (res?.data?.triggerJobRun) { + toast.success( + 'The job has been triggered successfully, it may take a few minutes to process.' + ) + updateDocumentItemById(id) + } else { + toast.error(res?.error?.message || 'Failed to trigger job') + } + }) + } + + const onCheckedChange = (id: string, checked: boolean) => { + if (processingIds.has(id)) return + + setProcessingIds(prev => { + const nextSet = new Set(prev) + nextSet.add(id) + return nextSet + }) + + // optimistic update + setList(l => + l?.map(o => { + if (o.node.id === id) { + return { + ...o, + node: { + ...o.node, + isActive: checked + } + } + } + return o + }) + ) + + setPresetDocumentActive({ + input: { + id, + active: checked + } + }) + .then(res => { + if (!res?.data?.setPresetDocumentActive) { + const errorMessage = res?.error?.message ?? 'Failed to update' + toast.error(errorMessage) + setList(l => + l?.map(o => { + if (o.node.id !== id) { + return o + } + return { + ...o, + node: { + ...o.node, + isActive: !checked + } + } + }) + ) + } + }) + .finally(() => { + setProcessingIds(prev => { + const nextSet = new Set(prev) + nextSet.delete(id) + return nextSet + }) + updateDocumentItemById(id) + }) + } + + const clearFilter = () => { + setFilterPattern('') + inputRef.current?.focus() + } + + useEffect(() => { + setList(data?.presetWebDocuments?.edges) + }, [data]) + + const onInputKeyDown = ( + event: React.KeyboardEvent + ): void => { + if (event.key === 'Enter' && !event.nativeEvent.isComposing) { + setFilterOpen(false) + } + } + + const filteredList = useMemo(() => { + if (!debouncedFilterPattern || !list?.length) return list ?? [] + + const result = fuzzy(debouncedFilterPattern, list, { + key: item => item.node.name + }) + return result.map(o => o.obj) + }, [debouncedFilterPattern, list]) + + const currentList = useMemo(() => { + return filteredList?.slice((page - 1) * pageSize, page * pageSize) + }, [filteredList, page, pageSize]) + + // reset pageNo + useEffect(() => { + setPage(1) + }, [debouncedFilterPattern]) + + return ( +
    + + + + + + Name + + + + + +
    + inputRef.current?.focus()} + /> + setFilterPattern(e.target.value)} + ref={inputRef} + placeholder="Search..." + onKeyDown={onInputKeyDown} + /> + {filterPattern ? ( + + ) : null} +
    +
    +
    +
    + Job + +
    +
    + + {!currentList?.length ? ( + + + {!list?.length ? 'No data' : 'No matches data'} + + + ) : ( + <> + {currentList?.map(x => { + return ( + + + {x.node.name} + + + {x.node.isActive ? ( + { + if (x.node?.jobInfo?.command) { + handleTriggerJobRun( + x.node.id, + x.node?.jobInfo.command + ) + } + }} + /> + ) : null} + + + + onCheckedChange(x.node.id, checked) + } + className="my-1" + /> + + + ) + })} + + )} + +
    + { + setPage(page) + setPageSize(pageSize) + }} + /> +
    +
    + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/doc/new/page.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/doc/new/page.tsx new file mode 100644 index 000000000000..0d6dfd2c7367 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/doc/new/page.tsx @@ -0,0 +1,5 @@ +import CreateCustomDocument from '../components/create-custom-doc' + +export default function CreateCustomDocumentPage() { + return +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/doc/page.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/doc/page.tsx new file mode 100644 index 000000000000..668aa60e64ce --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/doc/page.tsx @@ -0,0 +1,34 @@ +import { CardContent } from '@/components/ui/card' +import { SubHeader } from '@/components/sub-header' + +import CustomDocument from './components/custom-doc' +import PresetDocument from './components/preset-doc' + +export default function DocumentProviderPage() { + return ( + <> + + +

    + Documents are a critical source for engineering knowledge. Tabby + provides an easy way to include these documents when interacting + with LLM in chat interfaces (e.g., Answer Engine, Chat Panel, etc.). + Simply press the @ button in the chat interface and select the + document you wish to include. +

    +
    + +
    + + +

    + You can also include your own developer documents here. Please + ensure that the URLs are accessible from the Tabby server to + guarantee successful crawling. +

    +
    + +
    + + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/git/components/git.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/git/components/git.tsx new file mode 100644 index 000000000000..83b83ef58b37 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/git/components/git.tsx @@ -0,0 +1,20 @@ +'use client' + +import Link from 'next/link' + +import { buttonVariants } from '@/components/ui/button' + +import RepositoryTable from './repository-table' + +export default function Git() { + return ( + <> +
    + + Create + +
    + + + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/git/components/repository-table.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/git/components/repository-table.tsx new file mode 100644 index 000000000000..2c00d4b906b6 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/git/components/repository-table.tsx @@ -0,0 +1,469 @@ +'use client' + +import React from 'react' +import { zodResolver } from '@hookform/resolvers/zod' +import { useForm } from 'react-hook-form' +import { toast } from 'sonner' +import useSWR from 'swr' +import { useQuery } from 'urql' +import * as z from 'zod' + +import { DEFAULT_PAGE_SIZE } from '@/lib/constants' +import { graphql } from '@/lib/gql/generates' +import { useMutation } from '@/lib/tabby/gql' +import { listRepositories, userGroupsQuery } from '@/lib/tabby/query' +import { Button } from '@/components/ui/button' +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle +} from '@/components/ui/dialog' +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from '@/components/ui/form' +import { IconPencil, IconTrash } from '@/components/ui/icons' +import { Input } from '@/components/ui/input' +import { + Pagination, + PaginationContent, + PaginationItem, + PaginationNext, + PaginationPrevious +} from '@/components/ui/pagination' +import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from '@/components/ui/table' +import { TagInput } from '@/components/ui/tag-input' +import LoadingWrapper from '@/components/loading-wrapper' + +import { AccessPolicyView } from '../../components/access-policy-view' +import { JobInfoView } from '../../components/job-trigger' +import { triggerJobRunMutation } from '../../query' + +const deleteRepositoryMutation = graphql(/* GraphQL */ ` + mutation deleteGitRepository($id: ID!) { + deleteGitRepository(id: $id) + } +`) + +const updateRepositoryMutation = graphql(/* GraphQL */ ` + mutation updateGitRepository($id: ID!, $refs: [String!]) { + updateGitRepository(id: $id, refs: $refs) + } +`) + +const formSchema = z.object({ + name: z.string(), + gitUrl: z.string(), + refs: z.array(z.string()).optional() +}) + +type FormValues = z.infer + +const PAGE_SIZE = DEFAULT_PAGE_SIZE + +export default function RepositoryTable() { + const [before, setBefore] = React.useState() + const [{ data, fetching }, reexecuteQuery] = useQuery({ + query: listRepositories, + variables: { last: PAGE_SIZE, before } + }) + + useSWR( + ['refresh_repos', before], + () => { + reexecuteQuery() + }, + { + revalidateOnFocus: true, + revalidateOnReconnect: true, + revalidateOnMount: false, + refreshInterval: 10 * 1000 + } + ) + + const [{ data: userGroupData, fetching: fetchingUserGroups }] = useQuery({ + query: userGroupsQuery + }) + + const [currentPage, setCurrentPage] = React.useState(1) + const edges = React.useMemo(() => { + return data?.gitRepositories?.edges?.slice().reverse() + }, [data?.gitRepositories?.edges]) + const pageInfo = data?.gitRepositories?.pageInfo + const pageNum = Math.ceil((edges?.length || 0) / PAGE_SIZE) + + const getBeforeCursor = (page: number) => { + return edges?.slice(0, (page - 1) * PAGE_SIZE)?.pop()?.cursor + } + + const fetchPage = (page: number) => { + setBefore(getBeforeCursor(page)) + } + + const currentPageRepos = React.useMemo(() => { + return edges?.slice?.( + (currentPage - 1) * PAGE_SIZE, + currentPage * PAGE_SIZE + ) + }, [currentPage, edges]) + + const hasNextPage = pageInfo?.hasPreviousPage || currentPage < pageNum + const hasPrevPage = currentPage > 1 + const showPagination = + !!currentPageRepos?.length && (hasNextPage || hasPrevPage) + + const deleteRepository = useMutation(deleteRepositoryMutation) + const updateRepository = useMutation(updateRepositoryMutation) + const triggerJobRun = useMutation(triggerJobRunMutation) + const [editingRepo, setEditingRepo] = React.useState<{ + id: string + name: string + gitUrl: string + refs: string[] + } | null>(null) + + const handleNavToPrevPage = () => { + if (currentPage <= 1) return + if (fetching) return + + const prevPage = currentPage - 1 + fetchPage(prevPage) + setCurrentPage(prevPage) + } + + const handleNavToNextPage = () => { + if (!hasNextPage) return + if (fetching) return + + const nextPage = currentPage + 1 + fetchPage(nextPage) + setCurrentPage(nextPage) + } + + const handleDeleteRepository = (id: string, isLast: boolean) => { + deleteRepository({ id }).then(res => { + if (res?.data?.deleteGitRepository) { + fetchPage(isLast ? currentPage - 1 : currentPage) + } else { + toast.error(res?.error?.message || 'Failed to delete repository') + } + }) + } + + const handleTriggerJobRun = (command: string) => { + return triggerJobRun({ command }).then(res => { + if (res?.data?.triggerJobRun) { + toast.success( + 'The job has been triggered successfully, it may take a few minutes to process.' + ) + reexecuteQuery() + } else { + toast.error(res?.error?.message || 'Failed to trigger job') + } + }) + } + + const handleEditRepository = (repo: { + id: string + name: string + gitUrl: string + refs: Array<{ name: string }> + }) => { + setEditingRepo({ + id: repo.id, + name: repo.name, + gitUrl: repo.gitUrl, + refs: repo.refs.map(r => { + // Extract branch name from refs/heads/xxx or refs/tags/xxx + if (r.name.startsWith('refs/heads/')) { + return r.name.substring('refs/heads/'.length) + } else if (r.name.startsWith('refs/tags/')) { + return r.name.substring('refs/tags/'.length) + } + return r.name + }) + }) + } + + const handleUpdateRepository = (values: FormValues) => { + if (!editingRepo) return + + updateRepository({ + id: editingRepo.id, + refs: values.refs && values.refs.length > 0 ? values.refs : undefined + }).then(res => { + if (res?.data?.updateGitRepository) { + toast.success('Repository updated successfully') + setEditingRepo(null) + reexecuteQuery() + } else { + toast.error(res?.error?.message || 'Failed to update repository') + } + }) + } + + React.useEffect(() => { + if (fetching) return + if (pageNum < currentPage && currentPage > 1) { + setCurrentPage(pageNum) + } + }, [pageNum, currentPage]) + + return ( + <> + + + + + + Name + Git URL + Access + Job + + + + + {!currentPageRepos?.length && currentPage === 1 ? ( + + + No Data + + + ) : ( + <> + {currentPageRepos?.map(x => { + return ( + + + {x.node.name} + + + {x.node.gitUrl} + + + + + + + handleTriggerJobRun(x.node.jobInfo.command) + } + /> + + +
    + + +
    +
    {' '} +
    + ) + })} + + )} +
    +
    + +
    + {showPagination && ( + + + + + + + + + + + )} +
    + { + if (!open) setEditingRepo(null) + }} + onSubmit={handleUpdateRepository} + /> + + ) +} + +function EditRepositoryDialog({ + repo, + open, + onOpenChange, + onSubmit +}: { + repo: { id: string; name: string; gitUrl: string; refs: string[] } | null + open: boolean + onOpenChange: (open: boolean) => void + onSubmit: (values: FormValues) => void +}) { + const form = useForm({ + resolver: zodResolver(formSchema) + }) + + React.useEffect(() => { + if (repo) { + form.reset({ + name: repo.name, + gitUrl: repo.gitUrl, + refs: repo.refs + }) + } + }, [repo, form]) + + const { isSubmitting } = form.formState + + return ( + + + + Edit Repository + + Update the repository branches to index + + +
    + + ( + + Name + + + + + + )} + /> + ( + + Git URL + Remote or local Git URL + + + + + + )} + /> + ( + + Branches + + Branches to index (press Enter to select, leave empty for + default branch) + + + + + + + )} + /> +
    + + +
    + + +
    +
    + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/git/layout.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/git/layout.tsx new file mode 100644 index 000000000000..cb417de83aaf --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/git/layout.tsx @@ -0,0 +1,18 @@ +import { SubHeader } from '@/components/sub-header' + +export default function GitProviderLayout({ + children +}: { + children: React.ReactNode +}) { + return ( + <> + + Connect to remote and local Git repositories, utilizing these + repositories as context to enhance the performance of large language + models. + + {children} + + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/git/new/components/create-repository-form.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/git/new/components/create-repository-form.tsx new file mode 100644 index 000000000000..fcab95008ec9 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/git/new/components/create-repository-form.tsx @@ -0,0 +1,149 @@ +'use client' + +import * as React from 'react' +import { useRouter } from 'next/navigation' +import { zodResolver } from '@hookform/resolvers/zod' +import { useForm } from 'react-hook-form' +import * as z from 'zod' + +import { graphql } from '@/lib/gql/generates' +import { useMutation } from '@/lib/tabby/gql' +import { Button } from '@/components/ui/button' +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from '@/components/ui/form' +import { Input } from '@/components/ui/input' +import { TagInput } from '@/components/ui/tag-input' + +const createRepositoryMutation = graphql(/* GraphQL */ ` + mutation createGitRepository( + $name: String! + $gitUrl: String! + $refs: [String!] + ) { + createGitRepository(name: $name, gitUrl: $gitUrl, refs: $refs) + } +`) + +const formSchema = z.object({ + name: z.string(), + gitUrl: z.string(), + refs: z.array(z.string()).optional() +}) + +export default function CreateRepositoryForm({ + onCreated +}: { + onCreated: () => void +}) { + const form = useForm>({ + resolver: zodResolver(formSchema) + }) + + const { isSubmitting } = form.formState + const createRepository = useMutation(createRepositoryMutation, { + onCompleted() { + form.reset({ name: undefined, gitUrl: undefined, refs: undefined }) + onCreated() + }, + form + }) + + const onSubmit = (values: z.infer) => { + createRepository({ + name: values.name, + gitUrl: values.gitUrl, + refs: values.refs && values.refs.length > 0 ? values.refs : undefined + }) + } + + const router = useRouter() + return ( +
    +
    + + ( + + Name + + + + + + )} + /> + ( + + Git URL + Remote or local Git URL + + + + + + )} + /> + ( + + Branches + + Branches to index (press Enter to select, leave empty for + default branch) + + + + + + + )} + /> +
    + + +
    + + +
    + + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/git/new/components/new-page.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/git/new/components/new-page.tsx new file mode 100644 index 000000000000..6ac774d515bc --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/git/new/components/new-page.tsx @@ -0,0 +1,19 @@ +'use client' + +import { useRouter } from 'next/navigation' + +import RepositoryForm from './create-repository-form' + +export const NewRepository = () => { + const router = useRouter() + + const onCreated = () => { + router.back() + } + + return ( + <> + + + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/git/new/page.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/git/new/page.tsx new file mode 100644 index 000000000000..b06ad0722e21 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/git/new/page.tsx @@ -0,0 +1,5 @@ +import { NewRepository } from './components/new-page' + +export default function IndexPage() { + return +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/git/page.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/git/page.tsx new file mode 100644 index 000000000000..1c76ded7af79 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/git/page.tsx @@ -0,0 +1,5 @@ +import Git from './components/git' + +export default function GitTabPage() { + return +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/layout.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/layout.tsx new file mode 100644 index 000000000000..10b4f61b12cf --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/layout.tsx @@ -0,0 +1,24 @@ +import { Metadata } from 'next' + +import { ScrollArea } from '@/components/ui/scroll-area' + +import ProviderNavBar from './components/nav-bar' + +export const metadata: Metadata = { + title: 'Context Providers' +} + +export default function ProvidersLayout({ + children +}: { + children: React.ReactNode +}) { + return ( +
    + + +
    {children}
    +
    +
    + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/query.ts b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/query.ts new file mode 100644 index 000000000000..30dd578c7442 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/providers/query.ts @@ -0,0 +1,7 @@ +import { graphql } from '@/lib/gql/generates' + +export const triggerJobRunMutation = graphql(/* GraphQL */ ` + mutation triggerJobRun($command: String!) { + triggerJobRun(command: $command) + } +`) diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/components/credential-list.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/components/credential-list.tsx new file mode 100644 index 000000000000..402758ff79cb --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/components/credential-list.tsx @@ -0,0 +1,229 @@ +'use client' + +import React, { useMemo } from 'react' +import Link from 'next/link' +import { useRouter } from 'next/navigation' +import { compact, find } from 'lodash-es' +import { useQuery } from 'urql' + +import { + AuthProviderKind, + LdapCredentialQuery, + LicenseType, + OAuthCredentialQuery, + OAuthProvider +} from '@/lib/gql/generates/graphql' +import { ldapCredentialQuery, oauthCredential } from '@/lib/tabby/query' +import { Button, buttonVariants } from '@/components/ui/button' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { + IconGitHub, + IconGitLab, + IconGoogle, + IconUsers +} from '@/components/ui/icons' +import { Skeleton } from '@/components/ui/skeleton' +import { LicenseGuard } from '@/components/license-guard' +import LoadingWrapper from '@/components/loading-wrapper' + +import { PROVIDER_METAS } from '../constant' + +export const CredentialList = () => { + const authProviderKindCount = useMemo(() => { + return Object.keys(AuthProviderKind).length + }, []) + + const [{ data: githubData, fetching: fetchingGithub }] = useQuery({ + query: oauthCredential, + variables: { provider: OAuthProvider.Github } + }) + const [{ data: googleData, fetching: fetchingGoogle }] = useQuery({ + query: oauthCredential, + variables: { provider: OAuthProvider.Google } + }) + const [{ data: gitlabData, fetching: fetchingGitlab }] = useQuery({ + query: oauthCredential, + variables: { provider: OAuthProvider.Gitlab } + }) + + const [{ data: oidcData, fetching: fetchingOic } = {}] = useQuery({ + query: oauthCredential, + variables: { provider: OAuthProvider.Oidc } + }) + + const [{ data: ldapData, fetching: fetchingLdap }] = useQuery({ + query: ldapCredentialQuery + }) + + const isLoading = + fetchingGithub || + fetchingGoogle || + fetchingGitlab || + fetchingLdap || + fetchingOic + + const credentialList = React.useMemo(() => { + return compact([ + githubData?.oauthCredential, + googleData?.oauthCredential, + gitlabData?.oauthCredential, + oidcData?.oauthCredential, + ldapData?.ldapCredential + ]) + }, [githubData, googleData, gitlabData, oidcData, ldapData]) + + const router = useRouter() + const createButton = ( + + {({ hasValidLicense }) => ( + + )} + + ) + + if (!credentialList?.length) { + return ( +
    + + + +
    + } + > +
    +
    No Data
    +
    + + Create + +
    +
    + + + ) + } + + return ( +
    +
    + {credentialList.map(credential => { + if ('provider' in credential) { + return ( + + ) + } else { + return + } + })} +
    + {credentialList.length < authProviderKindCount && ( +
    {createButton}
    + )} +
    + ) +} + +const OauthCredentialCard = ({ + data +}: { + data: OAuthCredentialQuery['oauthCredential'] +}) => { + const meta = React.useMemo(() => { + return find(PROVIDER_METAS, { enum: data?.provider })?.meta + }, [data]) + + if (!data) return null + + return ( + + +
    + + + {meta?.displayName || data.provider} + + + View + +
    +
    + +
    + Type + OAuth 2.0 +
    +
    + Host + {meta?.domain} +
    +
    +
    + ) +} + +const LDAPCredentialCard = ({ + data +}: { + data: LdapCredentialQuery['ldapCredential'] +}) => { + if (!data) return null + + return ( + + +
    + + + LDAP + + + View + +
    +
    + +
    + Type + LDAP +
    +
    + Host + {data?.host} +
    +
    +
    + ) +} + +function OAuthProviderIcon({ provider }: { provider: OAuthProvider }) { + switch (provider) { + case OAuthProvider.Github: + return + case OAuthProvider.Google: + return + case OAuthProvider.Gitlab: + return + default: + return null + } +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/components/form-sub-title.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/components/form-sub-title.tsx new file mode 100644 index 000000000000..fc9ec7a96a69 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/components/form-sub-title.tsx @@ -0,0 +1,8 @@ +import { cn } from '@/lib/utils' + +export function SubTitle({ + className, + ...rest +}: React.HTMLAttributes) { + return
    +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/components/ldap-credential-form.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/components/ldap-credential-form.tsx new file mode 100644 index 000000000000..b699f9dde69c --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/components/ldap-credential-form.tsx @@ -0,0 +1,532 @@ +'use client' + +import * as React from 'react' +import { useRouter } from 'next/navigation' +import { zodResolver } from '@hookform/resolvers/zod' +import { isEmpty } from 'lodash-es' +import { useForm } from 'react-hook-form' +import { toast } from 'sonner' +import { useClient } from 'urql' +import * as z from 'zod' + +import { graphql } from '@/lib/gql/generates' +import { LdapEncryptionKind, LicenseType } from '@/lib/gql/generates/graphql' +import { useMutation } from '@/lib/tabby/gql' +import { ldapCredentialQuery } from '@/lib/tabby/query' +import { cn } from '@/lib/utils' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger +} from '@/components/ui/alert-dialog' +import { Button, buttonVariants } from '@/components/ui/button' +import { Checkbox } from '@/components/ui/checkbox' +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from '@/components/ui/form' +import { IconSpinner } from '@/components/ui/icons' +import { Input } from '@/components/ui/input' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from '@/components/ui/select' +import { Separator } from '@/components/ui/separator' +import { LicenseGuard } from '@/components/license-guard' + +import { SubTitle } from './form-sub-title' + +const testLdapConnectionMutation = graphql(/* GraphQL */ ` + mutation testLdapConnection($input: UpdateLdapCredentialInput!) { + testLdapConnection(input: $input) + } +`) + +const updateLdapCredentialMutation = graphql(/* GraphQL */ ` + mutation updateLdapCredential($input: UpdateLdapCredentialInput!) { + updateLdapCredential(input: $input) + } +`) + +const deleteLdapCredentialMutation = graphql(/* GraphQL */ ` + mutation deleteLdapCredential { + deleteLdapCredential + } +`) + +const formSchema = z.object({ + host: z.string(), + port: z.coerce.number({ + required_error: 'Required', + invalid_type_error: 'Invalid port' + }), + bindDn: z.string(), + bindPassword: z.string().optional(), + baseDn: z.string(), + userFilter: z.string(), + encryption: z.nativeEnum(LdapEncryptionKind), + skipTlsVerify: z.boolean(), + emailAttribute: z.string(), + nameAttribute: z.string().optional() +}) + +export type LDAPFormValues = z.infer + +interface LDAPFormProps extends React.HTMLAttributes { + isNew?: boolean + defaultValues?: Partial | undefined + onSuccess?: (formValues: LDAPFormValues) => void + existed?: boolean +} + +const providerExistedError = + 'LDAP provider already exists and cannot be created again.' + +export function LDAPCredentialForm({ + className, + isNew, + defaultValues, + onSuccess, + existed, + ...props +}: LDAPFormProps) { + const router = useRouter() + const client = useClient() + const formRef = React.useRef(null) + const [isTesting, setIsTesting] = React.useState(false) + const formatedDefaultValues = React.useMemo(() => { + return { + ...(defaultValues || {}) + } + }, []) + + const [deleteAlertVisible, setDeleteAlertVisible] = React.useState(false) + const [isDeleting, setIsDeleting] = React.useState(false) + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: formatedDefaultValues + }) + const isDirty = !isEmpty(form.formState.dirtyFields) + const isValid = form.formState.isValid + const { isSubmitting } = form.formState + + const navigateToSSOSettings = () => { + router.replace('/settings/sso') + } + + const updateOauthCredential = useMutation(updateLdapCredentialMutation, { + onCompleted(values) { + if (values?.updateLdapCredential) { + onSuccess?.(form.getValues()) + } + }, + form + }) + + const testLdapConnection = useMutation(testLdapConnectionMutation, { + onError(err) { + toast.error(err.message) + }, + onCompleted(data) { + if (data?.testLdapConnection) { + toast.success('LDAP connection test success.', { + className: 'mb-10' + }) + } else { + toast.error('LDAP connection test failed.') + } + } + }) + + const deleteLdapCredential = useMutation(deleteLdapCredentialMutation) + + const onSubmit = async (values: LDAPFormValues) => { + if (isNew) { + const hasExistingProvider = await client + .query(ldapCredentialQuery, {}) + .then(res => !!res?.data?.ldapCredential) + if (hasExistingProvider) { + form.setError('root', { + message: providerExistedError + }) + return + } + } + + return updateOauthCredential({ input: values }) + } + + const onDelete: React.MouseEventHandler = e => { + e.preventDefault() + setIsDeleting(true) + deleteLdapCredential().then(res => { + if (res?.data?.deleteLdapCredential) { + navigateToSSOSettings() + } else { + setIsDeleting(false) + if (res?.error) { + toast.error(res?.error?.message) + } + } + }) + } + + const onTestLdapCredential = () => { + if (!formRef.current) return + form.trigger().then(isValid => { + if (!isValid) return + + setIsTesting(true) + + return testLdapConnection({ + input: formSchema.parse(form.getValues()) + }).finally(() => { + setIsTesting(false) + }) + }) + } + + const passwordPlaceholder = React.useMemo(() => { + if (!isNew) return new Array(36).fill('*').join('') + + return undefined + }, [isNew]) + + return ( +
    +
    + {existed && ( +
    + {providerExistedError} +
    + )} + +
    + LDAP provider information + + The information is provided by your identity provider. + +
    +
    + ( + + Host + + + + + + )} + /> + ( + + Port + + + + + + )} + /> +
    +
    + ( + + Bind DN + + + + + + )} + /> + ( + + Bind Password + + + + + + )} + /> +
    + ( + + Base DN + + + + + + )} + /> + ( + + User Filter + + + + + + )} + /> + ( + + Encryption + + + + )} + /> + ( + + Connection security +
    + + + + + Skip TLS Verify + +
    + +
    + )} + /> +
    + User information mapping + + Maps the field names from user info API to the Tabby user. + +
    + ( + + Email + + + + + + )} + /> + ( + + Name + + + + + + )} + /> + +
    + +
    + + {!isNew && ( + + + + + + + + Are you absolutely sure? + + + This action cannot be undone. It will permanently delete + the current credential. + + + + Cancel + + {isDeleting && ( + + )} + Yes, delete it + + + + + )} + + {({ hasValidLicense }) => ( + + )} + +
    +
    + + +
    + + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/components/oauth-credential-form.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/components/oauth-credential-form.tsx new file mode 100644 index 000000000000..ba0cf9931799 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/components/oauth-credential-form.tsx @@ -0,0 +1,504 @@ +'use client' + +import * as React from 'react' +import { useRouter } from 'next/navigation' +import { zodResolver } from '@hookform/resolvers/zod' +import { isEmpty } from 'lodash-es' +import { useForm } from 'react-hook-form' +import { toast } from 'sonner' +import { useClient, useQuery } from 'urql' +import * as z from 'zod' + +import { graphql } from '@/lib/gql/generates' +import { LicenseType, OAuthProvider } from '@/lib/gql/generates/graphql' +import { useMutation } from '@/lib/tabby/gql' +import { oauthCredential } from '@/lib/tabby/query' +import { cn } from '@/lib/utils' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger +} from '@/components/ui/alert-dialog' +import { Button, buttonVariants } from '@/components/ui/button' +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from '@/components/ui/form' +import { + IconGitHub, + IconGitLab, + IconGoogle, + IconOidc, + IconSpinner +} from '@/components/ui/icons' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' +import { Separator } from '@/components/ui/separator' +import { CopyButton } from '@/components/copy-button' +import { LicenseGuard } from '@/components/license-guard' + +import { SubTitle } from './form-sub-title' + +export const updateOauthCredentialMutation = graphql(/* GraphQL */ ` + mutation updateOauthCredential($input: UpdateOAuthCredentialInput!) { + updateOauthCredential(input: $input) + } +`) + +export const deleteOauthCredentialMutation = graphql(/* GraphQL */ ` + mutation deleteOauthCredential($provider: OAuthProvider!) { + deleteOauthCredential(provider: $provider) + } +`) + +const oauthCallbackUrl = graphql(/* GraphQL */ ` + query OAuthCallbackUrl($provider: OAuthProvider!) { + oauthCallbackUrl(provider: $provider) + } +`) + +const defaultFormSchema = z.object({ + clientId: z.string(), + clientSecret: z.string(), + configUrl: z.string().optional(), + configScopes: z.string().optional(), + provider: z.nativeEnum(OAuthProvider) +}) + +const updateFormSchema = defaultFormSchema.extend({ + clientSecret: z.string().optional() +}) + +const providerExistedError = + 'Provider already exists. Please choose another one' + +const oidcConfigUrlError = 'Config URL must be set for OpenID Connect' +const oidcConfigScopesError = 'Config Scopes must be set for OpenID Connect' + +interface OAuthCredentialFormProps + extends React.HTMLAttributes { + isNew?: boolean + defaultProvider: OAuthProvider + defaultValues?: Partial> | undefined + onSuccess?: (formValues: z.infer) => void + /** + * for creation, if there are existed providers, show a error message + */ + existedProviders?: OAuthProvider[] +} + +export default function OAuthCredentialForm({ + className, + isNew, + defaultProvider, + defaultValues, + onSuccess, + existedProviders, + ...props +}: OAuthCredentialFormProps) { + const router = useRouter() + const client = useClient() + const formatedDefaultValues = React.useMemo(() => { + return { + ...(defaultValues || {}), + provider: defaultProvider || OAuthProvider.Github + } + }, []) + + const [deleteAlertVisible, setDeleteAlertVisible] = React.useState(false) + const [isDeleting, setIsDeleting] = React.useState(false) + + const formSchema = React.useMemo(() => { + if (!isNew) return updateFormSchema + + return defaultFormSchema.extend({ + provider: z + .nativeEnum(OAuthProvider) + .refine(v => !existedProviders?.includes(v), { + message: providerExistedError + }) + }) + }, [isNew, existedProviders]) + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: formatedDefaultValues + }) + const providerValue = form.watch('provider') + const isDirty = !isEmpty(form.formState.dirtyFields) + + const { isSubmitting } = form.formState + + const navigateToSSOSettings = () => { + router.replace('/settings/sso') + } + + const updateOauthCredential = useMutation(updateOauthCredentialMutation, { + onCompleted(values) { + if (values?.updateOauthCredential) { + onSuccess?.(form.getValues()) + } + }, + form + }) + + const provider = form.watch('provider') + + React.useEffect(() => { + if (!isNew) { + return + } + + if (provider && existedProviders?.includes(provider)) { + form.setError('provider', { + message: providerExistedError + }) + } else { + form.clearErrors('provider') + } + }, [provider, isNew, existedProviders]) + + const deleteOAuthCredential = useMutation(deleteOauthCredentialMutation) + + const onSubmit = async (values: z.infer) => { + let validationError = false + if (provider === OAuthProvider.Oidc) { + if (!values.configUrl || values.configUrl === '') { + form.setError('configUrl', { + message: oidcConfigUrlError + }) + + validationError = true + } + + if (!values.configScopes || values.configScopes === '') { + form.setError('configScopes', { + message: oidcConfigScopesError + }) + + validationError = true + } + } + + if (isNew) { + const hasExistingProvider = await client + .query(oauthCredential, { provider: values.provider }) + .then(res => !!res?.data?.oauthCredential) + if (hasExistingProvider) { + form.setError('provider', { + message: providerExistedError + }) + validationError = true + } + } + + if (validationError) { + return + } + + updateOauthCredential({ input: values }) + } + + const onDelete: React.MouseEventHandler = e => { + e.preventDefault() + setIsDeleting(true) + deleteOAuthCredential({ provider: providerValue }).then(res => { + if (res?.data?.deleteOauthCredential) { + navigateToSSOSettings() + } else { + setIsDeleting(false) + if (res?.error) { + toast.error(res?.error?.message) + } + } + }) + } + + const [{ data: oauthRedirectUrlData }] = useQuery({ + query: oauthCallbackUrl, + variables: { provider: providerValue } + }) + + const accessTokenPlaceholder = React.useMemo(() => { + if (!isNew) return new Array(36).fill('*').join('') + + return 'e.g. e363c08d7e9ca4e66e723a53f38a21f6a54c1b83' + }, [isNew]) + + return ( +
    +
    + + Basic information + ( + + Provider + + +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + +
    + )} + /> + + {oauthRedirectUrlData && ( + +
    +
    + Create your OAuth2 application with the following information +
    +
    +
    + Authorization callback URL +
    + + + {oauthRedirectUrlData.oauthCallbackUrl} + + + +
    +
    +
    + )} + +
    + OAuth provider information + + The information is provided by your identity provider. + +
    + {providerValue == OAuthProvider.Oidc && ( + ( + + Configuration URL + + + + + NOTE: The URL above should include the path but not include + ".well-known/openid-configuration" + + + + )} + /> + )} + {providerValue == OAuthProvider.Oidc && ( + ( + + Configuration Scopes + + + + + + )} + /> + )} + ( + + Client ID + + + + + + )} + /> + ( + + Client Secret + + + + + + )} + /> + +
    + + {!isNew && ( + + + + + + + + Are you absolutely sure? + + + This action cannot be undone. It will permanently delete + the current credential. + + + + Cancel + + {isDeleting && ( + + )} + Yes, delete it + + + + + )} + + {({ hasValidLicense }) => ( + + )} + +
    + + +
    + + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/components/sso-type-radio.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/components/sso-type-radio.tsx new file mode 100644 index 000000000000..6a0f338abb51 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/components/sso-type-radio.tsx @@ -0,0 +1,50 @@ +import { SSOType } from '@/lib/types' +import { cn } from '@/lib/utils' +import { Label } from '@/components/ui/label' +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' + +interface SSOTypeRadioProps { + value: SSOType + onChange?: (value: SSOType) => void + className?: string + readonly?: boolean +} + +export function SSOTypeRadio({ + value, + onChange, + className, + readonly +}: SSOTypeRadioProps) { + return ( +
    + + onChange?.(v as SSOType)} + className="flex gap-8" + orientation="horizontal" + disabled={readonly} + > +
    + + +
    +
    + + +
    +
    +
    + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/constant.ts b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/constant.ts new file mode 100644 index 000000000000..5161f11cd96e --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/constant.ts @@ -0,0 +1,43 @@ +import { OAuthProvider } from '@/lib/gql/generates/graphql' + +export const PROVIDER_METAS: Array<{ + name: string + enum: OAuthProvider + meta: { + domain: string + displayName: string + } +}> = [ + { + name: 'github', + enum: OAuthProvider.Github, + meta: { + domain: 'github.com', + displayName: 'GitHub' + } + }, + { + name: 'google', + enum: OAuthProvider.Google, + meta: { + domain: 'google.com', + displayName: 'Google' + } + }, + { + name: 'gitlab', + enum: OAuthProvider.Gitlab, + meta: { + domain: 'gitlab.com', + displayName: 'GitLab' + } + }, + { + name: 'oidc', + enum: OAuthProvider.Oidc, + meta: { + domain: 'oidc', + displayName: 'OIDC' + } + } +] diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/detail/[oauth-provider]/components/oauth-credential-detail.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/detail/[oauth-provider]/components/oauth-credential-detail.tsx new file mode 100644 index 000000000000..0356117f42d9 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/detail/[oauth-provider]/components/oauth-credential-detail.tsx @@ -0,0 +1,59 @@ +'use client' + +import React from 'react' +import { useRouter } from 'next/navigation' +import { isNil, pickBy } from 'lodash-es' +import { useQuery } from 'urql' + +import { OAuthProvider } from '@/lib/gql/generates/graphql' +import { oauthCredential } from '@/lib/tabby/query' +import LoadingWrapper from '@/components/loading-wrapper' +import { ListSkeleton } from '@/components/skeleton' + +import OAuthCredentialForm from '../../../components/oauth-credential-form' +import { SSOTypeRadio } from '../../../components/sso-type-radio' + +interface OAuthCredentialDetailProps + extends React.HTMLAttributes { + provider: OAuthProvider +} + +const OAuthCredentialDetail: React.FC = ({ + provider +}) => { + const router = useRouter() + const [{ data, fetching }] = useQuery({ + query: oauthCredential, + variables: { provider } + }) + + const credential = data?.oauthCredential + + const defaultValues = React.useMemo(() => { + if (!credential) return undefined + return pickBy(credential, v => !isNil(v)) + }, [credential]) + + const onSubmitSuccess = () => { + router.push('/settings/sso') + } + + return ( +
    + } + > + + + +
    + ) +} + +export { OAuthCredentialDetail } diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/detail/[oauth-provider]/page.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/detail/[oauth-provider]/page.tsx new file mode 100644 index 000000000000..8b4d55444e3a --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/detail/[oauth-provider]/page.tsx @@ -0,0 +1,26 @@ +import React from 'react' +import type { NextPage } from 'next' +import { find } from 'lodash-es' + +import { PROVIDER_METAS } from '../../constant' +import { OAuthCredentialDetail } from './components/oauth-credential-detail' + +type Params = { + 'oauth-provider': string +} + +export function generateStaticParams() { + return PROVIDER_METAS.map(item => ({ 'oauth-provider': item.name })) +} + +const OAuthCredentialDetailPage: NextPage<{ params: Params }> = ({ + params +}) => { + const provider = find(PROVIDER_METAS, { + name: params['oauth-provider'] + })!.enum + + return +} + +export default OAuthCredentialDetailPage diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/detail/ldap/components/ldap-credential-detail.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/detail/ldap/components/ldap-credential-detail.tsx new file mode 100644 index 000000000000..731970c51a41 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/detail/ldap/components/ldap-credential-detail.tsx @@ -0,0 +1,52 @@ +'use client' + +import React from 'react' +import { useRouter } from 'next/navigation' +import { isNil, pickBy } from 'lodash-es' +import { useQuery } from 'urql' + +import { ldapCredentialQuery } from '@/lib/tabby/query' +import LoadingWrapper from '@/components/loading-wrapper' +import { ListSkeleton } from '@/components/skeleton' + +import { LDAPCredentialForm } from '../../../components/ldap-credential-form' +import { SSOTypeRadio } from '../../../components/sso-type-radio' + +interface OAuthCredentialDetailProps + extends React.HTMLAttributes {} + +export const LdapCredentialDetail: React.FC< + OAuthCredentialDetailProps +> = () => { + const router = useRouter() + const [{ data, fetching }] = useQuery({ + query: ldapCredentialQuery + }) + + const credential = data?.ldapCredential + + const defaultValues = React.useMemo(() => { + if (!credential) return undefined + return pickBy(credential, v => !isNil(v)) + }, [credential]) + + const onSubmitSuccess = () => { + router.push('/settings/sso') + } + + return ( +
    + } + > + + + +
    + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/detail/ldap/page.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/detail/ldap/page.tsx new file mode 100644 index 000000000000..09934d0acc35 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/detail/ldap/page.tsx @@ -0,0 +1,7 @@ +import React from 'react' + +import { LdapCredentialDetail } from './components/ldap-credential-detail' + +export default function LDAPCredentialDetailPage() { + return +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/layout.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/layout.tsx new file mode 100644 index 000000000000..34fc4c5b10a0 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/layout.tsx @@ -0,0 +1,35 @@ +import { Metadata } from 'next' + +import { cn } from '@/lib/utils' +import { IconExternalLink } from '@/components/ui/icons' + +export const metadata: Metadata = { + title: 'SSO' +} + +export default function SSOLayout({ children }: { children: React.ReactNode }) { + return ( + <> + + {children} + + ) +} + +function SSOHeader({ className }: { className?: string }) { + return ( +
    +
    + Single Sign-On (SSO) is an authentication method that enables users to + authenticate with multiple applications and websites via a single set of + credentials. + {false && ( + + Learn more + + + )} +
    +
    + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/new/component/new-page.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/new/component/new-page.tsx new file mode 100644 index 000000000000..68a87c8401bf --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/new/component/new-page.tsx @@ -0,0 +1,75 @@ +'use client' + +import { useState } from 'react' +import { useRouter } from 'next/navigation' +import { compact } from 'lodash-es' +import { useQuery } from 'urql' + +import { OAuthProvider } from '@/lib/gql/generates/graphql' +import { ldapCredentialQuery, oauthCredential } from '@/lib/tabby/query' +import { SSOType } from '@/lib/types' +import LoadingWrapper from '@/components/loading-wrapper' +import { ListSkeleton } from '@/components/skeleton' + +import { LDAPCredentialForm } from '../../components/ldap-credential-form' +import OAuthCredentialForm from '../../components/oauth-credential-form' +import { SSOTypeRadio } from '../../components/sso-type-radio' + +export function NewPage() { + const [type, setType] = useState('oauth') + const router = useRouter() + const [{ data: githubData, fetching: fetchingGithub }] = useQuery({ + query: oauthCredential, + variables: { provider: OAuthProvider.Github } + }) + const [{ data: googleData, fetching: fetchingGoogle }] = useQuery({ + query: oauthCredential, + variables: { provider: OAuthProvider.Google } + }) + const [{ data: gitlabData, fetching: fetchingGitlab }] = useQuery({ + query: oauthCredential, + variables: { provider: OAuthProvider.Gitlab } + }) + + const [{ data: ldapData, fetching: fetchingLdap }] = useQuery({ + query: ldapCredentialQuery + }) + + const fetchingProviders = + fetchingGithub || fetchingGoogle || fetchingGitlab || fetchingLdap + + const isLdapExisted = !!ldapData?.ldapCredential + const existedProviders = compact([ + githubData?.oauthCredential && OAuthProvider.Github, + googleData?.oauthCredential && OAuthProvider.Google, + gitlabData?.oauthCredential && OAuthProvider.Gitlab + ]) + + const onCreateSuccess = () => { + router.replace('/settings/sso') + } + + return ( + }> +
    + + {type === 'oauth' ? ( + + ) : ( + + )} +
    +
    + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/new/page.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/new/page.tsx new file mode 100644 index 000000000000..79089535c3c5 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/new/page.tsx @@ -0,0 +1,7 @@ +import { NewPage as NewPageComponent } from './component/new-page' + +const NewPage = () => { + return +} + +export default NewPage diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/page.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/page.tsx new file mode 100644 index 000000000000..4b8fce3e59fc --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/page.tsx @@ -0,0 +1,5 @@ +import { CredentialList } from './components/credential-list' + +export default function CredentialPage() { + return +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/general/components/branding-form.tsx b/ee/tabby-ui/app/(dashboard)/settings/general/components/branding-form.tsx new file mode 100644 index 000000000000..95e1f923484e --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/general/components/branding-form.tsx @@ -0,0 +1,288 @@ +'use client' + +import React, { ChangeEvent } from 'react' +import { zodResolver } from '@hookform/resolvers/zod' +import { useForm } from 'react-hook-form' +import { toast } from 'sonner' +import { useQuery } from 'urql' +import * as z from 'zod' + +import { graphql } from '@/lib/gql/generates' +import { useMutation } from '@/lib/tabby/gql' +import { brandingSettingQuery } from '@/lib/tabby/query' +import { cn } from '@/lib/utils' +import { Button } from '@/components/ui/button' +import { + Form, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from '@/components/ui/form' +import { IconClose, IconCloudUpload, IconSpinner } from '@/components/ui/icons' +import LoadingWrapper from '@/components/loading-wrapper' +import { FormSkeleton } from '@/components/skeleton' + +const updateBrandingSettingMutation = graphql(/* GraphQL */ ` + mutation GeneralBrandingMutation($input: BrandingSettingInput!) { + updateBrandingSetting(input: $input) + } +`) + +const formSchema = z.object({ + brandingLogo: z.string().optional(), + brandingIcon: z.string().optional() +}) + +const MAX_UPLOAD_SIZE_KB = 500 + +interface BrandingFormProps { + defaultValues?: Partial + onSuccess?: () => void +} + +type BrandingFormValues = z.infer + +const BrandingForm: React.FC = ({ + defaultValues, + onSuccess +}) => { + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues + }) + + const { brandingLogo, brandingIcon } = form.watch() + + const updateBrandingSetting = useMutation(updateBrandingSettingMutation, { + form, + onCompleted(values) { + if (values?.updateBrandingSetting) { + toast.success('Branding settings updated!') + onSuccess?.() + form.reset(form.getValues()) + } + } + }) + + const onFileChange = ( + e: ChangeEvent, + field: 'brandingLogo' | 'brandingIcon' + ) => { + const file = e.target.files?.[0] + if (!file) return + + const fileSizeInKB = parseFloat((file.size / 1024).toFixed(2)) + if (fileSizeInKB > MAX_UPLOAD_SIZE_KB) { + toast.error( + `The image you are attempting to upload is too large. Please ensure the file size is under ${MAX_UPLOAD_SIZE_KB}KB and try again.` + ) + return + } + + const reader = new FileReader() + reader.onloadend = () => { + form.setValue(field, reader.result as string, { shouldDirty: true }) + } + reader.readAsDataURL(file) + } + + const removeImage = (field: 'brandingLogo' | 'brandingIcon') => { + form.setValue(field, '', { shouldDirty: true }) + } + + const onSubmit = async (values: BrandingFormValues) => { + await updateBrandingSetting({ + input: { + brandingLogo: + values.brandingLogo === '' + ? null + : values.brandingLogo?.startsWith('data:') + ? values.brandingLogo + : undefined, + brandingIcon: + values.brandingIcon === '' + ? null + : values.brandingIcon?.startsWith('data:') + ? values.brandingIcon + : undefined + } + }) + } + + return ( +
    + + { + return ( + + Logo + + The suggested logo size should be 5:2 aspect ratio, e.g 100 x + 40. + +
    + + onFileChange(e, 'brandingLogo')} + /> + {brandingLogo ? ( +
    + logo + +
    + ) : ( +
    + )} +
    + + + ) + }} + > + + { + return ( + + Icon + + The suggested icon size should be square, e.g 40 x 40. + +
    + + onFileChange(e, 'brandingIcon')} + /> + {brandingIcon ? ( +
    + icon + +
    + ) : ( +
    + )} +
    + + + ) + }} + /> + +
    +
    + +
    +
    + {form.formState.isDirty && !form.formState.isSubmitting && ( + + )} + +
    +
    + + + ) +} + +export const GeneralBrandingForm = () => { + const [{ data, fetching, stale }, reexecuteQuery] = useQuery({ + query: brandingSettingQuery + }) + + const onSuccess = () => { + reexecuteQuery({ requestPolicy: 'network-only' }) + } + + return ( +
    + }> + + +
    + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/general/components/form-section.tsx b/ee/tabby-ui/app/(dashboard)/settings/general/components/form-section.tsx new file mode 100644 index 000000000000..a7dbb323aee6 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/general/components/form-section.tsx @@ -0,0 +1,26 @@ +import React from 'react' + +import { cn } from '@/lib/utils' + +interface GeneralFormSectionProps + extends Omit, 'title'> { + title: string +} + +export const GeneralFormSection: React.FC = ({ + title, + className, + children, + ...props +}) => { + return ( +
    +
    +

    {title}

    +
    +
    +
    {children}
    +
    +
    + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/general/components/general.tsx b/ee/tabby-ui/app/(dashboard)/settings/general/components/general.tsx new file mode 100644 index 000000000000..b76df8100677 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/general/components/general.tsx @@ -0,0 +1,50 @@ +'use client' + +import React from 'react' + +import { + GetLicenseInfoQuery, + LicenseFeature, + LicenseStatus +} from '@/lib/gql/generates/graphql' +import { useLicense } from '@/lib/hooks/use-license' +import { Separator } from '@/components/ui/separator' + +import { GeneralBrandingForm as BrandingForm } from './branding-form' +import { GeneralFormSection } from './form-section' +import { GeneralNetworkForm } from './network-form' +import { GeneralSecurityForm } from './security-form' + +export default function General() { + const [{ data }] = useLicense() + return ( +
    + + + + {hasValidLicenseFeature(data, LicenseFeature.CustomLogo) && ( + <> + + + + + + )} + + + + +
    + ) +} + +const hasValidLicenseFeature = ( + licenseData: GetLicenseInfoQuery | undefined, + feature: LicenseFeature +): boolean => { + return ( + !!licenseData?.license && + licenseData.license.status === LicenseStatus.Ok && + !!licenseData.license.features?.includes(feature) + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/general/components/network-form.tsx b/ee/tabby-ui/app/(dashboard)/settings/general/components/network-form.tsx new file mode 100644 index 000000000000..48b083904213 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/general/components/network-form.tsx @@ -0,0 +1,131 @@ +'use client' + +import React from 'react' +import { zodResolver } from '@hookform/resolvers/zod' +import { isEmpty } from 'lodash-es' +import { useForm } from 'react-hook-form' +import { toast } from 'sonner' +import * as z from 'zod' + +import { graphql } from '@/lib/gql/generates' +import { useNetworkSetting } from '@/lib/hooks/use-network-setting' +import { useMutation } from '@/lib/tabby/gql' +import { Button } from '@/components/ui/button' +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from '@/components/ui/form' +import { Input } from '@/components/ui/input' +import LoadingWrapper from '@/components/loading-wrapper' +import { FormSkeleton } from '@/components/skeleton' + +const updateNetworkSettingMutation = graphql(/* GraphQL */ ` + mutation updateNetworkSettingMutation($input: NetworkSettingInput!) { + updateNetworkSetting(input: $input) + } +`) + +const formSchema = z.object({ + externalUrl: z.string().url() +}) + +type NetworkFormValues = z.infer + +interface NetworkFormProps { + defaultValues?: Partial + onSuccess?: () => void +} + +const NetworkForm: React.FC = ({ + onSuccess, + defaultValues +}) => { + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues + }) + + const isDirty = !isEmpty(form.formState.dirtyFields) + + const updateNetworkSetting = useMutation(updateNetworkSettingMutation, { + form, + onCompleted(values) { + if (values?.updateNetworkSetting) { + onSuccess?.() + form.reset(form.getValues()) + } + } + }) + + const onSubmit = async () => { + const { externalUrl } = form.getValues() + await updateNetworkSetting({ + input: { + externalUrl: new URL(externalUrl).origin + } + }) + } + + return ( +
    +
    + + ( + + External URL + + The external URL where user visits Tabby, must start with + http:// or https://. + + + + + + + )} + /> +
    + +
    + + +
    + + ) +} + +export const GeneralNetworkForm = () => { + const [{ data, stale }, reexecuteQuery] = useNetworkSetting() + + const onSuccess = () => { + toast.success('Network configuration is updated') + reexecuteQuery() + } + + return ( +
    + }> + + +
    + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/general/components/security-form.tsx b/ee/tabby-ui/app/(dashboard)/settings/general/components/security-form.tsx new file mode 100644 index 000000000000..14e86989c044 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/general/components/security-form.tsx @@ -0,0 +1,274 @@ +'use client' + +import React from 'react' +import { zodResolver } from '@hookform/resolvers/zod' +import { compact, isEmpty } from 'lodash-es' +import { useFieldArray, useForm } from 'react-hook-form' +import { toast } from 'sonner' +import { useQuery } from 'urql' +import * as z from 'zod' + +import { graphql } from '@/lib/gql/generates' +import { LicenseType } from '@/lib/gql/generates/graphql' +import { useMutation } from '@/lib/tabby/gql' +import { cn } from '@/lib/utils' +import { Button } from '@/components/ui/button' +import { Checkbox } from '@/components/ui/checkbox' +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from '@/components/ui/form' +import { IconTrash } from '@/components/ui/icons' +import { Input } from '@/components/ui/input' +import { LicenseGuard } from '@/components/license-guard' +import LoadingWrapper from '@/components/loading-wrapper' +import { FormSkeleton } from '@/components/skeleton' + +const updateSecuritySettingMutation = graphql(/* GraphQL */ ` + mutation updateSecuritySetting($input: SecuritySettingInput!) { + updateSecuritySetting(input: $input) + } +`) + +export const securitySetting = graphql(/* GraphQL */ ` + query SecuritySetting { + securitySetting { + allowedRegisterDomainList + disableClientSideTelemetry + disablePasswordLogin + } + } +`) + +const formSchema = z.object({ + disableClientSideTelemetry: z.boolean(), + disablePasswordLogin: z.boolean(), + // https://github.com/shadcn-ui/ui/issues/384 + // https://github.com/shadcn-ui/ui/blob/main/apps/www/app/examples/forms/profile-form.tsx + allowedRegisterDomainList: z + .array( + z.object({ + value: z.string() + }) + ) + .optional() +}) + +type SecurityFormValues = z.infer + +interface SecurityFormProps { + defaultValues?: SecurityFormValues + onSuccess?: () => void +} + +const SecurityForm: React.FC = ({ + onSuccess, + defaultValues: propsDefaultValues +}) => { + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: propsDefaultValues + }) + + const { fields, append, remove, update } = useFieldArray({ + control: form.control, + name: 'allowedRegisterDomainList' + }) + + const isDirty = !isEmpty(form.formState.dirtyFields) + + const onRemoveDomainItem = (index: number) => { + if (fields?.length === 1 && index === 0) { + update(index, { value: '' }) + } else { + remove(index) + } + } + + const handleDomainListKeyDown: React.KeyboardEventHandler< + HTMLInputElement + > = e => { + if (e.key === 'Enter' && !e.nativeEvent.isComposing) { + e.preventDefault() + append({ value: '' }) + } + } + + const updateSecuritySetting = useMutation(updateSecuritySettingMutation, { + form, + onCompleted(values) { + if (values?.updateSecuritySetting) { + onSuccess?.() + form.reset(form.getValues()) + } + } + }) + + const onSubmit = async ({ + allowedRegisterDomainList, + ...values + }: SecurityFormValues) => { + await updateSecuritySetting({ + input: { + allowedRegisterDomainList: buildListValuesFromField( + allowedRegisterDomainList + ), + ...values + } + }) + } + + return ( +
    +
    + + ( + +
    + + + + + Disabling Client Side Telemetry + +
    + + When activated, the client-side telemetry (IDE/Extensions) + will be disabled, regardless of the client-side settings. + +
    + )} + /> + ( + +
    + + + + + Disabling Password Login + +
    + + When activated, password login will be hidden. + +
    + )} + /> +
    + {fields.map((field, index) => ( + ( + + + Authentication Domains + + + Enable users to sign up automatically with an email + address on domains. + +
    + + + + +
    + +
    + )} + /> + ))} +
    + +
    +
    +
    + + {({ hasValidLicense }) => { + return ( + + ) + }} + +
    + + +
    + + ) +} + +function buildListFieldFromValues(list: string[] | undefined) { + const domains = list?.map(item => ({ value: item })) + if (!domains || domains.length === 0) { + return [{ value: '' }] + } else { + return domains + } +} + +function buildListValuesFromField(fieldListValue?: Array<{ value: string }>) { + const list = compact(fieldListValue?.map(item => item.value)) + return list +} + +export const GeneralSecurityForm = () => { + const [{ data, stale, fetching }, reexecuteQuery] = useQuery({ + query: securitySetting + }) + const onSuccess = () => { + toast.success('Security configuration is updated') + reexecuteQuery() + } + const defaultValues = data && { + ...data.securitySetting, + allowedRegisterDomainList: buildListFieldFromValues( + data.securitySetting.allowedRegisterDomainList + ) + } + + return ( + }> + + + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/general/page.tsx b/ee/tabby-ui/app/(dashboard)/settings/general/page.tsx new file mode 100644 index 000000000000..2361323aa5ae --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/general/page.tsx @@ -0,0 +1,11 @@ +import { Metadata } from 'next' + +import General from './components/general' + +export const metadata: Metadata = { + title: 'General' +} + +export default function GeneralSettings() { + return +} diff --git a/ee/tabby-ui/app/(dashboard)/settings/subscription/components/license-form.tsx b/ee/tabby-ui/app/(dashboard)/settings/subscription/components/license-form.tsx new file mode 100644 index 000000000000..ffe1734cec34 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/settings/subscription/components/license-form.tsx @@ -0,0 +1,201 @@ +'use client' + +import * as React from 'react' +import { zodResolver } from '@hookform/resolvers/zod' +import { useForm } from 'react-hook-form' +import { toast } from 'sonner' +import * as z from 'zod' + +import { graphql } from '@/lib/gql/generates' +import { useDebounceCallback } from '@/lib/hooks/use-debounce' +import { useMutation } from '@/lib/tabby/gql' +import { cn } from '@/lib/utils' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger +} from '@/components/ui/alert-dialog' +import { Button, buttonVariants } from '@/components/ui/button' +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage +} from '@/components/ui/form' +import { IconSpinner } from '@/components/ui/icons' +import { Textarea } from '@/components/ui/textarea' + +const formSchema = z.object({ + license: z.string() +}) + +type FormValues = z.infer + +interface LicenseFormProps extends React.HTMLAttributes { + onSuccess?: () => void + canReset?: boolean +} + +const uploadLicenseMutation = graphql(/* GraphQL */ ` + mutation UploadLicense($license: String!) { + uploadLicense(license: $license) + } +`) + +const resetLicenseMutation = graphql(/* GraphQL */ ` + mutation ResetLicense { + resetLicense + } +`) + +export function LicenseForm({ + className, + onSuccess, + canReset, + ...props +}: LicenseFormProps) { + const form = useForm({ + resolver: zodResolver(formSchema) + }) + const license = form.watch('license') + const [isSubmitting, setIsSubmitting] = React.useState(false) + const [resetDialogOpen, setResetDialogOpen] = React.useState(false) + const [isResetting, setIsResetting] = React.useState(false) + + const toggleSubmitting = useDebounceCallback( + (value: boolean, success?: boolean) => { + setIsSubmitting(value) + if (success) { + form.reset({ license: '' }) + toast.success('License is uploaded') + onSuccess?.() + } + }, + 500, + { leading: true } + ) + + const toggleResetting = useDebounceCallback( + (value: boolean, success?: boolean) => { + setIsResetting(value) + if (success) { + setResetDialogOpen(false) + onSuccess?.() + } + }, + 500, + { leading: true } + ) + + const uploadLicense = useMutation(uploadLicenseMutation, { + form + }) + + const resetLicense = useMutation(resetLicenseMutation) + + const onSubmit = (values: FormValues) => { + toggleSubmitting.run(true) + return uploadLicense(values).then(res => { + toggleSubmitting.run(false, res?.data?.uploadLicense) + }) + } + + const onReset: React.MouseEventHandler = e => { + e.preventDefault() + toggleResetting.run(true) + resetLicense().then(res => { + const isSuccess = res?.data?.resetLicense + toggleResetting.run(false, isSuccess) + + if (res?.error) { + toast.error(res.error.message ?? 'reset failed') + } + }) + } + + const onResetDialogOpenChange = (v: boolean) => { + if (isResetting) return + setResetDialogOpen(v) + } + + return ( +
    +
    + + ( + + +
    \ No newline at end of file diff --git a/ee/tabby-webserver/ui/playground.txt b/ee/tabby-webserver/ui/playground.txt deleted file mode 100644 index 21e3519a31c2..000000000000 --- a/ee/tabby-webserver/ui/playground.txt +++ /dev/null @@ -1,12 +0,0 @@ -1:HL["/_next/static/media/86fdec36ddd9097e-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] -2:HL["/_next/static/media/c9a5bc6a7c948fb0-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] -3:HL["/_next/static/css/99bd59844a757a8f.css","style"] -0:["tMrzerf0188ZcKSR-xbdl",[[["",{"children":["playground",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],"$L4",[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/99bd59844a757a8f.css","precedence":"next"}]],"$L5"]]]] -6:I{"id":42761,"chunks":["5328:static/chunks/5328-77e3e9e48a9bea9e.js","1424:static/chunks/1424-78cca59938e343eb.js","1629:static/chunks/1629-85e5cded21f77d75.js","9503:static/chunks/9503-5e587cc77d308fb5.js","5224:static/chunks/5224-49d6d8b64810cc3e.js","7820:static/chunks/7820-ceb91098cbc2ba2b.js","3185:static/chunks/app/layout-a55e9467d63125e4.js"],"name":"Toaster","async":false} -7:I{"id":78495,"chunks":["5328:static/chunks/5328-77e3e9e48a9bea9e.js","1424:static/chunks/1424-78cca59938e343eb.js","1629:static/chunks/1629-85e5cded21f77d75.js","9503:static/chunks/9503-5e587cc77d308fb5.js","5224:static/chunks/5224-49d6d8b64810cc3e.js","7820:static/chunks/7820-ceb91098cbc2ba2b.js","3185:static/chunks/app/layout-a55e9467d63125e4.js"],"name":"Providers","async":false} -8:I{"id":81443,"chunks":["2272:static/chunks/webpack-d1a565a4b87ec7af.js","2971:static/chunks/fd9d1056-a5449620cb20bbc5.js","7864:static/chunks/7864-e09e568e91e5ed29.js"],"name":"","async":false} -9:I{"id":18639,"chunks":["2272:static/chunks/webpack-d1a565a4b87ec7af.js","2971:static/chunks/fd9d1056-a5449620cb20bbc5.js","7864:static/chunks/7864-e09e568e91e5ed29.js"],"name":"","async":false} -b:I{"id":40006,"chunks":["5328:static/chunks/5328-77e3e9e48a9bea9e.js","1424:static/chunks/1424-78cca59938e343eb.js","2047:static/chunks/2047-2ade212d24811181.js","4012:static/chunks/4012-44f2b4af6b9fad01.js","1629:static/chunks/1629-85e5cded21f77d75.js","9503:static/chunks/9503-5e587cc77d308fb5.js","5479:static/chunks/5479-8336a6ddd5158337.js","1621:static/chunks/1621-8722ce7f0a9da221.js","2949:static/chunks/2949-edc71bee11e51f46.js","5224:static/chunks/5224-49d6d8b64810cc3e.js","2248:static/chunks/2248-566dbbd63d7f8694.js","7820:static/chunks/7820-ceb91098cbc2ba2b.js","1894:static/chunks/1894-3767909c2d941ab2.js","2383:static/chunks/app/playground/page-69382445d3321de6.js"],"name":"","async":false} -5:[["$","meta","0",{"charSet":"utf-8"}],["$","title","1",{"children":"Tabby - Playground"}],["$","meta","2",{"name":"description","content":"Tabby, an opensource, self-hosted AI coding assistant."}],["$","meta","3",{"name":"theme-color","media":"(prefers-color-scheme: light)","content":"white"}],["$","meta","4",{"name":"theme-color","media":"(prefers-color-scheme: dark)","content":"black"}],["$","meta","5",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","6",{"name":"next-size-adjust"}]] -4:[null,["$","html",null,{"lang":"en","suppressHydrationWarning":true,"children":[["$","head",null,{}],["$","body",null,{"className":"font-sans antialiased __variable_e66fe9 __variable_bd9c35","children":[["$","$L6",null,{"richColors":true}],["$","$L7",null,{"attribute":"class","defaultTheme":"system","enableSystem":true,"children":[["$","div",null,{"className":"flex min-h-screen flex-col","children":["$","$L8",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$L9",null,{}],"templateStyles":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[],"childProp":{"current":["$","$L8",null,{"parallelRouterKey":"children","segmentPath":["children","playground","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$L9",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$La",["$","main",null,{"className":"flex flex-1 flex-col","children":["$","$Lb",null,{}]}],null],"segment":"__PAGE__"},"styles":[]}],"segment":"playground"},"styles":[]}]}],null]}]]}]]}],null] -a:null diff --git a/ee/tabby-webserver/ui/profile.html b/ee/tabby-webserver/ui/profile.html new file mode 100644 index 000000000000..c8949c6d9bde --- /dev/null +++ b/ee/tabby-webserver/ui/profile.html @@ -0,0 +1 @@ +Tabby - Profile \ No newline at end of file diff --git a/ee/tabby-webserver/ui/profile.txt b/ee/tabby-webserver/ui/profile.txt new file mode 100644 index 000000000000..5c0a8d38525b --- /dev/null +++ b/ee/tabby-webserver/ui/profile.txt @@ -0,0 +1,17 @@ +1:HL["/_next/static/media/a34f9d1faa5f3315-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +2:HL["/_next/static/media/bb3ef058b751a6ad-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +3:HL["/_next/static/media/f75d6d02e2924b13-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +4:HL["/_next/static/css/1df4303b470c4ade.css","style"] +0:["6THjZwODOtNUmFQ4Gzp5t",[[["",{"children":["(dashboard)",{"children":["profile",{"children":["__PAGE__",{}]}]}]},"$undefined","$undefined",true],"$L5",[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/1df4303b470c4ade.css","precedence":"next"}]],"$L6"]]]] +7:I{"id":35590,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Providers","async":false} +8:I{"id":32191,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"","async":false} +9:I{"id":32892,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +a:I{"id":95814,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +b:I{"id":69579,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","4546:static/chunks/4546-05756522a4929864.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","1544:static/chunks/1544-f96cdf5bcfa8f0e4.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9160:static/chunks/app/not-found-8fdd48e65bd803b1.js"],"name":"","async":false} +c:I{"id":90155,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"LicenseBanner","async":false} +d:I{"id":86150,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"","async":false} +f:I{"id":79879,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","240:static/chunks/240-6e4dd6e33fcf5a86.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","4546:static/chunks/4546-05756522a4929864.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","5688:static/chunks/app/(dashboard)/profile/page-fa19607bf35351b7.js"],"name":"","async":false} +10:I{"id":80629,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Toaster","async":false} +6:[["$","meta","0",{"charSet":"utf-8"}],["$","title","1",{"children":"Tabby - Profile"}],["$","meta","2",{"name":"description","content":"Tabby, an opensource, self-hosted AI coding assistant."}],["$","meta","3",{"name":"theme-color","media":"(prefers-color-scheme: light)","content":"white"}],["$","meta","4",{"name":"theme-color","media":"(prefers-color-scheme: dark)","content":"black"}],["$","meta","5",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","link","6",{"rel":"icon","href":"/favicon.ico"}],["$","meta","7",{"name":"next-size-adjust"}]] +5:[null,["$","html",null,{"lang":"en","suppressHydrationWarning":true,"children":[["$","head",null,{}],["$","body",null,{"className":"bg-transparent font-sans antialiased __variable_d65c78 __variable_3c557b __variable_f44606","children":["$","$L7",null,{"attribute":"class","defaultTheme":"system","enableSystem":true,"children":[["$","$L8",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":["$","$Lb",null,{}],"notFoundStyles":[],"childProp":{"current":[null,[["$","$Lc",null,{}],["$","$Ld",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","profile","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$Le",["$","$Lf",null,{}],null],"segment":"__PAGE__"},"styles":[]}],"segment":"profile"},"styles":[]}]}]],null],"segment":"(dashboard)"},"styles":[]}]}],["$","$L10",null,{"richColors":true,"closeButton":true}],null]}]}]]}],null] +e:null diff --git a/ee/tabby-webserver/ui/reports.html b/ee/tabby-webserver/ui/reports.html new file mode 100644 index 000000000000..aa677fe17fd7 --- /dev/null +++ b/ee/tabby-webserver/ui/reports.html @@ -0,0 +1 @@ +Tabby - Reports \ No newline at end of file diff --git a/ee/tabby-webserver/ui/reports.txt b/ee/tabby-webserver/ui/reports.txt new file mode 100644 index 000000000000..28ce9fb5b52c --- /dev/null +++ b/ee/tabby-webserver/ui/reports.txt @@ -0,0 +1,17 @@ +1:HL["/_next/static/media/a34f9d1faa5f3315-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +2:HL["/_next/static/media/bb3ef058b751a6ad-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +3:HL["/_next/static/media/f75d6d02e2924b13-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +4:HL["/_next/static/css/1df4303b470c4ade.css","style"] +0:["6THjZwODOtNUmFQ4Gzp5t",[[["",{"children":["(dashboard)",{"children":["reports",{"children":["__PAGE__",{}]}]}]},"$undefined","$undefined",true],"$L5",[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/1df4303b470c4ade.css","precedence":"next"}]],"$L6"]]]] +7:I{"id":35590,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Providers","async":false} +8:I{"id":32191,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"","async":false} +9:I{"id":32892,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +a:I{"id":95814,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +b:I{"id":69579,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","4546:static/chunks/4546-05756522a4929864.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","1544:static/chunks/1544-f96cdf5bcfa8f0e4.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9160:static/chunks/app/not-found-8fdd48e65bd803b1.js"],"name":"","async":false} +c:I{"id":90155,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"LicenseBanner","async":false} +d:I{"id":86150,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"","async":false} +f:I{"id":5973,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","3179:static/chunks/3179-72a03be258e531a2.js","1002:static/chunks/1002-09e560a83c07e9f4.js","6805:static/chunks/6805-48f0b1146fe740f9.js","4546:static/chunks/4546-05756522a4929864.js","1889:static/chunks/1889-808f5c268ba4d728.js","9604:static/chunks/9604-e2b028696ed86f56.js","9536:static/chunks/9536-cc91f53b736e156c.js","7350:static/chunks/7350-fbd056e17fc64df9.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","4017:static/chunks/4017-15bebe1f3a3c44e0.js","960:static/chunks/app/(dashboard)/reports/page-807426d1dd1598fa.js"],"name":"Report","async":false} +10:I{"id":80629,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Toaster","async":false} +6:[["$","meta","0",{"charSet":"utf-8"}],["$","title","1",{"children":"Tabby - Reports"}],["$","meta","2",{"name":"description","content":"Tabby, an opensource, self-hosted AI coding assistant."}],["$","meta","3",{"name":"theme-color","media":"(prefers-color-scheme: light)","content":"white"}],["$","meta","4",{"name":"theme-color","media":"(prefers-color-scheme: dark)","content":"black"}],["$","meta","5",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","link","6",{"rel":"icon","href":"/favicon.ico"}],["$","meta","7",{"name":"next-size-adjust"}]] +5:[null,["$","html",null,{"lang":"en","suppressHydrationWarning":true,"children":[["$","head",null,{}],["$","body",null,{"className":"bg-transparent font-sans antialiased __variable_d65c78 __variable_3c557b __variable_f44606","children":["$","$L7",null,{"attribute":"class","defaultTheme":"system","enableSystem":true,"children":[["$","$L8",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":["$","$Lb",null,{}],"notFoundStyles":[],"childProp":{"current":[null,[["$","$Lc",null,{}],["$","$Ld",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","reports","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$Le",["$","$Lf",null,{}],null],"segment":"__PAGE__"},"styles":[]}],"segment":"reports"},"styles":[]}]}]],null],"segment":"(dashboard)"},"styles":[]}]}],["$","$L10",null,{"richColors":true,"closeButton":true}],null]}]}]]}],null] +e:null diff --git a/ee/tabby-webserver/ui/search.html b/ee/tabby-webserver/ui/search.html new file mode 100644 index 000000000000..2e4e7eaff46a --- /dev/null +++ b/ee/tabby-webserver/ui/search.html @@ -0,0 +1 @@ +Tabby - Search \ No newline at end of file diff --git a/ee/tabby-webserver/ui/search.txt b/ee/tabby-webserver/ui/search.txt new file mode 100644 index 000000000000..e57e1cc834b6 --- /dev/null +++ b/ee/tabby-webserver/ui/search.txt @@ -0,0 +1,16 @@ +1:HL["/_next/static/media/a34f9d1faa5f3315-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +2:HL["/_next/static/media/bb3ef058b751a6ad-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +3:HL["/_next/static/media/f75d6d02e2924b13-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +4:HL["/_next/static/css/1df4303b470c4ade.css","style"] +0:["6THjZwODOtNUmFQ4Gzp5t",[[["",{"children":["search",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],"$L5",[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/1df4303b470c4ade.css","precedence":"next"}]],"$L6"]]]] +7:HL["/_next/static/css/68f52fface9b0518.css","style"] +8:I{"id":35590,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Providers","async":false} +9:I{"id":32191,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"","async":false} +a:I{"id":32892,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +b:I{"id":95814,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +c:I{"id":69579,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","4546:static/chunks/4546-05756522a4929864.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","1544:static/chunks/1544-f96cdf5bcfa8f0e4.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9160:static/chunks/app/not-found-8fdd48e65bd803b1.js"],"name":"","async":false} +e:I{"id":13255,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4648:static/chunks/95919c35-2a0243d5f6d0cb88.js","6527:static/chunks/d04f95b7-32b56781246774be.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","240:static/chunks/240-6e4dd6e33fcf5a86.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","3179:static/chunks/3179-72a03be258e531a2.js","4421:static/chunks/4421-0da05343b4d5e17a.js","7070:static/chunks/7070-0361b26f2c124ca9.js","1002:static/chunks/1002-09e560a83c07e9f4.js","1460:static/chunks/1460-849850f3e0fcced7.js","6805:static/chunks/6805-48f0b1146fe740f9.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6635:static/chunks/6635-85423d316922c3d8.js","5723:static/chunks/5723-34db62fac9667d56.js","2928:static/chunks/2928-98ebfedf06caf8e1.js","9860:static/chunks/9860-b2cab0e640a501ef.js","5665:static/chunks/5665-32ab36ba93f52f1f.js","6095:static/chunks/6095-23d24a26b0b31c28.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","4526:static/chunks/4526-b6d4584afffa9c34.js","8708:static/chunks/8708-1cf948906a401249.js","2797:static/chunks/app/search/page-f08cae58be4c1433.js"],"name":"Search","async":false} +f:I{"id":80629,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Toaster","async":false} +6:[["$","meta","0",{"charSet":"utf-8"}],["$","title","1",{"children":"Tabby - Search"}],["$","meta","2",{"name":"description","content":"Tabby, an opensource, self-hosted AI coding assistant."}],["$","meta","3",{"name":"theme-color","media":"(prefers-color-scheme: light)","content":"white"}],["$","meta","4",{"name":"theme-color","media":"(prefers-color-scheme: dark)","content":"black"}],["$","meta","5",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","link","6",{"rel":"icon","href":"/favicon.ico"}],["$","meta","7",{"name":"next-size-adjust"}]] +5:[null,["$","html",null,{"lang":"en","suppressHydrationWarning":true,"children":[["$","head",null,{}],["$","body",null,{"className":"bg-transparent font-sans antialiased __variable_d65c78 __variable_3c557b __variable_f44606","children":["$","$L8",null,{"attribute":"class","defaultTheme":"system","enableSystem":true,"children":[["$","$L9",null,{"children":["$","$La",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$Lb",null,{}],"templateStyles":"$undefined","notFound":["$","$Lc",null,{}],"notFoundStyles":[],"childProp":{"current":["$","$La",null,{"parallelRouterKey":"children","segmentPath":["children","search","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$Lb",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$Ld",["$","$Le",null,{}],null],"segment":"__PAGE__"},"styles":[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/68f52fface9b0518.css","precedence":"next"}]]}],"segment":"search"},"styles":[]}]}],["$","$Lf",null,{"richColors":true,"closeButton":true}],null]}]}]]}],null] +d:null diff --git a/ee/tabby-webserver/ui/settings/general.html b/ee/tabby-webserver/ui/settings/general.html new file mode 100644 index 000000000000..5750cc889424 --- /dev/null +++ b/ee/tabby-webserver/ui/settings/general.html @@ -0,0 +1 @@ +Tabby - General \ No newline at end of file diff --git a/ee/tabby-webserver/ui/settings/general.txt b/ee/tabby-webserver/ui/settings/general.txt new file mode 100644 index 000000000000..1c3deb598810 --- /dev/null +++ b/ee/tabby-webserver/ui/settings/general.txt @@ -0,0 +1,17 @@ +1:HL["/_next/static/media/a34f9d1faa5f3315-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +2:HL["/_next/static/media/bb3ef058b751a6ad-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +3:HL["/_next/static/media/f75d6d02e2924b13-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +4:HL["/_next/static/css/1df4303b470c4ade.css","style"] +0:["6THjZwODOtNUmFQ4Gzp5t",[[["",{"children":["(dashboard)",{"children":["settings",{"children":["general",{"children":["__PAGE__",{}]}]}]}]},"$undefined","$undefined",true],"$L5",[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/1df4303b470c4ade.css","precedence":"next"}]],"$L6"]]]] +7:I{"id":35590,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Providers","async":false} +8:I{"id":32191,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"","async":false} +9:I{"id":32892,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +a:I{"id":95814,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +b:I{"id":69579,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","4546:static/chunks/4546-05756522a4929864.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","1544:static/chunks/1544-f96cdf5bcfa8f0e4.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9160:static/chunks/app/not-found-8fdd48e65bd803b1.js"],"name":"","async":false} +c:I{"id":90155,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"LicenseBanner","async":false} +d:I{"id":86150,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"","async":false} +f:I{"id":23171,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","240:static/chunks/240-6e4dd6e33fcf5a86.js","8194:static/chunks/8194-f2af5792032c1d6a.js","1009:static/chunks/1009-d77352b43d12203a.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","1519:static/chunks/app/(dashboard)/settings/general/page-9e03891fb9a47616.js"],"name":"","async":false} +10:I{"id":80629,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Toaster","async":false} +6:[["$","meta","0",{"charSet":"utf-8"}],["$","title","1",{"children":"Tabby - General"}],["$","meta","2",{"name":"description","content":"Tabby, an opensource, self-hosted AI coding assistant."}],["$","meta","3",{"name":"theme-color","media":"(prefers-color-scheme: light)","content":"white"}],["$","meta","4",{"name":"theme-color","media":"(prefers-color-scheme: dark)","content":"black"}],["$","meta","5",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","link","6",{"rel":"icon","href":"/favicon.ico"}],["$","meta","7",{"name":"next-size-adjust"}]] +5:[null,["$","html",null,{"lang":"en","suppressHydrationWarning":true,"children":[["$","head",null,{}],["$","body",null,{"className":"bg-transparent font-sans antialiased __variable_d65c78 __variable_3c557b __variable_f44606","children":["$","$L7",null,{"attribute":"class","defaultTheme":"system","enableSystem":true,"children":[["$","$L8",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":["$","$Lb",null,{}],"notFoundStyles":[],"childProp":{"current":[null,[["$","$Lc",null,{}],["$","$Ld",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","general","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$Le",["$","$Lf",null,{}],null],"segment":"__PAGE__"},"styles":[]}],"segment":"general"},"styles":[]}],"segment":"settings"},"styles":[]}]}]],null],"segment":"(dashboard)"},"styles":[]}]}],["$","$L10",null,{"richColors":true,"closeButton":true}],null]}]}]]}],null] +e:null diff --git a/ee/tabby-webserver/ui/settings/mail.html b/ee/tabby-webserver/ui/settings/mail.html new file mode 100644 index 000000000000..baa2f0871748 --- /dev/null +++ b/ee/tabby-webserver/ui/settings/mail.html @@ -0,0 +1 @@ +Tabby - Mail Delivery \ No newline at end of file diff --git a/ee/tabby-webserver/ui/settings/mail.txt b/ee/tabby-webserver/ui/settings/mail.txt new file mode 100644 index 000000000000..c77a88e407bb --- /dev/null +++ b/ee/tabby-webserver/ui/settings/mail.txt @@ -0,0 +1,17 @@ +1:HL["/_next/static/media/a34f9d1faa5f3315-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +2:HL["/_next/static/media/bb3ef058b751a6ad-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +3:HL["/_next/static/media/f75d6d02e2924b13-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +4:HL["/_next/static/css/1df4303b470c4ade.css","style"] +0:["6THjZwODOtNUmFQ4Gzp5t",[[["",{"children":["(dashboard)",{"children":["settings",{"children":["(integrations)",{"children":["mail",{"children":["__PAGE__",{}]}]}]}]}]},"$undefined","$undefined",true],"$L5",[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/1df4303b470c4ade.css","precedence":"next"}]],"$L6"]]]] +7:I{"id":35590,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Providers","async":false} +8:I{"id":32191,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"","async":false} +9:I{"id":32892,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +a:I{"id":95814,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +b:I{"id":69579,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","4546:static/chunks/4546-05756522a4929864.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","1544:static/chunks/1544-f96cdf5bcfa8f0e4.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9160:static/chunks/app/not-found-8fdd48e65bd803b1.js"],"name":"","async":false} +c:I{"id":90155,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"LicenseBanner","async":false} +d:I{"id":86150,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"","async":false} +f:I{"id":86921,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","240:static/chunks/240-6e4dd6e33fcf5a86.js","3179:static/chunks/3179-72a03be258e531a2.js","1460:static/chunks/1460-849850f3e0fcced7.js","1889:static/chunks/1889-808f5c268ba4d728.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","951:static/chunks/app/(dashboard)/settings/(integrations)/mail/page-47f53ca20844ff8e.js"],"name":"Mail","async":false} +10:I{"id":80629,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Toaster","async":false} +6:[["$","meta","0",{"charSet":"utf-8"}],["$","title","1",{"children":"Tabby - Mail Delivery"}],["$","meta","2",{"name":"description","content":"Tabby, an opensource, self-hosted AI coding assistant."}],["$","meta","3",{"name":"theme-color","media":"(prefers-color-scheme: light)","content":"white"}],["$","meta","4",{"name":"theme-color","media":"(prefers-color-scheme: dark)","content":"black"}],["$","meta","5",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","link","6",{"rel":"icon","href":"/favicon.ico"}],["$","meta","7",{"name":"next-size-adjust"}]] +5:[null,["$","html",null,{"lang":"en","suppressHydrationWarning":true,"children":[["$","head",null,{}],["$","body",null,{"className":"bg-transparent font-sans antialiased __variable_d65c78 __variable_3c557b __variable_f44606","children":["$","$L7",null,{"attribute":"class","defaultTheme":"system","enableSystem":true,"children":[["$","$L8",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":["$","$Lb",null,{}],"notFoundStyles":[],"childProp":{"current":[null,[["$","$Lc",null,{}],["$","$Ld",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","mail","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$Le",["$","$Lf",null,{}],null],"segment":"__PAGE__"},"styles":[]}],"segment":"mail"},"styles":[]}],"segment":"(integrations)"},"styles":[]}],"segment":"settings"},"styles":[]}]}]],null],"segment":"(dashboard)"},"styles":[]}]}],["$","$L10",null,{"richColors":true,"closeButton":true}],null]}]}]]}],null] +e:null diff --git a/ee/tabby-webserver/ui/settings/providers/doc.html b/ee/tabby-webserver/ui/settings/providers/doc.html new file mode 100644 index 000000000000..d7fc0148bb59 --- /dev/null +++ b/ee/tabby-webserver/ui/settings/providers/doc.html @@ -0,0 +1 @@ +Tabby - Context Providers \ No newline at end of file diff --git a/ee/tabby-webserver/ui/settings/providers/doc.txt b/ee/tabby-webserver/ui/settings/providers/doc.txt new file mode 100644 index 000000000000..3395d06bf245 --- /dev/null +++ b/ee/tabby-webserver/ui/settings/providers/doc.txt @@ -0,0 +1,20 @@ +1:HL["/_next/static/media/a34f9d1faa5f3315-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +2:HL["/_next/static/media/bb3ef058b751a6ad-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +3:HL["/_next/static/media/f75d6d02e2924b13-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +4:HL["/_next/static/css/1df4303b470c4ade.css","style"] +0:["6THjZwODOtNUmFQ4Gzp5t",[[["",{"children":["(dashboard)",{"children":["settings",{"children":["(integrations)",{"children":["providers",{"children":["doc",{"children":["__PAGE__",{}]}]}]}]}]}]},"$undefined","$undefined",true],"$L5",[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/1df4303b470c4ade.css","precedence":"next"}]],"$L6"]]]] +7:I{"id":35590,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Providers","async":false} +8:I{"id":32191,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"","async":false} +9:I{"id":32892,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +a:I{"id":95814,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +b:I{"id":69579,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","4546:static/chunks/4546-05756522a4929864.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","1544:static/chunks/1544-f96cdf5bcfa8f0e4.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9160:static/chunks/app/not-found-8fdd48e65bd803b1.js"],"name":"","async":false} +c:I{"id":90155,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"LicenseBanner","async":false} +d:I{"id":86150,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"","async":false} +e:I{"id":69145,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","6807:static/chunks/app/(dashboard)/settings/(integrations)/providers/layout-73ce9c8d662cc1f1.js"],"name":"","async":false} +f:I{"id":57830,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","6807:static/chunks/app/(dashboard)/settings/(integrations)/providers/layout-73ce9c8d662cc1f1.js"],"name":"ScrollArea","async":false} +11:I{"id":10059,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","3179:static/chunks/3179-72a03be258e531a2.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","6805:static/chunks/6805-48f0b1146fe740f9.js","1889:static/chunks/1889-808f5c268ba4d728.js","6065:static/chunks/6065-17553e3578c58f7b.js","6432:static/chunks/6432-33866c51c441c1bc.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9631:static/chunks/app/(dashboard)/settings/(integrations)/providers/doc/page-95cb24a09802216c.js"],"name":"","async":false} +12:I{"id":81487,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","3179:static/chunks/3179-72a03be258e531a2.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","6805:static/chunks/6805-48f0b1146fe740f9.js","1889:static/chunks/1889-808f5c268ba4d728.js","6065:static/chunks/6065-17553e3578c58f7b.js","6432:static/chunks/6432-33866c51c441c1bc.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9631:static/chunks/app/(dashboard)/settings/(integrations)/providers/doc/page-95cb24a09802216c.js"],"name":"","async":false} +13:I{"id":80629,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Toaster","async":false} +6:[["$","meta","0",{"charSet":"utf-8"}],["$","title","1",{"children":"Tabby - Context Providers"}],["$","meta","2",{"name":"description","content":"Tabby, an opensource, self-hosted AI coding assistant."}],["$","meta","3",{"name":"theme-color","media":"(prefers-color-scheme: light)","content":"white"}],["$","meta","4",{"name":"theme-color","media":"(prefers-color-scheme: dark)","content":"black"}],["$","meta","5",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","link","6",{"rel":"icon","href":"/favicon.ico"}],["$","meta","7",{"name":"next-size-adjust"}]] +5:[null,["$","html",null,{"lang":"en","suppressHydrationWarning":true,"children":[["$","head",null,{}],["$","body",null,{"className":"bg-transparent font-sans antialiased __variable_d65c78 __variable_3c557b __variable_f44606","children":["$","$L7",null,{"attribute":"class","defaultTheme":"system","enableSystem":true,"children":[["$","$L8",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":["$","$Lb",null,{}],"notFoundStyles":[],"childProp":{"current":[null,[["$","$Lc",null,{}],["$","$Ld",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,["$","div",null,{"className":"-m-4 flex lg:-m-10","children":[["$","$Le",null,{"className":"w-[200px] pl-4 pt-4 lg:w-[250px]"}],["$","$Lf",null,{"className":"flex-1","children":["$","div",null,{"className":"p-4 lg:p-10","children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children","doc","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$L10",[["$","div",null,{"className":"p-6 pt-0 pl-0","children":[["$","div",null,{"className":"mb-4 flex items-center gap-4","children":["$","div",null,{"className":"flex-1 text-sm text-muted-foreground","children":[["$","p",null,{"children":"Documents are a critical source for engineering knowledge. Tabby provides an easy way to include these documents when interacting with LLM in chat interfaces (e.g., Answer Engine, Chat Panel, etc.). Simply press the @ button in the chat interface and select the document you wish to include."}],false]}]}],["$","$L11",null,{}]]}],["$","div",null,{"className":"p-6 pl-0 pt-8 xl:pb-32","children":[["$","div",null,{"className":"mb-4 flex items-center gap-4","children":["$","div",null,{"className":"flex-1 text-sm text-muted-foreground","children":[["$","p",null,{"children":"You can also include your own developer documents here. Please ensure that the URLs are accessible from the Tabby server to guarantee successful crawling."}],false]}]}],["$","$L12",null,{}]]}]],null],"segment":"__PAGE__"},"styles":[]}],"segment":"doc"},"styles":[]}]}]}]]}],null],"segment":"providers"},"styles":[]}],"segment":"(integrations)"},"styles":[]}],"segment":"settings"},"styles":[]}]}]],null],"segment":"(dashboard)"},"styles":[]}]}],["$","$L13",null,{"richColors":true,"closeButton":true}],null]}]}]]}],null] +10:null diff --git a/ee/tabby-webserver/ui/settings/providers/doc/new.html b/ee/tabby-webserver/ui/settings/providers/doc/new.html new file mode 100644 index 000000000000..48a681ab69a3 --- /dev/null +++ b/ee/tabby-webserver/ui/settings/providers/doc/new.html @@ -0,0 +1 @@ +Tabby - Context Providers \ No newline at end of file diff --git a/ee/tabby-webserver/ui/settings/providers/doc/new.txt b/ee/tabby-webserver/ui/settings/providers/doc/new.txt new file mode 100644 index 000000000000..8581b6df3534 --- /dev/null +++ b/ee/tabby-webserver/ui/settings/providers/doc/new.txt @@ -0,0 +1,19 @@ +1:HL["/_next/static/media/a34f9d1faa5f3315-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +2:HL["/_next/static/media/bb3ef058b751a6ad-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +3:HL["/_next/static/media/f75d6d02e2924b13-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +4:HL["/_next/static/css/1df4303b470c4ade.css","style"] +0:["6THjZwODOtNUmFQ4Gzp5t",[[["",{"children":["(dashboard)",{"children":["settings",{"children":["(integrations)",{"children":["providers",{"children":["doc",{"children":["new",{"children":["__PAGE__",{}]}]}]}]}]}]}]},"$undefined","$undefined",true],"$L5",[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/1df4303b470c4ade.css","precedence":"next"}]],"$L6"]]]] +7:I{"id":35590,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Providers","async":false} +8:I{"id":32191,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"","async":false} +9:I{"id":32892,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +a:I{"id":95814,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +b:I{"id":69579,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","4546:static/chunks/4546-05756522a4929864.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","1544:static/chunks/1544-f96cdf5bcfa8f0e4.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9160:static/chunks/app/not-found-8fdd48e65bd803b1.js"],"name":"","async":false} +c:I{"id":90155,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"LicenseBanner","async":false} +d:I{"id":86150,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"","async":false} +e:I{"id":69145,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","6807:static/chunks/app/(dashboard)/settings/(integrations)/providers/layout-73ce9c8d662cc1f1.js"],"name":"","async":false} +f:I{"id":57830,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","6807:static/chunks/app/(dashboard)/settings/(integrations)/providers/layout-73ce9c8d662cc1f1.js"],"name":"ScrollArea","async":false} +11:I{"id":43158,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","9906:static/chunks/9906-125e9641f98e1ae5.js","240:static/chunks/240-6e4dd6e33fcf5a86.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3117:static/chunks/app/(dashboard)/settings/(integrations)/providers/doc/new/page-b558042910b63bae.js"],"name":"","async":false} +12:I{"id":80629,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Toaster","async":false} +6:[["$","meta","0",{"charSet":"utf-8"}],["$","title","1",{"children":"Tabby - Context Providers"}],["$","meta","2",{"name":"description","content":"Tabby, an opensource, self-hosted AI coding assistant."}],["$","meta","3",{"name":"theme-color","media":"(prefers-color-scheme: light)","content":"white"}],["$","meta","4",{"name":"theme-color","media":"(prefers-color-scheme: dark)","content":"black"}],["$","meta","5",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","link","6",{"rel":"icon","href":"/favicon.ico"}],["$","meta","7",{"name":"next-size-adjust"}]] +5:[null,["$","html",null,{"lang":"en","suppressHydrationWarning":true,"children":[["$","head",null,{}],["$","body",null,{"className":"bg-transparent font-sans antialiased __variable_d65c78 __variable_3c557b __variable_f44606","children":["$","$L7",null,{"attribute":"class","defaultTheme":"system","enableSystem":true,"children":[["$","$L8",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":["$","$Lb",null,{}],"notFoundStyles":[],"childProp":{"current":[null,[["$","$Lc",null,{}],["$","$Ld",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,["$","div",null,{"className":"-m-4 flex lg:-m-10","children":[["$","$Le",null,{"className":"w-[200px] pl-4 pt-4 lg:w-[250px]"}],["$","$Lf",null,{"className":"flex-1","children":["$","div",null,{"className":"p-4 lg:p-10","children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children","doc","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children","doc","children","new","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$L10",["$","$L11",null,{}],null],"segment":"__PAGE__"},"styles":[]}],"segment":"new"},"styles":[]}],"segment":"doc"},"styles":[]}]}]}]]}],null],"segment":"providers"},"styles":[]}],"segment":"(integrations)"},"styles":[]}],"segment":"settings"},"styles":[]}]}]],null],"segment":"(dashboard)"},"styles":[]}]}],["$","$L12",null,{"richColors":true,"closeButton":true}],null]}]}]]}],null] +10:null diff --git a/ee/tabby-webserver/ui/settings/providers/git.html b/ee/tabby-webserver/ui/settings/providers/git.html new file mode 100644 index 000000000000..9b06a5b92bca --- /dev/null +++ b/ee/tabby-webserver/ui/settings/providers/git.html @@ -0,0 +1 @@ +Tabby - Context Providers \ No newline at end of file diff --git a/ee/tabby-webserver/ui/settings/providers/git.txt b/ee/tabby-webserver/ui/settings/providers/git.txt new file mode 100644 index 000000000000..e03038a857e8 --- /dev/null +++ b/ee/tabby-webserver/ui/settings/providers/git.txt @@ -0,0 +1,21 @@ +1:HL["/_next/static/media/a34f9d1faa5f3315-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +2:HL["/_next/static/media/bb3ef058b751a6ad-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +3:HL["/_next/static/media/f75d6d02e2924b13-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +4:HL["/_next/static/css/1df4303b470c4ade.css","style"] +0:["6THjZwODOtNUmFQ4Gzp5t",[[["",{"children":["(dashboard)",{"children":["settings",{"children":["(integrations)",{"children":["providers",{"children":["git",{"children":["__PAGE__",{}]}]}]}]}]}]},"$undefined","$undefined",true],"$L5",[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/1df4303b470c4ade.css","precedence":"next"}]],"$L6"]]]] +7:I{"id":35590,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Providers","async":false} +8:I{"id":32191,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"","async":false} +9:I{"id":32892,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +a:I{"id":95814,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +b:I{"id":69579,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","4546:static/chunks/4546-05756522a4929864.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","1544:static/chunks/1544-f96cdf5bcfa8f0e4.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9160:static/chunks/app/not-found-8fdd48e65bd803b1.js"],"name":"","async":false} +c:I{"id":90155,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"LicenseBanner","async":false} +d:I{"id":86150,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"","async":false} +e:I{"id":69145,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","6807:static/chunks/app/(dashboard)/settings/(integrations)/providers/layout-73ce9c8d662cc1f1.js"],"name":"","async":false} +f:I{"id":57830,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","6807:static/chunks/app/(dashboard)/settings/(integrations)/providers/layout-73ce9c8d662cc1f1.js"],"name":"ScrollArea","async":false} +10:I{"id":54007,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","436:static/chunks/436-401a51e7af01c90f.js","4007:static/chunks/4007-80fb4e0338c67b28.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","7915:static/chunks/app/(dashboard)/settings/(integrations)/providers/git/layout-4f6c4896a9b7682c.js"],"name":"","async":false} +11:I{"id":81565,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","436:static/chunks/436-401a51e7af01c90f.js","4007:static/chunks/4007-80fb4e0338c67b28.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","7915:static/chunks/app/(dashboard)/settings/(integrations)/providers/git/layout-4f6c4896a9b7682c.js"],"name":"IconExternalLink","async":false} +13:I{"id":71901,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","6805:static/chunks/6805-48f0b1146fe740f9.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","214:static/chunks/app/(dashboard)/settings/(integrations)/providers/git/page-24e0bfa359cf56d3.js"],"name":"","async":false} +14:I{"id":80629,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Toaster","async":false} +6:[["$","meta","0",{"charSet":"utf-8"}],["$","title","1",{"children":"Tabby - Context Providers"}],["$","meta","2",{"name":"description","content":"Tabby, an opensource, self-hosted AI coding assistant."}],["$","meta","3",{"name":"theme-color","media":"(prefers-color-scheme: light)","content":"white"}],["$","meta","4",{"name":"theme-color","media":"(prefers-color-scheme: dark)","content":"black"}],["$","meta","5",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","link","6",{"rel":"icon","href":"/favicon.ico"}],["$","meta","7",{"name":"next-size-adjust"}]] +5:[null,["$","html",null,{"lang":"en","suppressHydrationWarning":true,"children":[["$","head",null,{}],["$","body",null,{"className":"bg-transparent font-sans antialiased __variable_d65c78 __variable_3c557b __variable_f44606","children":["$","$L7",null,{"attribute":"class","defaultTheme":"system","enableSystem":true,"children":[["$","$L8",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":["$","$Lb",null,{}],"notFoundStyles":[],"childProp":{"current":[null,[["$","$Lc",null,{}],["$","$Ld",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,["$","div",null,{"className":"-m-4 flex lg:-m-10","children":[["$","$Le",null,{"className":"w-[200px] pl-4 pt-4 lg:w-[250px]"}],["$","$Lf",null,{"className":"flex-1","children":["$","div",null,{"className":"p-4 lg:p-10","children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,[["$","div",null,{"className":"mb-4 flex items-center gap-4","children":["$","div",null,{"className":"flex-1 text-sm text-muted-foreground","children":["Connect to remote and local Git repositories, utilizing these repositories as context to enhance the performance of large language models.",["$","$L10",null,{"className":"ml-2 inline-flex cursor-pointer flex-row items-center text-primary hover:underline","href":"https://tabby.tabbyml.com/blog/2023/10/16/repository-context-for-code-completion","target":"_blank","children":["Learn more",["$","$L11",null,{"className":"ml-1"}]]}]]}]}],["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children","git","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$L12",["$","$L13",null,{}],null],"segment":"__PAGE__"},"styles":[]}]],null],"segment":"git"},"styles":[]}]}]}]]}],null],"segment":"providers"},"styles":[]}],"segment":"(integrations)"},"styles":[]}],"segment":"settings"},"styles":[]}]}]],null],"segment":"(dashboard)"},"styles":[]}]}],["$","$L14",null,{"richColors":true,"closeButton":true}],null]}]}]]}],null] +12:null diff --git a/ee/tabby-webserver/ui/settings/providers/git/new.html b/ee/tabby-webserver/ui/settings/providers/git/new.html new file mode 100644 index 000000000000..bd734a46a141 --- /dev/null +++ b/ee/tabby-webserver/ui/settings/providers/git/new.html @@ -0,0 +1 @@ +Tabby - Context Providers \ No newline at end of file diff --git a/ee/tabby-webserver/ui/settings/providers/git/new.txt b/ee/tabby-webserver/ui/settings/providers/git/new.txt new file mode 100644 index 000000000000..c94e738e5aa7 --- /dev/null +++ b/ee/tabby-webserver/ui/settings/providers/git/new.txt @@ -0,0 +1,21 @@ +1:HL["/_next/static/media/a34f9d1faa5f3315-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +2:HL["/_next/static/media/bb3ef058b751a6ad-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +3:HL["/_next/static/media/f75d6d02e2924b13-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +4:HL["/_next/static/css/1df4303b470c4ade.css","style"] +0:["6THjZwODOtNUmFQ4Gzp5t",[[["",{"children":["(dashboard)",{"children":["settings",{"children":["(integrations)",{"children":["providers",{"children":["git",{"children":["new",{"children":["__PAGE__",{}]}]}]}]}]}]}]},"$undefined","$undefined",true],"$L5",[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/1df4303b470c4ade.css","precedence":"next"}]],"$L6"]]]] +7:I{"id":35590,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Providers","async":false} +8:I{"id":32191,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"","async":false} +9:I{"id":32892,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +a:I{"id":95814,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +b:I{"id":69579,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","4546:static/chunks/4546-05756522a4929864.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","1544:static/chunks/1544-f96cdf5bcfa8f0e4.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9160:static/chunks/app/not-found-8fdd48e65bd803b1.js"],"name":"","async":false} +c:I{"id":90155,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"LicenseBanner","async":false} +d:I{"id":86150,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"","async":false} +e:I{"id":69145,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","6807:static/chunks/app/(dashboard)/settings/(integrations)/providers/layout-73ce9c8d662cc1f1.js"],"name":"","async":false} +f:I{"id":57830,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","6807:static/chunks/app/(dashboard)/settings/(integrations)/providers/layout-73ce9c8d662cc1f1.js"],"name":"ScrollArea","async":false} +10:I{"id":54007,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","436:static/chunks/436-401a51e7af01c90f.js","4007:static/chunks/4007-80fb4e0338c67b28.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","7915:static/chunks/app/(dashboard)/settings/(integrations)/providers/git/layout-4f6c4896a9b7682c.js"],"name":"","async":false} +11:I{"id":81565,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","436:static/chunks/436-401a51e7af01c90f.js","4007:static/chunks/4007-80fb4e0338c67b28.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","7915:static/chunks/app/(dashboard)/settings/(integrations)/providers/git/layout-4f6c4896a9b7682c.js"],"name":"IconExternalLink","async":false} +13:I{"id":83272,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","9906:static/chunks/9906-125e9641f98e1ae5.js","240:static/chunks/240-6e4dd6e33fcf5a86.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","817:static/chunks/app/(dashboard)/settings/(integrations)/providers/git/new/page-ff974a5ec621eea3.js"],"name":"NewRepository","async":false} +14:I{"id":80629,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Toaster","async":false} +6:[["$","meta","0",{"charSet":"utf-8"}],["$","title","1",{"children":"Tabby - Context Providers"}],["$","meta","2",{"name":"description","content":"Tabby, an opensource, self-hosted AI coding assistant."}],["$","meta","3",{"name":"theme-color","media":"(prefers-color-scheme: light)","content":"white"}],["$","meta","4",{"name":"theme-color","media":"(prefers-color-scheme: dark)","content":"black"}],["$","meta","5",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","link","6",{"rel":"icon","href":"/favicon.ico"}],["$","meta","7",{"name":"next-size-adjust"}]] +5:[null,["$","html",null,{"lang":"en","suppressHydrationWarning":true,"children":[["$","head",null,{}],["$","body",null,{"className":"bg-transparent font-sans antialiased __variable_d65c78 __variable_3c557b __variable_f44606","children":["$","$L7",null,{"attribute":"class","defaultTheme":"system","enableSystem":true,"children":[["$","$L8",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":["$","$Lb",null,{}],"notFoundStyles":[],"childProp":{"current":[null,[["$","$Lc",null,{}],["$","$Ld",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,["$","div",null,{"className":"-m-4 flex lg:-m-10","children":[["$","$Le",null,{"className":"w-[200px] pl-4 pt-4 lg:w-[250px]"}],["$","$Lf",null,{"className":"flex-1","children":["$","div",null,{"className":"p-4 lg:p-10","children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,[["$","div",null,{"className":"mb-4 flex items-center gap-4","children":["$","div",null,{"className":"flex-1 text-sm text-muted-foreground","children":["Connect to remote and local Git repositories, utilizing these repositories as context to enhance the performance of large language models.",["$","$L10",null,{"className":"ml-2 inline-flex cursor-pointer flex-row items-center text-primary hover:underline","href":"https://tabby.tabbyml.com/blog/2023/10/16/repository-context-for-code-completion","target":"_blank","children":["Learn more",["$","$L11",null,{"className":"ml-1"}]]}]]}]}],["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children","git","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children","git","children","new","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$L12",["$","$L13",null,{}],null],"segment":"__PAGE__"},"styles":[]}],"segment":"new"},"styles":[]}]],null],"segment":"git"},"styles":[]}]}]}]]}],null],"segment":"providers"},"styles":[]}],"segment":"(integrations)"},"styles":[]}],"segment":"settings"},"styles":[]}]}]],null],"segment":"(dashboard)"},"styles":[]}]}],["$","$L14",null,{"richColors":true,"closeButton":true}],null]}]}]]}],null] +12:null diff --git a/ee/tabby-webserver/ui/settings/providers/github-self-hosted.html b/ee/tabby-webserver/ui/settings/providers/github-self-hosted.html new file mode 100644 index 000000000000..01834b448ec7 --- /dev/null +++ b/ee/tabby-webserver/ui/settings/providers/github-self-hosted.html @@ -0,0 +1 @@ +Tabby - Context Providers \ No newline at end of file diff --git a/ee/tabby-webserver/ui/settings/providers/github-self-hosted.txt b/ee/tabby-webserver/ui/settings/providers/github-self-hosted.txt new file mode 100644 index 000000000000..0d7d6f45da0e --- /dev/null +++ b/ee/tabby-webserver/ui/settings/providers/github-self-hosted.txt @@ -0,0 +1,21 @@ +1:HL["/_next/static/media/a34f9d1faa5f3315-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +2:HL["/_next/static/media/bb3ef058b751a6ad-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +3:HL["/_next/static/media/f75d6d02e2924b13-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +4:HL["/_next/static/css/1df4303b470c4ade.css","style"] +0:["6THjZwODOtNUmFQ4Gzp5t",[[["",{"children":["(dashboard)",{"children":["settings",{"children":["(integrations)",{"children":["providers",{"children":[["kind","github-self-hosted","d"],{"children":["__PAGE__?{\"kind\":\"github-self-hosted\"}",{}]}]}]}]}]}]},"$undefined","$undefined",true],"$L5",[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/1df4303b470c4ade.css","precedence":"next"}]],"$L6"]]]] +7:I{"id":35590,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Providers","async":false} +8:I{"id":32191,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"","async":false} +9:I{"id":32892,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +a:I{"id":95814,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +b:I{"id":69579,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","4546:static/chunks/4546-05756522a4929864.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","1544:static/chunks/1544-f96cdf5bcfa8f0e4.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9160:static/chunks/app/not-found-8fdd48e65bd803b1.js"],"name":"","async":false} +c:I{"id":90155,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"LicenseBanner","async":false} +d:I{"id":86150,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"","async":false} +e:I{"id":69145,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","6807:static/chunks/app/(dashboard)/settings/(integrations)/providers/layout-73ce9c8d662cc1f1.js"],"name":"","async":false} +f:I{"id":57830,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","6807:static/chunks/app/(dashboard)/settings/(integrations)/providers/layout-73ce9c8d662cc1f1.js"],"name":"ScrollArea","async":false} +10:I{"id":54007,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","436:static/chunks/436-401a51e7af01c90f.js","4007:static/chunks/4007-80fb4e0338c67b28.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","2219:static/chunks/app/(dashboard)/settings/(integrations)/providers/[kind]/layout-9688fa1599db2b7b.js"],"name":"","async":false} +11:I{"id":81565,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","436:static/chunks/436-401a51e7af01c90f.js","4007:static/chunks/4007-80fb4e0338c67b28.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","2219:static/chunks/app/(dashboard)/settings/(integrations)/providers/[kind]/layout-9688fa1599db2b7b.js"],"name":"IconExternalLink","async":false} +13:I{"id":6194,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","4007:static/chunks/4007-80fb4e0338c67b28.js","7070:static/chunks/7070-0361b26f2c124ca9.js","7548:static/chunks/7548-9d3520e9c948edb5.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","410:static/chunks/410-2d9f0445fa0cca52.js","9667:static/chunks/app/(dashboard)/settings/(integrations)/providers/[kind]/page-8f2bf20ea03adec0.js"],"name":"","async":false} +14:I{"id":80629,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Toaster","async":false} +6:[["$","meta","0",{"charSet":"utf-8"}],["$","title","1",{"children":"Tabby - Context Providers"}],["$","meta","2",{"name":"description","content":"Tabby, an opensource, self-hosted AI coding assistant."}],["$","meta","3",{"name":"theme-color","media":"(prefers-color-scheme: light)","content":"white"}],["$","meta","4",{"name":"theme-color","media":"(prefers-color-scheme: dark)","content":"black"}],["$","meta","5",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","link","6",{"rel":"icon","href":"/favicon.ico"}],["$","meta","7",{"name":"next-size-adjust"}]] +5:[null,["$","html",null,{"lang":"en","suppressHydrationWarning":true,"children":[["$","head",null,{}],["$","body",null,{"className":"bg-transparent font-sans antialiased __variable_d65c78 __variable_3c557b __variable_f44606","children":["$","$L7",null,{"attribute":"class","defaultTheme":"system","enableSystem":true,"children":[["$","$L8",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":["$","$Lb",null,{}],"notFoundStyles":[],"childProp":{"current":[null,[["$","$Lc",null,{}],["$","$Ld",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,["$","div",null,{"className":"-m-4 flex lg:-m-10","children":[["$","$Le",null,{"className":"w-[200px] pl-4 pt-4 lg:w-[250px]"}],["$","$Lf",null,{"className":"flex-1","children":["$","div",null,{"className":"p-4 lg:p-10","children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,[["$","div",null,{"className":"mb-4 flex items-center gap-4","children":["$","div",null,{"className":"flex-1 text-sm text-muted-foreground","children":["Connect to Self-Hosted GitHub as a provider, and select repositories from this provider to serve as context, thereby improving the performance of large language models",["$","$L10",null,{"className":"ml-2 inline-flex cursor-pointer flex-row items-center text-primary hover:underline","href":"https://tabby.tabbyml.com/blog/2023/10/16/repository-context-for-code-completion","target":"_blank","children":["Learn more",["$","$L11",null,{"className":"ml-1"}]]}]]}]}],["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children",["kind","github-self-hosted","d"],"children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$L12",["$","$L13",null,{}],null],"segment":"__PAGE__?{\"kind\":\"github-self-hosted\"}"},"styles":[]}]],null],"segment":["kind","github-self-hosted","d"]},"styles":[]}]}]}]]}],null],"segment":"providers"},"styles":[]}],"segment":"(integrations)"},"styles":[]}],"segment":"settings"},"styles":[]}]}]],null],"segment":"(dashboard)"},"styles":[]}]}],["$","$L14",null,{"richColors":true,"closeButton":true}],null]}]}]]}],null] +12:null diff --git a/ee/tabby-webserver/ui/settings/providers/github-self-hosted/detail.html b/ee/tabby-webserver/ui/settings/providers/github-self-hosted/detail.html new file mode 100644 index 000000000000..029af582deeb --- /dev/null +++ b/ee/tabby-webserver/ui/settings/providers/github-self-hosted/detail.html @@ -0,0 +1 @@ +Tabby - Context Providers \ No newline at end of file diff --git a/ee/tabby-webserver/ui/settings/providers/github-self-hosted/detail.txt b/ee/tabby-webserver/ui/settings/providers/github-self-hosted/detail.txt new file mode 100644 index 000000000000..33c75cd58d4e --- /dev/null +++ b/ee/tabby-webserver/ui/settings/providers/github-self-hosted/detail.txt @@ -0,0 +1,21 @@ +1:HL["/_next/static/media/a34f9d1faa5f3315-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +2:HL["/_next/static/media/bb3ef058b751a6ad-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +3:HL["/_next/static/media/f75d6d02e2924b13-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +4:HL["/_next/static/css/1df4303b470c4ade.css","style"] +0:["6THjZwODOtNUmFQ4Gzp5t",[[["",{"children":["(dashboard)",{"children":["settings",{"children":["(integrations)",{"children":["providers",{"children":[["kind","github-self-hosted","d"],{"children":["detail",{"children":["__PAGE__?{\"kind\":\"github-self-hosted\"}",{}]}]}]}]}]}]}]},"$undefined","$undefined",true],"$L5",[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/1df4303b470c4ade.css","precedence":"next"}]],"$L6"]]]] +7:I{"id":35590,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Providers","async":false} +8:I{"id":32191,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"","async":false} +9:I{"id":32892,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +a:I{"id":95814,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +b:I{"id":69579,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","4546:static/chunks/4546-05756522a4929864.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","1544:static/chunks/1544-f96cdf5bcfa8f0e4.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9160:static/chunks/app/not-found-8fdd48e65bd803b1.js"],"name":"","async":false} +c:I{"id":90155,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"LicenseBanner","async":false} +d:I{"id":86150,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"","async":false} +e:I{"id":69145,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","6807:static/chunks/app/(dashboard)/settings/(integrations)/providers/layout-73ce9c8d662cc1f1.js"],"name":"","async":false} +f:I{"id":57830,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","6807:static/chunks/app/(dashboard)/settings/(integrations)/providers/layout-73ce9c8d662cc1f1.js"],"name":"ScrollArea","async":false} +10:I{"id":54007,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","436:static/chunks/436-401a51e7af01c90f.js","4007:static/chunks/4007-80fb4e0338c67b28.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","2219:static/chunks/app/(dashboard)/settings/(integrations)/providers/[kind]/layout-9688fa1599db2b7b.js"],"name":"","async":false} +11:I{"id":81565,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","436:static/chunks/436-401a51e7af01c90f.js","4007:static/chunks/4007-80fb4e0338c67b28.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","2219:static/chunks/app/(dashboard)/settings/(integrations)/providers/[kind]/layout-9688fa1599db2b7b.js"],"name":"IconExternalLink","async":false} +13:I{"id":48973,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","240:static/chunks/240-6e4dd6e33fcf5a86.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","3179:static/chunks/3179-72a03be258e531a2.js","4421:static/chunks/4421-0da05343b4d5e17a.js","7070:static/chunks/7070-0361b26f2c124ca9.js","1002:static/chunks/1002-09e560a83c07e9f4.js","1460:static/chunks/1460-849850f3e0fcced7.js","6805:static/chunks/6805-48f0b1146fe740f9.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","9464:static/chunks/app/(dashboard)/settings/(integrations)/providers/[kind]/detail/page-3e8a5c95f318fe68.js"],"name":"","async":false} +14:I{"id":80629,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Toaster","async":false} +6:[["$","meta","0",{"charSet":"utf-8"}],["$","title","1",{"children":"Tabby - Context Providers"}],["$","meta","2",{"name":"description","content":"Tabby, an opensource, self-hosted AI coding assistant."}],["$","meta","3",{"name":"theme-color","media":"(prefers-color-scheme: light)","content":"white"}],["$","meta","4",{"name":"theme-color","media":"(prefers-color-scheme: dark)","content":"black"}],["$","meta","5",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","link","6",{"rel":"icon","href":"/favicon.ico"}],["$","meta","7",{"name":"next-size-adjust"}]] +5:[null,["$","html",null,{"lang":"en","suppressHydrationWarning":true,"children":[["$","head",null,{}],["$","body",null,{"className":"bg-transparent font-sans antialiased __variable_d65c78 __variable_3c557b __variable_f44606","children":["$","$L7",null,{"attribute":"class","defaultTheme":"system","enableSystem":true,"children":[["$","$L8",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":["$","$Lb",null,{}],"notFoundStyles":[],"childProp":{"current":[null,[["$","$Lc",null,{}],["$","$Ld",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,["$","div",null,{"className":"-m-4 flex lg:-m-10","children":[["$","$Le",null,{"className":"w-[200px] pl-4 pt-4 lg:w-[250px]"}],["$","$Lf",null,{"className":"flex-1","children":["$","div",null,{"className":"p-4 lg:p-10","children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,[["$","div",null,{"className":"mb-4 flex items-center gap-4","children":["$","div",null,{"className":"flex-1 text-sm text-muted-foreground","children":["Connect to Self-Hosted GitHub as a provider, and select repositories from this provider to serve as context, thereby improving the performance of large language models",["$","$L10",null,{"className":"ml-2 inline-flex cursor-pointer flex-row items-center text-primary hover:underline","href":"https://tabby.tabbyml.com/blog/2023/10/16/repository-context-for-code-completion","target":"_blank","children":["Learn more",["$","$L11",null,{"className":"ml-1"}]]}]]}]}],["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children",["kind","github-self-hosted","d"],"children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children",["kind","github-self-hosted","d"],"children","detail","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$L12",["$","$L13",null,{}],null],"segment":"__PAGE__?{\"kind\":\"github-self-hosted\"}"},"styles":[]}],"segment":"detail"},"styles":[]}]],null],"segment":["kind","github-self-hosted","d"]},"styles":[]}]}]}]]}],null],"segment":"providers"},"styles":[]}],"segment":"(integrations)"},"styles":[]}],"segment":"settings"},"styles":[]}]}]],null],"segment":"(dashboard)"},"styles":[]}]}],["$","$L14",null,{"richColors":true,"closeButton":true}],null]}]}]]}],null] +12:null diff --git a/ee/tabby-webserver/ui/settings/providers/github-self-hosted/new.html b/ee/tabby-webserver/ui/settings/providers/github-self-hosted/new.html new file mode 100644 index 000000000000..bcfc82a0d4d3 --- /dev/null +++ b/ee/tabby-webserver/ui/settings/providers/github-self-hosted/new.html @@ -0,0 +1 @@ +Tabby - Context Providers \ No newline at end of file diff --git a/ee/tabby-webserver/ui/settings/providers/github-self-hosted/new.txt b/ee/tabby-webserver/ui/settings/providers/github-self-hosted/new.txt new file mode 100644 index 000000000000..85a18fb059f6 --- /dev/null +++ b/ee/tabby-webserver/ui/settings/providers/github-self-hosted/new.txt @@ -0,0 +1,21 @@ +1:HL["/_next/static/media/a34f9d1faa5f3315-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +2:HL["/_next/static/media/bb3ef058b751a6ad-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +3:HL["/_next/static/media/f75d6d02e2924b13-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +4:HL["/_next/static/css/1df4303b470c4ade.css","style"] +0:["6THjZwODOtNUmFQ4Gzp5t",[[["",{"children":["(dashboard)",{"children":["settings",{"children":["(integrations)",{"children":["providers",{"children":[["kind","github-self-hosted","d"],{"children":["new",{"children":["__PAGE__?{\"kind\":\"github-self-hosted\"}",{}]}]}]}]}]}]}]},"$undefined","$undefined",true],"$L5",[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/1df4303b470c4ade.css","precedence":"next"}]],"$L6"]]]] +7:I{"id":35590,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Providers","async":false} +8:I{"id":32191,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"","async":false} +9:I{"id":32892,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +a:I{"id":95814,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +b:I{"id":69579,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","4546:static/chunks/4546-05756522a4929864.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","1544:static/chunks/1544-f96cdf5bcfa8f0e4.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9160:static/chunks/app/not-found-8fdd48e65bd803b1.js"],"name":"","async":false} +c:I{"id":90155,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"LicenseBanner","async":false} +d:I{"id":86150,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"","async":false} +e:I{"id":69145,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","6807:static/chunks/app/(dashboard)/settings/(integrations)/providers/layout-73ce9c8d662cc1f1.js"],"name":"","async":false} +f:I{"id":57830,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","6807:static/chunks/app/(dashboard)/settings/(integrations)/providers/layout-73ce9c8d662cc1f1.js"],"name":"ScrollArea","async":false} +10:I{"id":54007,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","436:static/chunks/436-401a51e7af01c90f.js","4007:static/chunks/4007-80fb4e0338c67b28.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","2219:static/chunks/app/(dashboard)/settings/(integrations)/providers/[kind]/layout-9688fa1599db2b7b.js"],"name":"","async":false} +11:I{"id":81565,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","436:static/chunks/436-401a51e7af01c90f.js","4007:static/chunks/4007-80fb4e0338c67b28.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","2219:static/chunks/app/(dashboard)/settings/(integrations)/providers/[kind]/layout-9688fa1599db2b7b.js"],"name":"IconExternalLink","async":false} +13:I{"id":54616,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","240:static/chunks/240-6e4dd6e33fcf5a86.js","3179:static/chunks/3179-72a03be258e531a2.js","7070:static/chunks/7070-0361b26f2c124ca9.js","1460:static/chunks/1460-849850f3e0fcced7.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9436:static/chunks/app/(dashboard)/settings/(integrations)/providers/[kind]/new/page-2078198166a995dc.js"],"name":"NewProvider","async":false} +14:I{"id":80629,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Toaster","async":false} +6:[["$","meta","0",{"charSet":"utf-8"}],["$","title","1",{"children":"Tabby - Context Providers"}],["$","meta","2",{"name":"description","content":"Tabby, an opensource, self-hosted AI coding assistant."}],["$","meta","3",{"name":"theme-color","media":"(prefers-color-scheme: light)","content":"white"}],["$","meta","4",{"name":"theme-color","media":"(prefers-color-scheme: dark)","content":"black"}],["$","meta","5",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","link","6",{"rel":"icon","href":"/favicon.ico"}],["$","meta","7",{"name":"next-size-adjust"}]] +5:[null,["$","html",null,{"lang":"en","suppressHydrationWarning":true,"children":[["$","head",null,{}],["$","body",null,{"className":"bg-transparent font-sans antialiased __variable_d65c78 __variable_3c557b __variable_f44606","children":["$","$L7",null,{"attribute":"class","defaultTheme":"system","enableSystem":true,"children":[["$","$L8",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":["$","$Lb",null,{}],"notFoundStyles":[],"childProp":{"current":[null,[["$","$Lc",null,{}],["$","$Ld",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,["$","div",null,{"className":"-m-4 flex lg:-m-10","children":[["$","$Le",null,{"className":"w-[200px] pl-4 pt-4 lg:w-[250px]"}],["$","$Lf",null,{"className":"flex-1","children":["$","div",null,{"className":"p-4 lg:p-10","children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,[["$","div",null,{"className":"mb-4 flex items-center gap-4","children":["$","div",null,{"className":"flex-1 text-sm text-muted-foreground","children":["Connect to Self-Hosted GitHub as a provider, and select repositories from this provider to serve as context, thereby improving the performance of large language models",["$","$L10",null,{"className":"ml-2 inline-flex cursor-pointer flex-row items-center text-primary hover:underline","href":"https://tabby.tabbyml.com/blog/2023/10/16/repository-context-for-code-completion","target":"_blank","children":["Learn more",["$","$L11",null,{"className":"ml-1"}]]}]]}]}],["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children",["kind","github-self-hosted","d"],"children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children",["kind","github-self-hosted","d"],"children","new","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$L12",["$","$L13",null,{}],null],"segment":"__PAGE__?{\"kind\":\"github-self-hosted\"}"},"styles":[]}],"segment":"new"},"styles":[]}]],null],"segment":["kind","github-self-hosted","d"]},"styles":[]}]}]}]]}],null],"segment":"providers"},"styles":[]}],"segment":"(integrations)"},"styles":[]}],"segment":"settings"},"styles":[]}]}]],null],"segment":"(dashboard)"},"styles":[]}]}],["$","$L14",null,{"richColors":true,"closeButton":true}],null]}]}]]}],null] +12:null diff --git a/ee/tabby-webserver/ui/settings/providers/github.html b/ee/tabby-webserver/ui/settings/providers/github.html new file mode 100644 index 000000000000..5b02405154f4 --- /dev/null +++ b/ee/tabby-webserver/ui/settings/providers/github.html @@ -0,0 +1 @@ +Tabby - Context Providers \ No newline at end of file diff --git a/ee/tabby-webserver/ui/settings/providers/github.txt b/ee/tabby-webserver/ui/settings/providers/github.txt new file mode 100644 index 000000000000..83b243f0939e --- /dev/null +++ b/ee/tabby-webserver/ui/settings/providers/github.txt @@ -0,0 +1,21 @@ +1:HL["/_next/static/media/a34f9d1faa5f3315-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +2:HL["/_next/static/media/bb3ef058b751a6ad-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +3:HL["/_next/static/media/f75d6d02e2924b13-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +4:HL["/_next/static/css/1df4303b470c4ade.css","style"] +0:["6THjZwODOtNUmFQ4Gzp5t",[[["",{"children":["(dashboard)",{"children":["settings",{"children":["(integrations)",{"children":["providers",{"children":[["kind","github","d"],{"children":["__PAGE__?{\"kind\":\"github\"}",{}]}]}]}]}]}]},"$undefined","$undefined",true],"$L5",[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/1df4303b470c4ade.css","precedence":"next"}]],"$L6"]]]] +7:I{"id":35590,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Providers","async":false} +8:I{"id":32191,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"","async":false} +9:I{"id":32892,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +a:I{"id":95814,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +b:I{"id":69579,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","4546:static/chunks/4546-05756522a4929864.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","1544:static/chunks/1544-f96cdf5bcfa8f0e4.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9160:static/chunks/app/not-found-8fdd48e65bd803b1.js"],"name":"","async":false} +c:I{"id":90155,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"LicenseBanner","async":false} +d:I{"id":86150,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"","async":false} +e:I{"id":69145,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","6807:static/chunks/app/(dashboard)/settings/(integrations)/providers/layout-73ce9c8d662cc1f1.js"],"name":"","async":false} +f:I{"id":57830,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","6807:static/chunks/app/(dashboard)/settings/(integrations)/providers/layout-73ce9c8d662cc1f1.js"],"name":"ScrollArea","async":false} +10:I{"id":54007,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","436:static/chunks/436-401a51e7af01c90f.js","4007:static/chunks/4007-80fb4e0338c67b28.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","2219:static/chunks/app/(dashboard)/settings/(integrations)/providers/[kind]/layout-9688fa1599db2b7b.js"],"name":"","async":false} +11:I{"id":81565,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","436:static/chunks/436-401a51e7af01c90f.js","4007:static/chunks/4007-80fb4e0338c67b28.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","2219:static/chunks/app/(dashboard)/settings/(integrations)/providers/[kind]/layout-9688fa1599db2b7b.js"],"name":"IconExternalLink","async":false} +13:I{"id":6194,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","4007:static/chunks/4007-80fb4e0338c67b28.js","7070:static/chunks/7070-0361b26f2c124ca9.js","7548:static/chunks/7548-9d3520e9c948edb5.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","410:static/chunks/410-2d9f0445fa0cca52.js","9667:static/chunks/app/(dashboard)/settings/(integrations)/providers/[kind]/page-8f2bf20ea03adec0.js"],"name":"","async":false} +14:I{"id":80629,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Toaster","async":false} +6:[["$","meta","0",{"charSet":"utf-8"}],["$","title","1",{"children":"Tabby - Context Providers"}],["$","meta","2",{"name":"description","content":"Tabby, an opensource, self-hosted AI coding assistant."}],["$","meta","3",{"name":"theme-color","media":"(prefers-color-scheme: light)","content":"white"}],["$","meta","4",{"name":"theme-color","media":"(prefers-color-scheme: dark)","content":"black"}],["$","meta","5",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","link","6",{"rel":"icon","href":"/favicon.ico"}],["$","meta","7",{"name":"next-size-adjust"}]] +5:[null,["$","html",null,{"lang":"en","suppressHydrationWarning":true,"children":[["$","head",null,{}],["$","body",null,{"className":"bg-transparent font-sans antialiased __variable_d65c78 __variable_3c557b __variable_f44606","children":["$","$L7",null,{"attribute":"class","defaultTheme":"system","enableSystem":true,"children":[["$","$L8",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":["$","$Lb",null,{}],"notFoundStyles":[],"childProp":{"current":[null,[["$","$Lc",null,{}],["$","$Ld",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,["$","div",null,{"className":"-m-4 flex lg:-m-10","children":[["$","$Le",null,{"className":"w-[200px] pl-4 pt-4 lg:w-[250px]"}],["$","$Lf",null,{"className":"flex-1","children":["$","div",null,{"className":"p-4 lg:p-10","children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,[["$","div",null,{"className":"mb-4 flex items-center gap-4","children":["$","div",null,{"className":"flex-1 text-sm text-muted-foreground","children":["Connect to GitHub as a provider, and select repositories from this provider to serve as context, thereby improving the performance of large language models",["$","$L10",null,{"className":"ml-2 inline-flex cursor-pointer flex-row items-center text-primary hover:underline","href":"https://tabby.tabbyml.com/blog/2023/10/16/repository-context-for-code-completion","target":"_blank","children":["Learn more",["$","$L11",null,{"className":"ml-1"}]]}]]}]}],["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children",["kind","github","d"],"children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$L12",["$","$L13",null,{}],null],"segment":"__PAGE__?{\"kind\":\"github\"}"},"styles":[]}]],null],"segment":["kind","github","d"]},"styles":[]}]}]}]]}],null],"segment":"providers"},"styles":[]}],"segment":"(integrations)"},"styles":[]}],"segment":"settings"},"styles":[]}]}]],null],"segment":"(dashboard)"},"styles":[]}]}],["$","$L14",null,{"richColors":true,"closeButton":true}],null]}]}]]}],null] +12:null diff --git a/ee/tabby-webserver/ui/settings/providers/github/detail.html b/ee/tabby-webserver/ui/settings/providers/github/detail.html new file mode 100644 index 000000000000..5b1cdf46c5ae --- /dev/null +++ b/ee/tabby-webserver/ui/settings/providers/github/detail.html @@ -0,0 +1 @@ +Tabby - Context Providers \ No newline at end of file diff --git a/ee/tabby-webserver/ui/settings/providers/github/detail.txt b/ee/tabby-webserver/ui/settings/providers/github/detail.txt new file mode 100644 index 000000000000..df6cf1b0baae --- /dev/null +++ b/ee/tabby-webserver/ui/settings/providers/github/detail.txt @@ -0,0 +1,21 @@ +1:HL["/_next/static/media/a34f9d1faa5f3315-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +2:HL["/_next/static/media/bb3ef058b751a6ad-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +3:HL["/_next/static/media/f75d6d02e2924b13-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +4:HL["/_next/static/css/1df4303b470c4ade.css","style"] +0:["6THjZwODOtNUmFQ4Gzp5t",[[["",{"children":["(dashboard)",{"children":["settings",{"children":["(integrations)",{"children":["providers",{"children":[["kind","github","d"],{"children":["detail",{"children":["__PAGE__?{\"kind\":\"github\"}",{}]}]}]}]}]}]}]},"$undefined","$undefined",true],"$L5",[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/1df4303b470c4ade.css","precedence":"next"}]],"$L6"]]]] +7:I{"id":35590,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Providers","async":false} +8:I{"id":32191,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"","async":false} +9:I{"id":32892,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +a:I{"id":95814,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +b:I{"id":69579,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","4546:static/chunks/4546-05756522a4929864.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","1544:static/chunks/1544-f96cdf5bcfa8f0e4.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9160:static/chunks/app/not-found-8fdd48e65bd803b1.js"],"name":"","async":false} +c:I{"id":90155,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"LicenseBanner","async":false} +d:I{"id":86150,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"","async":false} +e:I{"id":69145,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","6807:static/chunks/app/(dashboard)/settings/(integrations)/providers/layout-73ce9c8d662cc1f1.js"],"name":"","async":false} +f:I{"id":57830,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","6807:static/chunks/app/(dashboard)/settings/(integrations)/providers/layout-73ce9c8d662cc1f1.js"],"name":"ScrollArea","async":false} +10:I{"id":54007,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","436:static/chunks/436-401a51e7af01c90f.js","4007:static/chunks/4007-80fb4e0338c67b28.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","2219:static/chunks/app/(dashboard)/settings/(integrations)/providers/[kind]/layout-9688fa1599db2b7b.js"],"name":"","async":false} +11:I{"id":81565,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","436:static/chunks/436-401a51e7af01c90f.js","4007:static/chunks/4007-80fb4e0338c67b28.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","2219:static/chunks/app/(dashboard)/settings/(integrations)/providers/[kind]/layout-9688fa1599db2b7b.js"],"name":"IconExternalLink","async":false} +13:I{"id":48973,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","240:static/chunks/240-6e4dd6e33fcf5a86.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","3179:static/chunks/3179-72a03be258e531a2.js","4421:static/chunks/4421-0da05343b4d5e17a.js","7070:static/chunks/7070-0361b26f2c124ca9.js","1002:static/chunks/1002-09e560a83c07e9f4.js","1460:static/chunks/1460-849850f3e0fcced7.js","6805:static/chunks/6805-48f0b1146fe740f9.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","9464:static/chunks/app/(dashboard)/settings/(integrations)/providers/[kind]/detail/page-3e8a5c95f318fe68.js"],"name":"","async":false} +14:I{"id":80629,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Toaster","async":false} +6:[["$","meta","0",{"charSet":"utf-8"}],["$","title","1",{"children":"Tabby - Context Providers"}],["$","meta","2",{"name":"description","content":"Tabby, an opensource, self-hosted AI coding assistant."}],["$","meta","3",{"name":"theme-color","media":"(prefers-color-scheme: light)","content":"white"}],["$","meta","4",{"name":"theme-color","media":"(prefers-color-scheme: dark)","content":"black"}],["$","meta","5",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","link","6",{"rel":"icon","href":"/favicon.ico"}],["$","meta","7",{"name":"next-size-adjust"}]] +5:[null,["$","html",null,{"lang":"en","suppressHydrationWarning":true,"children":[["$","head",null,{}],["$","body",null,{"className":"bg-transparent font-sans antialiased __variable_d65c78 __variable_3c557b __variable_f44606","children":["$","$L7",null,{"attribute":"class","defaultTheme":"system","enableSystem":true,"children":[["$","$L8",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":["$","$Lb",null,{}],"notFoundStyles":[],"childProp":{"current":[null,[["$","$Lc",null,{}],["$","$Ld",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,["$","div",null,{"className":"-m-4 flex lg:-m-10","children":[["$","$Le",null,{"className":"w-[200px] pl-4 pt-4 lg:w-[250px]"}],["$","$Lf",null,{"className":"flex-1","children":["$","div",null,{"className":"p-4 lg:p-10","children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,[["$","div",null,{"className":"mb-4 flex items-center gap-4","children":["$","div",null,{"className":"flex-1 text-sm text-muted-foreground","children":["Connect to GitHub as a provider, and select repositories from this provider to serve as context, thereby improving the performance of large language models",["$","$L10",null,{"className":"ml-2 inline-flex cursor-pointer flex-row items-center text-primary hover:underline","href":"https://tabby.tabbyml.com/blog/2023/10/16/repository-context-for-code-completion","target":"_blank","children":["Learn more",["$","$L11",null,{"className":"ml-1"}]]}]]}]}],["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children",["kind","github","d"],"children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children",["kind","github","d"],"children","detail","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$L12",["$","$L13",null,{}],null],"segment":"__PAGE__?{\"kind\":\"github\"}"},"styles":[]}],"segment":"detail"},"styles":[]}]],null],"segment":["kind","github","d"]},"styles":[]}]}]}]]}],null],"segment":"providers"},"styles":[]}],"segment":"(integrations)"},"styles":[]}],"segment":"settings"},"styles":[]}]}]],null],"segment":"(dashboard)"},"styles":[]}]}],["$","$L14",null,{"richColors":true,"closeButton":true}],null]}]}]]}],null] +12:null diff --git a/ee/tabby-webserver/ui/settings/providers/github/new.html b/ee/tabby-webserver/ui/settings/providers/github/new.html new file mode 100644 index 000000000000..410c60331e08 --- /dev/null +++ b/ee/tabby-webserver/ui/settings/providers/github/new.html @@ -0,0 +1 @@ +Tabby - Context Providers \ No newline at end of file diff --git a/ee/tabby-webserver/ui/settings/providers/github/new.txt b/ee/tabby-webserver/ui/settings/providers/github/new.txt new file mode 100644 index 000000000000..323a9691399e --- /dev/null +++ b/ee/tabby-webserver/ui/settings/providers/github/new.txt @@ -0,0 +1,21 @@ +1:HL["/_next/static/media/a34f9d1faa5f3315-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +2:HL["/_next/static/media/bb3ef058b751a6ad-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +3:HL["/_next/static/media/f75d6d02e2924b13-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +4:HL["/_next/static/css/1df4303b470c4ade.css","style"] +0:["6THjZwODOtNUmFQ4Gzp5t",[[["",{"children":["(dashboard)",{"children":["settings",{"children":["(integrations)",{"children":["providers",{"children":[["kind","github","d"],{"children":["new",{"children":["__PAGE__?{\"kind\":\"github\"}",{}]}]}]}]}]}]}]},"$undefined","$undefined",true],"$L5",[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/1df4303b470c4ade.css","precedence":"next"}]],"$L6"]]]] +7:I{"id":35590,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Providers","async":false} +8:I{"id":32191,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"","async":false} +9:I{"id":32892,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +a:I{"id":95814,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +b:I{"id":69579,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","4546:static/chunks/4546-05756522a4929864.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","1544:static/chunks/1544-f96cdf5bcfa8f0e4.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9160:static/chunks/app/not-found-8fdd48e65bd803b1.js"],"name":"","async":false} +c:I{"id":90155,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"LicenseBanner","async":false} +d:I{"id":86150,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"","async":false} +e:I{"id":69145,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","6807:static/chunks/app/(dashboard)/settings/(integrations)/providers/layout-73ce9c8d662cc1f1.js"],"name":"","async":false} +f:I{"id":57830,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","6807:static/chunks/app/(dashboard)/settings/(integrations)/providers/layout-73ce9c8d662cc1f1.js"],"name":"ScrollArea","async":false} +10:I{"id":54007,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","436:static/chunks/436-401a51e7af01c90f.js","4007:static/chunks/4007-80fb4e0338c67b28.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","2219:static/chunks/app/(dashboard)/settings/(integrations)/providers/[kind]/layout-9688fa1599db2b7b.js"],"name":"","async":false} +11:I{"id":81565,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","436:static/chunks/436-401a51e7af01c90f.js","4007:static/chunks/4007-80fb4e0338c67b28.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","2219:static/chunks/app/(dashboard)/settings/(integrations)/providers/[kind]/layout-9688fa1599db2b7b.js"],"name":"IconExternalLink","async":false} +13:I{"id":54616,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","240:static/chunks/240-6e4dd6e33fcf5a86.js","3179:static/chunks/3179-72a03be258e531a2.js","7070:static/chunks/7070-0361b26f2c124ca9.js","1460:static/chunks/1460-849850f3e0fcced7.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9436:static/chunks/app/(dashboard)/settings/(integrations)/providers/[kind]/new/page-2078198166a995dc.js"],"name":"NewProvider","async":false} +14:I{"id":80629,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Toaster","async":false} +6:[["$","meta","0",{"charSet":"utf-8"}],["$","title","1",{"children":"Tabby - Context Providers"}],["$","meta","2",{"name":"description","content":"Tabby, an opensource, self-hosted AI coding assistant."}],["$","meta","3",{"name":"theme-color","media":"(prefers-color-scheme: light)","content":"white"}],["$","meta","4",{"name":"theme-color","media":"(prefers-color-scheme: dark)","content":"black"}],["$","meta","5",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","link","6",{"rel":"icon","href":"/favicon.ico"}],["$","meta","7",{"name":"next-size-adjust"}]] +5:[null,["$","html",null,{"lang":"en","suppressHydrationWarning":true,"children":[["$","head",null,{}],["$","body",null,{"className":"bg-transparent font-sans antialiased __variable_d65c78 __variable_3c557b __variable_f44606","children":["$","$L7",null,{"attribute":"class","defaultTheme":"system","enableSystem":true,"children":[["$","$L8",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":["$","$Lb",null,{}],"notFoundStyles":[],"childProp":{"current":[null,[["$","$Lc",null,{}],["$","$Ld",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,["$","div",null,{"className":"-m-4 flex lg:-m-10","children":[["$","$Le",null,{"className":"w-[200px] pl-4 pt-4 lg:w-[250px]"}],["$","$Lf",null,{"className":"flex-1","children":["$","div",null,{"className":"p-4 lg:p-10","children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,[["$","div",null,{"className":"mb-4 flex items-center gap-4","children":["$","div",null,{"className":"flex-1 text-sm text-muted-foreground","children":["Connect to GitHub as a provider, and select repositories from this provider to serve as context, thereby improving the performance of large language models",["$","$L10",null,{"className":"ml-2 inline-flex cursor-pointer flex-row items-center text-primary hover:underline","href":"https://tabby.tabbyml.com/blog/2023/10/16/repository-context-for-code-completion","target":"_blank","children":["Learn more",["$","$L11",null,{"className":"ml-1"}]]}]]}]}],["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children",["kind","github","d"],"children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children",["kind","github","d"],"children","new","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$L12",["$","$L13",null,{}],null],"segment":"__PAGE__?{\"kind\":\"github\"}"},"styles":[]}],"segment":"new"},"styles":[]}]],null],"segment":["kind","github","d"]},"styles":[]}]}]}]]}],null],"segment":"providers"},"styles":[]}],"segment":"(integrations)"},"styles":[]}],"segment":"settings"},"styles":[]}]}]],null],"segment":"(dashboard)"},"styles":[]}]}],["$","$L14",null,{"richColors":true,"closeButton":true}],null]}]}]]}],null] +12:null diff --git a/ee/tabby-webserver/ui/settings/providers/gitlab-self-hosted.html b/ee/tabby-webserver/ui/settings/providers/gitlab-self-hosted.html new file mode 100644 index 000000000000..800c54cf470e --- /dev/null +++ b/ee/tabby-webserver/ui/settings/providers/gitlab-self-hosted.html @@ -0,0 +1 @@ +Tabby - Context Providers \ No newline at end of file diff --git a/ee/tabby-webserver/ui/settings/providers/gitlab-self-hosted.txt b/ee/tabby-webserver/ui/settings/providers/gitlab-self-hosted.txt new file mode 100644 index 000000000000..3d7d687a2278 --- /dev/null +++ b/ee/tabby-webserver/ui/settings/providers/gitlab-self-hosted.txt @@ -0,0 +1,21 @@ +1:HL["/_next/static/media/a34f9d1faa5f3315-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +2:HL["/_next/static/media/bb3ef058b751a6ad-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +3:HL["/_next/static/media/f75d6d02e2924b13-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +4:HL["/_next/static/css/1df4303b470c4ade.css","style"] +0:["6THjZwODOtNUmFQ4Gzp5t",[[["",{"children":["(dashboard)",{"children":["settings",{"children":["(integrations)",{"children":["providers",{"children":[["kind","gitlab-self-hosted","d"],{"children":["__PAGE__?{\"kind\":\"gitlab-self-hosted\"}",{}]}]}]}]}]}]},"$undefined","$undefined",true],"$L5",[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/1df4303b470c4ade.css","precedence":"next"}]],"$L6"]]]] +7:I{"id":35590,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Providers","async":false} +8:I{"id":32191,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"","async":false} +9:I{"id":32892,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +a:I{"id":95814,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +b:I{"id":69579,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","4546:static/chunks/4546-05756522a4929864.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","1544:static/chunks/1544-f96cdf5bcfa8f0e4.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9160:static/chunks/app/not-found-8fdd48e65bd803b1.js"],"name":"","async":false} +c:I{"id":90155,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"LicenseBanner","async":false} +d:I{"id":86150,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"","async":false} +e:I{"id":69145,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","6807:static/chunks/app/(dashboard)/settings/(integrations)/providers/layout-73ce9c8d662cc1f1.js"],"name":"","async":false} +f:I{"id":57830,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","6807:static/chunks/app/(dashboard)/settings/(integrations)/providers/layout-73ce9c8d662cc1f1.js"],"name":"ScrollArea","async":false} +10:I{"id":54007,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","436:static/chunks/436-401a51e7af01c90f.js","4007:static/chunks/4007-80fb4e0338c67b28.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","2219:static/chunks/app/(dashboard)/settings/(integrations)/providers/[kind]/layout-9688fa1599db2b7b.js"],"name":"","async":false} +11:I{"id":81565,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","436:static/chunks/436-401a51e7af01c90f.js","4007:static/chunks/4007-80fb4e0338c67b28.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","2219:static/chunks/app/(dashboard)/settings/(integrations)/providers/[kind]/layout-9688fa1599db2b7b.js"],"name":"IconExternalLink","async":false} +13:I{"id":6194,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","4007:static/chunks/4007-80fb4e0338c67b28.js","7070:static/chunks/7070-0361b26f2c124ca9.js","7548:static/chunks/7548-9d3520e9c948edb5.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","410:static/chunks/410-2d9f0445fa0cca52.js","9667:static/chunks/app/(dashboard)/settings/(integrations)/providers/[kind]/page-8f2bf20ea03adec0.js"],"name":"","async":false} +14:I{"id":80629,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Toaster","async":false} +6:[["$","meta","0",{"charSet":"utf-8"}],["$","title","1",{"children":"Tabby - Context Providers"}],["$","meta","2",{"name":"description","content":"Tabby, an opensource, self-hosted AI coding assistant."}],["$","meta","3",{"name":"theme-color","media":"(prefers-color-scheme: light)","content":"white"}],["$","meta","4",{"name":"theme-color","media":"(prefers-color-scheme: dark)","content":"black"}],["$","meta","5",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","link","6",{"rel":"icon","href":"/favicon.ico"}],["$","meta","7",{"name":"next-size-adjust"}]] +5:[null,["$","html",null,{"lang":"en","suppressHydrationWarning":true,"children":[["$","head",null,{}],["$","body",null,{"className":"bg-transparent font-sans antialiased __variable_d65c78 __variable_3c557b __variable_f44606","children":["$","$L7",null,{"attribute":"class","defaultTheme":"system","enableSystem":true,"children":[["$","$L8",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":["$","$Lb",null,{}],"notFoundStyles":[],"childProp":{"current":[null,[["$","$Lc",null,{}],["$","$Ld",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,["$","div",null,{"className":"-m-4 flex lg:-m-10","children":[["$","$Le",null,{"className":"w-[200px] pl-4 pt-4 lg:w-[250px]"}],["$","$Lf",null,{"className":"flex-1","children":["$","div",null,{"className":"p-4 lg:p-10","children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,[["$","div",null,{"className":"mb-4 flex items-center gap-4","children":["$","div",null,{"className":"flex-1 text-sm text-muted-foreground","children":["Connect to Self-Hosted GitLab as a provider, and select repositories from this provider to serve as context, thereby improving the performance of large language models",["$","$L10",null,{"className":"ml-2 inline-flex cursor-pointer flex-row items-center text-primary hover:underline","href":"https://tabby.tabbyml.com/blog/2023/10/16/repository-context-for-code-completion","target":"_blank","children":["Learn more",["$","$L11",null,{"className":"ml-1"}]]}]]}]}],["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children",["kind","gitlab-self-hosted","d"],"children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$L12",["$","$L13",null,{}],null],"segment":"__PAGE__?{\"kind\":\"gitlab-self-hosted\"}"},"styles":[]}]],null],"segment":["kind","gitlab-self-hosted","d"]},"styles":[]}]}]}]]}],null],"segment":"providers"},"styles":[]}],"segment":"(integrations)"},"styles":[]}],"segment":"settings"},"styles":[]}]}]],null],"segment":"(dashboard)"},"styles":[]}]}],["$","$L14",null,{"richColors":true,"closeButton":true}],null]}]}]]}],null] +12:null diff --git a/ee/tabby-webserver/ui/settings/providers/gitlab-self-hosted/detail.html b/ee/tabby-webserver/ui/settings/providers/gitlab-self-hosted/detail.html new file mode 100644 index 000000000000..afc91131b87c --- /dev/null +++ b/ee/tabby-webserver/ui/settings/providers/gitlab-self-hosted/detail.html @@ -0,0 +1 @@ +Tabby - Context Providers \ No newline at end of file diff --git a/ee/tabby-webserver/ui/settings/providers/gitlab-self-hosted/detail.txt b/ee/tabby-webserver/ui/settings/providers/gitlab-self-hosted/detail.txt new file mode 100644 index 000000000000..9e12b73ce829 --- /dev/null +++ b/ee/tabby-webserver/ui/settings/providers/gitlab-self-hosted/detail.txt @@ -0,0 +1,21 @@ +1:HL["/_next/static/media/a34f9d1faa5f3315-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +2:HL["/_next/static/media/bb3ef058b751a6ad-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +3:HL["/_next/static/media/f75d6d02e2924b13-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +4:HL["/_next/static/css/1df4303b470c4ade.css","style"] +0:["6THjZwODOtNUmFQ4Gzp5t",[[["",{"children":["(dashboard)",{"children":["settings",{"children":["(integrations)",{"children":["providers",{"children":[["kind","gitlab-self-hosted","d"],{"children":["detail",{"children":["__PAGE__?{\"kind\":\"gitlab-self-hosted\"}",{}]}]}]}]}]}]}]},"$undefined","$undefined",true],"$L5",[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/1df4303b470c4ade.css","precedence":"next"}]],"$L6"]]]] +7:I{"id":35590,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Providers","async":false} +8:I{"id":32191,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"","async":false} +9:I{"id":32892,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +a:I{"id":95814,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +b:I{"id":69579,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","4546:static/chunks/4546-05756522a4929864.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","1544:static/chunks/1544-f96cdf5bcfa8f0e4.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9160:static/chunks/app/not-found-8fdd48e65bd803b1.js"],"name":"","async":false} +c:I{"id":90155,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"LicenseBanner","async":false} +d:I{"id":86150,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"","async":false} +e:I{"id":69145,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","6807:static/chunks/app/(dashboard)/settings/(integrations)/providers/layout-73ce9c8d662cc1f1.js"],"name":"","async":false} +f:I{"id":57830,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","6807:static/chunks/app/(dashboard)/settings/(integrations)/providers/layout-73ce9c8d662cc1f1.js"],"name":"ScrollArea","async":false} +10:I{"id":54007,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","436:static/chunks/436-401a51e7af01c90f.js","4007:static/chunks/4007-80fb4e0338c67b28.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","2219:static/chunks/app/(dashboard)/settings/(integrations)/providers/[kind]/layout-9688fa1599db2b7b.js"],"name":"","async":false} +11:I{"id":81565,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","436:static/chunks/436-401a51e7af01c90f.js","4007:static/chunks/4007-80fb4e0338c67b28.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","2219:static/chunks/app/(dashboard)/settings/(integrations)/providers/[kind]/layout-9688fa1599db2b7b.js"],"name":"IconExternalLink","async":false} +13:I{"id":48973,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","240:static/chunks/240-6e4dd6e33fcf5a86.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","3179:static/chunks/3179-72a03be258e531a2.js","4421:static/chunks/4421-0da05343b4d5e17a.js","7070:static/chunks/7070-0361b26f2c124ca9.js","1002:static/chunks/1002-09e560a83c07e9f4.js","1460:static/chunks/1460-849850f3e0fcced7.js","6805:static/chunks/6805-48f0b1146fe740f9.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","9464:static/chunks/app/(dashboard)/settings/(integrations)/providers/[kind]/detail/page-3e8a5c95f318fe68.js"],"name":"","async":false} +14:I{"id":80629,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Toaster","async":false} +6:[["$","meta","0",{"charSet":"utf-8"}],["$","title","1",{"children":"Tabby - Context Providers"}],["$","meta","2",{"name":"description","content":"Tabby, an opensource, self-hosted AI coding assistant."}],["$","meta","3",{"name":"theme-color","media":"(prefers-color-scheme: light)","content":"white"}],["$","meta","4",{"name":"theme-color","media":"(prefers-color-scheme: dark)","content":"black"}],["$","meta","5",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","link","6",{"rel":"icon","href":"/favicon.ico"}],["$","meta","7",{"name":"next-size-adjust"}]] +5:[null,["$","html",null,{"lang":"en","suppressHydrationWarning":true,"children":[["$","head",null,{}],["$","body",null,{"className":"bg-transparent font-sans antialiased __variable_d65c78 __variable_3c557b __variable_f44606","children":["$","$L7",null,{"attribute":"class","defaultTheme":"system","enableSystem":true,"children":[["$","$L8",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":["$","$Lb",null,{}],"notFoundStyles":[],"childProp":{"current":[null,[["$","$Lc",null,{}],["$","$Ld",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,["$","div",null,{"className":"-m-4 flex lg:-m-10","children":[["$","$Le",null,{"className":"w-[200px] pl-4 pt-4 lg:w-[250px]"}],["$","$Lf",null,{"className":"flex-1","children":["$","div",null,{"className":"p-4 lg:p-10","children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,[["$","div",null,{"className":"mb-4 flex items-center gap-4","children":["$","div",null,{"className":"flex-1 text-sm text-muted-foreground","children":["Connect to Self-Hosted GitLab as a provider, and select repositories from this provider to serve as context, thereby improving the performance of large language models",["$","$L10",null,{"className":"ml-2 inline-flex cursor-pointer flex-row items-center text-primary hover:underline","href":"https://tabby.tabbyml.com/blog/2023/10/16/repository-context-for-code-completion","target":"_blank","children":["Learn more",["$","$L11",null,{"className":"ml-1"}]]}]]}]}],["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children",["kind","gitlab-self-hosted","d"],"children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children",["kind","gitlab-self-hosted","d"],"children","detail","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$L12",["$","$L13",null,{}],null],"segment":"__PAGE__?{\"kind\":\"gitlab-self-hosted\"}"},"styles":[]}],"segment":"detail"},"styles":[]}]],null],"segment":["kind","gitlab-self-hosted","d"]},"styles":[]}]}]}]]}],null],"segment":"providers"},"styles":[]}],"segment":"(integrations)"},"styles":[]}],"segment":"settings"},"styles":[]}]}]],null],"segment":"(dashboard)"},"styles":[]}]}],["$","$L14",null,{"richColors":true,"closeButton":true}],null]}]}]]}],null] +12:null diff --git a/ee/tabby-webserver/ui/settings/providers/gitlab-self-hosted/new.html b/ee/tabby-webserver/ui/settings/providers/gitlab-self-hosted/new.html new file mode 100644 index 000000000000..59ff4e0caf46 --- /dev/null +++ b/ee/tabby-webserver/ui/settings/providers/gitlab-self-hosted/new.html @@ -0,0 +1 @@ +Tabby - Context Providers \ No newline at end of file diff --git a/ee/tabby-webserver/ui/settings/providers/gitlab-self-hosted/new.txt b/ee/tabby-webserver/ui/settings/providers/gitlab-self-hosted/new.txt new file mode 100644 index 000000000000..0f2ddfa4498e --- /dev/null +++ b/ee/tabby-webserver/ui/settings/providers/gitlab-self-hosted/new.txt @@ -0,0 +1,21 @@ +1:HL["/_next/static/media/a34f9d1faa5f3315-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +2:HL["/_next/static/media/bb3ef058b751a6ad-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +3:HL["/_next/static/media/f75d6d02e2924b13-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +4:HL["/_next/static/css/1df4303b470c4ade.css","style"] +0:["6THjZwODOtNUmFQ4Gzp5t",[[["",{"children":["(dashboard)",{"children":["settings",{"children":["(integrations)",{"children":["providers",{"children":[["kind","gitlab-self-hosted","d"],{"children":["new",{"children":["__PAGE__?{\"kind\":\"gitlab-self-hosted\"}",{}]}]}]}]}]}]}]},"$undefined","$undefined",true],"$L5",[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/1df4303b470c4ade.css","precedence":"next"}]],"$L6"]]]] +7:I{"id":35590,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Providers","async":false} +8:I{"id":32191,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"","async":false} +9:I{"id":32892,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +a:I{"id":95814,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +b:I{"id":69579,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","4546:static/chunks/4546-05756522a4929864.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","1544:static/chunks/1544-f96cdf5bcfa8f0e4.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9160:static/chunks/app/not-found-8fdd48e65bd803b1.js"],"name":"","async":false} +c:I{"id":90155,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"LicenseBanner","async":false} +d:I{"id":86150,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"","async":false} +e:I{"id":69145,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","6807:static/chunks/app/(dashboard)/settings/(integrations)/providers/layout-73ce9c8d662cc1f1.js"],"name":"","async":false} +f:I{"id":57830,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","6807:static/chunks/app/(dashboard)/settings/(integrations)/providers/layout-73ce9c8d662cc1f1.js"],"name":"ScrollArea","async":false} +10:I{"id":54007,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","436:static/chunks/436-401a51e7af01c90f.js","4007:static/chunks/4007-80fb4e0338c67b28.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","2219:static/chunks/app/(dashboard)/settings/(integrations)/providers/[kind]/layout-9688fa1599db2b7b.js"],"name":"","async":false} +11:I{"id":81565,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","436:static/chunks/436-401a51e7af01c90f.js","4007:static/chunks/4007-80fb4e0338c67b28.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","2219:static/chunks/app/(dashboard)/settings/(integrations)/providers/[kind]/layout-9688fa1599db2b7b.js"],"name":"IconExternalLink","async":false} +13:I{"id":54616,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","240:static/chunks/240-6e4dd6e33fcf5a86.js","3179:static/chunks/3179-72a03be258e531a2.js","7070:static/chunks/7070-0361b26f2c124ca9.js","1460:static/chunks/1460-849850f3e0fcced7.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9436:static/chunks/app/(dashboard)/settings/(integrations)/providers/[kind]/new/page-2078198166a995dc.js"],"name":"NewProvider","async":false} +14:I{"id":80629,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Toaster","async":false} +6:[["$","meta","0",{"charSet":"utf-8"}],["$","title","1",{"children":"Tabby - Context Providers"}],["$","meta","2",{"name":"description","content":"Tabby, an opensource, self-hosted AI coding assistant."}],["$","meta","3",{"name":"theme-color","media":"(prefers-color-scheme: light)","content":"white"}],["$","meta","4",{"name":"theme-color","media":"(prefers-color-scheme: dark)","content":"black"}],["$","meta","5",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","link","6",{"rel":"icon","href":"/favicon.ico"}],["$","meta","7",{"name":"next-size-adjust"}]] +5:[null,["$","html",null,{"lang":"en","suppressHydrationWarning":true,"children":[["$","head",null,{}],["$","body",null,{"className":"bg-transparent font-sans antialiased __variable_d65c78 __variable_3c557b __variable_f44606","children":["$","$L7",null,{"attribute":"class","defaultTheme":"system","enableSystem":true,"children":[["$","$L8",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":["$","$Lb",null,{}],"notFoundStyles":[],"childProp":{"current":[null,[["$","$Lc",null,{}],["$","$Ld",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,["$","div",null,{"className":"-m-4 flex lg:-m-10","children":[["$","$Le",null,{"className":"w-[200px] pl-4 pt-4 lg:w-[250px]"}],["$","$Lf",null,{"className":"flex-1","children":["$","div",null,{"className":"p-4 lg:p-10","children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,[["$","div",null,{"className":"mb-4 flex items-center gap-4","children":["$","div",null,{"className":"flex-1 text-sm text-muted-foreground","children":["Connect to Self-Hosted GitLab as a provider, and select repositories from this provider to serve as context, thereby improving the performance of large language models",["$","$L10",null,{"className":"ml-2 inline-flex cursor-pointer flex-row items-center text-primary hover:underline","href":"https://tabby.tabbyml.com/blog/2023/10/16/repository-context-for-code-completion","target":"_blank","children":["Learn more",["$","$L11",null,{"className":"ml-1"}]]}]]}]}],["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children",["kind","gitlab-self-hosted","d"],"children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children",["kind","gitlab-self-hosted","d"],"children","new","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$L12",["$","$L13",null,{}],null],"segment":"__PAGE__?{\"kind\":\"gitlab-self-hosted\"}"},"styles":[]}],"segment":"new"},"styles":[]}]],null],"segment":["kind","gitlab-self-hosted","d"]},"styles":[]}]}]}]]}],null],"segment":"providers"},"styles":[]}],"segment":"(integrations)"},"styles":[]}],"segment":"settings"},"styles":[]}]}]],null],"segment":"(dashboard)"},"styles":[]}]}],["$","$L14",null,{"richColors":true,"closeButton":true}],null]}]}]]}],null] +12:null diff --git a/ee/tabby-webserver/ui/settings/providers/gitlab.html b/ee/tabby-webserver/ui/settings/providers/gitlab.html new file mode 100644 index 000000000000..d5f51334bcd2 --- /dev/null +++ b/ee/tabby-webserver/ui/settings/providers/gitlab.html @@ -0,0 +1 @@ +Tabby - Context Providers \ No newline at end of file diff --git a/ee/tabby-webserver/ui/settings/providers/gitlab.txt b/ee/tabby-webserver/ui/settings/providers/gitlab.txt new file mode 100644 index 000000000000..0b8b5b5faa27 --- /dev/null +++ b/ee/tabby-webserver/ui/settings/providers/gitlab.txt @@ -0,0 +1,21 @@ +1:HL["/_next/static/media/a34f9d1faa5f3315-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +2:HL["/_next/static/media/bb3ef058b751a6ad-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +3:HL["/_next/static/media/f75d6d02e2924b13-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +4:HL["/_next/static/css/1df4303b470c4ade.css","style"] +0:["6THjZwODOtNUmFQ4Gzp5t",[[["",{"children":["(dashboard)",{"children":["settings",{"children":["(integrations)",{"children":["providers",{"children":[["kind","gitlab","d"],{"children":["__PAGE__?{\"kind\":\"gitlab\"}",{}]}]}]}]}]}]},"$undefined","$undefined",true],"$L5",[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/1df4303b470c4ade.css","precedence":"next"}]],"$L6"]]]] +7:I{"id":35590,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Providers","async":false} +8:I{"id":32191,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"","async":false} +9:I{"id":32892,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +a:I{"id":95814,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +b:I{"id":69579,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","4546:static/chunks/4546-05756522a4929864.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","1544:static/chunks/1544-f96cdf5bcfa8f0e4.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9160:static/chunks/app/not-found-8fdd48e65bd803b1.js"],"name":"","async":false} +c:I{"id":90155,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"LicenseBanner","async":false} +d:I{"id":86150,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"","async":false} +e:I{"id":69145,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","6807:static/chunks/app/(dashboard)/settings/(integrations)/providers/layout-73ce9c8d662cc1f1.js"],"name":"","async":false} +f:I{"id":57830,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","6807:static/chunks/app/(dashboard)/settings/(integrations)/providers/layout-73ce9c8d662cc1f1.js"],"name":"ScrollArea","async":false} +10:I{"id":54007,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","436:static/chunks/436-401a51e7af01c90f.js","4007:static/chunks/4007-80fb4e0338c67b28.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","2219:static/chunks/app/(dashboard)/settings/(integrations)/providers/[kind]/layout-9688fa1599db2b7b.js"],"name":"","async":false} +11:I{"id":81565,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","436:static/chunks/436-401a51e7af01c90f.js","4007:static/chunks/4007-80fb4e0338c67b28.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","2219:static/chunks/app/(dashboard)/settings/(integrations)/providers/[kind]/layout-9688fa1599db2b7b.js"],"name":"IconExternalLink","async":false} +13:I{"id":6194,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","4007:static/chunks/4007-80fb4e0338c67b28.js","7070:static/chunks/7070-0361b26f2c124ca9.js","7548:static/chunks/7548-9d3520e9c948edb5.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","410:static/chunks/410-2d9f0445fa0cca52.js","9667:static/chunks/app/(dashboard)/settings/(integrations)/providers/[kind]/page-8f2bf20ea03adec0.js"],"name":"","async":false} +14:I{"id":80629,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Toaster","async":false} +6:[["$","meta","0",{"charSet":"utf-8"}],["$","title","1",{"children":"Tabby - Context Providers"}],["$","meta","2",{"name":"description","content":"Tabby, an opensource, self-hosted AI coding assistant."}],["$","meta","3",{"name":"theme-color","media":"(prefers-color-scheme: light)","content":"white"}],["$","meta","4",{"name":"theme-color","media":"(prefers-color-scheme: dark)","content":"black"}],["$","meta","5",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","link","6",{"rel":"icon","href":"/favicon.ico"}],["$","meta","7",{"name":"next-size-adjust"}]] +5:[null,["$","html",null,{"lang":"en","suppressHydrationWarning":true,"children":[["$","head",null,{}],["$","body",null,{"className":"bg-transparent font-sans antialiased __variable_d65c78 __variable_3c557b __variable_f44606","children":["$","$L7",null,{"attribute":"class","defaultTheme":"system","enableSystem":true,"children":[["$","$L8",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":["$","$Lb",null,{}],"notFoundStyles":[],"childProp":{"current":[null,[["$","$Lc",null,{}],["$","$Ld",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,["$","div",null,{"className":"-m-4 flex lg:-m-10","children":[["$","$Le",null,{"className":"w-[200px] pl-4 pt-4 lg:w-[250px]"}],["$","$Lf",null,{"className":"flex-1","children":["$","div",null,{"className":"p-4 lg:p-10","children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,[["$","div",null,{"className":"mb-4 flex items-center gap-4","children":["$","div",null,{"className":"flex-1 text-sm text-muted-foreground","children":["Connect to GitLab as a provider, and select repositories from this provider to serve as context, thereby improving the performance of large language models",["$","$L10",null,{"className":"ml-2 inline-flex cursor-pointer flex-row items-center text-primary hover:underline","href":"https://tabby.tabbyml.com/blog/2023/10/16/repository-context-for-code-completion","target":"_blank","children":["Learn more",["$","$L11",null,{"className":"ml-1"}]]}]]}]}],["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children",["kind","gitlab","d"],"children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$L12",["$","$L13",null,{}],null],"segment":"__PAGE__?{\"kind\":\"gitlab\"}"},"styles":[]}]],null],"segment":["kind","gitlab","d"]},"styles":[]}]}]}]]}],null],"segment":"providers"},"styles":[]}],"segment":"(integrations)"},"styles":[]}],"segment":"settings"},"styles":[]}]}]],null],"segment":"(dashboard)"},"styles":[]}]}],["$","$L14",null,{"richColors":true,"closeButton":true}],null]}]}]]}],null] +12:null diff --git a/ee/tabby-webserver/ui/settings/providers/gitlab/detail.html b/ee/tabby-webserver/ui/settings/providers/gitlab/detail.html new file mode 100644 index 000000000000..e3ff6d0a99ad --- /dev/null +++ b/ee/tabby-webserver/ui/settings/providers/gitlab/detail.html @@ -0,0 +1 @@ +Tabby - Context Providers \ No newline at end of file diff --git a/ee/tabby-webserver/ui/settings/providers/gitlab/detail.txt b/ee/tabby-webserver/ui/settings/providers/gitlab/detail.txt new file mode 100644 index 000000000000..58dc6ef535dc --- /dev/null +++ b/ee/tabby-webserver/ui/settings/providers/gitlab/detail.txt @@ -0,0 +1,21 @@ +1:HL["/_next/static/media/a34f9d1faa5f3315-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +2:HL["/_next/static/media/bb3ef058b751a6ad-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +3:HL["/_next/static/media/f75d6d02e2924b13-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +4:HL["/_next/static/css/1df4303b470c4ade.css","style"] +0:["6THjZwODOtNUmFQ4Gzp5t",[[["",{"children":["(dashboard)",{"children":["settings",{"children":["(integrations)",{"children":["providers",{"children":[["kind","gitlab","d"],{"children":["detail",{"children":["__PAGE__?{\"kind\":\"gitlab\"}",{}]}]}]}]}]}]}]},"$undefined","$undefined",true],"$L5",[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/1df4303b470c4ade.css","precedence":"next"}]],"$L6"]]]] +7:I{"id":35590,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Providers","async":false} +8:I{"id":32191,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"","async":false} +9:I{"id":32892,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +a:I{"id":95814,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +b:I{"id":69579,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","4546:static/chunks/4546-05756522a4929864.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","1544:static/chunks/1544-f96cdf5bcfa8f0e4.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9160:static/chunks/app/not-found-8fdd48e65bd803b1.js"],"name":"","async":false} +c:I{"id":90155,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"LicenseBanner","async":false} +d:I{"id":86150,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"","async":false} +e:I{"id":69145,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","6807:static/chunks/app/(dashboard)/settings/(integrations)/providers/layout-73ce9c8d662cc1f1.js"],"name":"","async":false} +f:I{"id":57830,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","6807:static/chunks/app/(dashboard)/settings/(integrations)/providers/layout-73ce9c8d662cc1f1.js"],"name":"ScrollArea","async":false} +10:I{"id":54007,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","436:static/chunks/436-401a51e7af01c90f.js","4007:static/chunks/4007-80fb4e0338c67b28.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","2219:static/chunks/app/(dashboard)/settings/(integrations)/providers/[kind]/layout-9688fa1599db2b7b.js"],"name":"","async":false} +11:I{"id":81565,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","436:static/chunks/436-401a51e7af01c90f.js","4007:static/chunks/4007-80fb4e0338c67b28.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","2219:static/chunks/app/(dashboard)/settings/(integrations)/providers/[kind]/layout-9688fa1599db2b7b.js"],"name":"IconExternalLink","async":false} +13:I{"id":48973,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","240:static/chunks/240-6e4dd6e33fcf5a86.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","3179:static/chunks/3179-72a03be258e531a2.js","4421:static/chunks/4421-0da05343b4d5e17a.js","7070:static/chunks/7070-0361b26f2c124ca9.js","1002:static/chunks/1002-09e560a83c07e9f4.js","1460:static/chunks/1460-849850f3e0fcced7.js","6805:static/chunks/6805-48f0b1146fe740f9.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","9464:static/chunks/app/(dashboard)/settings/(integrations)/providers/[kind]/detail/page-3e8a5c95f318fe68.js"],"name":"","async":false} +14:I{"id":80629,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Toaster","async":false} +6:[["$","meta","0",{"charSet":"utf-8"}],["$","title","1",{"children":"Tabby - Context Providers"}],["$","meta","2",{"name":"description","content":"Tabby, an opensource, self-hosted AI coding assistant."}],["$","meta","3",{"name":"theme-color","media":"(prefers-color-scheme: light)","content":"white"}],["$","meta","4",{"name":"theme-color","media":"(prefers-color-scheme: dark)","content":"black"}],["$","meta","5",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","link","6",{"rel":"icon","href":"/favicon.ico"}],["$","meta","7",{"name":"next-size-adjust"}]] +5:[null,["$","html",null,{"lang":"en","suppressHydrationWarning":true,"children":[["$","head",null,{}],["$","body",null,{"className":"bg-transparent font-sans antialiased __variable_d65c78 __variable_3c557b __variable_f44606","children":["$","$L7",null,{"attribute":"class","defaultTheme":"system","enableSystem":true,"children":[["$","$L8",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":["$","$Lb",null,{}],"notFoundStyles":[],"childProp":{"current":[null,[["$","$Lc",null,{}],["$","$Ld",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,["$","div",null,{"className":"-m-4 flex lg:-m-10","children":[["$","$Le",null,{"className":"w-[200px] pl-4 pt-4 lg:w-[250px]"}],["$","$Lf",null,{"className":"flex-1","children":["$","div",null,{"className":"p-4 lg:p-10","children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,[["$","div",null,{"className":"mb-4 flex items-center gap-4","children":["$","div",null,{"className":"flex-1 text-sm text-muted-foreground","children":["Connect to GitLab as a provider, and select repositories from this provider to serve as context, thereby improving the performance of large language models",["$","$L10",null,{"className":"ml-2 inline-flex cursor-pointer flex-row items-center text-primary hover:underline","href":"https://tabby.tabbyml.com/blog/2023/10/16/repository-context-for-code-completion","target":"_blank","children":["Learn more",["$","$L11",null,{"className":"ml-1"}]]}]]}]}],["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children",["kind","gitlab","d"],"children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children",["kind","gitlab","d"],"children","detail","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$L12",["$","$L13",null,{}],null],"segment":"__PAGE__?{\"kind\":\"gitlab\"}"},"styles":[]}],"segment":"detail"},"styles":[]}]],null],"segment":["kind","gitlab","d"]},"styles":[]}]}]}]]}],null],"segment":"providers"},"styles":[]}],"segment":"(integrations)"},"styles":[]}],"segment":"settings"},"styles":[]}]}]],null],"segment":"(dashboard)"},"styles":[]}]}],["$","$L14",null,{"richColors":true,"closeButton":true}],null]}]}]]}],null] +12:null diff --git a/ee/tabby-webserver/ui/settings/providers/gitlab/new.html b/ee/tabby-webserver/ui/settings/providers/gitlab/new.html new file mode 100644 index 000000000000..8ed69c12a6fe --- /dev/null +++ b/ee/tabby-webserver/ui/settings/providers/gitlab/new.html @@ -0,0 +1 @@ +Tabby - Context Providers \ No newline at end of file diff --git a/ee/tabby-webserver/ui/settings/providers/gitlab/new.txt b/ee/tabby-webserver/ui/settings/providers/gitlab/new.txt new file mode 100644 index 000000000000..1f75f0f8b685 --- /dev/null +++ b/ee/tabby-webserver/ui/settings/providers/gitlab/new.txt @@ -0,0 +1,21 @@ +1:HL["/_next/static/media/a34f9d1faa5f3315-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +2:HL["/_next/static/media/bb3ef058b751a6ad-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +3:HL["/_next/static/media/f75d6d02e2924b13-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +4:HL["/_next/static/css/1df4303b470c4ade.css","style"] +0:["6THjZwODOtNUmFQ4Gzp5t",[[["",{"children":["(dashboard)",{"children":["settings",{"children":["(integrations)",{"children":["providers",{"children":[["kind","gitlab","d"],{"children":["new",{"children":["__PAGE__?{\"kind\":\"gitlab\"}",{}]}]}]}]}]}]}]},"$undefined","$undefined",true],"$L5",[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/1df4303b470c4ade.css","precedence":"next"}]],"$L6"]]]] +7:I{"id":35590,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Providers","async":false} +8:I{"id":32191,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"","async":false} +9:I{"id":32892,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +a:I{"id":95814,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +b:I{"id":69579,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","4546:static/chunks/4546-05756522a4929864.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","1544:static/chunks/1544-f96cdf5bcfa8f0e4.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9160:static/chunks/app/not-found-8fdd48e65bd803b1.js"],"name":"","async":false} +c:I{"id":90155,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"LicenseBanner","async":false} +d:I{"id":86150,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"","async":false} +e:I{"id":69145,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","6807:static/chunks/app/(dashboard)/settings/(integrations)/providers/layout-73ce9c8d662cc1f1.js"],"name":"","async":false} +f:I{"id":57830,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","6065:static/chunks/6065-17553e3578c58f7b.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","6807:static/chunks/app/(dashboard)/settings/(integrations)/providers/layout-73ce9c8d662cc1f1.js"],"name":"ScrollArea","async":false} +10:I{"id":54007,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","436:static/chunks/436-401a51e7af01c90f.js","4007:static/chunks/4007-80fb4e0338c67b28.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","2219:static/chunks/app/(dashboard)/settings/(integrations)/providers/[kind]/layout-9688fa1599db2b7b.js"],"name":"","async":false} +11:I{"id":81565,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","436:static/chunks/436-401a51e7af01c90f.js","4007:static/chunks/4007-80fb4e0338c67b28.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","2219:static/chunks/app/(dashboard)/settings/(integrations)/providers/[kind]/layout-9688fa1599db2b7b.js"],"name":"IconExternalLink","async":false} +13:I{"id":54616,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","240:static/chunks/240-6e4dd6e33fcf5a86.js","3179:static/chunks/3179-72a03be258e531a2.js","7070:static/chunks/7070-0361b26f2c124ca9.js","1460:static/chunks/1460-849850f3e0fcced7.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9436:static/chunks/app/(dashboard)/settings/(integrations)/providers/[kind]/new/page-2078198166a995dc.js"],"name":"NewProvider","async":false} +14:I{"id":80629,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Toaster","async":false} +6:[["$","meta","0",{"charSet":"utf-8"}],["$","title","1",{"children":"Tabby - Context Providers"}],["$","meta","2",{"name":"description","content":"Tabby, an opensource, self-hosted AI coding assistant."}],["$","meta","3",{"name":"theme-color","media":"(prefers-color-scheme: light)","content":"white"}],["$","meta","4",{"name":"theme-color","media":"(prefers-color-scheme: dark)","content":"black"}],["$","meta","5",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","link","6",{"rel":"icon","href":"/favicon.ico"}],["$","meta","7",{"name":"next-size-adjust"}]] +5:[null,["$","html",null,{"lang":"en","suppressHydrationWarning":true,"children":[["$","head",null,{}],["$","body",null,{"className":"bg-transparent font-sans antialiased __variable_d65c78 __variable_3c557b __variable_f44606","children":["$","$L7",null,{"attribute":"class","defaultTheme":"system","enableSystem":true,"children":[["$","$L8",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":["$","$Lb",null,{}],"notFoundStyles":[],"childProp":{"current":[null,[["$","$Lc",null,{}],["$","$Ld",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,["$","div",null,{"className":"-m-4 flex lg:-m-10","children":[["$","$Le",null,{"className":"w-[200px] pl-4 pt-4 lg:w-[250px]"}],["$","$Lf",null,{"className":"flex-1","children":["$","div",null,{"className":"p-4 lg:p-10","children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,[["$","div",null,{"className":"mb-4 flex items-center gap-4","children":["$","div",null,{"className":"flex-1 text-sm text-muted-foreground","children":["Connect to GitLab as a provider, and select repositories from this provider to serve as context, thereby improving the performance of large language models",["$","$L10",null,{"className":"ml-2 inline-flex cursor-pointer flex-row items-center text-primary hover:underline","href":"https://tabby.tabbyml.com/blog/2023/10/16/repository-context-for-code-completion","target":"_blank","children":["Learn more",["$","$L11",null,{"className":"ml-1"}]]}]]}]}],["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children",["kind","gitlab","d"],"children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","providers","children",["kind","gitlab","d"],"children","new","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$L12",["$","$L13",null,{}],null],"segment":"__PAGE__?{\"kind\":\"gitlab\"}"},"styles":[]}],"segment":"new"},"styles":[]}]],null],"segment":["kind","gitlab","d"]},"styles":[]}]}]}]]}],null],"segment":"providers"},"styles":[]}],"segment":"(integrations)"},"styles":[]}],"segment":"settings"},"styles":[]}]}]],null],"segment":"(dashboard)"},"styles":[]}]}],["$","$L14",null,{"richColors":true,"closeButton":true}],null]}]}]]}],null] +12:null diff --git a/ee/tabby-webserver/ui/settings/sso.html b/ee/tabby-webserver/ui/settings/sso.html new file mode 100644 index 000000000000..f632ac8f5aa4 --- /dev/null +++ b/ee/tabby-webserver/ui/settings/sso.html @@ -0,0 +1 @@ +Tabby - SSO \ No newline at end of file diff --git a/ee/tabby-webserver/ui/settings/sso.txt b/ee/tabby-webserver/ui/settings/sso.txt new file mode 100644 index 000000000000..2065c4c7b518 --- /dev/null +++ b/ee/tabby-webserver/ui/settings/sso.txt @@ -0,0 +1,17 @@ +1:HL["/_next/static/media/a34f9d1faa5f3315-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +2:HL["/_next/static/media/bb3ef058b751a6ad-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +3:HL["/_next/static/media/f75d6d02e2924b13-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +4:HL["/_next/static/css/1df4303b470c4ade.css","style"] +0:["6THjZwODOtNUmFQ4Gzp5t",[[["",{"children":["(dashboard)",{"children":["settings",{"children":["(integrations)",{"children":["sso",{"children":["__PAGE__",{}]}]}]}]}]},"$undefined","$undefined",true],"$L5",[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/1df4303b470c4ade.css","precedence":"next"}]],"$L6"]]]] +7:I{"id":35590,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Providers","async":false} +8:I{"id":32191,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"","async":false} +9:I{"id":32892,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +a:I{"id":95814,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +b:I{"id":69579,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","4546:static/chunks/4546-05756522a4929864.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","1544:static/chunks/1544-f96cdf5bcfa8f0e4.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9160:static/chunks/app/not-found-8fdd48e65bd803b1.js"],"name":"","async":false} +c:I{"id":90155,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"LicenseBanner","async":false} +d:I{"id":86150,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"","async":false} +f:I{"id":3518,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","7070:static/chunks/7070-0361b26f2c124ca9.js","8194:static/chunks/8194-f2af5792032c1d6a.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","9157:static/chunks/app/(dashboard)/settings/(integrations)/sso/page-702da305aed26a3c.js"],"name":"CredentialList","async":false} +10:I{"id":80629,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Toaster","async":false} +6:[["$","meta","0",{"charSet":"utf-8"}],["$","title","1",{"children":"Tabby - SSO"}],["$","meta","2",{"name":"description","content":"Tabby, an opensource, self-hosted AI coding assistant."}],["$","meta","3",{"name":"theme-color","media":"(prefers-color-scheme: light)","content":"white"}],["$","meta","4",{"name":"theme-color","media":"(prefers-color-scheme: dark)","content":"black"}],["$","meta","5",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","link","6",{"rel":"icon","href":"/favicon.ico"}],["$","meta","7",{"name":"next-size-adjust"}]] +5:[null,["$","html",null,{"lang":"en","suppressHydrationWarning":true,"children":[["$","head",null,{}],["$","body",null,{"className":"bg-transparent font-sans antialiased __variable_d65c78 __variable_3c557b __variable_f44606","children":["$","$L7",null,{"attribute":"class","defaultTheme":"system","enableSystem":true,"children":[["$","$L8",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":["$","$Lb",null,{}],"notFoundStyles":[],"childProp":{"current":[null,[["$","$Lc",null,{}],["$","$Ld",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,[["$","div",null,{"className":"min-h-8 mb-4 flex items-center gap-4","children":["$","div",null,{"className":"flex-1 text-sm text-muted-foreground","children":["Single Sign-On (SSO) is an authentication method that enables users to authenticate with multiple applications and websites via a single set of credentials.",false]}]}],["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","sso","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$Le",["$","$Lf",null,{}],null],"segment":"__PAGE__"},"styles":[]}]],null],"segment":"sso"},"styles":[]}],"segment":"(integrations)"},"styles":[]}],"segment":"settings"},"styles":[]}]}]],null],"segment":"(dashboard)"},"styles":[]}]}],["$","$L10",null,{"richColors":true,"closeButton":true}],null]}]}]]}],null] +e:null diff --git a/ee/tabby-webserver/ui/settings/sso/detail/github.html b/ee/tabby-webserver/ui/settings/sso/detail/github.html new file mode 100644 index 000000000000..1a3c90a7ae40 --- /dev/null +++ b/ee/tabby-webserver/ui/settings/sso/detail/github.html @@ -0,0 +1 @@ +Tabby - SSO \ No newline at end of file diff --git a/ee/tabby-webserver/ui/settings/sso/detail/github.txt b/ee/tabby-webserver/ui/settings/sso/detail/github.txt new file mode 100644 index 000000000000..18687a60e03e --- /dev/null +++ b/ee/tabby-webserver/ui/settings/sso/detail/github.txt @@ -0,0 +1,17 @@ +1:HL["/_next/static/media/a34f9d1faa5f3315-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +2:HL["/_next/static/media/bb3ef058b751a6ad-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +3:HL["/_next/static/media/f75d6d02e2924b13-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +4:HL["/_next/static/css/1df4303b470c4ade.css","style"] +0:["6THjZwODOtNUmFQ4Gzp5t",[[["",{"children":["(dashboard)",{"children":["settings",{"children":["(integrations)",{"children":["sso",{"children":["detail",{"children":[["oauth-provider","github","d"],{"children":["__PAGE__?{\"oauth-provider\":\"github\"}",{}]}]}]}]}]}]}]},"$undefined","$undefined",true],"$L5",[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/1df4303b470c4ade.css","precedence":"next"}]],"$L6"]]]] +7:I{"id":35590,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Providers","async":false} +8:I{"id":32191,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"","async":false} +9:I{"id":32892,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +a:I{"id":95814,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +b:I{"id":69579,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","4546:static/chunks/4546-05756522a4929864.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","1544:static/chunks/1544-f96cdf5bcfa8f0e4.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9160:static/chunks/app/not-found-8fdd48e65bd803b1.js"],"name":"","async":false} +c:I{"id":90155,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"LicenseBanner","async":false} +d:I{"id":86150,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"","async":false} +f:I{"id":274,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","240:static/chunks/240-6e4dd6e33fcf5a86.js","3179:static/chunks/3179-72a03be258e531a2.js","7070:static/chunks/7070-0361b26f2c124ca9.js","1460:static/chunks/1460-849850f3e0fcced7.js","8194:static/chunks/8194-f2af5792032c1d6a.js","5972:static/chunks/5972-14d626d9e01ad6c3.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","8040:static/chunks/8040-e8a363d23bfc20c6.js","4159:static/chunks/app/(dashboard)/settings/(integrations)/sso/detail/[oauth-provider]/page-5ce39dfa6afbb089.js"],"name":"OAuthCredentialDetail","async":false} +10:I{"id":80629,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Toaster","async":false} +6:[["$","meta","0",{"charSet":"utf-8"}],["$","title","1",{"children":"Tabby - SSO"}],["$","meta","2",{"name":"description","content":"Tabby, an opensource, self-hosted AI coding assistant."}],["$","meta","3",{"name":"theme-color","media":"(prefers-color-scheme: light)","content":"white"}],["$","meta","4",{"name":"theme-color","media":"(prefers-color-scheme: dark)","content":"black"}],["$","meta","5",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","link","6",{"rel":"icon","href":"/favicon.ico"}],["$","meta","7",{"name":"next-size-adjust"}]] +5:[null,["$","html",null,{"lang":"en","suppressHydrationWarning":true,"children":[["$","head",null,{}],["$","body",null,{"className":"bg-transparent font-sans antialiased __variable_d65c78 __variable_3c557b __variable_f44606","children":["$","$L7",null,{"attribute":"class","defaultTheme":"system","enableSystem":true,"children":[["$","$L8",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":["$","$Lb",null,{}],"notFoundStyles":[],"childProp":{"current":[null,[["$","$Lc",null,{}],["$","$Ld",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,[["$","div",null,{"className":"min-h-8 mb-4 flex items-center gap-4","children":["$","div",null,{"className":"flex-1 text-sm text-muted-foreground","children":["Single Sign-On (SSO) is an authentication method that enables users to authenticate with multiple applications and websites via a single set of credentials.",false]}]}],["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","sso","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","sso","children","detail","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","sso","children","detail","children",["oauth-provider","github","d"],"children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$Le",["$","$Lf",null,{"provider":"GITHUB"}],null],"segment":"__PAGE__?{\"oauth-provider\":\"github\"}"},"styles":[]}],"segment":["oauth-provider","github","d"]},"styles":[]}],"segment":"detail"},"styles":[]}]],null],"segment":"sso"},"styles":[]}],"segment":"(integrations)"},"styles":[]}],"segment":"settings"},"styles":[]}]}]],null],"segment":"(dashboard)"},"styles":[]}]}],["$","$L10",null,{"richColors":true,"closeButton":true}],null]}]}]]}],null] +e:null diff --git a/ee/tabby-webserver/ui/settings/sso/detail/gitlab.html b/ee/tabby-webserver/ui/settings/sso/detail/gitlab.html new file mode 100644 index 000000000000..a976b25dcb1b --- /dev/null +++ b/ee/tabby-webserver/ui/settings/sso/detail/gitlab.html @@ -0,0 +1 @@ +Tabby - SSO \ No newline at end of file diff --git a/ee/tabby-webserver/ui/settings/sso/detail/gitlab.txt b/ee/tabby-webserver/ui/settings/sso/detail/gitlab.txt new file mode 100644 index 000000000000..2a7dc1f913fa --- /dev/null +++ b/ee/tabby-webserver/ui/settings/sso/detail/gitlab.txt @@ -0,0 +1,17 @@ +1:HL["/_next/static/media/a34f9d1faa5f3315-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +2:HL["/_next/static/media/bb3ef058b751a6ad-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +3:HL["/_next/static/media/f75d6d02e2924b13-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +4:HL["/_next/static/css/1df4303b470c4ade.css","style"] +0:["6THjZwODOtNUmFQ4Gzp5t",[[["",{"children":["(dashboard)",{"children":["settings",{"children":["(integrations)",{"children":["sso",{"children":["detail",{"children":[["oauth-provider","gitlab","d"],{"children":["__PAGE__?{\"oauth-provider\":\"gitlab\"}",{}]}]}]}]}]}]}]},"$undefined","$undefined",true],"$L5",[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/1df4303b470c4ade.css","precedence":"next"}]],"$L6"]]]] +7:I{"id":35590,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Providers","async":false} +8:I{"id":32191,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"","async":false} +9:I{"id":32892,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +a:I{"id":95814,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +b:I{"id":69579,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","4546:static/chunks/4546-05756522a4929864.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","1544:static/chunks/1544-f96cdf5bcfa8f0e4.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9160:static/chunks/app/not-found-8fdd48e65bd803b1.js"],"name":"","async":false} +c:I{"id":90155,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"LicenseBanner","async":false} +d:I{"id":86150,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"","async":false} +f:I{"id":274,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","240:static/chunks/240-6e4dd6e33fcf5a86.js","3179:static/chunks/3179-72a03be258e531a2.js","7070:static/chunks/7070-0361b26f2c124ca9.js","1460:static/chunks/1460-849850f3e0fcced7.js","8194:static/chunks/8194-f2af5792032c1d6a.js","5972:static/chunks/5972-14d626d9e01ad6c3.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","8040:static/chunks/8040-e8a363d23bfc20c6.js","4159:static/chunks/app/(dashboard)/settings/(integrations)/sso/detail/[oauth-provider]/page-5ce39dfa6afbb089.js"],"name":"OAuthCredentialDetail","async":false} +10:I{"id":80629,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Toaster","async":false} +6:[["$","meta","0",{"charSet":"utf-8"}],["$","title","1",{"children":"Tabby - SSO"}],["$","meta","2",{"name":"description","content":"Tabby, an opensource, self-hosted AI coding assistant."}],["$","meta","3",{"name":"theme-color","media":"(prefers-color-scheme: light)","content":"white"}],["$","meta","4",{"name":"theme-color","media":"(prefers-color-scheme: dark)","content":"black"}],["$","meta","5",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","link","6",{"rel":"icon","href":"/favicon.ico"}],["$","meta","7",{"name":"next-size-adjust"}]] +5:[null,["$","html",null,{"lang":"en","suppressHydrationWarning":true,"children":[["$","head",null,{}],["$","body",null,{"className":"bg-transparent font-sans antialiased __variable_d65c78 __variable_3c557b __variable_f44606","children":["$","$L7",null,{"attribute":"class","defaultTheme":"system","enableSystem":true,"children":[["$","$L8",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":["$","$Lb",null,{}],"notFoundStyles":[],"childProp":{"current":[null,[["$","$Lc",null,{}],["$","$Ld",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,[["$","div",null,{"className":"min-h-8 mb-4 flex items-center gap-4","children":["$","div",null,{"className":"flex-1 text-sm text-muted-foreground","children":["Single Sign-On (SSO) is an authentication method that enables users to authenticate with multiple applications and websites via a single set of credentials.",false]}]}],["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","sso","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","sso","children","detail","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","sso","children","detail","children",["oauth-provider","gitlab","d"],"children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$Le",["$","$Lf",null,{"provider":"GITLAB"}],null],"segment":"__PAGE__?{\"oauth-provider\":\"gitlab\"}"},"styles":[]}],"segment":["oauth-provider","gitlab","d"]},"styles":[]}],"segment":"detail"},"styles":[]}]],null],"segment":"sso"},"styles":[]}],"segment":"(integrations)"},"styles":[]}],"segment":"settings"},"styles":[]}]}]],null],"segment":"(dashboard)"},"styles":[]}]}],["$","$L10",null,{"richColors":true,"closeButton":true}],null]}]}]]}],null] +e:null diff --git a/ee/tabby-webserver/ui/settings/sso/detail/google.html b/ee/tabby-webserver/ui/settings/sso/detail/google.html new file mode 100644 index 000000000000..2f7271be45d9 --- /dev/null +++ b/ee/tabby-webserver/ui/settings/sso/detail/google.html @@ -0,0 +1 @@ +Tabby - SSO \ No newline at end of file diff --git a/ee/tabby-webserver/ui/settings/sso/detail/google.txt b/ee/tabby-webserver/ui/settings/sso/detail/google.txt new file mode 100644 index 000000000000..08bd1dfca865 --- /dev/null +++ b/ee/tabby-webserver/ui/settings/sso/detail/google.txt @@ -0,0 +1,17 @@ +1:HL["/_next/static/media/a34f9d1faa5f3315-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +2:HL["/_next/static/media/bb3ef058b751a6ad-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +3:HL["/_next/static/media/f75d6d02e2924b13-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +4:HL["/_next/static/css/1df4303b470c4ade.css","style"] +0:["6THjZwODOtNUmFQ4Gzp5t",[[["",{"children":["(dashboard)",{"children":["settings",{"children":["(integrations)",{"children":["sso",{"children":["detail",{"children":[["oauth-provider","google","d"],{"children":["__PAGE__?{\"oauth-provider\":\"google\"}",{}]}]}]}]}]}]}]},"$undefined","$undefined",true],"$L5",[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/1df4303b470c4ade.css","precedence":"next"}]],"$L6"]]]] +7:I{"id":35590,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Providers","async":false} +8:I{"id":32191,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"","async":false} +9:I{"id":32892,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +a:I{"id":95814,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +b:I{"id":69579,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","4546:static/chunks/4546-05756522a4929864.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","1544:static/chunks/1544-f96cdf5bcfa8f0e4.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9160:static/chunks/app/not-found-8fdd48e65bd803b1.js"],"name":"","async":false} +c:I{"id":90155,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"LicenseBanner","async":false} +d:I{"id":86150,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"","async":false} +f:I{"id":274,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","240:static/chunks/240-6e4dd6e33fcf5a86.js","3179:static/chunks/3179-72a03be258e531a2.js","7070:static/chunks/7070-0361b26f2c124ca9.js","1460:static/chunks/1460-849850f3e0fcced7.js","8194:static/chunks/8194-f2af5792032c1d6a.js","5972:static/chunks/5972-14d626d9e01ad6c3.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","8040:static/chunks/8040-e8a363d23bfc20c6.js","4159:static/chunks/app/(dashboard)/settings/(integrations)/sso/detail/[oauth-provider]/page-5ce39dfa6afbb089.js"],"name":"OAuthCredentialDetail","async":false} +10:I{"id":80629,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Toaster","async":false} +6:[["$","meta","0",{"charSet":"utf-8"}],["$","title","1",{"children":"Tabby - SSO"}],["$","meta","2",{"name":"description","content":"Tabby, an opensource, self-hosted AI coding assistant."}],["$","meta","3",{"name":"theme-color","media":"(prefers-color-scheme: light)","content":"white"}],["$","meta","4",{"name":"theme-color","media":"(prefers-color-scheme: dark)","content":"black"}],["$","meta","5",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","link","6",{"rel":"icon","href":"/favicon.ico"}],["$","meta","7",{"name":"next-size-adjust"}]] +5:[null,["$","html",null,{"lang":"en","suppressHydrationWarning":true,"children":[["$","head",null,{}],["$","body",null,{"className":"bg-transparent font-sans antialiased __variable_d65c78 __variable_3c557b __variable_f44606","children":["$","$L7",null,{"attribute":"class","defaultTheme":"system","enableSystem":true,"children":[["$","$L8",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":["$","$Lb",null,{}],"notFoundStyles":[],"childProp":{"current":[null,[["$","$Lc",null,{}],["$","$Ld",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,[["$","div",null,{"className":"min-h-8 mb-4 flex items-center gap-4","children":["$","div",null,{"className":"flex-1 text-sm text-muted-foreground","children":["Single Sign-On (SSO) is an authentication method that enables users to authenticate with multiple applications and websites via a single set of credentials.",false]}]}],["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","sso","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","sso","children","detail","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","sso","children","detail","children",["oauth-provider","google","d"],"children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$Le",["$","$Lf",null,{"provider":"GOOGLE"}],null],"segment":"__PAGE__?{\"oauth-provider\":\"google\"}"},"styles":[]}],"segment":["oauth-provider","google","d"]},"styles":[]}],"segment":"detail"},"styles":[]}]],null],"segment":"sso"},"styles":[]}],"segment":"(integrations)"},"styles":[]}],"segment":"settings"},"styles":[]}]}]],null],"segment":"(dashboard)"},"styles":[]}]}],["$","$L10",null,{"richColors":true,"closeButton":true}],null]}]}]]}],null] +e:null diff --git a/ee/tabby-webserver/ui/settings/sso/detail/ldap.html b/ee/tabby-webserver/ui/settings/sso/detail/ldap.html new file mode 100644 index 000000000000..2b405a7f2f8b --- /dev/null +++ b/ee/tabby-webserver/ui/settings/sso/detail/ldap.html @@ -0,0 +1 @@ +Tabby - SSO \ No newline at end of file diff --git a/ee/tabby-webserver/ui/settings/sso/detail/ldap.txt b/ee/tabby-webserver/ui/settings/sso/detail/ldap.txt new file mode 100644 index 000000000000..eb9661408733 --- /dev/null +++ b/ee/tabby-webserver/ui/settings/sso/detail/ldap.txt @@ -0,0 +1,17 @@ +1:HL["/_next/static/media/a34f9d1faa5f3315-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +2:HL["/_next/static/media/bb3ef058b751a6ad-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +3:HL["/_next/static/media/f75d6d02e2924b13-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +4:HL["/_next/static/css/1df4303b470c4ade.css","style"] +0:["6THjZwODOtNUmFQ4Gzp5t",[[["",{"children":["(dashboard)",{"children":["settings",{"children":["(integrations)",{"children":["sso",{"children":["detail",{"children":["ldap",{"children":["__PAGE__",{}]}]}]}]}]}]}]},"$undefined","$undefined",true],"$L5",[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/1df4303b470c4ade.css","precedence":"next"}]],"$L6"]]]] +7:I{"id":35590,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Providers","async":false} +8:I{"id":32191,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"","async":false} +9:I{"id":32892,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +a:I{"id":95814,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +b:I{"id":69579,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","4546:static/chunks/4546-05756522a4929864.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","1544:static/chunks/1544-f96cdf5bcfa8f0e4.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9160:static/chunks/app/not-found-8fdd48e65bd803b1.js"],"name":"","async":false} +c:I{"id":90155,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"LicenseBanner","async":false} +d:I{"id":86150,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"","async":false} +f:I{"id":61367,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","240:static/chunks/240-6e4dd6e33fcf5a86.js","3179:static/chunks/3179-72a03be258e531a2.js","7070:static/chunks/7070-0361b26f2c124ca9.js","1460:static/chunks/1460-849850f3e0fcced7.js","1889:static/chunks/1889-808f5c268ba4d728.js","8194:static/chunks/8194-f2af5792032c1d6a.js","5972:static/chunks/5972-14d626d9e01ad6c3.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","8040:static/chunks/8040-e8a363d23bfc20c6.js","1704:static/chunks/app/(dashboard)/settings/(integrations)/sso/detail/ldap/page-7c7b53efa1463ce7.js"],"name":"LdapCredentialDetail","async":false} +10:I{"id":80629,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Toaster","async":false} +6:[["$","meta","0",{"charSet":"utf-8"}],["$","title","1",{"children":"Tabby - SSO"}],["$","meta","2",{"name":"description","content":"Tabby, an opensource, self-hosted AI coding assistant."}],["$","meta","3",{"name":"theme-color","media":"(prefers-color-scheme: light)","content":"white"}],["$","meta","4",{"name":"theme-color","media":"(prefers-color-scheme: dark)","content":"black"}],["$","meta","5",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","link","6",{"rel":"icon","href":"/favicon.ico"}],["$","meta","7",{"name":"next-size-adjust"}]] +5:[null,["$","html",null,{"lang":"en","suppressHydrationWarning":true,"children":[["$","head",null,{}],["$","body",null,{"className":"bg-transparent font-sans antialiased __variable_d65c78 __variable_3c557b __variable_f44606","children":["$","$L7",null,{"attribute":"class","defaultTheme":"system","enableSystem":true,"children":[["$","$L8",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":["$","$Lb",null,{}],"notFoundStyles":[],"childProp":{"current":[null,[["$","$Lc",null,{}],["$","$Ld",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,[["$","div",null,{"className":"min-h-8 mb-4 flex items-center gap-4","children":["$","div",null,{"className":"flex-1 text-sm text-muted-foreground","children":["Single Sign-On (SSO) is an authentication method that enables users to authenticate with multiple applications and websites via a single set of credentials.",false]}]}],["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","sso","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","sso","children","detail","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","sso","children","detail","children","ldap","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$Le",["$","$Lf",null,{}],null],"segment":"__PAGE__"},"styles":[]}],"segment":"ldap"},"styles":[]}],"segment":"detail"},"styles":[]}]],null],"segment":"sso"},"styles":[]}],"segment":"(integrations)"},"styles":[]}],"segment":"settings"},"styles":[]}]}]],null],"segment":"(dashboard)"},"styles":[]}]}],["$","$L10",null,{"richColors":true,"closeButton":true}],null]}]}]]}],null] +e:null diff --git a/ee/tabby-webserver/ui/settings/sso/new.html b/ee/tabby-webserver/ui/settings/sso/new.html new file mode 100644 index 000000000000..73cff8c51542 --- /dev/null +++ b/ee/tabby-webserver/ui/settings/sso/new.html @@ -0,0 +1 @@ +Tabby - SSO \ No newline at end of file diff --git a/ee/tabby-webserver/ui/settings/sso/new.txt b/ee/tabby-webserver/ui/settings/sso/new.txt new file mode 100644 index 000000000000..cbb03cbf4cab --- /dev/null +++ b/ee/tabby-webserver/ui/settings/sso/new.txt @@ -0,0 +1,17 @@ +1:HL["/_next/static/media/a34f9d1faa5f3315-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +2:HL["/_next/static/media/bb3ef058b751a6ad-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +3:HL["/_next/static/media/f75d6d02e2924b13-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +4:HL["/_next/static/css/1df4303b470c4ade.css","style"] +0:["6THjZwODOtNUmFQ4Gzp5t",[[["",{"children":["(dashboard)",{"children":["settings",{"children":["(integrations)",{"children":["sso",{"children":["new",{"children":["__PAGE__",{}]}]}]}]}]}]},"$undefined","$undefined",true],"$L5",[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/1df4303b470c4ade.css","precedence":"next"}]],"$L6"]]]] +7:I{"id":35590,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Providers","async":false} +8:I{"id":32191,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"","async":false} +9:I{"id":32892,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +a:I{"id":95814,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +b:I{"id":69579,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","4546:static/chunks/4546-05756522a4929864.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","1544:static/chunks/1544-f96cdf5bcfa8f0e4.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9160:static/chunks/app/not-found-8fdd48e65bd803b1.js"],"name":"","async":false} +c:I{"id":90155,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"LicenseBanner","async":false} +d:I{"id":86150,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"","async":false} +f:I{"id":54304,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","240:static/chunks/240-6e4dd6e33fcf5a86.js","3179:static/chunks/3179-72a03be258e531a2.js","1460:static/chunks/1460-849850f3e0fcced7.js","1889:static/chunks/1889-808f5c268ba4d728.js","8194:static/chunks/8194-f2af5792032c1d6a.js","5972:static/chunks/5972-14d626d9e01ad6c3.js","9435:static/chunks/9435-d0785650712e5454.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","8040:static/chunks/8040-e8a363d23bfc20c6.js","2436:static/chunks/app/(dashboard)/settings/(integrations)/sso/new/page-3def33fbf85c587b.js"],"name":"NewPage","async":false} +10:I{"id":80629,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Toaster","async":false} +6:[["$","meta","0",{"charSet":"utf-8"}],["$","title","1",{"children":"Tabby - SSO"}],["$","meta","2",{"name":"description","content":"Tabby, an opensource, self-hosted AI coding assistant."}],["$","meta","3",{"name":"theme-color","media":"(prefers-color-scheme: light)","content":"white"}],["$","meta","4",{"name":"theme-color","media":"(prefers-color-scheme: dark)","content":"black"}],["$","meta","5",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","link","6",{"rel":"icon","href":"/favicon.ico"}],["$","meta","7",{"name":"next-size-adjust"}]] +5:[null,["$","html",null,{"lang":"en","suppressHydrationWarning":true,"children":[["$","head",null,{}],["$","body",null,{"className":"bg-transparent font-sans antialiased __variable_d65c78 __variable_3c557b __variable_f44606","children":["$","$L7",null,{"attribute":"class","defaultTheme":"system","enableSystem":true,"children":[["$","$L8",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":["$","$Lb",null,{}],"notFoundStyles":[],"childProp":{"current":[null,[["$","$Lc",null,{}],["$","$Ld",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,[["$","div",null,{"className":"min-h-8 mb-4 flex items-center gap-4","children":["$","div",null,{"className":"flex-1 text-sm text-muted-foreground","children":["Single Sign-On (SSO) is an authentication method that enables users to authenticate with multiple applications and websites via a single set of credentials.",false]}]}],["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","sso","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","(integrations)","children","sso","children","new","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$Le",["$","$Lf",null,{}],null],"segment":"__PAGE__"},"styles":[]}],"segment":"new"},"styles":[]}]],null],"segment":"sso"},"styles":[]}],"segment":"(integrations)"},"styles":[]}],"segment":"settings"},"styles":[]}]}]],null],"segment":"(dashboard)"},"styles":[]}]}],["$","$L10",null,{"richColors":true,"closeButton":true}],null]}]}]]}],null] +e:null diff --git a/ee/tabby-webserver/ui/settings/subscription.html b/ee/tabby-webserver/ui/settings/subscription.html new file mode 100644 index 000000000000..077b94ebd0e6 --- /dev/null +++ b/ee/tabby-webserver/ui/settings/subscription.html @@ -0,0 +1 @@ +Tabby - Subscription \ No newline at end of file diff --git a/ee/tabby-webserver/ui/settings/subscription.txt b/ee/tabby-webserver/ui/settings/subscription.txt new file mode 100644 index 000000000000..aa2e57dcf914 --- /dev/null +++ b/ee/tabby-webserver/ui/settings/subscription.txt @@ -0,0 +1,17 @@ +1:HL["/_next/static/media/a34f9d1faa5f3315-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +2:HL["/_next/static/media/bb3ef058b751a6ad-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +3:HL["/_next/static/media/f75d6d02e2924b13-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +4:HL["/_next/static/css/1df4303b470c4ade.css","style"] +0:["6THjZwODOtNUmFQ4Gzp5t",[[["",{"children":["(dashboard)",{"children":["settings",{"children":["subscription",{"children":["__PAGE__",{}]}]}]}]},"$undefined","$undefined",true],"$L5",[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/1df4303b470c4ade.css","precedence":"next"}]],"$L6"]]]] +7:I{"id":35590,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Providers","async":false} +8:I{"id":32191,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"","async":false} +9:I{"id":32892,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +a:I{"id":95814,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +b:I{"id":69579,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","4546:static/chunks/4546-05756522a4929864.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","1544:static/chunks/1544-f96cdf5bcfa8f0e4.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9160:static/chunks/app/not-found-8fdd48e65bd803b1.js"],"name":"","async":false} +c:I{"id":90155,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"LicenseBanner","async":false} +d:I{"id":86150,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"","async":false} +f:I{"id":47166,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","240:static/chunks/240-6e4dd6e33fcf5a86.js","3179:static/chunks/3179-72a03be258e531a2.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1460:static/chunks/1460-849850f3e0fcced7.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","4303:static/chunks/app/(dashboard)/settings/subscription/page-e862c807ef8188f0.js"],"name":"","async":false} +10:I{"id":80629,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Toaster","async":false} +6:[["$","meta","0",{"charSet":"utf-8"}],["$","title","1",{"children":"Tabby - Subscription"}],["$","meta","2",{"name":"description","content":"Tabby, an opensource, self-hosted AI coding assistant."}],["$","meta","3",{"name":"theme-color","media":"(prefers-color-scheme: light)","content":"white"}],["$","meta","4",{"name":"theme-color","media":"(prefers-color-scheme: dark)","content":"black"}],["$","meta","5",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","link","6",{"rel":"icon","href":"/favicon.ico"}],["$","meta","7",{"name":"next-size-adjust"}]] +5:[null,["$","html",null,{"lang":"en","suppressHydrationWarning":true,"children":[["$","head",null,{}],["$","body",null,{"className":"bg-transparent font-sans antialiased __variable_d65c78 __variable_3c557b __variable_f44606","children":["$","$L7",null,{"attribute":"class","defaultTheme":"system","enableSystem":true,"children":[["$","$L8",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":["$","$Lb",null,{}],"notFoundStyles":[],"childProp":{"current":[null,[["$","$Lc",null,{}],["$","$Ld",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","subscription","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$Le",["$","$Lf",null,{}],null],"segment":"__PAGE__"},"styles":[]}],"segment":"subscription"},"styles":[]}],"segment":"settings"},"styles":[]}]}]],null],"segment":"(dashboard)"},"styles":[]}]}],["$","$L10",null,{"richColors":true,"closeButton":true}],null]}]}]]}],null] +e:null diff --git a/ee/tabby-webserver/ui/settings/team.html b/ee/tabby-webserver/ui/settings/team.html new file mode 100644 index 000000000000..b2a5c01f3260 --- /dev/null +++ b/ee/tabby-webserver/ui/settings/team.html @@ -0,0 +1 @@ +Tabby - Members \ No newline at end of file diff --git a/ee/tabby-webserver/ui/settings/team.txt b/ee/tabby-webserver/ui/settings/team.txt new file mode 100644 index 000000000000..d653a973f664 --- /dev/null +++ b/ee/tabby-webserver/ui/settings/team.txt @@ -0,0 +1,18 @@ +1:HL["/_next/static/media/a34f9d1faa5f3315-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +2:HL["/_next/static/media/bb3ef058b751a6ad-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +3:HL["/_next/static/media/f75d6d02e2924b13-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +4:HL["/_next/static/css/1df4303b470c4ade.css","style"] +0:["6THjZwODOtNUmFQ4Gzp5t",[[["",{"children":["(dashboard)",{"children":["settings",{"children":["team",{"children":["__PAGE__",{}]}]}]}]},"$undefined","$undefined",true],"$L5",[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/1df4303b470c4ade.css","precedence":"next"}]],"$L6"]]]] +7:I{"id":35590,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Providers","async":false} +8:I{"id":32191,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"","async":false} +9:I{"id":32892,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +a:I{"id":95814,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +b:I{"id":69579,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","4546:static/chunks/4546-05756522a4929864.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","1544:static/chunks/1544-f96cdf5bcfa8f0e4.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9160:static/chunks/app/not-found-8fdd48e65bd803b1.js"],"name":"","async":false} +c:I{"id":90155,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"LicenseBanner","async":false} +d:I{"id":86150,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"","async":false} +e:I{"id":65717,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","4691:static/chunks/4691-3ed340b4558906b1.js","340:static/chunks/340-8b69198c1e94e4f2.js","4007:static/chunks/4007-80fb4e0338c67b28.js","2718:static/chunks/2718-d762fd3fac1c2178.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","7961:static/chunks/app/(dashboard)/settings/team/layout-0b7a68a98ff6c355.js"],"name":"TeamNav","async":false} +10:I{"id":49016,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","240:static/chunks/240-6e4dd6e33fcf5a86.js","3179:static/chunks/3179-72a03be258e531a2.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1460:static/chunks/1460-849850f3e0fcced7.js","5605:static/chunks/5605-b7e57fceb5667e91.js","8194:static/chunks/8194-f2af5792032c1d6a.js","2959:static/chunks/2959-2f5ae7e1fa23b9a8.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3816:static/chunks/app/(dashboard)/settings/team/page-2dad63389dceb323.js"],"name":"","async":false} +11:I{"id":80629,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Toaster","async":false} +6:[["$","meta","0",{"charSet":"utf-8"}],["$","title","1",{"children":"Tabby - Members"}],["$","meta","2",{"name":"description","content":"Tabby, an opensource, self-hosted AI coding assistant."}],["$","meta","3",{"name":"theme-color","media":"(prefers-color-scheme: light)","content":"white"}],["$","meta","4",{"name":"theme-color","media":"(prefers-color-scheme: dark)","content":"black"}],["$","meta","5",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","link","6",{"rel":"icon","href":"/favicon.ico"}],["$","meta","7",{"name":"next-size-adjust"}]] +5:[null,["$","html",null,{"lang":"en","suppressHydrationWarning":true,"children":[["$","head",null,{}],["$","body",null,{"className":"bg-transparent font-sans antialiased __variable_d65c78 __variable_3c557b __variable_f44606","children":["$","$L7",null,{"attribute":"class","defaultTheme":"system","enableSystem":true,"children":[["$","$L8",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":["$","$Lb",null,{}],"notFoundStyles":[],"childProp":{"current":[null,[["$","$Lc",null,{}],["$","$Ld",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,[["$","$Le",null,{}],["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","team","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$Lf",["$","$L10",null,{}],null],"segment":"__PAGE__"},"styles":[]}]],null],"segment":"team"},"styles":[]}],"segment":"settings"},"styles":[]}]}]],null],"segment":"(dashboard)"},"styles":[]}]}],["$","$L11",null,{"richColors":true,"closeButton":true}],null]}]}]]}],null] +f:null diff --git a/ee/tabby-webserver/ui/settings/team/groups.html b/ee/tabby-webserver/ui/settings/team/groups.html new file mode 100644 index 000000000000..fb319ec80bed --- /dev/null +++ b/ee/tabby-webserver/ui/settings/team/groups.html @@ -0,0 +1 @@ +Tabby - Groups \ No newline at end of file diff --git a/ee/tabby-webserver/ui/settings/team/groups.txt b/ee/tabby-webserver/ui/settings/team/groups.txt new file mode 100644 index 000000000000..17c3ac7c11c6 --- /dev/null +++ b/ee/tabby-webserver/ui/settings/team/groups.txt @@ -0,0 +1,18 @@ +1:HL["/_next/static/media/a34f9d1faa5f3315-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +2:HL["/_next/static/media/bb3ef058b751a6ad-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +3:HL["/_next/static/media/f75d6d02e2924b13-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +4:HL["/_next/static/css/1df4303b470c4ade.css","style"] +0:["6THjZwODOtNUmFQ4Gzp5t",[[["",{"children":["(dashboard)",{"children":["settings",{"children":["team",{"children":["groups",{"children":["__PAGE__",{}]}]}]}]}]},"$undefined","$undefined",true],"$L5",[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/1df4303b470c4ade.css","precedence":"next"}]],"$L6"]]]] +7:I{"id":35590,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Providers","async":false} +8:I{"id":32191,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"","async":false} +9:I{"id":32892,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +a:I{"id":95814,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +b:I{"id":69579,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","4546:static/chunks/4546-05756522a4929864.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","1544:static/chunks/1544-f96cdf5bcfa8f0e4.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9160:static/chunks/app/not-found-8fdd48e65bd803b1.js"],"name":"","async":false} +c:I{"id":90155,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"LicenseBanner","async":false} +d:I{"id":86150,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"","async":false} +e:I{"id":65717,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","4691:static/chunks/4691-3ed340b4558906b1.js","340:static/chunks/340-8b69198c1e94e4f2.js","4007:static/chunks/4007-80fb4e0338c67b28.js","2718:static/chunks/2718-d762fd3fac1c2178.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","7961:static/chunks/app/(dashboard)/settings/team/layout-0b7a68a98ff6c355.js"],"name":"TeamNav","async":false} +10:I{"id":67220,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","240:static/chunks/240-6e4dd6e33fcf5a86.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","3179:static/chunks/3179-72a03be258e531a2.js","1002:static/chunks/1002-09e560a83c07e9f4.js","1460:static/chunks/1460-849850f3e0fcced7.js","6805:static/chunks/6805-48f0b1146fe740f9.js","4546:static/chunks/4546-05756522a4929864.js","1889:static/chunks/1889-808f5c268ba4d728.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","4496:static/chunks/4496-17761fa90faf34fd.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","1760:static/chunks/app/(dashboard)/settings/team/groups/page-52d201da16a5f9af.js"],"name":"","async":false} +11:I{"id":80629,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Toaster","async":false} +6:[["$","meta","0",{"charSet":"utf-8"}],["$","title","1",{"children":"Tabby - Groups"}],["$","meta","2",{"name":"description","content":"Tabby, an opensource, self-hosted AI coding assistant."}],["$","meta","3",{"name":"theme-color","media":"(prefers-color-scheme: light)","content":"white"}],["$","meta","4",{"name":"theme-color","media":"(prefers-color-scheme: dark)","content":"black"}],["$","meta","5",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","link","6",{"rel":"icon","href":"/favicon.ico"}],["$","meta","7",{"name":"next-size-adjust"}]] +5:[null,["$","html",null,{"lang":"en","suppressHydrationWarning":true,"children":[["$","head",null,{}],["$","body",null,{"className":"bg-transparent font-sans antialiased __variable_d65c78 __variable_3c557b __variable_f44606","children":["$","$L7",null,{"attribute":"class","defaultTheme":"system","enableSystem":true,"children":[["$","$L8",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":["$","$Lb",null,{}],"notFoundStyles":[],"childProp":{"current":[null,[["$","$Lc",null,{}],["$","$Ld",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":[null,[["$","$Le",null,{}],["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","team","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","settings","children","team","children","groups","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$Lf",["$","$L10",null,{}],null],"segment":"__PAGE__"},"styles":[]}],"segment":"groups"},"styles":[]}]],null],"segment":"team"},"styles":[]}],"segment":"settings"},"styles":[]}]}]],null],"segment":"(dashboard)"},"styles":[]}]}],["$","$L11",null,{"richColors":true,"closeButton":true}],null]}]}]]}],null] +f:null diff --git a/ee/tabby-webserver/ui/system.html b/ee/tabby-webserver/ui/system.html new file mode 100644 index 000000000000..4c528bb741fc --- /dev/null +++ b/ee/tabby-webserver/ui/system.html @@ -0,0 +1 @@ +Tabby - System \ No newline at end of file diff --git a/ee/tabby-webserver/ui/system.txt b/ee/tabby-webserver/ui/system.txt new file mode 100644 index 000000000000..d5d255bdedeb --- /dev/null +++ b/ee/tabby-webserver/ui/system.txt @@ -0,0 +1,17 @@ +1:HL["/_next/static/media/a34f9d1faa5f3315-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +2:HL["/_next/static/media/bb3ef058b751a6ad-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +3:HL["/_next/static/media/f75d6d02e2924b13-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +4:HL["/_next/static/css/1df4303b470c4ade.css","style"] +0:["6THjZwODOtNUmFQ4Gzp5t",[[["",{"children":["(dashboard)",{"children":["system",{"children":["__PAGE__",{}]}]}]},"$undefined","$undefined",true],"$L5",[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/1df4303b470c4ade.css","precedence":"next"}]],"$L6"]]]] +7:I{"id":35590,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Providers","async":false} +8:I{"id":32191,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"","async":false} +9:I{"id":32892,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +a:I{"id":95814,"chunks":["2272:static/chunks/webpack-2a37924b5137759d.js","3375:static/chunks/21c8b353-925bb40fc9c4670c.js","5289:static/chunks/5289-682125a3379532f6.js"],"name":"","async":false} +b:I{"id":69579,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","4546:static/chunks/4546-05756522a4929864.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","1544:static/chunks/1544-f96cdf5bcfa8f0e4.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","9160:static/chunks/app/not-found-8fdd48e65bd803b1.js"],"name":"","async":false} +c:I{"id":90155,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"LicenseBanner","async":false} +d:I{"id":86150,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","1002:static/chunks/1002-09e560a83c07e9f4.js","4546:static/chunks/4546-05756522a4929864.js","6065:static/chunks/6065-17553e3578c58f7b.js","9275:static/chunks/9275-6df9aca1b01ee03d.js","5605:static/chunks/5605-b7e57fceb5667e91.js","3721:static/chunks/3721-353be68b8fa07c6b.js","7003:static/chunks/7003-05c7a4c5b4e5989f.js","6968:static/chunks/6968-4ea1bbf8c29f201a.js","6338:static/chunks/6338-49e7c4b55ba8981d.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","6978:static/chunks/6978-91523ca6231ea70b.js","9634:static/chunks/9634-eb02e61df3d21583.js","5642:static/chunks/app/(dashboard)/layout-de28a1a7a97aea73.js"],"name":"","async":false} +f:I{"id":27610,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","8511:static/chunks/8511-ad410fc9ccc47ec5.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","3179:static/chunks/3179-72a03be258e531a2.js","7070:static/chunks/7070-0361b26f2c124ca9.js","1889:static/chunks/1889-808f5c268ba4d728.js","9153:static/chunks/9153-56303a38b361663f.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","2666:static/chunks/app/(dashboard)/system/page-6326d40f0b98eb81.js"],"name":"","async":false} +10:I{"id":80629,"chunks":["7565:static/chunks/3c5aa50f-8b4f1f89b347e3ff.js","8415:static/chunks/9e33a154-31f26770480dd557.js","4691:static/chunks/4691-3ed340b4558906b1.js","55:static/chunks/55-55623c01484a5724.js","436:static/chunks/436-401a51e7af01c90f.js","340:static/chunks/340-8b69198c1e94e4f2.js","9906:static/chunks/9906-125e9641f98e1ae5.js","4007:static/chunks/4007-80fb4e0338c67b28.js","1283:static/chunks/1283-393050764b70ab39.js","3087:static/chunks/3087-090080c04c344643.js","2429:static/chunks/2429-f7966c8cf910a416.js","1454:static/chunks/1454-fd65c91d069bd7ed.js","4421:static/chunks/4421-0da05343b4d5e17a.js","9218:static/chunks/9218-32f27de639e19ba1.js","3492:static/chunks/3492-7ca10e8ef6aab493.js","1565:static/chunks/1565-6d9dbbf6faebff31.js","410:static/chunks/410-2d9f0445fa0cca52.js","3396:static/chunks/3396-2ca6ad3ee124641e.js","3185:static/chunks/app/layout-a9dfde7e4294922d.js"],"name":"Toaster","async":false} +6:[["$","meta","0",{"charSet":"utf-8"}],["$","title","1",{"children":"Tabby - System"}],["$","meta","2",{"name":"description","content":"Tabby, an opensource, self-hosted AI coding assistant."}],["$","meta","3",{"name":"theme-color","media":"(prefers-color-scheme: light)","content":"white"}],["$","meta","4",{"name":"theme-color","media":"(prefers-color-scheme: dark)","content":"black"}],["$","meta","5",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","link","6",{"rel":"icon","href":"/favicon.ico"}],["$","meta","7",{"name":"next-size-adjust"}]] +5:[null,["$","html",null,{"lang":"en","suppressHydrationWarning":true,"children":[["$","head",null,{}],["$","body",null,{"className":"bg-transparent font-sans antialiased __variable_d65c78 __variable_3c557b __variable_f44606","children":["$","$L7",null,{"attribute":"class","defaultTheme":"system","enableSystem":true,"children":[["$","$L8",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":["$","$Lb",null,{}],"notFoundStyles":[],"childProp":{"current":[null,[["$","$Lc",null,{}],["$","$Ld",null,{"children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","system","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$Le",["$","$Lf",null,{}],null],"segment":"__PAGE__"},"styles":[]}],"segment":"system"},"styles":[]}]}]],null],"segment":"(dashboard)"},"styles":[]}]}],["$","$L10",null,{"richColors":true,"closeButton":true}],null]}]}]]}],null] +e:null diff --git a/ee/tabby-webserver/ui/team.html b/ee/tabby-webserver/ui/team.html deleted file mode 100644 index c0f16d5da57e..000000000000 --- a/ee/tabby-webserver/ui/team.html +++ /dev/null @@ -1 +0,0 @@ -Tabby - Team Management

    Pending Invites

    Members

    \ No newline at end of file diff --git a/ee/tabby-webserver/ui/team.txt b/ee/tabby-webserver/ui/team.txt deleted file mode 100644 index 5a9957f434a4..000000000000 --- a/ee/tabby-webserver/ui/team.txt +++ /dev/null @@ -1,15 +0,0 @@ -1:HL["/_next/static/media/86fdec36ddd9097e-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] -2:HL["/_next/static/media/c9a5bc6a7c948fb0-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] -3:HL["/_next/static/css/99bd59844a757a8f.css","style"] -0:["tMrzerf0188ZcKSR-xbdl",[[["",{"children":["(dashboard)",{"children":["team",{"children":["__PAGE__",{}]}]}]},"$undefined","$undefined",true],"$L4",[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/99bd59844a757a8f.css","precedence":"next"}]],"$L5"]]]] -6:I{"id":42761,"chunks":["5328:static/chunks/5328-77e3e9e48a9bea9e.js","1424:static/chunks/1424-78cca59938e343eb.js","1629:static/chunks/1629-85e5cded21f77d75.js","9503:static/chunks/9503-5e587cc77d308fb5.js","5224:static/chunks/5224-49d6d8b64810cc3e.js","7820:static/chunks/7820-ceb91098cbc2ba2b.js","3185:static/chunks/app/layout-a55e9467d63125e4.js"],"name":"Toaster","async":false} -7:I{"id":78495,"chunks":["5328:static/chunks/5328-77e3e9e48a9bea9e.js","1424:static/chunks/1424-78cca59938e343eb.js","1629:static/chunks/1629-85e5cded21f77d75.js","9503:static/chunks/9503-5e587cc77d308fb5.js","5224:static/chunks/5224-49d6d8b64810cc3e.js","7820:static/chunks/7820-ceb91098cbc2ba2b.js","3185:static/chunks/app/layout-a55e9467d63125e4.js"],"name":"Providers","async":false} -8:I{"id":81443,"chunks":["2272:static/chunks/webpack-d1a565a4b87ec7af.js","2971:static/chunks/fd9d1056-a5449620cb20bbc5.js","7864:static/chunks/7864-e09e568e91e5ed29.js"],"name":"","async":false} -9:I{"id":18639,"chunks":["2272:static/chunks/webpack-d1a565a4b87ec7af.js","2971:static/chunks/fd9d1056-a5449620cb20bbc5.js","7864:static/chunks/7864-e09e568e91e5ed29.js"],"name":"","async":false} -a:I{"id":67854,"chunks":["5328:static/chunks/5328-77e3e9e48a9bea9e.js","2047:static/chunks/2047-2ade212d24811181.js","4012:static/chunks/4012-44f2b4af6b9fad01.js","1629:static/chunks/1629-85e5cded21f77d75.js","9503:static/chunks/9503-5e587cc77d308fb5.js","5479:static/chunks/5479-8336a6ddd5158337.js","1621:static/chunks/1621-8722ce7f0a9da221.js","6655:static/chunks/6655-15f6a1edc5fe710f.js","7820:static/chunks/7820-ceb91098cbc2ba2b.js","1894:static/chunks/1894-3767909c2d941ab2.js","5642:static/chunks/app/(dashboard)/layout-76d6887e34bd6ff1.js"],"name":"","async":false} -b:I{"id":18851,"chunks":["5328:static/chunks/5328-77e3e9e48a9bea9e.js","2047:static/chunks/2047-2ade212d24811181.js","4012:static/chunks/4012-44f2b4af6b9fad01.js","1629:static/chunks/1629-85e5cded21f77d75.js","9503:static/chunks/9503-5e587cc77d308fb5.js","5479:static/chunks/5479-8336a6ddd5158337.js","1621:static/chunks/1621-8722ce7f0a9da221.js","6655:static/chunks/6655-15f6a1edc5fe710f.js","7820:static/chunks/7820-ceb91098cbc2ba2b.js","1894:static/chunks/1894-3767909c2d941ab2.js","5642:static/chunks/app/(dashboard)/layout-76d6887e34bd6ff1.js"],"name":"ScrollArea","async":false} -c:I{"id":81496,"chunks":["5328:static/chunks/5328-77e3e9e48a9bea9e.js","2047:static/chunks/2047-2ade212d24811181.js","4012:static/chunks/4012-44f2b4af6b9fad01.js","1629:static/chunks/1629-85e5cded21f77d75.js","9503:static/chunks/9503-5e587cc77d308fb5.js","5479:static/chunks/5479-8336a6ddd5158337.js","1621:static/chunks/1621-8722ce7f0a9da221.js","6655:static/chunks/6655-15f6a1edc5fe710f.js","7820:static/chunks/7820-ceb91098cbc2ba2b.js","1894:static/chunks/1894-3767909c2d941ab2.js","5642:static/chunks/app/(dashboard)/layout-76d6887e34bd6ff1.js"],"name":"Header","async":false} -e:I{"id":84532,"chunks":["6990:static/chunks/13b76428-f0fe9fe157a3353a.js","5328:static/chunks/5328-77e3e9e48a9bea9e.js","1424:static/chunks/1424-78cca59938e343eb.js","5414:static/chunks/5414-06b80b739a31c06e.js","7820:static/chunks/7820-ceb91098cbc2ba2b.js","1894:static/chunks/1894-3767909c2d941ab2.js","339:static/chunks/app/(dashboard)/team/page-273dd73d6c8072a3.js"],"name":"","async":false} -5:[["$","meta","0",{"charSet":"utf-8"}],["$","title","1",{"children":"Tabby - Team Management"}],["$","meta","2",{"name":"description","content":"Tabby, an opensource, self-hosted AI coding assistant."}],["$","meta","3",{"name":"theme-color","media":"(prefers-color-scheme: light)","content":"white"}],["$","meta","4",{"name":"theme-color","media":"(prefers-color-scheme: dark)","content":"black"}],["$","meta","5",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","6",{"name":"next-size-adjust"}]] -4:[null,["$","html",null,{"lang":"en","suppressHydrationWarning":true,"children":[["$","head",null,{}],["$","body",null,{"className":"font-sans antialiased __variable_e66fe9 __variable_bd9c35","children":[["$","$L6",null,{"richColors":true}],["$","$L7",null,{"attribute":"class","defaultTheme":"system","enableSystem":true,"children":[["$","div",null,{"className":"flex min-h-screen flex-col","children":["$","$L8",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$L9",null,{}],"templateStyles":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[],"childProp":{"current":[null,["$","main",null,{"className":"flex flex-1 flex-col","children":["$","$La",null,{"className":"flex-1","children":["$","$Lb",null,{"className":"max-h-[100vh]","children":[["$","$Lc",null,{}],["$","div",null,{"className":"p-4","children":["$","$L8",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$L9",null,{}],"templateStyles":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[],"childProp":{"current":["$","$L8",null,{"parallelRouterKey":"children","segmentPath":["children","(dashboard)","children","team","children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$L9",null,{}],"templateStyles":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined","childProp":{"current":["$Ld",["$","$Le",null,{}],null],"segment":"__PAGE__"},"styles":[]}],"segment":"team"},"styles":[]}]}]]}]}]}],null],"segment":"(dashboard)"},"styles":[]}]}],null]}]]}]]}],null] -d:null diff --git a/experimental/model-converter/update-llama-model.sh b/experimental/model-converter/update-llama-model.sh index d33c22665031..a55882c0ccea 100755 --- a/experimental/model-converter/update-llama-model.sh +++ b/experimental/model-converter/update-llama-model.sh @@ -13,7 +13,7 @@ if [ -z "${ACCESS_TOKEN}" ]; then fi prepare_llama_cpp() { - git clone https://github.com/ggerganov/llama.cpp.git + git clone https://github.com/ggml-org/llama.cpp.git pushd llama.cpp git checkout 6961c4bd0b5176e10ab03b35394f1e9eab761792 diff --git a/package.json b/package.json index b7e93fc5ca25..b63299c27ce4 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,17 @@ { "private": true, - "workspaces": [ - "clients/tabby-agent", - "clients/vscode", - "clients/vim", - "clients/intellij", - "clients/example-vscode-lsp" - ], + "scripts": { + "build": "turbo build", + "vscode:dev": "turbo vscode:dev", + "lint": "turbo lint", + "lint:fix": "turbo lint:fix", + "test": "turbo test" + }, "engines": { - "node": ">=18" + "node": ">=18", + "pnpm": ">=9" + }, + "devDependencies": { + "turbo": "^1.13.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 000000000000..913b2950b8b3 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,24260 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + turbo: + specifier: ^1.13.3 + version: 1.13.3 + + clients/eclipse: + devDependencies: + fs-extra: + specifier: ^11.1.1 + version: 11.2.0 + tabby-agent: + specifier: workspace:* + version: link:../tabby-agent + tabby-chat-panel: + specifier: workspace:* + version: link:../tabby-chat-panel + + clients/intellij: + devDependencies: + tabby-agent: + specifier: workspace:* + version: link:../tabby-agent + tabby-chat-panel: + specifier: workspace:* + version: link:../tabby-chat-panel + + clients/tabby-agent: + devDependencies: + '@orama/orama': + specifier: ^2.0.18 + version: 2.0.18 + '@types/chai': + specifier: ^4.3.5 + version: 4.3.16 + '@types/dedent': + specifier: ^0.7.2 + version: 0.7.2 + '@types/deep-equal': + specifier: ^1.0.4 + version: 1.0.4 + '@types/diff': + specifier: ^5.2.1 + version: 5.2.1 + '@types/fast-levenshtein': + specifier: ^0.0.4 + version: 0.0.4 + '@types/fs-extra': + specifier: ^11.0.1 + version: 11.0.4 + '@types/glob': + specifier: ^7.2.0 + version: 7.2.0 + '@types/js-levenshtein': + specifier: ^1.1.3 + version: 1.1.3 + '@types/mocha': + specifier: ^10.0.1 + version: 10.0.6 + '@types/node': + specifier: 18.x + version: 18.19.33 + '@types/object-hash': + specifier: ^3.0.0 + version: 3.0.6 + '@types/readable-stream': + specifier: ^4.0.14 + version: 4.0.14 + '@types/semver': + specifier: ^7.5.8 + version: 7.5.8 + '@types/uuid': + specifier: ^9.0.0 + version: 9.0.8 + '@types/win-ca': + specifier: ^3.5.4 + version: 3.5.4 + '@typescript-eslint/eslint-plugin': + specifier: ^6.13.1 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3) + '@typescript-eslint/parser': + specifier: ^6.13.1 + version: 6.21.0(eslint@8.57.0)(typescript@5.3.3) + chai: + specifier: ^4.3.7 + version: 4.4.1 + chokidar: + specifier: ^3.5.3 + version: 3.6.0 + codiff: + specifier: ^0.1.2 + version: 0.1.2 + crypto-random-string: + specifier: ^5.0.0 + version: 5.0.0 + dedent: + specifier: ^0.7.0 + version: 0.7.0 + deep-equal: + specifier: ^2.2.1 + version: 2.2.3 + deepmerge-ts: + specifier: ^5.1.0 + version: 5.1.0 + diff: + specifier: ^5.2.0 + version: 5.2.0 + dot-prop: + specifier: ^8.0.2 + version: 8.0.2 + esbuild: + specifier: ^0.19.12 + version: 0.19.12 + esbuild-plugin-polyfill-node: + specifier: ^0.3.0 + version: 0.3.0(esbuild@0.19.12) + eslint: + specifier: ^8.55.0 + version: 8.57.0 + eslint-config-prettier: + specifier: ^9.0.0 + version: 9.1.0(eslint@8.57.0) + eventsource-parser: + specifier: ^1.1.2 + version: 1.1.2 + fast-levenshtein: + specifier: ^3.0.0 + version: 3.0.0 + file-stream-rotator: + specifier: ^1.0.0 + version: 1.0.0 + fs-extra: + specifier: ^11.1.1 + version: 11.2.0 + glob: + specifier: ^7.2.0 + version: 7.2.3 + js-levenshtein: + specifier: ^1.1.6 + version: 1.1.6 + jwt-decode: + specifier: ^3.1.2 + version: 3.1.2 + lru-cache: + specifier: ^9.1.1 + version: 9.1.2 + mac-ca: + specifier: ^2.0.3 + version: 2.0.3 + mocha: + specifier: ^10.2.0 + version: 10.4.0 + object-hash: + specifier: ^3.0.0 + version: 3.0.0 + openapi-fetch: + specifier: ^0.7.6 + version: 0.7.10 + pino: + specifier: ^8.14.1 + version: 8.21.0 + prettier: + specifier: ^3.0.0 + version: 3.2.5 + readable-from-web: + specifier: ^1.0.0 + version: 1.0.0 + readable-stream: + specifier: ^4.5.2 + version: 4.5.2 + semver: + specifier: ^7.6.0 + version: 7.6.2 + stats-logscale: + specifier: ^1.0.9 + version: 1.0.9 + tabby-openapi: + specifier: workspace:* + version: link:../tabby-openapi + toml: + specifier: ^3.0.0 + version: 3.0.0 + ts-node: + specifier: ^10.9.1 + version: 10.9.2(@swc/core@1.3.101(@swc/helpers@0.5.2))(@types/node@18.19.33)(typescript@5.3.3) + tsc-watch: + specifier: ^6.2.0 + version: 6.2.0(typescript@5.3.3) + tsup: + specifier: ^8.0.2 + version: 8.0.2(@swc/core@1.3.101(@swc/helpers@0.5.2))(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.3.101(@swc/helpers@0.5.2))(@types/node@18.19.33)(typescript@5.3.3))(typescript@5.3.3) + typescript: + specifier: ^5.3.2 + version: 5.3.3 + undici: + specifier: ^6.19.2 + version: 6.19.2 + uri-js: + specifier: ^4.4.1 + version: 4.4.1 + uuid: + specifier: ^9.0.0 + version: 9.0.1 + vscode-languageserver: + specifier: ^9.0.1 + version: 9.0.1 + vscode-languageserver-protocol: + specifier: ^3.17.5 + version: 3.17.5 + vscode-languageserver-textdocument: + specifier: ^1.0.11 + version: 1.0.11 + web-tree-sitter: + specifier: ^0.20.8 + version: 0.20.8 + win-ca: + specifier: ^3.5.1 + version: 3.5.1 + + clients/tabby-chat-panel: + dependencies: + react: + specifier: 18.2.0 + version: 18.2.0 + semver: + specifier: ^7.6.0 + version: 7.6.2 + tabby-threads: + specifier: workspace:* + version: link:../tabby-threads + devDependencies: + '@antfu/eslint-config': + specifier: ^2.16.0 + version: 2.18.1(@vue/compiler-sfc@3.4.27)(eslint-plugin-react-hooks@4.6.0(eslint@9.3.0))(eslint@9.3.0)(svelte@4.2.17)(typescript@5.4.5)(vitest@1.6.0(@types/node@20.12.12)(terser@5.31.0)) + '@antfu/ni': + specifier: ^0.21.12 + version: 0.21.12 + '@rollup/plugin-commonjs': + specifier: ^28.0.3 + version: 28.0.3(rollup@4.36.0) + '@rollup/plugin-node-resolve': + specifier: ^16.0.0 + version: 16.0.0(rollup@4.36.0) + '@rollup/plugin-terser': + specifier: ^0.4.4 + version: 0.4.4(rollup@4.36.0) + '@rollup/plugin-typescript': + specifier: ^12.1.1 + version: 12.1.1(rollup@4.36.0)(tslib@2.6.2)(typescript@5.4.5) + '@types/node': + specifier: ^20.12.7 + version: 20.12.12 + '@types/react': + specifier: 18.2.23 + version: 18.2.23 + '@types/semver': + specifier: ^7.5.8 + version: 7.5.8 + bumpp: + specifier: ^9.4.0 + version: 9.4.1 + eslint: + specifier: ^9.1.1 + version: 9.3.0 + eslint-formatter-mo: + specifier: ^1.2.0 + version: 1.2.0 + lint-staged: + specifier: ^15.2.2 + version: 15.2.4 + rimraf: + specifier: ^5.0.5 + version: 5.0.7 + rollup: + specifier: ^4.27.3 + version: 4.36.0 + tsx: + specifier: ^4.7.3 + version: 4.10.5 + typescript: + specifier: ^5.4.5 + version: 5.4.5 + unbuild: + specifier: ^2.0.0 + version: 2.0.0(typescript@5.4.5) + vitest: + specifier: ^1.5.2 + version: 1.6.0(@types/node@20.12.12)(terser@5.31.0) + + clients/tabby-openapi: + devDependencies: + openai: + specifier: ^4.52.7 + version: 4.52.7(encoding@0.1.13) + openapi-typescript: + specifier: ^7.0.2 + version: 7.0.2(encoding@0.1.13)(typescript@5.4.5) + typescript: + specifier: ^5.3.2 + version: 5.4.5 + + clients/tabby-threads: + dependencies: + '@quilted/events': + specifier: 2.0.0 + version: 2.0.0 + '@quilted/signals': + specifier: 0.2.1 + version: 0.2.1 + devDependencies: + '@rollup/plugin-typescript': + specifier: ^12.1.1 + version: 12.1.1(rollup@4.27.4)(tslib@2.6.2)(typescript@5.4.5) + rollup: + specifier: ^4.27.3 + version: 4.27.4 + typescript: + specifier: ^5.3.3 + version: 5.4.5 + + clients/vscode: + devDependencies: + '@types/chai': + specifier: ^4.3.5 + version: 4.3.16 + '@types/dedent': + specifier: ^0.7.2 + version: 0.7.2 + '@types/deep-equal': + specifier: ^1.0.4 + version: 1.0.4 + '@types/diff': + specifier: ^5.2.1 + version: 5.2.1 + '@types/fs-extra': + specifier: ^11.0.1 + version: 11.0.4 + '@types/mocha': + specifier: ^10.0.1 + version: 10.0.6 + '@types/node': + specifier: 18.x + version: 18.19.33 + '@types/object-hash': + specifier: ^3.0.0 + version: 3.0.6 + '@types/semver': + specifier: ^7.5.8 + version: 7.5.8 + '@types/uuid': + specifier: ^9.0.0 + version: 9.0.8 + '@types/vscode': + specifier: ^1.82.0 + version: 1.89.0 + '@typescript-eslint/eslint-plugin': + specifier: ^6.13.1 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/parser': + specifier: ^6.13.1 + version: 6.21.0(eslint@8.57.0)(typescript@5.4.5) + '@vscode/test-electron': + specifier: ^2.1.5 + version: 2.3.10 + '@vscode/test-web': + specifier: ^0.0.63 + version: 0.0.63 + '@vscode/vsce': + specifier: ^2.15.0 + version: 2.26.1 + assert: + specifier: ^2.0.0 + version: 2.1.0 + chai: + specifier: ^4.3.11 + version: 4.4.1 + debounce: + specifier: ^2.2.0 + version: 2.2.0 + dedent: + specifier: ^0.7.0 + version: 0.7.0 + deep-equal: + specifier: ^2.2.1 + version: 2.2.3 + diff: + specifier: ^5.2.0 + version: 5.2.0 + esbuild-plugin-copy: + specifier: ^2.1.1 + version: 2.1.1(esbuild@0.24.2) + esbuild-plugin-polyfill-node: + specifier: ^0.3.0 + version: 0.3.0(esbuild@0.24.2) + eslint: + specifier: ^8.55.0 + version: 8.57.0 + eslint-config-prettier: + specifier: ^9.0.0 + version: 9.1.0(eslint@8.57.0) + fs-extra: + specifier: ^11.1.1 + version: 11.2.0 + get-installed-path: + specifier: ^4.0.8 + version: 4.0.8 + mocha: + specifier: ^10.2.0 + version: 10.4.0 + object-hash: + specifier: ^3.0.0 + version: 3.0.0 + ovsx: + specifier: ^0.9.5 + version: 0.9.5 + prettier: + specifier: ^3.0.0 + version: 3.2.5 + semver: + specifier: ^7.6.0 + version: 7.6.2 + tabby-agent: + specifier: workspace:* + version: link:../tabby-agent + tabby-chat-panel: + specifier: workspace:* + version: link:../tabby-chat-panel + tabby-threads: + specifier: workspace:* + version: link:../tabby-threads + tsc-watch: + specifier: ^6.2.0 + version: 6.2.0(typescript@5.4.5) + tsup: + specifier: ^8.0.2 + version: 8.0.2(@swc/core@1.3.101(@swc/helpers@0.5.2))(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.3.101(@swc/helpers@0.5.2))(@types/node@18.19.33)(typescript@5.4.5))(typescript@5.4.5) + typescript: + specifier: ^5.3.2 + version: 5.4.5 + uuid: + specifier: ^9.0.0 + version: 9.0.1 + vscode-languageclient: + specifier: ^9.0.1 + version: 9.0.1 + + ee/tabby-email: + devDependencies: + '@react-email/components': + specifier: 0.0.18 + version: 0.0.18(@types/react@18.2.23)(react@18.2.0) + '@react-email/tailwind': + specifier: 0.0.17 + version: 0.0.17(react@18.2.0) + react: + specifier: 18.2.0 + version: 18.2.0 + react-email: + specifier: 2.1.3 + version: 2.1.3(@swc/helpers@0.5.2)(eslint@9.3.0)(ts-node@10.9.2(@swc/core@1.3.101(@swc/helpers@0.5.2))(@types/node@20.12.12)(typescript@5.8.2)) + + ee/tabby-ui: + dependencies: + '@codemirror/language': + specifier: ^6.10.1 + version: 6.10.1 + '@codemirror/search': + specifier: ^6.5.5 + version: 6.5.5 + '@codemirror/state': + specifier: ^6.4.0 + version: 6.4.0 + '@codemirror/theme-one-dark': + specifier: ^6.1.2 + version: 6.1.2 + '@codemirror/view': + specifier: ^6.23.0 + version: 6.23.0 + '@curvenote/ansi-to-react': + specifier: ^7.0.0 + version: 7.0.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@hookform/resolvers': + specifier: ^3.3.2 + version: 3.3.2(react-hook-form@7.48.2(react@18.2.0)) + '@radix-ui/react-accordion': + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-alert-dialog': + specifier: 1.0.4 + version: 1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-avatar': + specifier: ^1.0.4 + version: 1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-checkbox': + specifier: ^1.0.4 + version: 1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-collapsible': + specifier: ^1.0.3 + version: 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-dialog': + specifier: ^1.0.5 + version: 1.0.5(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-dropdown-menu': + specifier: ^2.0.6 + version: 2.0.6(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-hover-card': + specifier: ^1.0.7 + version: 1.0.7(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-icons': + specifier: ^1.3.0 + version: 1.3.0(react@18.2.0) + '@radix-ui/react-label': + specifier: ^2.0.2 + version: 2.0.2(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-popover': + specifier: ^1.0.7 + version: 1.0.7(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-radio-group': + specifier: ^1.1.3 + version: 1.1.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-scroll-area': + specifier: ^1.0.5 + version: 1.0.5(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-select': + specifier: ^1.2.2 + version: 1.2.2(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-separator': + specifier: ^1.0.3 + version: 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-slot': + specifier: ^1.0.2 + version: 1.0.2(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-switch': + specifier: ^1.0.3 + version: 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-tabs': + specifier: ^1.0.4 + version: 1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-toggle': + specifier: ^1.1.0 + version: 1.1.0(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-tooltip': + specifier: ^1.0.7 + version: 1.0.7(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@sindresorhus/slugify': + specifier: ^2.2.1 + version: 2.2.1 + '@tiptap/core': + specifier: ^2.6.6 + version: 2.10.4(@tiptap/pm@2.10.4) + '@tiptap/extension-document': + specifier: ^2.10.4 + version: 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4)) + '@tiptap/extension-mention': + specifier: ^2.10.4 + version: 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))(@tiptap/pm@2.10.4)(@tiptap/suggestion@2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))(@tiptap/pm@2.10.4)) + '@tiptap/extension-paragraph': + specifier: ^2.10.4 + version: 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4)) + '@tiptap/extension-placeholder': + specifier: ^2.10.4 + version: 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))(@tiptap/pm@2.10.4) + '@tiptap/extension-text': + specifier: ^2.10.4 + version: 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4)) + '@tiptap/pm': + specifier: ^2.6.6 + version: 2.10.4 + '@tiptap/react': + specifier: ^2.10.4 + version: 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))(@tiptap/pm@2.10.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@tiptap/starter-kit': + specifier: ^2.6.6 + version: 2.10.4 + '@tiptap/suggestion': + specifier: ^2.10.4 + version: 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))(@tiptap/pm@2.10.4) + '@types/mdast': + specifier: ^4.0.4 + version: 4.0.4 + '@types/unist': + specifier: ^3.0.3 + version: 3.0.3 + '@uidotdev/usehooks': + specifier: ^2.4.1 + version: 2.4.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@uiw/codemirror-extensions-langs': + specifier: ^4.21.21 + version: 4.21.21(@codemirror/autocomplete@6.11.1(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.0))(@codemirror/language-data@6.3.1(@codemirror/view@6.23.0))(@codemirror/language@6.10.1)(@codemirror/legacy-modes@6.3.3)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.0)(@lezer/highlight@1.2.0)(@lezer/javascript@1.4.12)(@lezer/lr@1.3.14) + '@urql/core': + specifier: ^4.2.3 + version: 4.2.3(graphql@16.8.1) + '@urql/exchange-auth': + specifier: ^2.1.6 + version: 2.1.6(graphql@16.8.1) + '@urql/exchange-graphcache': + specifier: ^6.4.0 + version: 6.4.0(graphql@16.8.1) + '@vercel/analytics': + specifier: ^1.0.0 + version: 1.0.2 + '@vercel/kv': + specifier: ^0.2.1 + version: 0.2.3(encoding@0.1.13) + '@vercel/og': + specifier: ^0.5.7 + version: 0.5.17 + class-variance-authority: + specifier: ^0.4.0 + version: 0.4.0(typescript@5.8.2) + clsx: + specifier: ^1.2.1 + version: 1.2.1 + cm6-graphql: + specifier: ^0.0.15 + version: 0.0.15(@codemirror/autocomplete@6.11.1(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.0))(@codemirror/language@6.10.1)(@codemirror/lint@6.4.2)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/highlight@1.2.0)(graphql@16.8.1) + cmdk: + specifier: ^1.0.0 + version: 1.0.0(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + color: + specifier: ^4.2.3 + version: 4.2.3 + compare-versions: + specifier: ^6.1.0 + version: 6.1.0 + copy-to-clipboard: + specifier: ^3.3.3 + version: 3.3.3 + date-fns: + specifier: ^3.6.0 + version: 3.6.0 + dompurify: + specifier: ^3.1.5 + version: 3.1.5 + downshift: + specifier: ^8.2.2 + version: 8.2.2(react@18.2.0) + embla-carousel-autoplay: + specifier: ^8.5.2 + version: 8.5.2(embla-carousel@8.5.2) + embla-carousel-react: + specifier: ^8.5.2 + version: 8.5.2(react@18.2.0) + eventsource-parser: + specifier: ^1.1.2 + version: 1.1.2 + focus-trap-react: + specifier: ^10.1.1 + version: 10.2.2(prop-types@15.8.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + framer-motion: + specifier: ^11.11.7 + version: 11.11.9(@emotion/is-prop-valid@0.8.8)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + fuzzysort: + specifier: ^3.0.2 + version: 3.0.2 + git-url-parse: + specifier: ^16.0.0 + version: 16.0.0 + graphql: + specifier: ^16.8.1 + version: 16.8.1 + graphql-ws: + specifier: ^5.16.0 + version: 5.16.0(graphql@16.8.1) + hast: + specifier: ^1.0.0 + version: 1.0.0 + humanize-duration: + specifier: ^3.31.0 + version: 3.31.0 + jwt-decode: + specifier: ^4.0.0 + version: 4.0.0 + lodash-es: + specifier: ^4.17.21 + version: 4.17.21 + lucide-react: + specifier: ^0.365.0 + version: 0.365.0(react@18.2.0) + marked: + specifier: ^13.0.0 + version: 13.0.0 + mdast: + specifier: ^3.0.0 + version: 3.0.0 + mitt: + specifier: ^3.0.1 + version: 3.0.1 + moment: + specifier: ^2.29.4 + version: 2.29.4 + moment-timezone: + specifier: ^0.5.45 + version: 0.5.45 + nanoid: + specifier: ^4.0.2 + version: 4.0.2 + next: + specifier: ^13.4.7 + version: 13.5.3(@babel/core@7.23.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + next-themes: + specifier: ^0.2.1 + version: 0.2.1(next@13.5.3(@babel/core@7.23.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + numeral: + specifier: ^2.0.6 + version: 2.0.6 + openai-edge: + specifier: ^0.5.1 + version: 0.5.1 + posthog-js: + specifier: ^1.139.0 + version: 1.139.0 + pretty-bytes: + specifier: ^6.1.1 + version: 6.1.1 + react: + specifier: ^18.2.0 + version: 18.2.0 + react-activity-calendar: + specifier: ^2.2.8 + version: 2.2.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react-day-picker: + specifier: ^8.10.0 + version: 8.10.0(date-fns@3.6.0)(react@18.2.0) + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) + react-error-boundary: + specifier: ^4.0.13 + version: 4.0.13(react@18.2.0) + react-hook-form: + specifier: ^7.48.2 + version: 7.48.2(react@18.2.0) + react-intersection-observer: + specifier: ^9.4.4 + version: 9.5.2(react@18.2.0) + react-json-view: + specifier: ^1.21.3 + version: 1.21.3(@types/react@18.2.23)(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react-lazy-load: + specifier: ^4.0.1 + version: 4.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react-markdown: + specifier: ^8.0.7 + version: 8.0.7(@types/react@18.2.23)(react@18.2.0) + react-nice-avatar: + specifier: ^1.5.0 + version: 1.5.0(react@18.2.0) + react-resizable-panels: + specifier: ^1.0.7 + version: 1.0.7(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react-syntax-highlighter: + specifier: ^15.5.0 + version: 15.5.0(react@18.2.0) + react-textarea-autosize: + specifier: ^8.5.3 + version: 8.5.3(@types/react@18.2.23)(react@18.2.0) + react-topbar-progress-indicator: + specifier: ^4.1.1 + version: 4.1.1(react@18.2.0) + recharts: + specifier: ^2.12.4 + version: 2.12.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rehype-raw: + specifier: 6.1.1 + version: 6.1.1 + rehype-sanitize: + specifier: ^6.0.0 + version: 6.0.0 + remark: + specifier: ^15.0.1 + version: 15.0.1 + remark-gfm: + specifier: ^3.0.1 + version: 3.0.1 + remark-math: + specifier: ^5.1.1 + version: 5.1.1 + remark-stringify: + specifier: ^11.0.0 + version: 11.0.0 + seedrandom: + specifier: ^3.0.5 + version: 3.0.5 + sonner: + specifier: ^1.3.1 + version: 1.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + swr: + specifier: ^2.2.4 + version: 2.2.4(react@18.2.0) + tabby-chat-panel: + specifier: workspace:* + version: link:../../clients/tabby-chat-panel + tippy.js: + specifier: ^6.3.7 + version: 6.3.7 + unified: + specifier: ^11.0.5 + version: 11.0.5 + unist-util-visit: + specifier: ^5.0.0 + version: 5.0.0 + urql: + specifier: ^4.0.6 + version: 4.0.6(graphql@16.8.1)(react@18.2.0) + use-local-storage: + specifier: ^3.0.0 + version: 3.0.0(react@18.2.0) + wonka: + specifier: ^6.3.4 + version: 6.3.4 + zod: + specifier: ^3.22.4 + version: 3.22.4 + zustand: + specifier: ^4.4.6 + version: 4.4.6(@types/react@18.2.23)(react@18.2.0) + devDependencies: + '@graphql-codegen/cli': + specifier: 5.0.0 + version: 5.0.0(@parcel/watcher@2.3.0)(@types/node@17.0.45)(encoding@0.1.13)(graphql@16.8.1)(typescript@5.8.2) + '@graphql-codegen/client-preset': + specifier: 4.1.0 + version: 4.1.0(encoding@0.1.13)(graphql@16.8.1) + '@graphql-typed-document-node/core': + specifier: 3.2.0 + version: 3.2.0(graphql@16.8.1) + '@ianvs/prettier-plugin-sort-imports': + specifier: ^4.1.1 + version: 4.1.1(@vue/compiler-sfc@3.4.27)(prettier@2.8.8) + '@parcel/watcher': + specifier: ^2.3.0 + version: 2.3.0 + '@tailwindcss/typography': + specifier: ^0.5.9 + version: 0.5.10(tailwindcss@3.3.3(ts-node@10.9.2(@types/node@17.0.45)(typescript@5.8.2))) + '@types/color': + specifier: ^3.0.6 + version: 3.0.6 + '@types/dompurify': + specifier: ^3.0.5 + version: 3.0.5 + '@types/git-url-parse': + specifier: ^9.0.3 + version: 9.0.3 + '@types/hast': + specifier: ^3.0.4 + version: 3.0.4 + '@types/he': + specifier: ^1.2.3 + version: 1.2.3 + '@types/humanize-duration': + specifier: ^3.27.4 + version: 3.27.4 + '@types/lodash-es': + specifier: ^4.17.10 + version: 4.17.10 + '@types/node': + specifier: ^17.0.12 + version: 17.0.45 + '@types/numeral': + specifier: ^2.0.5 + version: 2.0.5 + '@types/react': + specifier: ^18.0.22 + version: 18.2.23 + '@types/react-dom': + specifier: ^18.0.7 + version: 18.2.8 + '@types/react-syntax-highlighter': + specifier: ^15.5.6 + version: 15.5.7 + '@types/seedrandom': + specifier: ^3.0.8 + version: 3.0.8 + '@typescript-eslint/eslint-plugin': + specifier: ^7.4.0 + version: 7.4.0(@typescript-eslint/parser@5.62.0(eslint@8.50.0)(typescript@5.8.2))(eslint@8.50.0)(typescript@5.8.2) + '@typescript-eslint/parser': + specifier: ^5.59.7 + version: 5.62.0(eslint@8.50.0)(typescript@5.8.2) + autoprefixer: + specifier: ^10.4.13 + version: 10.4.16(postcss@8.4.31) + encoding: + specifier: ^0.1.13 + version: 0.1.13 + eslint: + specifier: ^8.31.0 + version: 8.50.0 + eslint-config-next: + specifier: 13.4.7-canary.1 + version: 13.4.7-canary.1(eslint@8.50.0)(typescript@5.8.2) + eslint-config-prettier: + specifier: ^8.3.0 + version: 8.10.0(eslint@8.50.0) + eslint-plugin-tailwindcss: + specifier: ^3.12.0 + version: 3.13.0(tailwindcss@3.3.3(ts-node@10.9.2(@types/node@17.0.45)(typescript@5.8.2))) + eslint-plugin-unused-imports: + specifier: ^3.0.0 + version: 3.0.0(@typescript-eslint/eslint-plugin@7.4.0(@typescript-eslint/parser@5.62.0(eslint@8.50.0)(typescript@5.8.2))(eslint@8.50.0)(typescript@5.8.2))(eslint@8.50.0) + from: + specifier: ^0.1.7 + version: 0.1.7 + he: + specifier: ^1.2.0 + version: 1.2.0 + import: + specifier: ^0.0.6 + version: 0.0.6 + npm-run-all: + specifier: ^4.1.5 + version: 4.1.5 + postcss: + specifier: ^8.4.21 + version: 8.4.31 + prettier: + specifier: ^2.7.1 + version: 2.8.8 + tabby-openapi: + specifier: workspace:0.12.0-dev + version: link:../../clients/tabby-openapi + tailwind-merge: + specifier: ^1.12.0 + version: 1.14.0 + tailwindcss: + specifier: ^3.3.1 + version: 3.3.3(ts-node@10.9.2(@types/node@17.0.45)(typescript@5.8.2)) + tailwindcss-animate: + specifier: ^1.0.5 + version: 1.0.7(tailwindcss@3.3.3(ts-node@10.9.2(@types/node@17.0.45)(typescript@5.8.2))) + typescript: + specifier: ^5.6.3 + version: 5.8.2 + vitest: + specifier: ^1.5.2 + version: 1.6.0(@types/node@17.0.45)(terser@5.31.0) + +packages: + + '@0no-co/graphql.web@1.0.4': + resolution: {integrity: sha512-W3ezhHGfO0MS1PtGloaTpg0PbaT8aZSmmaerL7idtU5F7oCI+uu25k+MsMS31BVFlp4aMkHSrNRxiD72IlK8TA==} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 + peerDependenciesMeta: + graphql: + optional: true + + '@aashutoshrathi/word-wrap@1.2.6': + resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} + engines: {node: '>=0.10.0'} + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@ampproject/remapping@2.2.1': + resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} + engines: {node: '>=6.0.0'} + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@antfu/eslint-config@2.18.1': + resolution: {integrity: sha512-6LkzQa96SHt47ZCvAcLJbQLUXmcpl9wI+eo5OeyB2YhHbsUBX7ufT0r4x6fx6Ci2694HRNLl8wY42LUvwidduw==} + hasBin: true + peerDependencies: + '@eslint-react/eslint-plugin': ^1.5.8 + '@prettier/plugin-xml': ^3.4.1 + '@unocss/eslint-plugin': '>=0.50.0' + astro-eslint-parser: ^0.16.3 + eslint: '>=8.40.0' + eslint-plugin-astro: ^0.31.4 + eslint-plugin-format: '>=0.1.0' + eslint-plugin-react-hooks: ^4.6.0 + eslint-plugin-react-refresh: ^0.4.4 + eslint-plugin-solid: ^0.13.2 + eslint-plugin-svelte: '>=2.35.1' + prettier-plugin-astro: ^0.13.0 + prettier-plugin-slidev: ^1.0.5 + svelte-eslint-parser: ^0.33.1 + peerDependenciesMeta: + '@eslint-react/eslint-plugin': + optional: true + '@prettier/plugin-xml': + optional: true + '@unocss/eslint-plugin': + optional: true + astro-eslint-parser: + optional: true + eslint-plugin-astro: + optional: true + eslint-plugin-format: + optional: true + eslint-plugin-react-hooks: + optional: true + eslint-plugin-react-refresh: + optional: true + eslint-plugin-solid: + optional: true + eslint-plugin-svelte: + optional: true + prettier-plugin-astro: + optional: true + prettier-plugin-slidev: + optional: true + svelte-eslint-parser: + optional: true + + '@antfu/install-pkg@0.3.3': + resolution: {integrity: sha512-nHHsk3NXQ6xkCfiRRC8Nfrg8pU5kkr3P3Y9s9dKqiuRmBD0Yap7fymNDjGFKeWhZQHqqbCS5CfeMy9wtExM24w==} + + '@antfu/ni@0.21.12': + resolution: {integrity: sha512-2aDL3WUv8hMJb2L3r/PIQWsTLyq7RQr3v9xD16fiz6O8ys1xEyLhhTOv8gxtZvJiTzjTF5pHoArvRdesGL1DMQ==} + hasBin: true + + '@antfu/utils@0.7.8': + resolution: {integrity: sha512-rWQkqXRESdjXtc+7NRfK9lASQjpXJu1ayp7qi1d23zZorY+wBHVLHHoVcMsEnkqEBWTFqbztO7/QdJFzyEcLTg==} + + '@ardatan/relay-compiler@12.0.0': + resolution: {integrity: sha512-9anThAaj1dQr6IGmzBMcfzOQKTa5artjuPmw8NYK/fiGEMjADbSguBY2FMDykt+QhilR3wc9VA/3yVju7JHg7Q==} + hasBin: true + peerDependencies: + graphql: '*' + + '@ardatan/sync-fetch@0.0.1': + resolution: {integrity: sha512-xhlTqH0m31mnsG0tIP4ETgfSB6gXDaYYsUWTrlUV93fFQPI9dd8hE0Ot6MHLCtqgB32hwJAC3YZMWlXZw7AleA==} + engines: {node: '>=14'} + + '@azure/abort-controller@1.1.0': + resolution: {integrity: sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==} + engines: {node: '>=12.0.0'} + + '@azure/abort-controller@2.1.2': + resolution: {integrity: sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==} + engines: {node: '>=18.0.0'} + + '@azure/core-auth@1.7.2': + resolution: {integrity: sha512-Igm/S3fDYmnMq1uKS38Ae1/m37B3zigdlZw+kocwEhh5GjyKjPrXKO2J6rzpC1wAxrNil/jX9BJRqBshyjnF3g==} + engines: {node: '>=18.0.0'} + + '@azure/core-client@1.9.2': + resolution: {integrity: sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w==} + engines: {node: '>=18.0.0'} + + '@azure/core-rest-pipeline@1.16.0': + resolution: {integrity: sha512-CeuTvsXxCUmEuxH5g/aceuSl6w2EugvNHKAtKKVdiX915EjJJxAwfzNNWZreNnbxHZ2fi0zaM6wwS23x2JVqSQ==} + engines: {node: '>=18.0.0'} + + '@azure/core-tracing@1.1.2': + resolution: {integrity: sha512-dawW9ifvWAWmUm9/h+/UQ2jrdvjCJ7VJEuCJ6XVNudzcOwm53BFZH4Q845vjfgoUAM8ZxokvVNxNxAITc502YA==} + engines: {node: '>=18.0.0'} + + '@azure/core-util@1.9.0': + resolution: {integrity: sha512-AfalUQ1ZppaKuxPPMsFEUdX6GZPB3d9paR9d/TTL7Ow2De8cJaC7ibi7kWVlFAVPCYo31OcnGymc0R89DX8Oaw==} + engines: {node: '>=18.0.0'} + + '@azure/identity@4.2.0': + resolution: {integrity: sha512-ve3aYv79qXOJ8wRxQ5jO0eIz2DZ4o0TyME4m4vlGV5YyePddVZ+pFMzusAMODNAflYAAv1cBIhKnd4xytmXyig==} + engines: {node: '>=18.0.0'} + + '@azure/logger@1.1.2': + resolution: {integrity: sha512-l170uE7bsKpIU6B/giRc9i4NI0Mj+tANMMMxf7Zi/5cKzEqPayP7+X1WPrG7e+91JgY8N+7K7nF2WOi7iVhXvg==} + engines: {node: '>=18.0.0'} + + '@azure/msal-browser@3.14.0': + resolution: {integrity: sha512-Un85LhOoecJ3HDTS3Uv3UWnXC9/43ZSO+Kc+anSqpZvcEt58SiO/3DuVCAe1A3I5UIBYJNMgTmZPGXQ0MVYrwA==} + engines: {node: '>=0.8.0'} + + '@azure/msal-common@14.10.0': + resolution: {integrity: sha512-Zk6DPDz7e1wPgLoLgAp0349Yay9RvcjPM5We/ehuenDNsz/t9QEFI7tRoHpp/e47I4p20XE3FiDlhKwAo3utDA==} + engines: {node: '>=0.8.0'} + + '@azure/msal-node@2.8.1': + resolution: {integrity: sha512-VcZZM+5VvCWRBTOF7SxMKaxrz+EXjntx2u5AQe7QE06e6FuPJElGBrImgNgCh5QmFaNCfVFO+3qNR7UoFD/Gfw==} + engines: {node: '>=16'} + + '@babel/code-frame@7.23.5': + resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==} + engines: {node: '>=6.9.0'} + + '@babel/code-frame@7.24.2': + resolution: {integrity: sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==} + engines: {node: '>=6.9.0'} + + '@babel/code-frame@7.26.2': + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.23.3': + resolution: {integrity: sha512-BmR4bWbDIoFJmJ9z2cZ8Gmm2MXgEDgjdWgpKmKWUt54UGFJdlj31ECtbaDvCG/qVdG3AQ1SfpZEs01lUFbzLOQ==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.26.8': + resolution: {integrity: sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.23.5': + resolution: {integrity: sha512-Cwc2XjUrG4ilcfOw4wBAK+enbdgwAcAJCfGUItPBKR7Mjw4aEfAFYrLxeRp4jWgtNIKn3n2AlBOfwwafl+42/g==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.26.10': + resolution: {integrity: sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.23.5': + resolution: {integrity: sha512-BPssCHrBD+0YrxviOa3QzpqwhNIXKEtOa2jQrm4FlmkC2apYgRnQcmPWiGZDlGxiNtltnUFolMe8497Esry+jA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.26.10': + resolution: {integrity: sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.22.5': + resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.22.15': + resolution: {integrity: sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.26.5': + resolution: {integrity: sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.22.15': + resolution: {integrity: sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-environment-visitor@7.22.20': + resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-function-name@7.23.0': + resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-hoist-variables@7.22.5': + resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.23.0': + resolution: {integrity: sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.22.15': + resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.25.9': + resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.23.3': + resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-module-transforms@7.26.0': + resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.22.5': + resolution: {integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.22.5': + resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-replace-supers@7.22.20': + resolution: {integrity: sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-simple-access@7.22.5': + resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-skip-transparent-expression-wrappers@7.22.5': + resolution: {integrity: sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-split-export-declaration@7.22.6': + resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.23.4': + resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.24.1': + resolution: {integrity: sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.25.9': + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.22.20': + resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.24.5': + resolution: {integrity: sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.22.15': + resolution: {integrity: sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.25.9': + resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.23.5': + resolution: {integrity: sha512-oO7us8FzTEsG3U6ag9MfdF1iA/7Z6dz+MtFhifZk8C8o453rGJFFWUP1t+ULM9TUIAzC9uxXEiXjOiVMyd7QPg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.26.10': + resolution: {integrity: sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==} + engines: {node: '>=6.9.0'} + + '@babel/highlight@7.23.4': + resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==} + engines: {node: '>=6.9.0'} + + '@babel/highlight@7.24.5': + resolution: {integrity: sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.23.5': + resolution: {integrity: sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/parser@7.24.1': + resolution: {integrity: sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/parser@7.26.10': + resolution: {integrity: sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/parser@7.27.0': + resolution: {integrity: sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-proposal-class-properties@7.18.6': + resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} + engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead. + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-proposal-object-rest-spread@7.20.7': + resolution: {integrity: sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==} + engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead. + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-properties@7.12.13': + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-flow@7.23.3': + resolution: {integrity: sha512-YZiAIpkJAwQXBJLIQbRFayR5c+gJ35Vcz3bg954k7cd73zqjvhacJuL9RbrzPz8qPmZdgqP6EUKwy0PCNhaaPA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-assertions@7.23.3': + resolution: {integrity: sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.23.3': + resolution: {integrity: sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-object-rest-spread@7.8.3': + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-arrow-functions@7.23.3': + resolution: {integrity: sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoped-functions@7.23.3': + resolution: {integrity: sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoping@7.23.3': + resolution: {integrity: sha512-QPZxHrThbQia7UdvfpaRRlq/J9ciz1J4go0k+lPBXbgaNeY7IQrBj/9ceWjvMMI07/ZBzHl/F0R/2K0qH7jCVw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-classes@7.23.3': + resolution: {integrity: sha512-FGEQmugvAEu2QtgtU0uTASXevfLMFfBeVCIIdcQhn/uBQsMTjBajdnAtanQlOcuihWh10PZ7+HWvc7NtBwP74w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-computed-properties@7.23.3': + resolution: {integrity: sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-destructuring@7.23.3': + resolution: {integrity: sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-flow-strip-types@7.23.3': + resolution: {integrity: sha512-26/pQTf9nQSNVJCrLB1IkHUKyPxR+lMrH2QDPG89+Znu9rAMbtrybdbWeE9bb7gzjmE5iXHEY+e0HUwM6Co93Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-for-of@7.23.3': + resolution: {integrity: sha512-X8jSm8X1CMwxmK878qsUGJRmbysKNbdpTv/O1/v0LuY/ZkZrng5WYiekYSdg9m09OTmDDUWeEDsTE+17WYbAZw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-function-name@7.23.3': + resolution: {integrity: sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-literals@7.23.3': + resolution: {integrity: sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-member-expression-literals@7.23.3': + resolution: {integrity: sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.23.3': + resolution: {integrity: sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-super@7.23.3': + resolution: {integrity: sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-parameters@7.23.3': + resolution: {integrity: sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-property-literals@7.23.3': + resolution: {integrity: sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-display-name@7.23.3': + resolution: {integrity: sha512-GnvhtVfA2OAtzdX58FJxU19rhoGeQzyVndw3GgtdECQvQFXPEZIOVULHVZGAYmOgmqjXpVpfocAbSjh99V/Fqw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx@7.22.15': + resolution: {integrity: sha512-oKckg2eZFa8771O/5vi7XeTvmM6+O9cxZu+kanTU7tD4sin5nO/G8jGJhq8Hvt2Z0kUoEDRayuZLaUlYl8QuGA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-shorthand-properties@7.23.3': + resolution: {integrity: sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-spread@7.23.3': + resolution: {integrity: sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-template-literals@7.23.3': + resolution: {integrity: sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.24.4': + resolution: {integrity: sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==} + engines: {node: '>=6.9.0'} + + '@babel/standalone@7.26.10': + resolution: {integrity: sha512-AYXK0hLWfEaK9WAePJqs30qro09a8w7X3YZzjukqtLXreE7xBZYdi5EMrP87T4UrVqmQ9tIX6L6SeTu5LDh3zw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.22.15': + resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.26.9': + resolution: {integrity: sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.23.5': + resolution: {integrity: sha512-czx7Xy5a6sapWWRx61m1Ke1Ra4vczu1mCTtJam5zRTBOonfdJ+S/B6HYmGYu3fJtr8GGET3si6IhgWVBhJ/m8w==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.26.10': + resolution: {integrity: sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.23.5': + resolution: {integrity: sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.24.5': + resolution: {integrity: sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.26.10': + resolution: {integrity: sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.27.0': + resolution: {integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==} + engines: {node: '>=6.9.0'} + + '@clack/core@0.3.4': + resolution: {integrity: sha512-H4hxZDXgHtWTwV3RAVenqcC4VbJZNegbBjlPvzOzCouXtS2y3sDvlO3IsbrPNWuLWPPlYVYPghQdSF64683Ldw==} + + '@clack/prompts@0.7.0': + resolution: {integrity: sha512-0MhX9/B4iL6Re04jPrttDm+BsP8y6mS7byuv0BvXgdXhbV5PdlsHt55dvNsuBCPZ7xq1oTAOOuotR9NFbQyMSA==} + bundledDependencies: + - is-unicode-supported + + '@codemirror/autocomplete@6.11.1': + resolution: {integrity: sha512-L5UInv8Ffd6BPw0P3EF7JLYAMeEbclY7+6Q11REt8vhih8RuLreKtPy/xk8wPxs4EQgYqzI7cdgpiYwWlbS/ow==} + peerDependencies: + '@codemirror/language': ^6.0.0 + '@codemirror/state': ^6.0.0 + '@codemirror/view': ^6.0.0 + '@lezer/common': ^1.0.0 + + '@codemirror/lang-angular@0.1.3': + resolution: {integrity: sha512-xgeWGJQQl1LyStvndWtruUvb4SnBZDAu/gvFH/ZU+c0W25tQR8e5hq7WTwiIY2dNxnf+49mRiGI/9yxIwB6f5w==} + + '@codemirror/lang-cpp@6.0.2': + resolution: {integrity: sha512-6oYEYUKHvrnacXxWxYa6t4puTlbN3dgV662BDfSH8+MfjQjVmP697/KYTDOqpxgerkvoNm7q5wlFMBeX8ZMocg==} + + '@codemirror/lang-css@6.2.1': + resolution: {integrity: sha512-/UNWDNV5Viwi/1lpr/dIXJNWiwDxpw13I4pTUAsNxZdg6E0mI2kTQb0P2iHczg1Tu+H4EBgJR+hYhKiHKko7qg==} + + '@codemirror/lang-css@6.3.1': + resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==} + + '@codemirror/lang-html@6.4.7': + resolution: {integrity: sha512-y9hWSSO41XlcL4uYwWyk0lEgTHcelWWfRuqmvcAmxfCs0HNWZdriWo/EU43S63SxEZpc1Hd50Itw7ktfQvfkUg==} + + '@codemirror/lang-html@6.4.9': + resolution: {integrity: sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==} + + '@codemirror/lang-java@6.0.1': + resolution: {integrity: sha512-OOnmhH67h97jHzCuFaIEspbmsT98fNdhVhmA3zCxW0cn7l8rChDhZtwiwJ/JOKXgfm4J+ELxQihxaI7bj7mJRg==} + + '@codemirror/lang-javascript@6.2.1': + resolution: {integrity: sha512-jlFOXTejVyiQCW3EQwvKH0m99bUYIw40oPmFjSX2VS78yzfe0HELZ+NEo9Yfo1MkGRpGlj3Gnu4rdxV1EnAs5A==} + + '@codemirror/lang-javascript@6.2.3': + resolution: {integrity: sha512-8PR3vIWg7pSu7ur8A07pGiYHgy3hHj+mRYRCSG8q+mPIrl0F02rgpGv+DsQTHRTc30rydOsf5PZ7yjKFg2Ackw==} + + '@codemirror/lang-json@6.0.1': + resolution: {integrity: sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==} + + '@codemirror/lang-less@6.0.2': + resolution: {integrity: sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ==} + + '@codemirror/lang-lezer@6.0.1': + resolution: {integrity: sha512-WHwjI7OqKFBEfkunohweqA5B/jIlxaZso6Nl3weVckz8EafYbPZldQEKSDb4QQ9H9BUkle4PVELP4sftKoA0uQ==} + + '@codemirror/lang-liquid@6.2.0': + resolution: {integrity: sha512-DRmtaBHtAP63I5IDa1OEk00oh3NsR7DFGkvFmLc9ODqdy2uEGkcjNn+QqgbLuy3zSjQcl5cdiX2FSjOXzPx5BA==} + + '@codemirror/lang-markdown@6.2.3': + resolution: {integrity: sha512-wCewRLWpdefWi7uVkHIDiE8+45Fe4buvMDZkihqEom5uRUQrl76Zb13emjeK3W+8pcRgRfAmwelURBbxNEKCIg==} + + '@codemirror/lang-markdown@6.3.2': + resolution: {integrity: sha512-c/5MYinGbFxYl4itE9q/rgN/sMTjOr8XL5OWnC+EaRMLfCbVUmmubTJfdgpfcSS2SCaT7b+Q+xi3l6CgoE+BsA==} + + '@codemirror/lang-php@6.0.1': + resolution: {integrity: sha512-ublojMdw/PNWa7qdN5TMsjmqkNuTBD3k6ndZ4Z0S25SBAiweFGyY68AS3xNcIOlb6DDFDvKlinLQ40vSLqf8xA==} + + '@codemirror/lang-python@6.1.3': + resolution: {integrity: sha512-S9w2Jl74hFlD5nqtUMIaXAq9t5WlM0acCkyuQWUUSvZclk1sV+UfnpFiZzuZSG+hfEaOmxKR5UxY/Uxswn7EhQ==} + + '@codemirror/lang-python@6.1.7': + resolution: {integrity: sha512-mZnFTsL4lW5p9ch8uKNKeRU3xGGxr1QpESLilfON2E3fQzOa/OygEMkaDvERvXDJWJA9U9oN/D4w0ZuUzNO4+g==} + + '@codemirror/lang-rust@6.0.1': + resolution: {integrity: sha512-344EMWFBzWArHWdZn/NcgkwMvZIWUR1GEBdwG8FEp++6o6vT6KL9V7vGs2ONsKxxFUPXKI0SPcWhyYyl2zPYxQ==} + + '@codemirror/lang-sass@6.0.2': + resolution: {integrity: sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q==} + + '@codemirror/lang-sql@6.5.5': + resolution: {integrity: sha512-DvOaP2RXLb2xlxJxxydTFfwyYw5YDqEFea6aAfgh9UH0kUD6J1KFZ0xPgPpw1eo/5s2w3L6uh5PVR7GM23GxkQ==} + + '@codemirror/lang-sql@6.8.0': + resolution: {integrity: sha512-aGLmY4OwGqN3TdSx3h6QeA1NrvaYtF7kkoWR/+W7/JzB0gQtJ+VJxewlnE3+VImhA4WVlhmkJr109PefOOhjLg==} + + '@codemirror/lang-vue@0.1.3': + resolution: {integrity: sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug==} + + '@codemirror/lang-wast@6.0.2': + resolution: {integrity: sha512-Imi2KTpVGm7TKuUkqyJ5NRmeFWF7aMpNiwHnLQe0x9kmrxElndyH0K6H/gXtWwY6UshMRAhpENsgfpSwsgmC6Q==} + + '@codemirror/lang-xml@6.0.2': + resolution: {integrity: sha512-JQYZjHL2LAfpiZI2/qZ/qzDuSqmGKMwyApYmEUUCTxLM4MWS7sATUEfIguZQr9Zjx/7gcdnewb039smF6nC2zw==} + + '@codemirror/lang-xml@6.1.0': + resolution: {integrity: sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==} + + '@codemirror/language-data@6.3.1': + resolution: {integrity: sha512-p6jhJmvhGe1TG1EGNhwH7nFWWFSTJ8NDKnB2fVx5g3t+PpO0+63R7GJNxjS0TmmH3cdMxZbzejsik+rlEh1EyQ==} + + '@codemirror/language@6.10.1': + resolution: {integrity: sha512-5GrXzrhq6k+gL5fjkAwt90nYDmjlzTIJV8THnxNFtNKWotMIlzzN+CpqxqwXOECnUdOndmSeWntVrVcv5axWRQ==} + + '@codemirror/language@6.11.0': + resolution: {integrity: sha512-A7+f++LodNNc1wGgoRDTt78cOwWm9KVezApgjOMp1W4hM0898nsqBXwF+sbePE7ZRcjN7Sa1Z5m2oN27XkmEjQ==} + + '@codemirror/legacy-modes@6.3.3': + resolution: {integrity: sha512-X0Z48odJ0KIoh/HY8Ltz75/4tDYc9msQf1E/2trlxFaFFhgjpVHjZ/BCXe1Lk7s4Gd67LL/CeEEHNI+xHOiESg==} + + '@codemirror/lint@6.4.2': + resolution: {integrity: sha512-wzRkluWb1ptPKdzlsrbwwjYCPLgzU6N88YBAmlZi8WFyuiEduSd05MnJYNogzyc8rPK7pj6m95ptUApc8sHKVA==} + + '@codemirror/search@6.5.5': + resolution: {integrity: sha512-PIEN3Ke1buPod2EHbJsoQwlbpkz30qGZKcnmH1eihq9+bPQx8gelauUwLYaY4vBOuBAuEhmpDLii4rj/uO0yMA==} + + '@codemirror/state@6.4.0': + resolution: {integrity: sha512-hm8XshYj5Fo30Bb922QX9hXB/bxOAVH+qaqHBzw5TKa72vOeslyGwd4X8M0c1dJ9JqxlaMceOQ8RsL9tC7gU0A==} + + '@codemirror/state@6.5.2': + resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==} + + '@codemirror/theme-one-dark@6.1.2': + resolution: {integrity: sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==} + + '@codemirror/view@6.23.0': + resolution: {integrity: sha512-/51px9N4uW8NpuWkyUX+iam5+PM6io2fm+QmRnzwqBy5v/pwGg9T0kILFtYeum8hjuvENtgsGNKluOfqIICmeQ==} + + '@codemirror/view@6.36.4': + resolution: {integrity: sha512-ZQ0V5ovw/miKEXTvjgzRyjnrk9TwriUB1k4R5p7uNnHR9Hus+D1SXHGdJshijEzPFjU25xea/7nhIeSqYFKdbA==} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@curvenote/ansi-to-react@7.0.0': + resolution: {integrity: sha512-+m4V86QPmaZ7udMp7Yg81A31dLBGO8gglkPGWPzUJNxLCSyOrJ04pQmdZQelNBEA7MSGz8wf+6RHcuEaujdhHw==} + engines: {node: '>=16'} + peerDependencies: + react: ^16.3.2 || ^17.0.0 || ^18.0.0 + react-dom: ^16.3.2 || ^17.0.0 || ^18.0.0 + + '@emotion/is-prop-valid@0.8.8': + resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==} + + '@emotion/memoize@0.7.4': + resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==} + + '@es-joy/jsdoccomment@0.43.0': + resolution: {integrity: sha512-Q1CnsQrytI3TlCB1IVWXWeqUIPGVEKGaE7IbVdt13Nq/3i0JESAkQQERrfiQkmlpijl+++qyqPgaS31Bvc1jRQ==} + engines: {node: '>=16'} + + '@esbuild/aix-ppc64@0.19.11': + resolution: {integrity: sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.19.12': + resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.20.2': + resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.24.2': + resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.19.11': + resolution: {integrity: sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.19.12': + resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.20.2': + resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.24.2': + resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.19.11': + resolution: {integrity: sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.19.12': + resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.20.2': + resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.24.2': + resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.19.11': + resolution: {integrity: sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.19.12': + resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.20.2': + resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.24.2': + resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.19.11': + resolution: {integrity: sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.19.12': + resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.20.2': + resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.24.2': + resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.19.11': + resolution: {integrity: sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.19.12': + resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.20.2': + resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.24.2': + resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.19.11': + resolution: {integrity: sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.19.12': + resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.20.2': + resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.24.2': + resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.19.11': + resolution: {integrity: sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.19.12': + resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.20.2': + resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.24.2': + resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.19.11': + resolution: {integrity: sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.19.12': + resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.20.2': + resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.24.2': + resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.19.11': + resolution: {integrity: sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.19.12': + resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.20.2': + resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.24.2': + resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.19.11': + resolution: {integrity: sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.19.12': + resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.20.2': + resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.24.2': + resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.19.11': + resolution: {integrity: sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.19.12': + resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.20.2': + resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.24.2': + resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.19.11': + resolution: {integrity: sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.19.12': + resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.20.2': + resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.24.2': + resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.19.11': + resolution: {integrity: sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.19.12': + resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.20.2': + resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.24.2': + resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.19.11': + resolution: {integrity: sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.19.12': + resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.20.2': + resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.24.2': + resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.19.11': + resolution: {integrity: sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.19.12': + resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.20.2': + resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.24.2': + resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.19.11': + resolution: {integrity: sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.19.12': + resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.20.2': + resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.24.2': + resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.24.2': + resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.19.11': + resolution: {integrity: sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.19.12': + resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.20.2': + resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.24.2': + resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.24.2': + resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.19.11': + resolution: {integrity: sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.19.12': + resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.20.2': + resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.24.2': + resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.19.11': + resolution: {integrity: sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.19.12': + resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.20.2': + resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.24.2': + resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.19.11': + resolution: {integrity: sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.19.12': + resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.20.2': + resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.24.2': + resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.19.11': + resolution: {integrity: sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.19.12': + resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.20.2': + resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.24.2': + resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.19.11': + resolution: {integrity: sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.19.12': + resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.20.2': + resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.24.2': + resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.4.0': + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.10.0': + resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/eslintrc@2.1.2': + resolution: {integrity: sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/eslintrc@3.1.0': + resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@8.50.0': + resolution: {integrity: sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@8.57.0': + resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@9.3.0': + resolution: {integrity: sha512-niBqk8iwv96+yuTwjM6bWg8ovzAPF9qkICsGtcoa5/dmqcEMfdwNAX7+/OHcJHc7wj7XqPxH98oAHytFYlw6Sw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@floating-ui/core@1.5.0': + resolution: {integrity: sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==} + + '@floating-ui/dom@1.5.3': + resolution: {integrity: sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==} + + '@floating-ui/react-dom@2.0.2': + resolution: {integrity: sha512-5qhlDvjaLmAst/rKb3VdlCinwTF4EYMiVxuuc/HVUjs46W0zgtbMmAZ1UTsDrRTxRmUEzl92mOtWbeeXL26lSQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.1.4': + resolution: {integrity: sha512-qprfWkn82Iw821mcKofJ5Pk9wgioHicxcQMxx+5zt5GSKoqdWvgG5AxVmpmUUjzTLPVSH5auBrhI93Deayn/DA==} + + '@graphql-codegen/add@5.0.0': + resolution: {integrity: sha512-ynWDOsK2yxtFHwcJTB9shoSkUd7YXd6ZE57f0nk7W5cu/nAgxZZpEsnTPEpZB/Mjf14YRGe2uJHQ7AfElHjqUQ==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-codegen/cli@5.0.0': + resolution: {integrity: sha512-A7J7+be/a6e+/ul2KI5sfJlpoqeqwX8EzktaKCeduyVKgOLA6W5t+NUGf6QumBDXU8PEOqXk3o3F+RAwCWOiqA==} + hasBin: true + peerDependencies: + '@parcel/watcher': ^2.1.0 + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + peerDependenciesMeta: + '@parcel/watcher': + optional: true + + '@graphql-codegen/client-preset@4.1.0': + resolution: {integrity: sha512-/3Ymb/fjxIF1+HGmaI1YwSZbWsrZAWMSQjh3dU425eBjctjsVQ6gzGRr+l/gE5F1mtmCf+vlbTAT03heAc/QIw==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-codegen/core@4.0.0': + resolution: {integrity: sha512-JAGRn49lEtSsZVxeIlFVIRxts2lWObR+OQo7V2LHDJ7ohYYw3ilv7nJ8pf8P4GTg/w6ptcYdSdVVdkI8kUHB/Q==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-codegen/gql-tag-operations@4.0.1': + resolution: {integrity: sha512-qF6wIbBzW8BNT+wiVsBxrYOs2oYcsxQ7mRvCpfEI3HnNZMAST/uX76W8MqFEJvj4mw7NIDv7xYJAcAZIWM5LWw==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-codegen/plugin-helpers@5.0.1': + resolution: {integrity: sha512-6L5sb9D8wptZhnhLLBcheSPU7Tg//DGWgc5tQBWX46KYTOTQHGqDpv50FxAJJOyFVJrveN9otWk9UT9/yfY4ww==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-codegen/schema-ast@4.0.0': + resolution: {integrity: sha512-WIzkJFa9Gz28FITAPILbt+7A8+yzOyd1NxgwFh7ie+EmO9a5zQK6UQ3U/BviirguXCYnn+AR4dXsoDrSrtRA1g==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-codegen/typed-document-node@5.0.1': + resolution: {integrity: sha512-VFkhCuJnkgtbbgzoCAwTdJe2G1H6sd3LfCrDqWUrQe53y2ukfSb5Ov1PhAIkCBStKCMQBUY9YgGz9GKR40qQ8g==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-codegen/typescript-operations@4.0.1': + resolution: {integrity: sha512-GpUWWdBVUec/Zqo23aFLBMrXYxN2irypHqDcKjN78JclDPdreasAEPcIpMfqf4MClvpmvDLy4ql+djVAwmkjbw==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-codegen/typescript@4.0.1': + resolution: {integrity: sha512-3YziQ21dCVdnHb+Us1uDb3pA6eG5Chjv0uTK+bt9dXeMlwYBU8MbtzvQTo4qvzWVC1AxSOKj0rgfNu1xCXqJyA==} + peerDependencies: + graphql: ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-codegen/visitor-plugin-common@4.0.1': + resolution: {integrity: sha512-Bi/1z0nHg4QMsAqAJhds+ForyLtk7A3HQOlkrZNm3xEkY7lcBzPtiOTLBtvziwopBsXUxqeSwVjOOFPLS5Yw1Q==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-tools/apollo-engine-loader@8.0.0': + resolution: {integrity: sha512-axQTbN5+Yxs1rJ6cWQBOfw3AEeC+fvIuZSfJLPLLvFJLj4pUm9fhxey/g6oQZAAQJqKPfw+tLDUQvnfvRK8Kmg==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/batch-execute@9.0.2': + resolution: {integrity: sha512-Y2uwdZI6ZnatopD/SYfZ1eGuQFI7OU2KGZ2/B/7G9ISmgMl5K+ZZWz/PfIEXeiHirIDhyk54s4uka5rj2xwKqQ==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/code-file-loader@8.0.3': + resolution: {integrity: sha512-gVnnlWs0Ua+5FkuHHEriFUOI3OIbHv6DS1utxf28n6NkfGMJldC4j0xlJRY0LS6dWK34IGYgD4HelKYz2l8KiA==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/delegate@10.0.3': + resolution: {integrity: sha512-Jor9oazZ07zuWkykD3OOhT/2XD74Zm6Ar0ENZMk75MDD51wB2UWUIMljtHxbJhV5A6UBC2v8x6iY0xdCGiIlyw==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/documents@1.0.0': + resolution: {integrity: sha512-rHGjX1vg/nZ2DKqRGfDPNC55CWZBMldEVcH+91BThRa6JeT80NqXknffLLEZLRUxyikCfkwMsk6xR3UNMqG0Rg==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/executor-graphql-ws@1.1.0': + resolution: {integrity: sha512-yM67SzwE8rYRpm4z4AuGtABlOp9mXXVy6sxXnTJRoYIdZrmDbKVfIY+CpZUJCqS0FX3xf2+GoHlsj7Qswaxgcg==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/executor-http@1.0.3': + resolution: {integrity: sha512-5WZIMBevRaxMabZ8U2Ty0dTUPy/PpeYSlMNEmC/YJjKKykgSfc/AwSejx2sE4FFKZ0I2kxRKRenyoWMHRAV49Q==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/executor-legacy-ws@1.0.4': + resolution: {integrity: sha512-b7aGuRekZDS+m3af3BIvMKxu15bmVPMt5eGQVuP2v5pxmbaPTh+iv5mx9b3Plt32z5Ke5tycBnNm5urSFtW8ng==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/executor@1.2.0': + resolution: {integrity: sha512-SKlIcMA71Dha5JnEWlw4XxcaJ+YupuXg0QCZgl2TOLFz4SkGCwU/geAsJvUJFwK2RbVLpQv/UMq67lOaBuwDtg==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/git-loader@8.0.3': + resolution: {integrity: sha512-Iz9KbRUAkuOe8JGTS0qssyJ+D5Snle17W+z9anwWrLFrkBhHrRFUy5AdjZqgJuhls0x30QkZBnnCtnHDBdQ4nA==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/github-loader@8.0.0': + resolution: {integrity: sha512-VuroArWKcG4yaOWzV0r19ElVIV6iH6UKDQn1MXemND0xu5TzrFme0kf3U9o0YwNo0kUYEk9CyFM0BYg4he17FA==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/graphql-file-loader@8.0.0': + resolution: {integrity: sha512-wRXj9Z1IFL3+zJG1HWEY0S4TXal7+s1vVhbZva96MSp0kbb/3JBF7j0cnJ44Eq0ClccMgGCDFqPFXty4JlpaPg==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/graphql-tag-pluck@8.1.0': + resolution: {integrity: sha512-kt5l6H/7QxQcIaewInTcune6NpATojdFEW98/8xWcgmy7dgXx5vU9e0AicFZIH+ewGyZzTpwFqO2RI03roxj2w==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/import@7.0.0': + resolution: {integrity: sha512-NVZiTO8o1GZs6OXzNfjB+5CtQtqsZZpQOq+Uu0w57kdUkT4RlQKlwhT8T81arEsbV55KpzkpFsOZP7J1wdmhBw==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/json-file-loader@8.0.0': + resolution: {integrity: sha512-ki6EF/mobBWJjAAC84xNrFMhNfnUFD6Y0rQMGXekrUgY0NdeYXHU0ZUgHzC9O5+55FslqUmAUHABePDHTyZsLg==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/load@8.0.0': + resolution: {integrity: sha512-Cy874bQJH0FP2Az7ELPM49iDzOljQmK1PPH6IuxsWzLSTxwTqd8dXA09dcVZrI7/LsN26heTY2R8q2aiiv0GxQ==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/merge@9.0.0': + resolution: {integrity: sha512-J7/xqjkGTTwOJmaJQJ2C+VDBDOWJL3lKrHJN4yMaRLAJH3PosB7GiPRaSDZdErs0+F77sH2MKs2haMMkywzx7Q==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/optimize@2.0.0': + resolution: {integrity: sha512-nhdT+CRGDZ+bk68ic+Jw1OZ99YCDIKYA5AlVAnBHJvMawSx9YQqQAIj4refNc1/LRieGiuWvhbG3jvPVYho0Dg==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/prisma-loader@8.0.2': + resolution: {integrity: sha512-8d28bIB0bZ9Bj0UOz9sHagVPW+6AHeqvGljjERtwCnWl8OCQw2c2pNboYXISLYUG5ub76r4lDciLLTU+Ks7Q0w==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/relay-operation-optimizer@7.0.0': + resolution: {integrity: sha512-UNlJi5y3JylhVWU4MBpL0Hun4Q7IoJwv9xYtmAz+CgRa066szzY7dcuPfxrA7cIGgG/Q6TVsKsYaiF4OHPs1Fw==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/schema@10.0.0': + resolution: {integrity: sha512-kf3qOXMFcMs2f/S8Y3A8fm/2w+GaHAkfr3Gnhh2LOug/JgpY/ywgFVxO3jOeSpSEdoYcDKLcXVjMigNbY4AdQg==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/url-loader@8.0.0': + resolution: {integrity: sha512-rPc9oDzMnycvz+X+wrN3PLrhMBQkG4+sd8EzaFN6dypcssiefgWKToXtRKI8HHK68n2xEq1PyrOpkjHFJB+GwA==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/utils@10.0.8': + resolution: {integrity: sha512-yjyA8ycSa1WRlJqyX/aLqXeE5DvF/H02+zXMUFnCzIDrj0UvLMUrxhmVFnMK0Q2n3bh4uuTeY3621m5za9ovXw==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/wrap@10.0.1': + resolution: {integrity: sha512-Cw6hVrKGM2OKBXeuAGltgy4tzuqQE0Nt7t/uAqnuokSXZhMHXJUb124Bnvxc2gPZn5chfJSDafDe4Cp8ZAVJgg==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-typed-document-node/core@3.2.0': + resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@hookform/resolvers@3.3.2': + resolution: {integrity: sha512-Tw+GGPnBp+5DOsSg4ek3LCPgkBOuOgS5DsDV7qsWNH9LZc433kgsWICjlsh2J9p04H2K66hsXPPb9qn9ILdUtA==} + peerDependencies: + react-hook-form: ^7.0.0 + + '@humanwhocodes/config-array@0.11.11': + resolution: {integrity: sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/config-array@0.11.14': + resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/config-array@0.13.0': + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@1.2.1': + resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} + deprecated: Use @eslint/object-schema instead + + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + + '@humanwhocodes/retry@0.3.0': + resolution: {integrity: sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==} + engines: {node: '>=18.18'} + + '@ianvs/prettier-plugin-sort-imports@4.1.1': + resolution: {integrity: sha512-kJhXq63ngpTQ2dxgf5GasbPJWsJA3LgoOdd7WGhpUSzLgLgI4IsIzYkbJf9kmpOHe7Vdm/o3PcRA3jmizXUuAQ==} + peerDependencies: + '@vue/compiler-sfc': '>=3.0.0' + prettier: 2 || 3 + peerDependenciesMeta: + '@vue/compiler-sfc': + optional: true + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jest/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jridgewell/gen-mapping@0.3.3': + resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} + engines: {node: '>=6.0.0'} + + '@jridgewell/gen-mapping@0.3.5': + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + + '@jridgewell/gen-mapping@0.3.8': + resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.1': + resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.1.2': + resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.6': + resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} + + '@jridgewell/sourcemap-codec@1.4.15': + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.20': + resolution: {integrity: sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@jsdevtools/ez-spawn@3.0.4': + resolution: {integrity: sha512-f5DRIOZf7wxogefH03RjMPMdBF7ADTWUMoOs9kaJo06EfwF+aFhMZMDZxHg/Xe12hptN9xoZjGso2fdjapBRIA==} + engines: {node: '>=10'} + + '@jspm/core@2.0.1': + resolution: {integrity: sha512-Lg3PnLp0QXpxwLIAuuJboLeRaIhrgJjeuh797QADg3xz8wGLugQOS5DpsE8A6i6Adgzf+bacllkKZG3J0tGfDw==} + + '@koa/cors@5.0.0': + resolution: {integrity: sha512-x/iUDjcS90W69PryLDIMgFyV21YLTnG9zOpPXS7Bkt2b8AsY3zZsIpOLBkYr9fBcF3HbkKaER5hOBZLfpLgYNw==} + engines: {node: '>= 14.0.0'} + + '@koa/router@13.1.0': + resolution: {integrity: sha512-mNVu1nvkpSd8Q8gMebGbCkDWJ51ODetrFvLKYusej+V0ByD4btqHYnPIzTBLXnQMVUlm/oxVwqmWBY3zQfZilw==} + engines: {node: '>= 18'} + + '@lezer/common@1.2.0': + resolution: {integrity: sha512-Wmvlm4q6tRpwiy20TnB3yyLTZim38Tkc50dPY8biQRwqE+ati/wD84rm3N15hikvdT4uSg9phs9ubjvcLmkpKg==} + + '@lezer/common@1.2.3': + resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==} + + '@lezer/cpp@1.1.2': + resolution: {integrity: sha512-macwKtyeUO0EW86r3xWQCzOV9/CF8imJLpJlPv3sDY57cPGeUZ8gXWOWNlJr52TVByMV3PayFQCA5SHEERDmVQ==} + + '@lezer/css@1.1.10': + resolution: {integrity: sha512-V5/89eDapjeAkWPBpWEfQjZ1Hag3aYUUJOL8213X0dFRuXJ4BXa5NKl9USzOnaLod4AOpmVCkduir2oKwZYZtg==} + + '@lezer/css@1.1.6': + resolution: {integrity: sha512-/HhbnfXchRc995VdDH9TBzd1B2CO/A4uhOhELqGjd7Bymgc+tGlb0W9Vp5GA1Otq8Ef4JCXpuKmr4hH3aFny6A==} + + '@lezer/highlight@1.2.0': + resolution: {integrity: sha512-WrS5Mw51sGrpqjlh3d4/fOwpEV2Hd3YOkp9DBt4k8XZQcoTHZFB7sx030A6OcahF4J1nDQAa3jXlTVVYH50IFA==} + + '@lezer/html@1.3.10': + resolution: {integrity: sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==} + + '@lezer/html@1.3.8': + resolution: {integrity: sha512-EXseJ3pUzWxE6XQBQdqWHZqqlGQRSuNMBcLb6mZWS2J2v+QZhOObD+3ZIKIcm59ntTzyor4LqFTb72iJc3k23Q==} + + '@lezer/java@1.1.1': + resolution: {integrity: sha512-mt3dX13fRlpY7RlWELYRakanXgmwXsLRCrhstrn+c1sZd7jR2xle46/3heoxGd+oHxnuTnpoyXTyxcLJQs9+mQ==} + + '@lezer/javascript@1.4.12': + resolution: {integrity: sha512-kwO5MftUiyfKBcECMEDc4HYnc10JME9kTJNPVoCXqJj/Y+ASWF0rgstORi3BThlQI6SoPSshrK5TjuiLFnr29A==} + + '@lezer/json@1.0.2': + resolution: {integrity: sha512-xHT2P4S5eeCYECyKNPhr4cbEL9tc8w83SPwRC373o9uEdrvGKTZoJVAGxpOsZckMlEh9W23Pc72ew918RWQOBQ==} + + '@lezer/lezer@1.1.2': + resolution: {integrity: sha512-O8yw3CxPhzYHB1hvwbdozjnAslhhR8A5BH7vfEMof0xk3p+/DFDfZkA9Tde6J+88WgtwaHy4Sy6ThZSkaI0Evw==} + + '@lezer/lr@1.3.14': + resolution: {integrity: sha512-z5mY4LStlA3yL7aHT/rqgG614cfcvklS+8oFRFBYrs4YaWLJyKKM4+nN6KopToX0o9Hj6zmH6M5kinOYuy06ug==} + + '@lezer/markdown@1.2.0': + resolution: {integrity: sha512-d7MwsfAukZJo1GpPrcPGa3MxaFFOqNp0gbqF+3F7pTeNDOgeJN1muXzx1XXDPt+Ac+/voCzsH7qXqnn+xReG/g==} + + '@lezer/markdown@1.4.2': + resolution: {integrity: sha512-iYewCigG/517D0xJPQd7RGaCjZAFwROiH8T9h7OTtz0bRVtkxzFhGBFJ9JGKgBBs4uuo1cvxzyQ5iKhDLMcLUQ==} + + '@lezer/php@1.0.2': + resolution: {integrity: sha512-GN7BnqtGRpFyeoKSEqxvGvhJQiI4zkgmYnDk/JIyc7H7Ifc1tkPnUn/R2R8meH3h/aBf5rzjvU8ZQoyiNDtDrA==} + + '@lezer/python@1.1.10': + resolution: {integrity: sha512-pvSjn+OWivmA/si/SFeGouHO50xoOZcPIFzf8dql0gRvcfCvLDpVIpnnGFFlB7wa0WDscDLo0NmH+4Tx80nBdQ==} + + '@lezer/python@1.1.16': + resolution: {integrity: sha512-ievIWylIZA5rNgAyHgA06/Y76vMUISKaYL9WrtjU8rCTTEzyZYo2jz9ER2YBdnN6dxCyS7eaK4HJCzamoAMKZw==} + + '@lezer/rust@1.0.2': + resolution: {integrity: sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==} + + '@lezer/sass@1.0.4': + resolution: {integrity: sha512-AqW4myvp73sbMk6y0+gJrMjN5xtqFZzqTftzO3YcO8gSL5d3pymIP3deQllAI8+s1ZoSzH6kD4hsoFLpkD9Kfg==} + + '@lezer/xml@1.0.4': + resolution: {integrity: sha512-WmXKb5eX8+rRfZYSNRR5TPee/ZoDgBdVS/rj1VCJGDKa5gNldIctQYibCoFVyNhvZsyL/8nHbZJZPM4gnXN2Vw==} + + '@lezer/xml@1.0.6': + resolution: {integrity: sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==} + + '@marijn/find-cluster-break@1.0.2': + resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} + + '@next/env@13.5.3': + resolution: {integrity: sha512-X4te86vsbjsB7iO4usY9jLPtZ827Mbx+WcwNBGUOIuswuTAKQtzsuoxc/6KLxCMvogKG795MhrR1LDhYgDvasg==} + + '@next/env@14.1.4': + resolution: {integrity: sha512-e7X7bbn3Z6DWnDi75UWn+REgAbLEqxI8Tq2pkFOFAMpWAWApz/YCUhtWMWn410h8Q2fYiYL7Yg5OlxMOCfFjJQ==} + + '@next/eslint-plugin-next@13.4.7-canary.1': + resolution: {integrity: sha512-Soo9dFbldN4KeFYPfEPf78xD9bn7g62nZdS6sUXAYWfeIhZPgcpxwD1TZbL8dvpOse6IksCixkMYH3nYknneUA==} + + '@next/swc-darwin-arm64@13.5.3': + resolution: {integrity: sha512-6hiYNJxJmyYvvKGrVThzo4nTcqvqUTA/JvKim7Auaj33NexDqSNwN5YrrQu+QhZJCIpv2tULSHt+lf+rUflLSw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-arm64@14.1.4': + resolution: {integrity: sha512-ubmUkbmW65nIAOmoxT1IROZdmmJMmdYvXIe8211send9ZYJu+SqxSnJM4TrPj9wmL6g9Atvj0S/2cFmMSS99jg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@13.5.3': + resolution: {integrity: sha512-UpBKxu2ob9scbpJyEq/xPgpdrgBgN3aLYlxyGqlYX5/KnwpJpFuIHU2lx8upQQ7L+MEmz+fA1XSgesoK92ppwQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-darwin-x64@14.1.4': + resolution: {integrity: sha512-b0Xo1ELj3u7IkZWAKcJPJEhBop117U78l70nfoQGo4xUSvv0PJSTaV4U9xQBLvZlnjsYkc8RwQN1HoH/oQmLlQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@13.5.3': + resolution: {integrity: sha512-5AzM7Yx1Ky+oLY6pHs7tjONTF22JirDPd5Jw/3/NazJ73uGB05NqhGhB4SbeCchg7SlVYVBeRMrMSZwJwq/xoA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-arm64-gnu@14.1.4': + resolution: {integrity: sha512-457G0hcLrdYA/u1O2XkRMsDKId5VKe3uKPvrKVOyuARa6nXrdhJOOYU9hkKKyQTMru1B8qEP78IAhf/1XnVqKA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-arm64-musl@13.5.3': + resolution: {integrity: sha512-A/C1shbyUhj7wRtokmn73eBksjTM7fFQoY2v/0rTM5wehpkjQRLOXI8WJsag2uLhnZ4ii5OzR1rFPwoD9cvOgA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-arm64-musl@14.1.4': + resolution: {integrity: sha512-l/kMG+z6MB+fKA9KdtyprkTQ1ihlJcBh66cf0HvqGP+rXBbOXX0dpJatjZbHeunvEHoBBS69GYQG5ry78JMy3g==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-x64-gnu@13.5.3': + resolution: {integrity: sha512-FubPuw/Boz8tKkk+5eOuDHOpk36F80rbgxlx4+xty/U71e3wZZxVYHfZXmf0IRToBn1Crb8WvLM9OYj/Ur815g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-linux-x64-gnu@14.1.4': + resolution: {integrity: sha512-BapIFZ3ZRnvQ1uWbmqEGJuPT9cgLwvKtxhK/L2t4QYO7l+/DxXuIGjvp1x8rvfa/x1FFSsipERZK70pewbtJtw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-linux-x64-musl@13.5.3': + resolution: {integrity: sha512-DPw8nFuM1uEpbX47tM3wiXIR0Qa+atSzs9Q3peY1urkhofx44o7E1svnq+a5Q0r8lAcssLrwiM+OyJJgV/oj7g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-linux-x64-musl@14.1.4': + resolution: {integrity: sha512-mqVxTwk4XuBl49qn2A5UmzFImoL1iLm0KQQwtdRJRKl21ylQwwGCxJtIYo2rbfkZHoSKlh/YgztY0qH3wG1xIg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-win32-arm64-msvc@13.5.3': + resolution: {integrity: sha512-zBPSP8cHL51Gub/YV8UUePW7AVGukp2D8JU93IHbVDu2qmhFAn9LWXiOOLKplZQKxnIPUkJTQAJDCWBWU4UWUA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-arm64-msvc@14.1.4': + resolution: {integrity: sha512-xzxF4ErcumXjO2Pvg/wVGrtr9QQJLk3IyQX1ddAC/fi6/5jZCZ9xpuL9Tzc4KPWMFq8GGWFVDMshZOdHGdkvag==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-ia32-msvc@13.5.3': + resolution: {integrity: sha512-ONcL/lYyGUj4W37D4I2I450SZtSenmFAvapkJQNIJhrPMhzDU/AdfLkW98NvH1D2+7FXwe7yclf3+B7v28uzBQ==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@next/swc-win32-ia32-msvc@14.1.4': + resolution: {integrity: sha512-WZiz8OdbkpRw6/IU/lredZWKKZopUMhcI2F+XiMAcPja0uZYdMTZQRoQ0WZcvinn9xZAidimE7tN9W5v9Yyfyw==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@next/swc-win32-x64-msvc@13.5.3': + resolution: {integrity: sha512-2Vz2tYWaLqJvLcWbbTlJ5k9AN6JD7a5CN2pAeIzpbecK8ZF/yobA39cXtv6e+Z8c5UJuVOmaTldEAIxvsIux/Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@next/swc-win32-x64-msvc@14.1.4': + resolution: {integrity: sha512-4Rto21sPfw555sZ/XNLqfxDUNeLhNYGO2dlPqsnuCg8N8a2a9u1ltqBOPQ4vj1Gf7eJC0W2hHG2eYUHuiXgY2w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@nextjournal/lang-clojure@1.0.0': + resolution: {integrity: sha512-gOCV71XrYD0DhwGoPMWZmZ0r92/lIHsqQu9QWdpZYYBwiChNwMO4sbVMP7eTuAqffFB2BTtCSC+1skSH9d3bNg==} + + '@nextjournal/lezer-clojure@1.0.0': + resolution: {integrity: sha512-VZyuGu4zw5mkTOwQBTaGVNWmsOZAPw5ZRxu1/Knk/Xfs7EDBIogwIs5UXTYkuECX5ZQB8eOB+wKA2pc7VyqaZQ==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@one-ini/wasm@0.1.1': + resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + + '@orama/orama@2.0.18': + resolution: {integrity: sha512-KSty7+r8JxXv3iCuhLqQfwMvUiSWnXPaTPQPPoVPpwdvynEKcp1JNWKzTMB2tISDggf8jKhQXHEP3QXlGkvvtA==} + engines: {node: '>= 16.0.0'} + + '@parcel/watcher-android-arm64@2.3.0': + resolution: {integrity: sha512-f4o9eA3dgk0XRT3XhB0UWpWpLnKgrh1IwNJKJ7UJek7eTYccQ8LR7XUWFKqw6aEq5KUNlCcGvSzKqSX/vtWVVA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.3.0': + resolution: {integrity: sha512-mKY+oijI4ahBMc/GygVGvEdOq0L4DxhYgwQqYAz/7yPzuGi79oXrZG52WdpGA1wLBPrYb0T8uBaGFo7I6rvSKw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.3.0': + resolution: {integrity: sha512-20oBj8LcEOnLE3mgpy6zuOq8AplPu9NcSSSfyVKgfOhNAc4eF4ob3ldj0xWjGGbOF7Dcy1Tvm6ytvgdjlfUeow==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.3.0': + resolution: {integrity: sha512-7LftKlaHunueAEiojhCn+Ef2CTXWsLgTl4hq0pkhkTBFI3ssj2bJXmH2L67mKpiAD5dz66JYk4zS66qzdnIOgw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.3.0': + resolution: {integrity: sha512-1apPw5cD2xBv1XIHPUlq0cO6iAaEUQ3BcY0ysSyD9Kuyw4MoWm1DV+W9mneWI+1g6OeP6dhikiFE6BlU+AToTQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.3.0': + resolution: {integrity: sha512-mQ0gBSQEiq1k/MMkgcSB0Ic47UORZBmWoAWlMrTW6nbAGoLZP+h7AtUM7H3oDu34TBFFvjy4JCGP43JlylkTQA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.3.0': + resolution: {integrity: sha512-LXZAExpepJew0Gp8ZkJ+xDZaTQjLHv48h0p0Vw2VMFQ8A+RKrAvpFuPVCVwKJCr5SE+zvaG+Etg56qXvTDIedw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.3.0': + resolution: {integrity: sha512-P7Wo91lKSeSgMTtG7CnBS6WrA5otr1K7shhSjKHNePVmfBHDoAOHYRXgUmhiNfbcGk0uMCHVcdbfxtuiZCHVow==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.3.0': + resolution: {integrity: sha512-+kiRE1JIq8QdxzwoYY+wzBs9YbJ34guBweTK8nlzLKimn5EQ2b2FSC+tAOpq302BuIMjyuUGvBiUhEcLIGMQ5g==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.3.0': + resolution: {integrity: sha512-35gXCnaz1AqIXpG42evcoP2+sNL62gZTMZne3IackM+6QlfMcJLy3DrjuL6Iks7Czpd3j4xRBzez3ADCj1l7Aw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.3.0': + resolution: {integrity: sha512-FJS/IBQHhRpZ6PiCjFt1UAcPr0YmCLHRbTc00IBTrelEjlmmgIVLeOx4MSXzx2HFEy5Jo5YdhGpxCuqCyDJ5ow==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.3.0': + resolution: {integrity: sha512-dLx+0XRdMnVI62kU3wbXvbIRhLck4aE28bIGKbRGS7BJNt54IIj9+c/Dkqb+7DJEbHUZAX1bwaoM8PqVlHJmCA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.3.0': + resolution: {integrity: sha512-pW7QaFiL11O0BphO+bq3MgqeX/INAk9jgBldVDYjlQPO4VddoZnF22TcF9onMhnLVHuNqBJeRf+Fj7eezi/+rQ==} + engines: {node: '>= 10.0.0'} + + '@peculiar/asn1-schema@2.3.8': + resolution: {integrity: sha512-ULB1XqHKx1WBU/tTFIA+uARuRoBVZ4pNdOA878RDrRbBfBGcSzi5HBkdScC6ZbHn8z7L8gmKCgPC1LHRrP46tA==} + + '@peculiar/json-schema@1.1.12': + resolution: {integrity: sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==} + engines: {node: '>=8.0.0'} + + '@peculiar/webcrypto@1.4.3': + resolution: {integrity: sha512-VtaY4spKTdN5LjJ04im/d/joXuvLbQdgy5Z4DXF4MFZhQ+MTrejbNMkfZBp1Bs3O5+bFqnJgyGdPuZQflvIa5A==} + engines: {node: '>=10.12.0'} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@playwright/browser-chromium@1.48.1': + resolution: {integrity: sha512-WNpIE+CpO4XNhdNTC/mVE8m/1gYR/hPswL8dnIZzElqxsOPm7qxIBdAChWm/7q1kkMiQ+gEGAaXXQfqL0vR28w==} + engines: {node: '>=18'} + + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + + '@preact/signals-core@1.8.0': + resolution: {integrity: sha512-OBvUsRZqNmjzCZXWLxkZfhcgT+Fk8DDcT/8vD6a1xhDemodyy87UJRJfASMuSD8FaAIeGgGm85ydXhm7lr4fyA==} + + '@quilted/events@2.0.0': + resolution: {integrity: sha512-0Q5w7jgHPj3nD8sTh1o7oa7LFxATK+dH0i4p8Y6BgKBLDOkr9/m01IDUXsbZdZ98XvxQaVkxBF/HUNAFfg/KfA==} + engines: {node: '>=14.0.0'} + + '@quilted/signals@0.2.1': + resolution: {integrity: sha512-uA3fFqJ/uSvk7h/zdPG0RkO/JO7R5smAkb5vdL1LQJa0oH9udLIVGw8T913dhy0KGxQx4W4NVyPnNVdlHaYe9g==} + engines: {node: '>=14.0.0'} + + '@radix-ui/colors@1.0.1': + resolution: {integrity: sha512-xySw8f0ZVsAEP+e7iLl3EvcBXX7gsIlC1Zso/sPBW9gIWerBTgz6axrjU+MZ39wD+WFi5h5zdWpsg3+hwt2Qsg==} + + '@radix-ui/number@1.0.1': + resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==} + + '@radix-ui/primitive@1.0.1': + resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==} + + '@radix-ui/primitive@1.1.0': + resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==} + + '@radix-ui/react-accordion@1.1.2': + resolution: {integrity: sha512-fDG7jcoNKVjSK6yfmuAs0EnPDro0WMXIhMtXdTBWqEioVW206ku+4Lw07e+13lUkFkpoEQ2PdeMIAGpdqEAmDg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-alert-dialog@1.0.4': + resolution: {integrity: sha512-jbfBCRlKYlhbitueOAv7z74PXYeIQmWpKwm3jllsdkw7fGWNkxqP3v0nY9WmOzcPqpQuoorNtvViBgL46n5gVg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-arrow@1.0.3': + resolution: {integrity: sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-avatar@1.0.4': + resolution: {integrity: sha512-kVK2K7ZD3wwj3qhle0ElXhOjbezIgyl2hVvgwfIdexL3rN6zJmy5AqqIf+D31lxVppdzV8CjAfZ6PklkmInZLw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-checkbox@1.0.4': + resolution: {integrity: sha512-CBuGQa52aAYnADZVt/KBQzXrwx6TqnlwtcIPGtVt5JkkzQwMOLJjPukimhfKEr4GQNd43C+djUh5Ikopj8pSLg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collapsible@1.0.3': + resolution: {integrity: sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.0.3': + resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.0.1': + resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-compose-refs@1.1.0': + resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.0.1': + resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.0.4': + resolution: {integrity: sha512-hJtRy/jPULGQZceSAP2Re6/4NpKo8im6V8P2hUqZsdFiSL8l35kYsw3qbRI6Ay5mQd2+wlLqje770eq+RJ3yZg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dialog@1.0.5': + resolution: {integrity: sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-direction@1.0.1': + resolution: {integrity: sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.0.4': + resolution: {integrity: sha512-7UpBa/RKMoHJYjie1gkF1DlK8l1fdU/VKDpoS3rCCo8YBJR294GwcEHyxHw72yvphJ7ld0AXEcSLAzY2F/WyCg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dismissable-layer@1.0.5': + resolution: {integrity: sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dropdown-menu@2.0.6': + resolution: {integrity: sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.0.1': + resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.0.3': + resolution: {integrity: sha512-upXdPfqI4islj2CslyfUBNlaJCPybbqRHAi1KER7Isel9Q2AtSJ0zRBZv8mWQiFXD2nyAJ4BhC3yXgZ6kMBSrQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-scope@1.0.4': + resolution: {integrity: sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-hover-card@1.0.7': + resolution: {integrity: sha512-OcUN2FU0YpmajD/qkph3XzMcK/NmSk9hGWnjV68p6QiZMgILugusgQwnLSDs3oFSJYGKf3Y49zgFedhGh04k9A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-icons@1.3.0': + resolution: {integrity: sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw==} + peerDependencies: + react: ^16.x || ^17.x || ^18.x + + '@radix-ui/react-id@1.0.1': + resolution: {integrity: sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-label@2.0.2': + resolution: {integrity: sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menu@2.0.6': + resolution: {integrity: sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popover@1.0.7': + resolution: {integrity: sha512-shtvVnlsxT6faMnK/a7n0wptwBD23xc1Z5mdrtKLwVEfsEMXodS0r5s0/g5P0hX//EKYZS2sxUjqfzlg52ZSnQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.1.2': + resolution: {integrity: sha512-1CnGGfFi/bbqtJZZ0P/NQY20xdG3E0LALJaLUEoKwPLwl6PPPfbeiCqMVQnhoFRAxjJj4RpBRJzDmUgsex2tSg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.1.3': + resolution: {integrity: sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.0.3': + resolution: {integrity: sha512-xLYZeHrWoPmA5mEKEfZZevoVRK/Q43GfzRXkWV6qawIWWK8t6ifIiLQdd7rmQ4Vk1bmI21XhqF9BN3jWf+phpA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.0.4': + resolution: {integrity: sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.0.1': + resolution: {integrity: sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@1.0.3': + resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.0.0': + resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-radio-group@1.1.3': + resolution: {integrity: sha512-x+yELayyefNeKeTx4fjK6j99Fs6c4qKm3aY38G3swQVTN6xMpsrbigC0uHs2L//g8q4qR7qOcww8430jJmi2ag==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.0.4': + resolution: {integrity: sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-scroll-area@1.0.5': + resolution: {integrity: sha512-b6PAgH4GQf9QEn8zbT2XUHpW5z8BzqEc7Kl11TwDrvuTrxlkcjTD5qa/bxgKr+nmuXKu4L/W5UZ4mlP/VG/5Gw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@1.2.2': + resolution: {integrity: sha512-zI7McXr8fNaSrUY9mZe4x/HC0jTLY9fWNhO1oLWYMQGDXuV4UCivIGTxwioSzO0ZCYX9iSLyWmAh/1TOmX3Cnw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-separator@1.0.3': + resolution: {integrity: sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.0.2': + resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-slot@1.1.0': + resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.0.3': + resolution: {integrity: sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tabs@1.0.4': + resolution: {integrity: sha512-egZfYY/+wRNCflXNHx+dePvnz9FbmssDTJBtgRfDY7e8SE5oIo3Py2eCB1ckAbh1Q7cQ/6yJZThJ++sgbxibog==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle-group@1.0.4': + resolution: {integrity: sha512-Uaj/M/cMyiyT9Bx6fOZO0SAG4Cls0GptBWiBmBxofmDbNVnYYoyRWj/2M/6VCi/7qcXFWnHhRUfdfZFvvkuu8A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle@1.0.3': + resolution: {integrity: sha512-Pkqg3+Bc98ftZGsl60CLANXQBBQ4W3mTFS9EJvNxKMZ7magklKV69/id1mlAlOFDDfHvlCms0fx8fA4CMKDJHg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle@1.1.0': + resolution: {integrity: sha512-gwoxaKZ0oJ4vIgzsfESBuSgJNdc0rv12VhHgcqN0TEJmmZixXG/2XpsLK8kzNWYcnaoRIEEQc0bEi3dIvdUpjw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tooltip@1.0.7': + resolution: {integrity: sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.0.1': + resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.0': + resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.0.1': + resolution: {integrity: sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.1.0': + resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.0.3': + resolution: {integrity: sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.0.1': + resolution: {integrity: sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.0.1': + resolution: {integrity: sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.0.1': + resolution: {integrity: sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.0.1': + resolution: {integrity: sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.0.3': + resolution: {integrity: sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.0.1': + resolution: {integrity: sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==} + + '@react-email/body@0.0.8': + resolution: {integrity: sha512-gqdkNYlIaIw0OdpWu8KjIcQSIFvx7t2bZpXVxMMvBS859Ia1+1X3b5RNbjI3S1ZqLddUf7owOHkO4MiXGE+nxg==} + peerDependencies: + react: ^18.2.0 + + '@react-email/button@0.0.15': + resolution: {integrity: sha512-9Zi6SO3E8PoHYDfcJTecImiHLyitYWmIRs0HE3Ogra60ZzlWP2EXu+AZqwQnhXuq+9pbgwBWNWxB5YPetNPTNA==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + + '@react-email/code-block@0.0.4': + resolution: {integrity: sha512-xjVLi/9dFNJ70N7hYme+21eQWa3b9/kgp4V+FKQJkQCuIMobxPRCIGM5jKD/0Vo2OqrE5chYv/dkg/aP8a8sPg==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + + '@react-email/code-inline@0.0.2': + resolution: {integrity: sha512-0cmgbbibFeOJl0q04K9jJlPDuJ+SEiX/OG6m3Ko7UOkG3TqjRD8Dtvkij6jNDVfUh/zESpqJCP2CxrCLLMUjdA==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + + '@react-email/column@0.0.10': + resolution: {integrity: sha512-MnP8Mnwipr0X3XtdD6jMLckb0sI5/IlS6Kl/2F6/rsSWBJy5Gg6nizlekTdkwDmy0kNSe3/1nGU0Zqo98pl63Q==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + + '@react-email/components@0.0.18': + resolution: {integrity: sha512-VxaEAbLTD/CbIVNeDCzPKBV2rIard88AE5gLmZ4HbaFH7i90NIF/5MiZX5l0bNzoM8N/kjN4CQlqZ1MwN5FdOg==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + + '@react-email/container@0.0.12': + resolution: {integrity: sha512-HFu8Pu5COPFfeZxSL+wKv/TV5uO/sp4zQ0XkRCdnGkj/xoq0lqOHVDL4yC2Pu6fxXF/9C3PHDA++5uEYV5WVJw==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + + '@react-email/font@0.0.6': + resolution: {integrity: sha512-sZZFvEZ4U3vNCAZ8wXqIO3DuGJR2qE/8m2fEH+tdqwa532zGO3zW+UlCTg0b9455wkJSzEBeaWik0IkNvjXzxw==} + peerDependencies: + react: ^18.2.0 + + '@react-email/head@0.0.9': + resolution: {integrity: sha512-dF3Uv1qy3oh+IU2atXdv5Xk0hk2udOlMb1A/MNGngC0eHyoEV9ThA0XvhN7mm5x9dDLkVamoWUKXDtmkiuSRqQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + + '@react-email/heading@0.0.12': + resolution: {integrity: sha512-eB7mpnAvDmwvQLoPuwEiPRH4fPXWe6ltz6Ptbry2BlI88F0a2k11Ghb4+sZHBqg7vVw/MKbqEgtLqr3QJ/KfCQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + + '@react-email/hr@0.0.8': + resolution: {integrity: sha512-JLVvpCg2wYKEB+n/PGCggWG9fRU5e4lxsGdpK5SDLsCL0ic3OLKSpHMfeE+ZSuw0GixAVVQN7F64PVJHQkd4MQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + + '@react-email/html@0.0.8': + resolution: {integrity: sha512-arII3wBNLpeJtwyIJXPaILm5BPKhA+nvdC1F9QkuKcOBJv2zXctn8XzPqyGqDfdplV692ulNJP7XY55YqbKp6w==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + + '@react-email/img@0.0.8': + resolution: {integrity: sha512-jx/rPuKo31tV18fu7P5rRqelaH5wkhg83Dq7uLwJpfqhbi4KFBGeBfD0Y3PiLPPoh+WvYf+Adv9W2ghNW8nOMQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + + '@react-email/link@0.0.8': + resolution: {integrity: sha512-nVikuTi8WJHa6Baad4VuRUbUCa/7EtZ1Qy73TRejaCHn+vhetc39XGqHzKLNh+Z/JFL8Hv9g+4AgG16o2R0ogQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + + '@react-email/markdown@0.0.10': + resolution: {integrity: sha512-MH0xO+NJ4IuJcx9nyxbgGKAMXyudFjCZ0A2GQvuWajemW9qy2hgnJ3mW3/z5lwcenG+JPn7JyO/iZpizQ7u1tA==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + + '@react-email/preview@0.0.9': + resolution: {integrity: sha512-2fyAA/zzZYfYmxfyn3p2YOIU30klyA6Dq4ytyWq4nfzQWWglt5hNDE0cMhObvRtfjM9ghMSVtoELAb0MWiF/kw==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + + '@react-email/render@0.0.14': + resolution: {integrity: sha512-vZD59BHp1HOatJAWydTOdgN8QbNgsjCOfmPcT9ShwisBIu3Rw8pvUVoWsrAqfHfzc4vcNTGnbt6/8mMa8LcsvQ==} + engines: {node: '>=18.0.0'} + + '@react-email/row@0.0.8': + resolution: {integrity: sha512-JsB6pxs/ZyjYpEML3nbwJRGAerjcN/Pa/QG48XUwnT/MioDWrUuyQuefw+CwCrSUZ2P1IDrv2tUD3/E3xzcoKw==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + + '@react-email/section@0.0.12': + resolution: {integrity: sha512-UCD/N/BeOTN4h3VZBUaFdiSem6HnpuxD1Q51TdBFnqeNqS5hBomp8LWJJ9s4gzwHWk1XPdNfLA3I/fJwulJshg==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + + '@react-email/tailwind@0.0.17': + resolution: {integrity: sha512-SVl0YO9b9/8EiNtvYnXTlimehwv6rz5v6JRb60IYqwWRRF6ZDHiLkAq/94o5SMrhLtPZWErcr4VleGumB+pFUg==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + + '@react-email/text@0.0.8': + resolution: {integrity: sha512-uvN2TNWMrfC9wv/LLmMLbbEN1GrMWZb9dBK14eYxHHAEHCeyvGb5ePZZ2MPyzO7Y5yTC+vFEnCEr76V+hWMxCQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + + '@redocly/ajv@8.11.0': + resolution: {integrity: sha512-9GWx27t7xWhDIR02PA18nzBdLcKQRgc46xNQvjFkrYk4UOmvKhJ/dawwiX0cCOeetN5LcaaiqQbVOWYK62SGHw==} + + '@redocly/config@0.6.3': + resolution: {integrity: sha512-hGWJgCsXRw0Ow4rplqRlUQifZvoSwZipkYnt11e3SeH1Eb23VUIDBcRuaQOUqy1wn0eevXkU2GzzQ8fbKdQ7Mg==} + + '@redocly/openapi-core@1.18.0': + resolution: {integrity: sha512-kcbt7w23pcVYGLnJkh2LZpXF1OX5RDM4DLOtwPug2HvRE8ow/YfY8ZEM1YCFlA41D8rBPBVP918cYeIx4BVUbw==} + engines: {node: '>=14.19.0', npm: '>=7.0.0'} + + '@remirror/core-constants@3.0.0': + resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} + + '@repeaterjs/repeater@3.0.5': + resolution: {integrity: sha512-l3YHBLAol6d/IKnB9LhpD0cEZWAoe3eFKUyTYWmFmCO2Q/WOckxLQAUyMZWwZV2M/m3+4vgRoaolFqaII82/TA==} + + '@replit/codemirror-lang-csharp@6.2.0': + resolution: {integrity: sha512-6utbaWkoymhoAXj051mkRp+VIJlpwUgCX9Toevz3YatiZsz512fw3OVCedXQx+WcR0wb6zVHjChnuxqfCLtFVQ==} + peerDependencies: + '@codemirror/autocomplete': ^6.0.0 + '@codemirror/language': ^6.0.0 + '@codemirror/state': ^6.0.0 + '@codemirror/view': ^6.0.0 + '@lezer/common': ^1.0.0 + '@lezer/highlight': ^1.0.0 + '@lezer/lr': ^1.0.0 + + '@replit/codemirror-lang-nix@6.0.1': + resolution: {integrity: sha512-lvzjoYn9nfJzBD5qdm3Ut6G3+Or2wEacYIDJ49h9+19WSChVnxv4ojf+rNmQ78ncuxIt/bfbMvDLMeMP0xze6g==} + peerDependencies: + '@codemirror/autocomplete': ^6.0.0 + '@codemirror/language': ^6.0.0 + '@codemirror/state': ^6.0.0 + '@codemirror/view': ^6.0.0 + '@lezer/common': ^1.0.0 + '@lezer/highlight': ^1.0.0 + '@lezer/lr': ^1.0.0 + + '@replit/codemirror-lang-solidity@6.0.1': + resolution: {integrity: sha512-kDnak0xZelGmvzJwKTpMTl6gYSfFq9hnxrkbLaMV0CARq/MFvDQJmcmYon/k8uZqXy6DfzewKDV8tx9kY2WUZg==} + peerDependencies: + '@codemirror/language': ^6.0.0 + + '@replit/codemirror-lang-svelte@6.0.0': + resolution: {integrity: sha512-U2OqqgMM6jKelL0GNWbAmqlu1S078zZNoBqlJBW+retTc5M4Mha6/Y2cf4SVg6ddgloJvmcSpt4hHrVoM4ePRA==} + peerDependencies: + '@codemirror/autocomplete': ^6.0.0 + '@codemirror/lang-css': ^6.0.1 + '@codemirror/lang-html': ^6.2.0 + '@codemirror/lang-javascript': ^6.1.1 + '@codemirror/language': ^6.0.0 + '@codemirror/state': ^6.0.0 + '@codemirror/view': ^6.0.0 + '@lezer/common': ^1.0.0 + '@lezer/highlight': ^1.0.0 + '@lezer/javascript': ^1.2.0 + '@lezer/lr': ^1.0.0 + + '@resvg/resvg-wasm@2.4.1': + resolution: {integrity: sha512-yi6R0HyHtsoWTRA06Col4WoDs7SvlXU3DLMNP2bdAgs7HK18dTEVl1weXgxRzi8gwLteGUbIg29zulxIB3GSdg==} + engines: {node: '>= 10'} + + '@rollup/plugin-alias@5.1.1': + resolution: {integrity: sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-commonjs@25.0.8': + resolution: {integrity: sha512-ZEZWTK5n6Qde0to4vS9Mr5x/0UZoqCxPVR9KRUjU4kA2sO7GEUn1fop0DAwpO6z0Nw/kJON9bDmSxdWxO/TT1A==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.68.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-commonjs@28.0.3': + resolution: {integrity: sha512-pyltgilam1QPdn+Zd9gaCfOLcnjMEJ9gV+bTw6/r73INdvzf1ah9zLIJBm+kW7R6IUFIQ1YO+VqZtYxZNWFPEQ==} + engines: {node: '>=16.0.0 || 14 >= 14.17'} + peerDependencies: + rollup: ^2.68.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-json@6.1.0': + resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-node-resolve@15.3.1': + resolution: {integrity: sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.78.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-node-resolve@16.0.0': + resolution: {integrity: sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.78.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-replace@5.0.7': + resolution: {integrity: sha512-PqxSfuorkHz/SPpyngLyg5GCEkOcee9M1bkxiVDr41Pd61mqP1PLOoDPbpl44SB2mQGKwV/In74gqQmGITOhEQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-terser@0.4.4': + resolution: {integrity: sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-typescript@12.1.1': + resolution: {integrity: sha512-t7O653DpfB5MbFrqPe/VcKFFkvRuFNp9qId3xq4Eth5xlyymzxNpye2z8Hrl0RIMuXTSr5GGcFpkdlMeacUiFQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.14.0||^3.0.0||^4.0.0 + tslib: '*' + typescript: '>=3.7.0' + peerDependenciesMeta: + rollup: + optional: true + tslib: + optional: true + + '@rollup/pluginutils@5.1.4': + resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.27.4': + resolution: {integrity: sha512-2Y3JT6f5MrQkICUyRVCw4oa0sutfAsgaSsb0Lmmy1Wi2y7X5vT9Euqw4gOsCyy0YfKURBg35nhUKZS4mDcfULw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm-eabi@4.36.0': + resolution: {integrity: sha512-jgrXjjcEwN6XpZXL0HUeOVGfjXhPyxAbbhD0BlXUB+abTOpbPiN5Wb3kOT7yb+uEtATNYF5x5gIfwutmuBA26w==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.27.4': + resolution: {integrity: sha512-wzKRQXISyi9UdCVRqEd0H4cMpzvHYt1f/C3CoIjES6cG++RHKhrBj2+29nPF0IB5kpy9MS71vs07fvrNGAl/iA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-android-arm64@4.36.0': + resolution: {integrity: sha512-NyfuLvdPdNUfUNeYKUwPwKsE5SXa2J6bCt2LdB/N+AxShnkpiczi3tcLJrm5mA+eqpy0HmaIY9F6XCa32N5yzg==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.27.4': + resolution: {integrity: sha512-PlNiRQapift4LNS8DPUHuDX/IdXiLjf8mc5vdEmUR0fF/pyy2qWwzdLjB+iZquGr8LuN4LnUoSEvKRwjSVYz3Q==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-arm64@4.36.0': + resolution: {integrity: sha512-JQ1Jk5G4bGrD4pWJQzWsD8I1n1mgPXq33+/vP4sk8j/z/C2siRuxZtaUA7yMTf71TCZTZl/4e1bfzwUmFb3+rw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.27.4': + resolution: {integrity: sha512-o9bH2dbdgBDJaXWJCDTNDYa171ACUdzpxSZt+u/AAeQ20Nk5x+IhA+zsGmrQtpkLiumRJEYef68gcpn2ooXhSQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.36.0': + resolution: {integrity: sha512-6c6wMZa1lrtiRsbDziCmjE53YbTkxMYhhnWnSW8R/yqsM7a6mSJ3uAVT0t8Y/DGt7gxUWYuFM4bwWk9XCJrFKA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.27.4': + resolution: {integrity: sha512-NBI2/i2hT9Q+HySSHTBh52da7isru4aAAo6qC3I7QFVsuhxi2gM8t/EI9EVcILiHLj1vfi+VGGPaLOUENn7pmw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-arm64@4.36.0': + resolution: {integrity: sha512-KXVsijKeJXOl8QzXTsA+sHVDsFOmMCdBRgFmBb+mfEb/7geR7+C8ypAml4fquUt14ZyVXaw2o1FWhqAfOvA4sg==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.27.4': + resolution: {integrity: sha512-wYcC5ycW2zvqtDYrE7deary2P2UFmSh85PUpAx+dwTCO9uw3sgzD6Gv9n5X4vLaQKsrfTSZZ7Z7uynQozPVvWA==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.36.0': + resolution: {integrity: sha512-dVeWq1ebbvByI+ndz4IJcD4a09RJgRYmLccwlQ8bPd4olz3Y213uf1iwvc7ZaxNn2ab7bjc08PrtBgMu6nb4pQ==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.27.4': + resolution: {integrity: sha512-9OwUnK/xKw6DyRlgx8UizeqRFOfi9mf5TYCw1uolDaJSbUmBxP85DE6T4ouCMoN6pXw8ZoTeZCSEfSaYo+/s1w==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-gnueabihf@4.36.0': + resolution: {integrity: sha512-bvXVU42mOVcF4le6XSjscdXjqx8okv4n5vmwgzcmtvFdifQ5U4dXFYaCB87namDRKlUL9ybVtLQ9ztnawaSzvg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.27.4': + resolution: {integrity: sha512-Vgdo4fpuphS9V24WOV+KwkCVJ72u7idTgQaBoLRD0UxBAWTF9GWurJO9YD9yh00BzbkhpeXtm6na+MvJU7Z73A==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.36.0': + resolution: {integrity: sha512-JFIQrDJYrxOnyDQGYkqnNBtjDwTgbasdbUiQvcU8JmGDfValfH1lNpng+4FWlhaVIR4KPkeddYjsVVbmJYvDcg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.27.4': + resolution: {integrity: sha512-pleyNgyd1kkBkw2kOqlBx+0atfIIkkExOTiifoODo6qKDSpnc6WzUY5RhHdmTdIJXBdSnh6JknnYTtmQyobrVg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.36.0': + resolution: {integrity: sha512-KqjYVh3oM1bj//5X7k79PSCZ6CvaVzb7Qs7VMWS+SlWB5M8p3FqufLP9VNp4CazJ0CsPDLwVD9r3vX7Ci4J56A==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.27.4': + resolution: {integrity: sha512-caluiUXvUuVyCHr5DxL8ohaaFFzPGmgmMvwmqAITMpV/Q+tPoaHZ/PWa3t8B2WyoRcIIuu1hkaW5KkeTDNSnMA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.36.0': + resolution: {integrity: sha512-QiGnhScND+mAAtfHqeT+cB1S9yFnNQ/EwCg5yE3MzoaZZnIV0RV9O5alJAoJKX/sBONVKeZdMfO8QSaWEygMhw==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.36.0': + resolution: {integrity: sha512-1ZPyEDWF8phd4FQtTzMh8FQwqzvIjLsl6/84gzUxnMNFBtExBtpL51H67mV9xipuxl1AEAerRBgBwFNpkw8+Lg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.27.4': + resolution: {integrity: sha512-FScrpHrO60hARyHh7s1zHE97u0KlT/RECzCKAdmI+LEoC1eDh/RDji9JgFqyO+wPDb86Oa/sXkily1+oi4FzJQ==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.36.0': + resolution: {integrity: sha512-VMPMEIUpPFKpPI9GZMhJrtu8rxnp6mJR3ZzQPykq4xc2GmdHj3Q4cA+7avMyegXy4n1v+Qynr9fR88BmyO74tg==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.27.4': + resolution: {integrity: sha512-qyyprhyGb7+RBfMPeww9FlHwKkCXdKHeGgSqmIXw9VSUtvyFZ6WZRtnxgbuz76FK7LyoN8t/eINRbPUcvXB5fw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.36.0': + resolution: {integrity: sha512-ttE6ayb/kHwNRJGYLpuAvB7SMtOeQnVXEIpMtAvx3kepFQeowVED0n1K9nAdraHUPJ5hydEMxBpIR7o4nrm8uA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.27.4': + resolution: {integrity: sha512-PFz+y2kb6tbh7m3A7nA9++eInGcDVZUACulf/KzDtovvdTizHpZaJty7Gp0lFwSQcrnebHOqxF1MaKZd7psVRg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.36.0': + resolution: {integrity: sha512-4a5gf2jpS0AIe7uBjxDeUMNcFmaRTbNv7NxI5xOCs4lhzsVyGR/0qBXduPnoWf6dGC365saTiwag8hP1imTgag==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.27.4': + resolution: {integrity: sha512-Ni8mMtfo+o/G7DVtweXXV/Ol2TFf63KYjTtoZ5f078AUgJTmaIJnj4JFU7TK/9SVWTaSJGxPi5zMDgK4w+Ez7Q==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.36.0': + resolution: {integrity: sha512-5KtoW8UWmwFKQ96aQL3LlRXX16IMwyzMq/jSSVIIyAANiE1doaQsx/KRyhAvpHlPjPiSU/AYX/8m+lQ9VToxFQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.27.4': + resolution: {integrity: sha512-5AeeAF1PB9TUzD+3cROzFTnAJAcVUGLuR8ng0E0WXGkYhp6RD6L+6szYVX+64Rs0r72019KHZS1ka1q+zU/wUw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.36.0': + resolution: {integrity: sha512-sycrYZPrv2ag4OCvaN5js+f01eoZ2U+RmT5as8vhxiFz+kxwlHrsxOwKPSA8WyS+Wc6Epid9QeI/IkQ9NkgYyQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.27.4': + resolution: {integrity: sha512-yOpVsA4K5qVwu2CaS3hHxluWIK5HQTjNV4tWjQXluMiiiu4pJj4BN98CvxohNCpcjMeTXk/ZMJBRbgRg8HBB6A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-arm64-msvc@4.36.0': + resolution: {integrity: sha512-qbqt4N7tokFwwSVlWDsjfoHgviS3n/vZ8LK0h1uLG9TYIRuUTJC88E1xb3LM2iqZ/WTqNQjYrtmtGmrmmawB6A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.27.4': + resolution: {integrity: sha512-KtwEJOaHAVJlxV92rNYiG9JQwQAdhBlrjNRp7P9L8Cb4Rer3in+0A+IPhJC9y68WAi9H0sX4AiG2NTsVlmqJeQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.36.0': + resolution: {integrity: sha512-t+RY0JuRamIocMuQcfwYSOkmdX9dtkr1PbhKW42AMvaDQa+jOdpUYysroTF/nuPpAaQMWp7ye+ndlmmthieJrQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.27.4': + resolution: {integrity: sha512-3j4jx1TppORdTAoBJRd+/wJRGCPC0ETWkXOecJ6PPZLj6SptXkrXcNqdj0oclbKML6FkQltdz7bBA3rUSirZug==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.36.0': + resolution: {integrity: sha512-aRXd7tRZkWLqGbChgcMMDEHjOKudo1kChb1Jt1IfR8cY/KIpgNviLeJy5FUb9IpSuQj8dU2fAYNMPW/hLKOSTw==} + cpu: [x64] + os: [win32] + + '@rushstack/eslint-patch@1.5.0': + resolution: {integrity: sha512-EF3948ckf3f5uPgYbQ6GhyA56Dmv8yg0+ir+BroRjwdxyZJsekhZzawOecC2rOTPCz173t7ZcR1HHZu0dZgOCw==} + + '@selderee/plugin-htmlparser2@0.11.0': + resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} + + '@shuding/opentype.js@1.4.0-beta.0': + resolution: {integrity: sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==} + engines: {node: '>= 8.0.0'} + hasBin: true + + '@sinclair/typebox@0.27.8': + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + + '@sindresorhus/slugify@2.2.1': + resolution: {integrity: sha512-MkngSCRZ8JdSOCHRaYd+D01XhvU3Hjy6MGl06zhOk614hp9EOAp5gIkBeQg7wtmxpitU6eAL4kdiRMcJa2dlrw==} + engines: {node: '>=12'} + + '@sindresorhus/transliterate@1.6.0': + resolution: {integrity: sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ==} + engines: {node: '>=12'} + + '@socket.io/component-emitter@3.1.2': + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + + '@stylistic/eslint-plugin-js@2.1.0': + resolution: {integrity: sha512-gdXUjGNSsnY6nPyqxu6lmDTtVrwCOjun4x8PUn0x04d5ucLI74N3MT1Q0UhdcOR9No3bo5PGDyBgXK+KmD787A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: '>=8.40.0' + + '@stylistic/eslint-plugin-jsx@2.1.0': + resolution: {integrity: sha512-mMD7S+IndZo2vxmwpHVTCwx2O1VdtE5tmpeNwgaEcXODzWV1WTWpnsc/PECQKIr/mkLPFWiSIqcuYNhQ/3l6AQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: '>=8.40.0' + + '@stylistic/eslint-plugin-plus@2.1.0': + resolution: {integrity: sha512-S5QAlgYXESJaSBFhBSBLZy9o36gXrXQwWSt6QkO+F0SrT9vpV5JF/VKoh+ojO7tHzd8Ckmyouq02TT9Sv2B0zQ==} + peerDependencies: + eslint: '*' + + '@stylistic/eslint-plugin-ts@2.1.0': + resolution: {integrity: sha512-2ioFibufHYBALx2TBrU4KXovCkN8qCqcb9yIHc0fyOfTaO5jw4d56WW7YRcF3Zgde6qFyXwAN6z/+w4pnmos1g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: '>=8.40.0' + + '@stylistic/eslint-plugin@2.1.0': + resolution: {integrity: sha512-cBBowKP2u/+uE5CzgH5w8pE9VKqcM7BXdIDPIbGt2rmLJGnA6MJPr9vYGaqgMoJFs7R/FzsMQerMvvEP40g2uw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: '>=8.40.0' + + '@swc/core-darwin-arm64@1.3.101': + resolution: {integrity: sha512-mNFK+uHNPRXSnfTOG34zJOeMl2waM4hF4a2NY7dkMXrPqw9CoJn4MwTXJcyMiSz1/BnNjjTCHF3Yhj0jPxmkzQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.3.101': + resolution: {integrity: sha512-B085j8XOx73Fg15KsHvzYWG262bRweGr3JooO1aW5ec5pYbz5Ew9VS5JKYS03w2UBSxf2maWdbPz2UFAxg0whw==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.3.101': + resolution: {integrity: sha512-9xLKRb6zSzRGPqdz52Hy5GuB1lSjmLqa0lST6MTFads3apmx4Vgs8Y5NuGhx/h2I8QM4jXdLbpqQlifpzTlSSw==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.3.101': + resolution: {integrity: sha512-oE+r1lo7g/vs96Weh2R5l971dt+ZLuhaUX+n3BfDdPxNHfObXgKMjO7E+QS5RbGjv/AwiPCxQmbdCp/xN5ICJA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-arm64-musl@1.3.101': + resolution: {integrity: sha512-OGjYG3H4BMOTnJWJyBIovCez6KiHF30zMIu4+lGJTCrxRI2fAjGLml3PEXj8tC3FMcud7U2WUn6TdG0/te2k6g==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-x64-gnu@1.3.101': + resolution: {integrity: sha512-/kBMcoF12PRO/lwa8Z7w4YyiKDcXQEiLvM+S3G9EvkoKYGgkkz4Q6PSNhF5rwg/E3+Hq5/9D2R+6nrkF287ihg==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-linux-x64-musl@1.3.101': + resolution: {integrity: sha512-kDN8lm4Eew0u1p+h1l3JzoeGgZPQ05qDE0czngnjmfpsH2sOZxVj1hdiCwS5lArpy7ktaLu5JdRnx70MkUzhXw==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-win32-arm64-msvc@1.3.101': + resolution: {integrity: sha512-9Wn8TTLWwJKw63K/S+jjrZb9yoJfJwCE2RV5vPCCWmlMf3U1AXj5XuWOLUX+Rp2sGKau7wZKsvywhheWm+qndQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.3.101': + resolution: {integrity: sha512-onO5KvICRVlu2xmr4//V2je9O2XgS1SGKpbX206KmmjcJhXN5EYLSxW9qgg+kgV5mip+sKTHTAu7IkzkAtElYA==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.3.101': + resolution: {integrity: sha512-T3GeJtNQV00YmiVw/88/nxJ/H43CJvFnpvBHCVn17xbahiVUOPOduh3rc9LgAkKiNt/aV8vU3OJR+6PhfMR7UQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.3.101': + resolution: {integrity: sha512-w5aQ9qYsd/IYmXADAnkXPGDMTqkQalIi+kfFf/MHRKTpaOL7DHjMXwPp/n8hJ0qNjRvchzmPtOqtPBiER50d8A==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': ^0.5.0 + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/helpers@0.5.2': + resolution: {integrity: sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==} + + '@swc/types@0.1.7': + resolution: {integrity: sha512-scHWahbHF0eyj3JsxG9CFJgFdFNaVQCNAimBlT6PzS3n/HptxqREjsm4OH6AN3lYcffZYSPxXW8ua2BEHp0lJQ==} + + '@tailwindcss/typography@0.5.10': + resolution: {integrity: sha512-Pe8BuPJQJd3FfRnm6H0ulKIGoMEQS+Vq01R6M5aCrFB/ccR/shT+0kXLjouGC1gFLm9hopTFN+DMP0pfwRWzPw==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders' + + '@tiptap/core@2.10.4': + resolution: {integrity: sha512-fExFRTRgb6MSpg2VvR5qO2dPTQAZWuUoU4UsBCurIVcPWcyVv4FG1YzgMyoLDKy44rebFtwUGJbfU9NzX7Q/bA==} + peerDependencies: + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-blockquote@2.10.4': + resolution: {integrity: sha512-4JSwAM3B92YWvGzu/Vd5rovPrCGwLSaSLD5rxcLyfxLSrTDQd3n7lp78pzVgGhunVECzaGF5A0ByWWpEyS0a3w==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-bold@2.10.4': + resolution: {integrity: sha512-SdO4oFQKaERCGfwOc1CLYQRtThENam2KWfWmvpsymknokt5qYzU57ft0SE1HQV9vVYEzZ9HrWIgv2xrgu0g9kg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-bubble-menu@2.10.4': + resolution: {integrity: sha512-GVtZwJaQyLBptMsmDtYl5GEobd1Uu7C9sc9Z+PdXwMuxmFfg+j07bCKCj5JJj/tjgXCSLVxWdTlDHxNrgzQHjw==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-bullet-list@2.10.4': + resolution: {integrity: sha512-JVwDPgOBYRU2ivaadOh4IaQYXQEiSw6sB36KT/bwqJF2GnEvLiMwptdRMn9Uuh6xYR3imjIZtV6uZAoneZdd6g==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-code-block@2.10.4': + resolution: {integrity: sha512-qS4jnbJqghNMT2+B+GQ807ATgqkL9OQ//NlL+ZwVSe+DPDduNA9B6IB9SrWENDfOnzekpi7kcEcm+RenELARRQ==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-code@2.10.4': + resolution: {integrity: sha512-Vj/N0nbSQiV1o7X7pRySK9Fu72Dd266gm27TSlsts6IwJu5MklFvz7ezJUWoLjt2wmCV8/U/USmk/39ic9qjvg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-document@2.10.4': + resolution: {integrity: sha512-1Pqrl6Rr9bVEHJ3zO2dM7UUA0Qn/r70JQ9YLlestjW1sbMaMuY3Ifvu2uSyUE7SAGV3gvxwNVQCrv8f0VlVEaA==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-dropcursor@2.10.4': + resolution: {integrity: sha512-0XEM/yNLaMc/sZlYOau7XpHyYiHT9LwXUe7kmze/L8eowIa/iLvmRbcnUd3rtlZ7x7wooE6UO9c7OtlREg4ZBw==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-floating-menu@2.10.4': + resolution: {integrity: sha512-K2MDiu6CwQ7+Jr6g1Lh3Tuxm1L6SefSHMpQO0UW3aRGwgEV5pjlrztnBFX4K9b7MNuQ4dJGCUK9u8Cv7Xss0qg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-gapcursor@2.10.4': + resolution: {integrity: sha512-KbJfoaqTZePpkWAN+klpK5j0UVtELxN7H5B0J556/UCB/rnq+OsdEFHPks2Ss9TidqWzRUqcxUE50UZ7b8h7Ug==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-hard-break@2.10.4': + resolution: {integrity: sha512-nW9wubW1A/CO2Ssn9wNMP08tR9Oarg9VUGzJ5qNuz38DDNyntE1SyDS+XStkeMq5nKqJ3YKhukyAJH/PiRq4Mg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-heading@2.10.4': + resolution: {integrity: sha512-7D0h0MIvE97Gx3Qwuo2xnPDK07WfCnyh4tpOPBOus4e1g6sgxVkwDwhbkYWiwvIrf4BUVJflnke/DEDCVp6/Eg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-history@2.10.4': + resolution: {integrity: sha512-fg6BNxbpMMtgKaiNI/GLcCzkxIQMwSYBhO9LA0CxLvmsWGU+My4r9W3DK6HwNoRJ9+6OleDPSLo1P73fbSTtEA==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-horizontal-rule@2.10.4': + resolution: {integrity: sha512-s9ycm/BOGoW3L0Epnj541vdngHbFbMM488HoODd1CmVSw1C+wBWFgsukgqKjlyE3VGfZXuSb1ur9zinW0RiLJQ==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-italic@2.10.4': + resolution: {integrity: sha512-8MIQ+wsbyxNCZDCFTVTOXrS2AvFyOhtlBNgVU2+6r6xnJV4AcfEA3qclysqrjOlL117ped/nzDeoB0AeX0CI+Q==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-list-item@2.10.4': + resolution: {integrity: sha512-8K3WUD5fPyw2poQKnJGGm7zlfeIbpld92+SRF4M9wkp95EzvgexTlodvxlrL3i8zKXcQQVyExWA8kCcGPFb9bA==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-mention@2.10.4': + resolution: {integrity: sha512-pVouKWxSVQSy4zn6HrljPIP1AG826gkm/w18Asi8QnZvR0AMqGLh9q7qd9Kc0j8NKoCzlzK8hECGlYPEaBldow==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + '@tiptap/suggestion': ^2.7.0 + + '@tiptap/extension-ordered-list@2.10.4': + resolution: {integrity: sha512-NaeEu+qFG2O0emc8WlwOM7DKNKOaqHWuNkuKrrmQzslgL+UQSEGlGMo6NEJ5sLLckPBDpIa0MuRm30407JE+cg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-paragraph@2.10.4': + resolution: {integrity: sha512-SRNVhT8OXqjpZtcyuOtofbtOpXXFrQrjqqCc/yXebda//2SfUTOvB16Lss77vQOWi6xr7TF1mZuowJgSTkcczw==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-placeholder@2.10.4': + resolution: {integrity: sha512-leWG4xP7cvddR6alGZS7yojOh9941bxehgAeQDLlEisaJcNa2Od5Vbap2zipjc5sXMxZakQVChL27oH1wWhHkQ==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-strike@2.10.4': + resolution: {integrity: sha512-OibipsomFpOJWTPVX/z4Z53HgwDA93lE/loHGa+ONJfML1dO6Zd6UTwzaVO1/g8WOwRgwkYu/6JnhxLKRlP8Lg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-text-style@2.10.4': + resolution: {integrity: sha512-ibq7avkcwHyUSG53Hf+P31rrwsKVbbiqbWZM4kXC7M2X3iUwFrtvaa+SWzyWQfE1jl2cCrD1+rfSkj/alcOKGg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-text@2.10.4': + resolution: {integrity: sha512-wPdVxCHrIS9S+8n08lgyyqRZPj9FBbyLlFt74/lV5yBC3LOorq1VKdjrTskmaj4jud7ImXoKDyBddAYTHdJ1xw==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/pm@2.10.4': + resolution: {integrity: sha512-pZ4NEkRtYoDLe0spARvXZ1N3hNv/5u6vfPdPtEbmNpoOSjSNqDC1kVM+qJY0iaCYpxbxcv7cxn3kBumcFLQpJQ==} + + '@tiptap/react@2.10.4': + resolution: {integrity: sha512-JTeqDB+xgjo46QC9ILRXe2TcSfxKVRwhZ3vDvYoemN7giRk5a/WsCF1VQIT1fax+tCl6kfv3U1f4Mkx0DkbPkA==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tiptap/starter-kit@2.10.4': + resolution: {integrity: sha512-tu/WCs9Mkr5Nt8c3/uC4VvAbQlVX0OY7ygcqdzHGUeG9zP3twdW7o5xM3kyDKR2++sbVzqu5Ll5qNU+1JZvPGQ==} + + '@tiptap/suggestion@2.10.4': + resolution: {integrity: sha512-7Bzcn1REA7OmVRxiMF2kVK9EhosXotdLAGaEvSbn4zQtHCJG0tREuYvPy53LGzVuPkBDR6Pf6sp1QbGvSne/8g==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tootallnate/once@1.1.2': + resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} + engines: {node: '>= 6'} + + '@trysound/sax@0.2.0': + resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} + engines: {node: '>=10.13.0'} + + '@tsconfig/node10@1.0.11': + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + '@types/chai@4.3.16': + resolution: {integrity: sha512-PatH4iOdyh3MyWtmHVFXLWCCIhUbopaltqddG9BzB+gMIzee2MJrvd+jouii9Z3wzQJruGWAm7WOMjgfG8hQlQ==} + + '@types/chroma-js@2.4.4': + resolution: {integrity: sha512-/DTccpHTaKomqussrn+ciEvfW4k6NAHzNzs/sts1TCqg333qNxOhy8TNIoQCmbGG3Tl8KdEhkGAssb1n3mTXiQ==} + + '@types/color-convert@2.0.3': + resolution: {integrity: sha512-2Q6wzrNiuEvYxVQqhh7sXM2mhIhvZR/Paq4FdsQkOMgWsCIkKvSGj8Le1/XalulrmgOzPMqNa0ix+ePY4hTrfg==} + + '@types/color-name@1.1.4': + resolution: {integrity: sha512-hulKeREDdLFesGQjl96+4aoJSHY5b2GRjagzzcqCfIrWhe5vkCqIvrLbqzBaI1q94Vg8DNJZZqTR5ocdWmWclg==} + + '@types/color@3.0.6': + resolution: {integrity: sha512-NMiNcZFRUAiUUCCf7zkAelY8eV3aKqfbzyFQlXpPIEeoNDbsEHGpb854V3gzTsGKYj830I5zPuOwU/TP5/cW6A==} + + '@types/cookie@0.4.1': + resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} + + '@types/cors@2.8.17': + resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} + + '@types/d3-array@3.2.1': + resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.0': + resolution: {integrity: sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==} + + '@types/d3-scale@4.0.8': + resolution: {integrity: sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==} + + '@types/d3-shape@3.1.6': + resolution: {integrity: sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==} + + '@types/d3-time@3.0.3': + resolution: {integrity: sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/debug@4.1.9': + resolution: {integrity: sha512-8Hz50m2eoS56ldRlepxSBa6PWEVCtzUo/92HgLc2qTMnotJNIm7xP+UZhyWoYsyOdd5dxZ+NZLb24rsKyFs2ow==} + + '@types/dedent@0.7.2': + resolution: {integrity: sha512-kRiitIeUg1mPV9yH4VUJ/1uk2XjyANfeL8/7rH1tsjvHeO9PJLBHJIYsFWmAvmGj5u8rj+1TZx7PZzW2qLw3Lw==} + + '@types/deep-equal@1.0.4': + resolution: {integrity: sha512-tqdiS4otQP4KmY0PR3u6KbZ5EWvhNdUoS/jc93UuK23C220lOZ/9TvjfxdPcKvqwwDVtmtSCrnr0p/2dirAxkA==} + + '@types/diff@5.2.1': + resolution: {integrity: sha512-uxpcuwWJGhe2AR1g8hD9F5OYGCqjqWnBUQFD8gMZsDbv8oPHzxJF6iMO6n8Tk0AdzlxoaaoQhOYlIg/PukVU8g==} + + '@types/dompurify@3.0.5': + resolution: {integrity: sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==} + + '@types/eslint-scope@3.7.7': + resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} + + '@types/eslint@8.56.10': + resolution: {integrity: sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==} + + '@types/estree@1.0.6': + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + + '@types/estree@1.0.7': + resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} + + '@types/fast-levenshtein@0.0.4': + resolution: {integrity: sha512-tkDveuitddQCxut1Db8eEFfMahTjOumTJGPHmT9E7KUH+DkVq9WTpVvlfenf3S+uCBeu8j5FP2xik/KfxOEjeA==} + + '@types/fs-extra@11.0.4': + resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} + + '@types/git-url-parse@9.0.3': + resolution: {integrity: sha512-Wrb8zeghhpKbYuqAOg203g+9YSNlrZWNZYvwxJuDF4dTmerijqpnGbI79yCuPtHSXHPEwv1pAFUB4zsSqn82Og==} + + '@types/glob@7.2.0': + resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} + + '@types/hast@2.3.6': + resolution: {integrity: sha512-47rJE80oqPmFdVDCD7IheXBrVdwuBgsYwoczFvKmwfo2Mzsnt+V9OONsYauFmICb6lQPpCuXYJWejBNs4pDJRg==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/he@1.2.3': + resolution: {integrity: sha512-q67/qwlxblDzEDvzHhVkwc1gzVWxaNxeyHUBF4xElrvjL11O+Ytze+1fGpBHlr/H9myiBUaUXNnNPmBHxxfAcA==} + + '@types/humanize-duration@3.27.4': + resolution: {integrity: sha512-yaf7kan2Sq0goxpbcwTQ+8E9RP6HutFBPv74T/IA/ojcHKhuKVlk2YFYyHhWZeLvZPzzLE3aatuQB4h0iqyyUA==} + + '@types/js-levenshtein@1.1.3': + resolution: {integrity: sha512-jd+Q+sD20Qfu9e2aEXogiO3vpOC1PYJOUdyN9gvs4Qrvkg4wF43L5OhqrPeokdv8TL0/mXoYfpkcoGZMNN2pkQ==} + + '@types/js-yaml@4.0.9': + resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/json-stable-stringify@1.0.36': + resolution: {integrity: sha512-b7bq23s4fgBB76n34m2b3RBf6M369B0Z9uRR8aHTMd8kZISRkmDEpPD8hhpYvDFzr3bJCPES96cm3Q6qRNDbQw==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/jsonfile@6.1.4': + resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} + + '@types/katex@0.16.3': + resolution: {integrity: sha512-CeVMX9EhVUW8MWnei05eIRks4D5Wscw/W9Byz1s3PA+yJvcdvq9SaDjiUKvRvEgjpdTyJMjQA43ae4KTwsvOPg==} + + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/lodash-es@4.17.10': + resolution: {integrity: sha512-YJP+w/2khSBwbUSFdGsSqmDvmnN3cCKoPOL7Zjle6s30ZtemkkqhjVfFqGwPN7ASil5VyjE2GtyU/yqYY6mC0A==} + + '@types/lodash@4.14.200': + resolution: {integrity: sha512-YI/M/4HRImtNf3pJgbF+W6FrXovqj+T+/HpENLTooK9PnkacBsDpeP3IpHab40CClUfhNmdM2WTNP2sa2dni5Q==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + + '@types/mdast@3.0.13': + resolution: {integrity: sha512-HjiGiWedR0DVFkeNljpa6Lv4/IZU1+30VY5d747K7lBudFc3R0Ibr6yJ9lN3BE28VnZyDfLF/VB1Ql1ZIbKrmg==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + + '@types/minimatch@5.1.2': + resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} + + '@types/mocha@10.0.6': + resolution: {integrity: sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg==} + + '@types/ms@0.7.32': + resolution: {integrity: sha512-xPSg0jm4mqgEkNhowKgZFBNtwoEwF6gJ4Dhww+GFpm3IgtNseHQZ5IqdNwnquZEoANxyDAKDRAdVo4Z72VvD/g==} + + '@types/node-fetch@2.6.6': + resolution: {integrity: sha512-95X8guJYhfqiuVVhRFxVQcf4hW/2bCuoPwDasMf/531STFoNoWTT7YDnWdXHEZKqAGUigmpG31r2FE70LwnzJw==} + + '@types/node-forge@1.3.11': + resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==} + + '@types/node@17.0.45': + resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} + + '@types/node@18.19.33': + resolution: {integrity: sha512-NR9+KrpSajr2qBVp/Yt5TU/rp+b5Mayi3+OlMlcg2cVCfRmcG5PWZ7S4+MG9PZ5gWBoc9Pd0BKSRViuBCRPu0A==} + + '@types/node@20.12.12': + resolution: {integrity: sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==} + + '@types/normalize-package-data@2.4.4': + resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + + '@types/numeral@2.0.5': + resolution: {integrity: sha512-kH8I7OSSwQu9DS9JYdFWbuvhVzvFRoCPCkGxNwoGgaPeDfEPJlcxNvEOypZhQ3XXHsGbfIuYcxcJxKUfJHnRfw==} + + '@types/object-hash@3.0.6': + resolution: {integrity: sha512-fOBV8C1FIu2ELinoILQ+ApxcUKz4ngq+IWUYrxSGjXzzjUALijilampwkMgEtJ+h2njAW3pi853QpzNVCHB73w==} + + '@types/parse-path@7.0.3': + resolution: {integrity: sha512-LriObC2+KYZD3FzCrgWGv/qufdUy4eXrxcLgQMfYXgPbLIecKIsVBaQgUPmxSSLcjmYbDTQbMgr6qr6l/eb7Bg==} + + '@types/parse5@6.0.3': + resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==} + + '@types/prismjs@1.26.4': + resolution: {integrity: sha512-rlAnzkW2sZOjbqZ743IHUhFcvzaGbqijwOu8QZnZCjfQzBqFE3s4lOTJEsxikImav9uzz/42I+O7YUs1mWgMlg==} + + '@types/prop-types@15.7.7': + resolution: {integrity: sha512-FbtmBWCcSa2J4zL781Zf1p5YUBXQomPEcep9QZCfRfQgTxz3pJWiDFLebohZ9fFntX5ibzOkSsrJ0TEew8cAog==} + + '@types/react-dom@18.2.23': + resolution: {integrity: sha512-ZQ71wgGOTmDYpnav2knkjr3qXdAFu0vsk8Ci5w3pGAIdj7/kKAyn+VsQDhXsmzzzepAiI9leWMmubXz690AI/A==} + + '@types/react-dom@18.2.8': + resolution: {integrity: sha512-bAIvO5lN/U8sPGvs1Xm61rlRHHaq5rp5N3kp9C+NJ/Q41P8iqjkXSu0+/qu8POsjH9pNWb0OYabFez7taP7omw==} + + '@types/react-syntax-highlighter@15.5.7': + resolution: {integrity: sha512-bo5fEO5toQeyCp0zVHBeggclqf5SQ/Z5blfFmjwO5dkMVGPgmiwZsJh9nu/Bo5L7IHTuGWrja6LxJVE2uB5ZrQ==} + + '@types/react@18.2.23': + resolution: {integrity: sha512-qHLW6n1q2+7KyBEYnrZpcsAmU/iiCh9WGCKgXvMxx89+TYdJWRjZohVIo9XTcoLhfX3+/hP0Pbulu3bCZQ9PSA==} + + '@types/readable-stream@4.0.14': + resolution: {integrity: sha512-xZn/AuUbCMShGsqH/ehZtGDwQtbx00M9rZ2ENLe4tOjFZ/JFeWMhEZkk2fEe1jAUqqEAURIkFJ7Az/go8mM1/w==} + + '@types/resolve@1.20.2': + resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + + '@types/scheduler@0.16.4': + resolution: {integrity: sha512-2L9ifAGl7wmXwP4v3pN4p2FLhD0O1qsJpvKmNin5VA8+UvNVb447UDaAEV6UdrkA+m/Xs58U1RFps44x6TFsVQ==} + + '@types/seedrandom@3.0.8': + resolution: {integrity: sha512-TY1eezMU2zH2ozQoAFAQFOPpvP15g+ZgSfTZt31AUUH/Rxtnz3H+A/Sv1Snw2/amp//omibc+AEkTaA8KUeOLQ==} + + '@types/semver@7.5.8': + resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} + + '@types/tinycolor2@1.4.6': + resolution: {integrity: sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@types/unist@2.0.8': + resolution: {integrity: sha512-d0XxK3YTObnWVp6rZuev3c49+j4Lo8g4L1ZRm9z5L0xpoZycUPshHgczK5gsUMaZOstjVYYi09p5gYvUtfChYw==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + + '@types/uuid@9.0.8': + resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} + + '@types/vscode@1.89.0': + resolution: {integrity: sha512-TMfGKLSVxfGfoO8JfIE/neZqv7QLwS4nwPwL/NwMvxtAY2230H2I4Z5xx6836pmJvMAzqooRQ4pmLm7RUicP3A==} + + '@types/webpack@5.28.5': + resolution: {integrity: sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==} + + '@types/win-ca@3.5.4': + resolution: {integrity: sha512-WDlA6ZvTg1g9ygKcDCQb4GjEtk0RcZyx09btQea9NZqfKPl+WtyOlEwNfYMhK2E5sRCo6hMtct1EX3l04yAC1A==} + + '@types/ws@8.5.9': + resolution: {integrity: sha512-jbdrY0a8lxfdTp/+r7Z4CkycbOFN8WX+IOchLJr3juT/xzbJ8URyTVSJ/hvNdadTgM1mnedb47n+Y31GsFnQlg==} + + '@typescript-eslint/eslint-plugin@6.21.0': + resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/eslint-plugin@7.10.0': + resolution: {integrity: sha512-PzCr+a/KAef5ZawX7nbyNwBDtM1HdLIT53aSA2DDlxmxMngZ43O8SIePOeX8H5S+FHXeI6t97mTt/dDdzY4Fyw==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + '@typescript-eslint/parser': ^7.0.0 + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/eslint-plugin@7.4.0': + resolution: {integrity: sha512-yHMQ/oFaM7HZdVrVm/M2WHaNPgyuJH4WelkSVEWSSsir34kxW2kDJCxlXRhhGWEsMN0WAW/vLpKfKVcm8k+MPw==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + '@typescript-eslint/parser': ^7.0.0 + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@5.62.0': + resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@6.21.0': + resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@7.10.0': + resolution: {integrity: sha512-2EjZMA0LUW5V5tGQiaa2Gys+nKdfrn2xiTIBLR4fxmPmVSvgPcKNW+AE/ln9k0A4zDUti0J/GZXMDupQoI+e1w==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/scope-manager@5.62.0': + resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@typescript-eslint/scope-manager@6.21.0': + resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} + engines: {node: ^16.0.0 || >=18.0.0} + + '@typescript-eslint/scope-manager@7.10.0': + resolution: {integrity: sha512-7L01/K8W/VGl7noe2mgH0K7BE29Sq6KAbVmxurj8GGaPDZXPr8EEQ2seOeAS+mEV9DnzxBQB6ax6qQQ5C6P4xg==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/scope-manager@7.4.0': + resolution: {integrity: sha512-68VqENG5HK27ypafqLVs8qO+RkNc7TezCduYrx8YJpXq2QGZ30vmNZGJJJC48+MVn4G2dCV8m5ZTVnzRexTVtw==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/type-utils@6.21.0': + resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/type-utils@7.10.0': + resolution: {integrity: sha512-D7tS4WDkJWrVkuzgm90qYw9RdgBcrWmbbRkrLA4d7Pg3w0ttVGDsvYGV19SH8gPR5L7OtcN5J1hTtyenO9xE9g==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/type-utils@7.4.0': + resolution: {integrity: sha512-247ETeHgr9WTRMqHbbQdzwzhuyaJ8dPTuyuUEMANqzMRB1rj/9qFIuIXK7l0FX9i9FXbHeBQl/4uz6mYuCE7Aw==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@5.62.0': + resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@typescript-eslint/types@6.21.0': + resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} + engines: {node: ^16.0.0 || >=18.0.0} + + '@typescript-eslint/types@7.10.0': + resolution: {integrity: sha512-7fNj+Ya35aNyhuqrA1E/VayQX9Elwr8NKZ4WueClR3KwJ7Xx9jcCdOrLW04h51de/+gNbyFMs+IDxh5xIwfbNg==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/types@7.4.0': + resolution: {integrity: sha512-mjQopsbffzJskos5B4HmbsadSJQWaRK0UxqQ7GuNA9Ga4bEKeiO6b2DnB6cM6bpc8lemaPseh0H9B/wyg+J7rw==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/typescript-estree@5.62.0': + resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/typescript-estree@6.21.0': + resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/typescript-estree@7.10.0': + resolution: {integrity: sha512-LXFnQJjL9XIcxeVfqmNj60YhatpRLt6UhdlFwAkjNc6jSUlK8zQOl1oktAP8PlWFzPQC1jny/8Bai3/HPuvN5g==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/typescript-estree@7.4.0': + resolution: {integrity: sha512-A99j5AYoME/UBQ1ucEbbMEmGkN7SE0BvZFreSnTd1luq7yulcHdyGamZKizU7canpGDWGJ+Q6ZA9SyQobipePg==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@6.21.0': + resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + + '@typescript-eslint/utils@7.10.0': + resolution: {integrity: sha512-olzif1Fuo8R8m/qKkzJqT7qwy16CzPRWBvERS0uvyc+DHd8AKbO4Jb7kpAvVzMmZm8TrHnI7hvjN4I05zow+tg==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + + '@typescript-eslint/utils@7.4.0': + resolution: {integrity: sha512-NQt9QLM4Tt8qrlBVY9lkMYzfYtNz8/6qwZg8pI3cMGlPnj6mOpRxxAm7BMJN9K0AiY+1BwJ5lVC650YJqYOuNg==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + + '@typescript-eslint/visitor-keys@5.62.0': + resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@typescript-eslint/visitor-keys@6.21.0': + resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} + engines: {node: ^16.0.0 || >=18.0.0} + + '@typescript-eslint/visitor-keys@7.10.0': + resolution: {integrity: sha512-9ntIVgsi6gg6FIq9xjEO4VQJvwOqA3jaBFQJ/6TK5AvEup2+cECI6Fh7QiBxmfMHXU0V0J4RyPeOU1VDNzl9cg==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/visitor-keys@7.4.0': + resolution: {integrity: sha512-0zkC7YM0iX5Y41homUUeW1CHtZR01K3ybjM1l6QczoMuay0XKtrb93kv95AxUGwdjGr64nNqnOCwmEl616N8CA==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@uidotdev/usehooks@2.4.1': + resolution: {integrity: sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg==} + engines: {node: '>=16'} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + + '@uiw/codemirror-extensions-langs@4.21.21': + resolution: {integrity: sha512-h08pw2NeGLDgBiY8Ju5GNjfVzq1f6+wc0uPdqN5tkYBaKmByyKI10l5Gds7wBPzFH0uZlevP+Jyf9oSTcula5Q==} + peerDependencies: + '@codemirror/language-data': '>=6.0.0' + '@codemirror/legacy-modes': '>=6.0.0' + + '@ungap/structured-clone@1.2.0': + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + + '@upstash/redis@1.22.0': + resolution: {integrity: sha512-sXoJDoEqqik0HbrNE7yRWckOySEFsoBxfRdCgOqkc0w6py19ZZG50SpGkDDEUXSnBqP8VgGYXhWAiBpqxrt5oA==} + + '@urql/core@4.2.3': + resolution: {integrity: sha512-DJ9q9+lcs5JL8DcU2J3NqsgeXYJva+1+Qt8HU94kzTPqVOIRRA7ouvy4ksUfPY+B5G2PQ+vLh+JJGyZCNXv0cg==} + + '@urql/exchange-auth@2.1.6': + resolution: {integrity: sha512-snOlt7p5kYq0KnPDuXkKe2qW3/BucQZOElvTeo3svLQuk9JiNJVnm6ffQ6QGiGO+G3AtMrctnno1+X44fLtDuQ==} + + '@urql/exchange-graphcache@6.4.0': + resolution: {integrity: sha512-VgcPdDNR3hSJDuf+mj0OZWzOzQccA8vT8xphxtO1MoJlgv1A4VhjLd75pjVvGz29ZHN90jEbdyBKJz6GShT7qA==} + + '@vercel/analytics@1.0.2': + resolution: {integrity: sha512-BZFxVrv24VbNNl5xMxqUojQIegEeXMI6rX3rg1uVLYUEXsuKNBSAEQf4BWEcjQDp/8aYJOj6m8V4PUA3x/cxgg==} + + '@vercel/kv@0.2.3': + resolution: {integrity: sha512-Wq1+EsRBQmvLlcqCZeYVg1MAARWrnETgLe3Sy3UCqG+zg7LThpkt0YHZe1NN3Aj4IRmCKQamotWrLDdEx+ZB3w==} + engines: {node: '>=14.6'} + + '@vercel/og@0.5.17': + resolution: {integrity: sha512-/GOyUBq3MhB3ygbhTJoZ0cHvwlyKdt0g8f4npuj4mwFlp57S7j4XWwgtqILK3XdFf25esN9i77fInrDkeRJfgA==} + engines: {node: '>=16'} + + '@vitest/expect@1.6.0': + resolution: {integrity: sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==} + + '@vitest/runner@1.6.0': + resolution: {integrity: sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==} + + '@vitest/snapshot@1.6.0': + resolution: {integrity: sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==} + + '@vitest/spy@1.6.0': + resolution: {integrity: sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==} + + '@vitest/utils@1.6.0': + resolution: {integrity: sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==} + + '@vscode/test-electron@2.3.10': + resolution: {integrity: sha512-FxMqrvUm6a8S5tP4CymNJ40e6kD+wUTWTc6K32U629yrCCa+kl/rmpkC2gKpN4F4zjg1r+0Hnk9sl0+N2atsYA==} + engines: {node: '>=16'} + + '@vscode/test-web@0.0.63': + resolution: {integrity: sha512-FIFfT2gZeEp0d1RnR+IYORS9HZPTtlTNuMLdZpO5c9sAtz5rCHPWsCD5VxnV4U08Slz5GdzgH7pDQspb9Oa8ZQ==} + engines: {node: '>=16'} + hasBin: true + + '@vscode/vsce-sign-alpine-arm64@2.0.2': + resolution: {integrity: sha512-E80YvqhtZCLUv3YAf9+tIbbqoinWLCO/B3j03yQPbjT3ZIHCliKZlsy1peNc4XNZ5uIb87Jn0HWx/ZbPXviuAQ==} + cpu: [arm64] + os: [alpine] + + '@vscode/vsce-sign-alpine-x64@2.0.2': + resolution: {integrity: sha512-n1WC15MSMvTaeJ5KjWCzo0nzjydwxLyoHiMJHu1Ov0VWTZiddasmOQHekA47tFRycnt4FsQrlkSCTdgHppn6bw==} + cpu: [x64] + os: [alpine] + + '@vscode/vsce-sign-darwin-arm64@2.0.2': + resolution: {integrity: sha512-rz8F4pMcxPj8fjKAJIfkUT8ycG9CjIp888VY/6pq6cuI2qEzQ0+b5p3xb74CJnBbSC0p2eRVoe+WgNCAxCLtzQ==} + cpu: [arm64] + os: [darwin] + + '@vscode/vsce-sign-darwin-x64@2.0.2': + resolution: {integrity: sha512-MCjPrQ5MY/QVoZ6n0D92jcRb7eYvxAujG/AH2yM6lI0BspvJQxp0o9s5oiAM9r32r9tkLpiy5s2icsbwefAQIw==} + cpu: [x64] + os: [darwin] + + '@vscode/vsce-sign-linux-arm64@2.0.2': + resolution: {integrity: sha512-Ybeu7cA6+/koxszsORXX0OJk9N0GgfHq70Wqi4vv2iJCZvBrOWwcIrxKjvFtwyDgdeQzgPheH5nhLVl5eQy7WA==} + cpu: [arm64] + os: [linux] + + '@vscode/vsce-sign-linux-arm@2.0.2': + resolution: {integrity: sha512-Fkb5jpbfhZKVw3xwR6t7WYfwKZktVGNXdg1m08uEx1anO0oUPUkoQRsNm4QniL3hmfw0ijg00YA6TrxCRkPVOQ==} + cpu: [arm] + os: [linux] + + '@vscode/vsce-sign-linux-x64@2.0.2': + resolution: {integrity: sha512-NsPPFVtLaTlVJKOiTnO8Cl78LZNWy0Q8iAg+LlBiCDEgC12Gt4WXOSs2pmcIjDYzj2kY4NwdeN1mBTaujYZaPg==} + cpu: [x64] + os: [linux] + + '@vscode/vsce-sign-win32-arm64@2.0.2': + resolution: {integrity: sha512-wPs848ymZ3Ny+Y1Qlyi7mcT6VSigG89FWQnp2qRYCyMhdJxOpA4lDwxzlpL8fG6xC8GjQjGDkwbkWUcCobvksQ==} + cpu: [arm64] + os: [win32] + + '@vscode/vsce-sign-win32-x64@2.0.2': + resolution: {integrity: sha512-pAiRN6qSAhDM5SVOIxgx+2xnoVUePHbRNC7OD2aOR3WltTKxxF25OfpK8h8UQ7A0BuRkSgREbB59DBlFk4iAeg==} + cpu: [x64] + os: [win32] + + '@vscode/vsce-sign@2.0.4': + resolution: {integrity: sha512-0uL32egStKYfy60IqnynAChMTbL0oqpqk0Ew0YHiIb+fayuGZWADuIPHWUcY1GCnAA+VgchOPDMxnc2R3XGWEA==} + + '@vscode/vsce@2.26.1': + resolution: {integrity: sha512-QOG6Ht7V93nhwcBxPWcG33UK0qDGEoJdg0xtVeaTN27W6PGdMJUJGTPhB/sNHUIFKwvwzv/zMAHvDgMNXbcwlA==} + engines: {node: '>= 16'} + hasBin: true + + '@vscode/vsce@3.1.1': + resolution: {integrity: sha512-N62Ca9ElRPLUUzf7l9CeEBlLrYzFPRQq7huKk4pVW+LjIOSXfFIPudixn5QvZcz+yXDOh15IopI3K2o3y9666Q==} + engines: {node: '>= 20'} + hasBin: true + + '@vue/compiler-core@3.4.27': + resolution: {integrity: sha512-E+RyqY24KnyDXsCuQrI+mlcdW3ALND6U7Gqa/+bVwbcpcR3BRRIckFoz7Qyd4TTlnugtwuI7YgjbvsLmxb+yvg==} + + '@vue/compiler-dom@3.4.27': + resolution: {integrity: sha512-kUTvochG/oVgE1w5ViSr3KUBh9X7CWirebA3bezTbB5ZKBQZwR2Mwj9uoSKRMFcz4gSMzzLXBPD6KpCLb9nvWw==} + + '@vue/compiler-sfc@3.4.27': + resolution: {integrity: sha512-nDwntUEADssW8e0rrmE0+OrONwmRlegDA1pD6QhVeXxjIytV03yDqTey9SBDiALsvAd5U4ZrEKbMyVXhX6mCGA==} + + '@vue/compiler-ssr@3.4.27': + resolution: {integrity: sha512-CVRzSJIltzMG5FcidsW0jKNQnNRYC8bT21VegyMMtHmhW3UOI7knmUehzswXLrExDLE6lQCZdrhD4ogI7c+vuw==} + + '@vue/shared@3.4.27': + resolution: {integrity: sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==} + + '@webassemblyjs/ast@1.12.1': + resolution: {integrity: sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==} + + '@webassemblyjs/floating-point-hex-parser@1.11.6': + resolution: {integrity: sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==} + + '@webassemblyjs/helper-api-error@1.11.6': + resolution: {integrity: sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==} + + '@webassemblyjs/helper-buffer@1.12.1': + resolution: {integrity: sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==} + + '@webassemblyjs/helper-numbers@1.11.6': + resolution: {integrity: sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==} + + '@webassemblyjs/helper-wasm-bytecode@1.11.6': + resolution: {integrity: sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==} + + '@webassemblyjs/helper-wasm-section@1.12.1': + resolution: {integrity: sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==} + + '@webassemblyjs/ieee754@1.11.6': + resolution: {integrity: sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==} + + '@webassemblyjs/leb128@1.11.6': + resolution: {integrity: sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==} + + '@webassemblyjs/utf8@1.11.6': + resolution: {integrity: sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==} + + '@webassemblyjs/wasm-edit@1.12.1': + resolution: {integrity: sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==} + + '@webassemblyjs/wasm-gen@1.12.1': + resolution: {integrity: sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==} + + '@webassemblyjs/wasm-opt@1.12.1': + resolution: {integrity: sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==} + + '@webassemblyjs/wasm-parser@1.12.1': + resolution: {integrity: sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==} + + '@webassemblyjs/wast-printer@1.12.1': + resolution: {integrity: sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==} + + '@whatwg-node/events@0.0.3': + resolution: {integrity: sha512-IqnKIDWfXBJkvy/k6tzskWTc2NK3LcqHlb+KHGCrjOCH4jfQckRX0NAiIcC/vIqQkzLYw2r2CTSwAxcrtcD6lA==} + + '@whatwg-node/events@0.1.1': + resolution: {integrity: sha512-AyQEn5hIPV7Ze+xFoXVU3QTHXVbWPrzaOkxtENMPMuNL6VVHrp4hHfDt9nrQpjO7BgvuM95dMtkycX5M/DZR3w==} + engines: {node: '>=16.0.0'} + + '@whatwg-node/fetch@0.8.8': + resolution: {integrity: sha512-CdcjGC2vdKhc13KKxgsc6/616BQ7ooDIgPeTuAiE8qfCnS0mGzcfCOoZXypQSz73nxI+GWc7ZReIAVhxoE1KCg==} + + '@whatwg-node/fetch@0.9.14': + resolution: {integrity: sha512-wurZC82zzZwXRDSW0OS9l141DynaJQh7Yt0FD1xZ8niX7/Et/7RoiLiltbVU1fSF1RR9z6ndEaTUQBAmddTm1w==} + engines: {node: '>=16.0.0'} + + '@whatwg-node/node-fetch@0.3.6': + resolution: {integrity: sha512-w9wKgDO4C95qnXZRwZTfCmLWqyRnooGjcIwG0wADWjw9/HN0p7dtvtgSvItZtUyNteEvgTrd8QojNEqV6DAGTA==} + + '@whatwg-node/node-fetch@0.5.0': + resolution: {integrity: sha512-q76lDAafvHNGWedNAVHrz/EyYTS8qwRLcwne8SJQdRN5P3HydxU6XROFvJfTML6KZXQX2FDdGY4/SnaNyd7M0Q==} + engines: {node: '>=16.0.0'} + + '@xtuc/ieee754@1.2.0': + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + + '@xtuc/long@4.2.2': + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + + abbrev@2.0.0: + resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + acorn-import-assertions@1.9.0: + resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} + deprecated: package has been renamed to acorn-import-attributes + peerDependencies: + acorn: ^8 + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn-walk@8.3.2: + resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} + engines: {node: '>=0.4.0'} + + acorn@8.11.3: + resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} + engines: {node: '>=0.4.0'} + hasBin: true + + acorn@8.14.1: + resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agent-base@7.1.0: + resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==} + engines: {node: '>= 14'} + + agent-base@7.1.1: + resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==} + engines: {node: '>= 14'} + + agentkeepalive@4.5.0: + resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==} + engines: {node: '>= 8.0.0'} + + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + + ajv-keywords@3.5.2: + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + anser@2.1.1: + resolution: {integrity: sha512-nqLm4HxOTpeLOxcmB3QWmV5TcDFhW9y/fyQ+hivtDFcK4OQ+pQ5fzPnXHM1Mfcm0VkLtvVi1TCPr++Qy0Q/3EQ==} + + ansi-colors@4.1.1: + resolution: {integrity: sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==} + engines: {node: '>=6'} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-escapes@6.2.1: + resolution: {integrity: sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==} + engines: {node: '>=14.16'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + are-docs-informative@0.0.2: + resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==} + engines: {node: '>=14'} + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-hidden@1.2.3: + resolution: {integrity: sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==} + engines: {node: '>=10'} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-buffer-byte-length@1.0.0: + resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} + + array-buffer-byte-length@1.0.1: + resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} + engines: {node: '>= 0.4'} + + array-includes@3.1.7: + resolution: {integrity: sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==} + engines: {node: '>= 0.4'} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + array.prototype.findlastindex@1.2.3: + resolution: {integrity: sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.2: + resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.2: + resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.2: + resolution: {integrity: sha512-HuQCHOlk1Weat5jzStICBCd83NxiIMwqDg/dHEsoefabn/hJRj5pVdWcPUSpRrwhwxZOsQassMpgN/xRYFBMIg==} + + arraybuffer.prototype.slice@1.0.2: + resolution: {integrity: sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==} + engines: {node: '>= 0.4'} + + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + + asn1js@3.0.5: + resolution: {integrity: sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==} + engines: {node: '>=12.0.0'} + + assert@2.1.0: + resolution: {integrity: sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==} + + assertion-error@1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + + ast-types-flow@0.0.7: + resolution: {integrity: sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==} + + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + + asynciterator.prototype@1.0.0: + resolution: {integrity: sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + auto-bind@4.0.0: + resolution: {integrity: sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ==} + engines: {node: '>=8'} + + autoprefixer@10.4.14: + resolution: {integrity: sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + autoprefixer@10.4.16: + resolution: {integrity: sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + autoprefixer@10.4.21: + resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + available-typed-arrays@1.0.5: + resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} + engines: {node: '>= 0.4'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axe-core@4.8.2: + resolution: {integrity: sha512-/dlp0fxyM3R8YW7MFzaHWXrf4zzbr0vaYb23VBFCl83R7nWNPg/yaQw2Dc8jzCMmDVLhSdzH8MjrsuIUuvX+6g==} + engines: {node: '>=4'} + + axobject-query@3.2.1: + resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + azure-devops-node-api@12.5.0: + resolution: {integrity: sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==} + + b4a@1.6.7: + resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==} + + babel-plugin-syntax-trailing-function-commas@7.0.0-beta.0: + resolution: {integrity: sha512-Xj9XuRuz3nTSbaTXWv3itLOcxyF4oPD8douBBmj7U9BBC6nEBYfyOJYQMf/8PJAFotC62UY5dFfIGEPr7WswzQ==} + + babel-preset-fbjs@3.4.0: + resolution: {integrity: sha512-9ywCsCvo1ojrw0b+XYk7aFvTH6D9064t0RIL1rtMf3nsa02Xw41MS7sZw216Im35xj/UY0PDBQsa1brUDDF1Ow==} + peerDependencies: + '@babel/core': ^7.0.0 + + babel-walk@3.0.0: + resolution: {integrity: sha512-fdRxJkQ9MUSEi4jH2DcV3FAPFktk0wefilxrwNyUuWpoWawQGN7G7cB+fOYTtFfI6XNkFgwqJ/D3G18BoJJ/jg==} + engines: {node: '>= 10.0.0'} + + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + bare-events@2.5.0: + resolution: {integrity: sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==} + + bare-fs@2.3.5: + resolution: {integrity: sha512-SlE9eTxifPDJrT6YgemQ1WGFleevzwY+XAP1Xqgl56HtcrisC2CHCZ2tq6dBpcH2TnNxwUEUGhweo+lrQtYuiw==} + + bare-os@2.4.4: + resolution: {integrity: sha512-z3UiI2yi1mK0sXeRdc4O1Kk8aOa/e+FNWZcTiPB/dfTWyLypuE99LibgRaQki914Jq//yAWylcAt+mknKdixRQ==} + + bare-path@2.1.3: + resolution: {integrity: sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==} + + bare-stream@2.3.2: + resolution: {integrity: sha512-EFZHSIBkDgSHIwj2l2QZfP4U5OcD4xFAOwhSb/vlr9PIqyGJGvB/nfClJbcnh3EY4jtPE4zsb5ztae96bVF79A==} + + base16@1.0.0: + resolution: {integrity: sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ==} + + base64-js@0.0.8: + resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==} + engines: {node: '>= 0.4'} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + base64id@2.0.0: + resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} + engines: {node: ^4.5.0 || >= 5.9} + + basic-auth@2.0.1: + resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} + engines: {node: '>= 0.8'} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browser-stdout@1.3.1: + resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} + + browserify-zlib@0.1.4: + resolution: {integrity: sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ==} + + browserslist@4.22.1: + resolution: {integrity: sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + browserslist@4.23.0: + resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + browserslist@4.24.4: + resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + builtin-modules@3.3.0: + resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} + engines: {node: '>=6'} + + bumpp@9.4.1: + resolution: {integrity: sha512-kzhp/LpNX0HkUpEyLd7sU2LTN/mbAVgcxJ1Zi2cAJTE/tul6rypSKGpH8UywDpzKWItL8LVdKsIFnwmylw0+7g==} + engines: {node: '>=10'} + hasBin: true + + bundle-require@4.1.0: + resolution: {integrity: sha512-FeArRFM+ziGkRViKRnSTbHZc35dgmR9yNog05Kn0+ItI59pOAISGvnnIwW1WgFZQW59IxD9QpJnUPkdIPfZuXg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.17' + + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + + c12@1.10.0: + resolution: {integrity: sha512-0SsG7UDhoRWcuSvKWHaXmu5uNjDCDN3nkQLRL4Q42IlFy+ze58FcCoI3uPwINXinkz7ZinbhEgyzYFw9u9ZV8g==} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + cache-content-type@1.0.1: + resolution: {integrity: sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==} + engines: {node: '>= 6.0.0'} + + call-bind@1.0.5: + resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} + + call-bind@1.0.7: + resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} + engines: {node: '>= 0.4'} + + call-me-maybe@1.0.2: + resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camel-case@4.1.2: + resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} + + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + camelize@1.0.1: + resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} + + caniuse-api@3.0.0: + resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} + + caniuse-lite@1.0.30001541: + resolution: {integrity: sha512-bLOsqxDgTqUBkzxbNlSBt8annkDpQB9NdzdTbO2ooJ+eC/IQcvDspDc058g84ejCelF7vHUx57KIOjEecOHXaw==} + + caniuse-lite@1.0.30001620: + resolution: {integrity: sha512-WJvYsOjd1/BYUY6SNGUosK9DUidBPDTnOARHp3fSmFO1ekdxaY6nKRttEVrfMmYi80ctS0kz1wiWmm14fVc3ew==} + + caniuse-lite@1.0.30001706: + resolution: {integrity: sha512-3ZczoTApMAZwPKYWmwVbQMFpXBDds3/0VciVoUwPUbldlYyVLmRVuRs/PcUZtHpbLRpzzDvrvnFuREsGt6lUug==} + + capital-case@1.0.4: + resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chai@4.4.1: + resolution: {integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==} + engines: {node: '>=4'} + + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + + chalk@3.0.0: + resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} + engines: {node: '>=8'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + change-case-all@1.0.15: + resolution: {integrity: sha512-3+GIFhk3sNuvFAJKU46o26OdzudQlPNBCu1ZQi3cMeMHhty1bhDxu2WrEilVNYaGvqUtR1VSigFcJOiS13dRhQ==} + + change-case@4.1.2: + resolution: {integrity: sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==} + + character-entities-legacy@1.1.4: + resolution: {integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==} + + character-entities@1.2.4: + resolution: {integrity: sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@1.1.4: + resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==} + + chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + + check-error@1.0.3: + resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + + cheerio-select@2.1.0: + resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} + + cheerio@1.0.0-rc.12: + resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==} + engines: {node: '>= 6'} + + chokidar@3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + + chroma-js@2.4.2: + resolution: {integrity: sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==} + + chrome-trace-event@1.0.3: + resolution: {integrity: sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==} + engines: {node: '>=6.0'} + + ci-info@2.0.0: + resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} + + ci-info@4.0.0: + resolution: {integrity: sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==} + engines: {node: '>=8'} + + citty@0.1.6: + resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + + class-variance-authority@0.4.0: + resolution: {integrity: sha512-74enNN8O9ZNieycac/y8FxqgyzZhZbxmCitAtAeUrLPlxjSd5zA7LfpprmxEcOmQBnaGs5hYhiSGnJ0mqrtBLQ==} + peerDependencies: + typescript: '>= 4.5.5 < 5' + peerDependenciesMeta: + typescript: + optional: true + + clean-regexp@1.0.0: + resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} + engines: {node: '>=4'} + + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-cursor@4.0.0: + resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + cli-spinners@2.9.1: + resolution: {integrity: sha512-jHgecW0pxkonBJdrKsqxgRX9AcG+u/5k0Q7WPDfi8AogLAdwxEkyYYNWwZ5GvVFoFx2uiY1eNcSK00fh+1+FyQ==} + engines: {node: '>=6'} + + cli-truncate@2.1.0: + resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} + engines: {node: '>=8'} + + cli-truncate@4.0.0: + resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} + engines: {node: '>=18'} + + cli-width@3.0.0: + resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} + engines: {node: '>= 10'} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + clsx@1.2.1: + resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} + engines: {node: '>=6'} + + clsx@2.1.0: + resolution: {integrity: sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==} + engines: {node: '>=6'} + + cm6-graphql@0.0.15: + resolution: {integrity: sha512-YEIfCj/jrgm0bfTND1l/T/ZwKPFLtqRvt/LfNfpUbPnRcJy2TvEoEIv3VqtJDCVnCHfoDfQDrRIqkIU8vo318A==} + peerDependencies: + '@codemirror/autocomplete': 6.2.0 + '@codemirror/language': 6.2.1 + '@codemirror/lint': 6.2.1 + '@codemirror/state': 6.2.0 + '@codemirror/view': 6.2.1 + '@lezer/highlight': ^1.0.0 + graphql: ^16.5.0 + + cmdk@1.0.0: + resolution: {integrity: sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + + co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + cockatiel@3.1.3: + resolution: {integrity: sha512-xC759TpZ69d7HhfDp8m2WkRwEUiCkxY8Ee2OQH/3H6zmy2D/5Sm+zSTbPRa+V2QyjDtpMvjOIAOVjA2gp6N1kQ==} + engines: {node: '>=16'} + + code-red@1.0.4: + resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==} + + codemirror-lang-mermaid@0.5.0: + resolution: {integrity: sha512-Taw/2gPCyNArQJCxIP/HSUif+3zrvD+6Ugt7KJZ2dUKou/8r3ZhcfG8krNTZfV2iu8AuGnymKuo7bLPFyqsh/A==} + + codiff@0.1.2: + resolution: {integrity: sha512-h3JEHAvaeUCvEA9ia8ttFe1yUcxXwg4xcVSCsJXzhE+o089cAF/l10O77glbxSW1k1owE+vLKCFuiBfLbBwyRQ==} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + + colord@2.9.3: + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + + colorette@1.4.0: + resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + comma-separated-tokens@1.0.8: + resolution: {integrity: sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + commander@6.2.1: + resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} + engines: {node: '>= 6'} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + comment-parser@1.4.1: + resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==} + engines: {node: '>= 12.0.0'} + + common-tags@1.8.2: + resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} + engines: {node: '>=4.0.0'} + + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + + compare-versions@6.1.0: + resolution: {integrity: sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg==} + + compute-scroll-into-view@3.1.0: + resolution: {integrity: sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + confbox@0.1.7: + resolution: {integrity: sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + + consola@3.2.3: + resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==} + engines: {node: ^14.18.0 || >=16.10.0} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + constant-case@3.0.4: + resolution: {integrity: sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@0.4.2: + resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} + engines: {node: '>= 0.6'} + + cookies@0.9.1: + resolution: {integrity: sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==} + engines: {node: '>= 0.8'} + + copy-to-clipboard@3.3.3: + resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} + + core-js-compat@3.37.1: + resolution: {integrity: sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + + cosmiconfig@8.3.6: + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + + cross-fetch@3.1.8: + resolution: {integrity: sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==} + + cross-inspect@1.0.0: + resolution: {integrity: sha512-4PFfn4b5ZN6FMNGSZlyb7wUhuN8wvj8t/VQHZdM4JsDcruGJ8L2kf9zao98QIrBPFCpdk27qst/AGTl7pL3ypQ==} + engines: {node: '>=16.0.0'} + + cross-spawn@6.0.5: + resolution: {integrity: sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==} + engines: {node: '>=4.8'} + + cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + + crypto-random-string@5.0.0: + resolution: {integrity: sha512-KWjTXWwxFd6a94m5CdRGW/t82Tr8DoBc9dNnPCAbFI1EBweN6v1tv8y4Y1m7ndkp/nkIBRxUxAzpaBnR2k3bcQ==} + engines: {node: '>=14.16'} + + css-background-parser@0.1.0: + resolution: {integrity: sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==} + + css-box-shadow@1.0.0-3: + resolution: {integrity: sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==} + + css-color-keywords@1.0.0: + resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==} + engines: {node: '>=4'} + + css-declaration-sorter@7.2.0: + resolution: {integrity: sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.0.9 + + css-select@5.1.0: + resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + + css-to-react-native@3.2.0: + resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==} + + css-tree@2.2.1: + resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + css-tree@2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + cssnano-preset-default@7.0.6: + resolution: {integrity: sha512-ZzrgYupYxEvdGGuqL+JKOY70s7+saoNlHSCK/OGn1vB2pQK8KSET8jvenzItcY+kA7NoWvfbb/YhlzuzNKjOhQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + cssnano-utils@5.0.0: + resolution: {integrity: sha512-Uij0Xdxc24L6SirFr25MlwC2rCFX6scyUmuKpzI+JQ7cyqDEwD42fJ0xfB3yLfOnRDU5LKGgjQ9FA6LYh76GWQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + cssnano@7.0.6: + resolution: {integrity: sha512-54woqx8SCbp8HwvNZYn68ZFAepuouZW4lTwiMVnBErM3VkO7/Sd4oTOt3Zz3bPx3kxQ36aISppyXj2Md4lg8bw==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + csso@5.0.5: + resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + csstype@3.1.2: + resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + damerau-levenshtein@1.0.8: + resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + + dataloader@2.2.2: + resolution: {integrity: sha512-8YnDaaf7N3k/q5HnTJVuzSyLETjoZjVmHc4AeKAzOvKHEFQKcn64OKBfzHYtE9zGjctNM7V9I0MfnUVLpi7M5g==} + + date-fns@3.6.0: + resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} + + debounce@1.2.1: + resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} + + debounce@2.0.0: + resolution: {integrity: sha512-xRetU6gL1VJbs85Mc4FoEGSjQxzpdxRyFhe3lmWFyy2EzydIcD4xzUvRJMD+NPDfMwKNhxa3PvsIOU32luIWeA==} + engines: {node: '>=18'} + + debounce@2.2.0: + resolution: {integrity: sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw==} + engines: {node: '>=18'} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + decamelize@4.0.0: + resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} + engines: {node: '>=10'} + + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + + decode-named-character-reference@1.0.2: + resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + dedent@0.7.0: + resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} + + deep-eql@4.1.3: + resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} + engines: {node: '>=6'} + + deep-equal@1.0.1: + resolution: {integrity: sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==} + + deep-equal@2.2.3: + resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} + engines: {node: '>= 0.4'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge-ts@5.1.0: + resolution: {integrity: sha512-eS8dRJOckyo9maw9Tu5O5RUi/4inFLrnoLkBe3cPfDMx3WZioXtmOew4TXQaxq7Rhl4xjDtR7c6x8nNTxOvbFw==} + engines: {node: '>=16.0.0'} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + define-data-property@1.1.1: + resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} + engines: {node: '>= 0.4'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + + depd@1.1.2: + resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} + engines: {node: '>= 0.6'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dependency-graph@0.11.0: + resolution: {integrity: sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==} + engines: {node: '>= 0.6.0'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + destr@2.0.3: + resolution: {integrity: sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + + diff@5.0.0: + resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==} + engines: {node: '>=0.3.1'} + + diff@5.2.0: + resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} + engines: {node: '>=0.3.1'} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + dompurify@3.1.5: + resolution: {integrity: sha512-lwG+n5h8QNpxtyrJW/gJWckL+1/DQiYMX8f7t8Z2AZTPw1esVrqjI63i7Zc2Gz0aKzLVMYC1V1PL/ky+aY/NgA==} + + domutils@3.1.0: + resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + + dot-case@3.0.4: + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + + dot-prop@8.0.2: + resolution: {integrity: sha512-xaBe6ZT4DHPkg0k4Ytbvn5xoxgpG0jOS1dYxSOwAHPuNLjP3/OzN0gH55SrLqpx8cBfSaVt91lXYkApjb+nYdQ==} + engines: {node: '>=16'} + + dotenv@16.0.3: + resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==} + engines: {node: '>=12'} + + dotenv@16.3.1: + resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==} + engines: {node: '>=12'} + + dotenv@16.4.5: + resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} + engines: {node: '>=12'} + + downshift@8.2.2: + resolution: {integrity: sha512-UmJHlNTzmFN3i427Hh9f1OXMnkhgSB/J+urC9ywabvwuftm0nB0/Utsb89OtDq+2UqyScQV4Ro7EM2PEV80N5w==} + peerDependencies: + react: '>=16.12.0' + + dset@3.1.3: + resolution: {integrity: sha512-20TuZZHCEZ2O71q9/+8BwKwZ0QtD9D8ObhrihJPr+vLLYlSuAU3/zL4cSlgbfeoGHTjCSJBa7NGcrF9/Bx/WJQ==} + engines: {node: '>=4'} + + duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + + duplexify@3.7.1: + resolution: {integrity: sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + editorconfig@1.0.4: + resolution: {integrity: sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==} + engines: {node: '>=14'} + hasBin: true + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + electron-to-chromium@1.4.537: + resolution: {integrity: sha512-W1+g9qs9hviII0HAwOdehGYkr+zt7KKdmCcJcjH0mYg6oL8+ioT3Skjmt7BLoAQqXhjf40AXd+HlR4oAWMlXjA==} + + electron-to-chromium@1.4.777: + resolution: {integrity: sha512-n02NCwLJ3wexLfK/yQeqfywCblZqLcXphzmid5e8yVPdtEcida7li0A5WQKghHNG0FeOMCzeFOzEbtAh5riXFw==} + + electron-to-chromium@1.5.123: + resolution: {integrity: sha512-refir3NlutEZqlKaBLK0tzlVLe5P2wDKS7UQt/3SpibizgsRAPOsqQC3ffw1nlv3ze5gjRQZYHoPymgVZkplFA==} + + embla-carousel-autoplay@8.5.2: + resolution: {integrity: sha512-27emJ0px3q/c0kCHCjwRrEbYcyYUPfGO3g5IBWF1i7714TTzE6L9P81V6PHLoSMAKJ1aHoT2e7YFOsuFKCbyag==} + peerDependencies: + embla-carousel: 8.5.2 + + embla-carousel-react@8.5.2: + resolution: {integrity: sha512-Tmx+uY3MqseIGdwp0ScyUuxpBgx5jX1f7od4Cm5mDwg/dptEiTKf9xp6tw0lZN2VA9JbnVMl/aikmbc53c6QFA==} + peerDependencies: + react: ^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + embla-carousel-reactive-utils@8.5.2: + resolution: {integrity: sha512-QC8/hYSK/pEmqEdU1IO5O+XNc/Ptmmq7uCB44vKplgLKhB/l0+yvYx0+Cv0sF6Ena8Srld5vUErZkT+yTahtDg==} + peerDependencies: + embla-carousel: 8.5.2 + + embla-carousel@8.5.2: + resolution: {integrity: sha512-xQ9oVLrun/eCG/7ru3R+I5bJ7shsD8fFwLEY7yPe27/+fDHCNj0OT5EoG5ZbFyOxOcG6yTwW8oTz/dWyFnyGpg==} + + emoji-regex@10.3.0: + resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + emphasize@4.2.0: + resolution: {integrity: sha512-yGKvcFUHlBsUPwlxTlzKLR8+zhpbitkFOMCUxN8fTJng9bdH3WNzUGkhdaGdjndSUgqmMPBN7umfwnUdLz5Axg==} + + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + + end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + + engine.io-client@6.5.3: + resolution: {integrity: sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==} + + engine.io-parser@5.2.2: + resolution: {integrity: sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==} + engines: {node: '>=10.0.0'} + + engine.io@6.5.4: + resolution: {integrity: sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==} + engines: {node: '>=10.2.0'} + + enhanced-resolve@5.15.0: + resolution: {integrity: sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==} + engines: {node: '>=10.13.0'} + + enhanced-resolve@5.16.1: + resolution: {integrity: sha512-4U5pNsuDl0EhuZpq46M5xPslstkviJuhrdobaRDBk2Jy2KO37FDAJl4lb2KlNabxT0m4MTK2UHNrsAcphE8nyw==} + engines: {node: '>=10.13.0'} + + entities@2.1.0: + resolution: {integrity: sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + es-abstract@1.22.2: + resolution: {integrity: sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.0: + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-get-iterator@1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + + es-iterator-helpers@1.0.15: + resolution: {integrity: sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g==} + + es-module-lexer@1.5.3: + resolution: {integrity: sha512-i1gCgmR9dCl6Vil6UKPI/trA69s08g/syhiDK9TG0Nf1RJjjFI+AzoWW7sPufzkgYAn861skuCwJa0pIIHYxvg==} + + es-set-tostringtag@2.0.1: + resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.0.0: + resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==} + + es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + + esbuild-plugin-copy@2.1.1: + resolution: {integrity: sha512-Bk66jpevTcV8KMFzZI1P7MZKZ+uDcrZm2G2egZ2jNIvVnivDpodZI+/KnpL3Jnap0PBdIHU7HwFGB8r+vV5CVw==} + peerDependencies: + esbuild: '>= 0.14.0' + + esbuild-plugin-polyfill-node@0.3.0: + resolution: {integrity: sha512-SHG6CKUfWfYyYXGpW143NEZtcVVn8S/WHcEOxk62LuDXnY4Zpmc+WmxJKN6GMTgTClXJXhEM5KQlxKY6YjbucQ==} + peerDependencies: + esbuild: '*' + + esbuild@0.19.11: + resolution: {integrity: sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.19.12: + resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.20.2: + resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.24.2: + resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.1.2: + resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} + engines: {node: '>=6'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-carriage@1.3.1: + resolution: {integrity: sha512-GwBr6yViW3ttx1kb7/Oh+gKQ1/TrhYwxKqVmg5gS+BK+Qe2KrOa/Vh7w3HPBvgGf0LfcDGoY9I6NHKoA5Hozhw==} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + eslint-compat-utils@0.5.0: + resolution: {integrity: sha512-dc6Y8tzEcSYZMHa+CMPLi/hyo1FzNeonbhJL7Ol0ccuKQkwopJcJBA9YL/xmMTLU1eKigXo9vj9nALElWYSowg==} + engines: {node: '>=12'} + peerDependencies: + eslint: '>=6.0.0' + + eslint-config-flat-gitignore@0.1.5: + resolution: {integrity: sha512-hEZLwuZjDBGDERA49c2q7vxc8sCGv8EdBp6PQYzGOMcHIgrfG9YOM6s/4jx24zhD+wnK9AI8mgN5RxSss5nClQ==} + + eslint-config-next@13.4.7-canary.1: + resolution: {integrity: sha512-zGyVDJlpL+9uJ1HH6Pvy0musF29kiTH8h0eEVCCyXdeIX9giUHTrdXhu2J0wCdsSNvp28OFLr3yMLdonv+Rq3Q==} + peerDependencies: + eslint: ^7.23.0 || ^8.0.0 + typescript: '>=3.3.1' + peerDependenciesMeta: + typescript: + optional: true + + eslint-config-prettier@8.10.0: + resolution: {integrity: sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-config-prettier@9.0.0: + resolution: {integrity: sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-config-prettier@9.1.0: + resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-config-turbo@1.10.12: + resolution: {integrity: sha512-z3jfh+D7UGYlzMWGh+Kqz++hf8LOE96q3o5R8X4HTjmxaBWlLAWG+0Ounr38h+JLR2TJno0hU9zfzoPNkR9BdA==} + peerDependencies: + eslint: '>6.6.0' + + eslint-flat-config-utils@0.2.5: + resolution: {integrity: sha512-iO+yLZtC/LKgACerkpvsZ6NoRVB2sxT04mOpnNcEM1aTwKy+6TsT46PUvrML4y2uVBS6I67hRCd2JiKAPaL/Uw==} + + eslint-formatter-mo@1.2.0: + resolution: {integrity: sha512-j/1oWwwIZWlpziMlPGmP2ZA/9LmcDzKcSHx0q9hqnOxAYy42NAoGD0YErU27crRX+sp64w+PfAd57YoxTtjvAQ==} + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-import-resolver-typescript@3.6.1: + resolution: {integrity: sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + + eslint-merge-processors@0.1.0: + resolution: {integrity: sha512-IvRXXtEajLeyssvW4wJcZ2etxkR9mUf4zpNwgI+m/Uac9RfXHskuJefkHUcawVzePnd6xp24enp5jfgdHzjRdQ==} + peerDependencies: + eslint: '*' + + eslint-module-utils@2.8.0: + resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-antfu@2.2.0: + resolution: {integrity: sha512-QHzHYP+fyfhSkIdcuT9JZ4rCPuJOoHRE27gglPYHlJ6lxB7pO9i45yAy4aurx/rleBuEC27U4c//1Nwtbasj4Q==} + peerDependencies: + eslint: '*' + + eslint-plugin-command@0.2.3: + resolution: {integrity: sha512-1bBYNfjZg60N2ZpLV5ATYSYyueIJ+zl5yKrTs0UFDdnyu07dNSZ7Xplnc+Wb6SXTdc1sIaoIrnuyhvztcltX6A==} + peerDependencies: + eslint: '*' + + eslint-plugin-es-x@7.6.0: + resolution: {integrity: sha512-I0AmeNgevgaTR7y2lrVCJmGYF0rjoznpDvqV/kIkZSZbZ8Rw3eu4cGlvBBULScfkSOCzqKbff5LR4CNrV7mZHA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '>=8' + + eslint-plugin-eslint-comments@3.2.0: + resolution: {integrity: sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==} + engines: {node: '>=6.5.0'} + peerDependencies: + eslint: '>=4.19.1' + + eslint-plugin-import-x@0.5.0: + resolution: {integrity: sha512-C7R8Z4IzxmsoOPMtSzwuOBW5FH6iRlxHR6iTks+MzVlrk3r3TUxokkWTx3ypdj9nGOEP+CG/5e6ebZzHbxgbbQ==} + engines: {node: '>=16'} + peerDependencies: + eslint: ^8.56.0 || ^9.0.0-0 + + eslint-plugin-import@2.28.1: + resolution: {integrity: sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-jsdoc@48.2.5: + resolution: {integrity: sha512-ZeTfKV474W1N9niWfawpwsXGu+ZoMXu4417eBROX31d7ZuOk8zyG66SO77DpJ2+A9Wa2scw/jRqBPnnQo7VbcQ==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 + + eslint-plugin-jsonc@2.15.1: + resolution: {integrity: sha512-PVFrqIJa8BbM/e828RSn0SwB/Z5ye+2LDuy2XqG6AymNgPsfApRRcznsbxP7VrjdLEU4Nb+g9n/d6opyp0jp9A==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + + eslint-plugin-jsx-a11y@6.7.1: + resolution: {integrity: sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==} + engines: {node: '>=4.0'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + + eslint-plugin-markdown@5.0.0: + resolution: {integrity: sha512-kY2u9yDhzvfZ0kmRTsvgm3mTnvZgTSGIIPeHg3yesSx4R5CTCnITUjCPhzCD1MUhNcqHU5Tr6lzx+02EclVPbw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: '>=8' + + eslint-plugin-n@17.7.0: + resolution: {integrity: sha512-4Jg4ZKVE4VjHig2caBqPHYNW5na84RVufUuipFLJbgM/G57O6FdpUKJbHakCDJb/yjQuyqVzYWRtU3HNYaZUwg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: '>=8.23.0' + + eslint-plugin-no-only-tests@3.1.0: + resolution: {integrity: sha512-Lf4YW/bL6Un1R6A76pRZyE1dl1vr31G/ev8UzIc/geCgFWyrKil8hVjYqWVKGB/UIGmb6Slzs9T0wNezdSVegw==} + engines: {node: '>=5.0.0'} + + eslint-plugin-perfectionist@2.10.0: + resolution: {integrity: sha512-P+tdrkHeMWBc55+DZsoDOAftV1WCsEoHaKm6JC7zajFus/syfT4vUPBFb3atGFSuyaVnGQGHlcKpP9X3Q0gH/w==} + peerDependencies: + astro-eslint-parser: ^0.16.0 + eslint: '>=8.0.0' + svelte: '>=3.0.0' + svelte-eslint-parser: ^0.33.0 + vue-eslint-parser: '>=9.0.0' + peerDependenciesMeta: + astro-eslint-parser: + optional: true + svelte: + optional: true + svelte-eslint-parser: + optional: true + vue-eslint-parser: + optional: true + + eslint-plugin-react-hooks@4.6.0: + resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + + eslint-plugin-react@7.33.2: + resolution: {integrity: sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + + eslint-plugin-regexp@2.6.0: + resolution: {integrity: sha512-FCL851+kislsTEQEMioAlpDuK5+E5vs0hi1bF8cFlPlHcEjeRhuAzEsGikXRreE+0j4WhW2uO54MqTjXtYOi3A==} + engines: {node: ^18 || >=20} + peerDependencies: + eslint: '>=8.44.0' + + eslint-plugin-tailwindcss@3.13.0: + resolution: {integrity: sha512-Fcep4KDRLWaK3KmkQbdyKHG0P4GdXFmXdDaweTIPcgOP60OOuWFbh1++dufRT28Q4zpKTKaHwTsXPJ4O/EjU2Q==} + engines: {node: '>=12.13.0'} + peerDependencies: + tailwindcss: ^3.3.2 + + eslint-plugin-toml@0.11.0: + resolution: {integrity: sha512-sau+YvPU4fWTjB+qtBt3n8WS87aoDCs+BVbSUAemGaIsRNbvR9uEk+Tt892iLHTGvp/DPWYoCX4/8DoyAbB+sQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + + eslint-plugin-turbo@1.10.12: + resolution: {integrity: sha512-uNbdj+ohZaYo4tFJ6dStRXu2FZigwulR1b3URPXe0Q8YaE7thuekKNP+54CHtZPH9Zey9dmDx5btAQl9mfzGOw==} + peerDependencies: + eslint: '>6.6.0' + + eslint-plugin-unicorn@53.0.0: + resolution: {integrity: sha512-kuTcNo9IwwUCfyHGwQFOK/HjJAYzbODHN3wP0PgqbW+jbXqpNWxNVpVhj2tO9SixBwuAdmal8rVcWKBxwFnGuw==} + engines: {node: '>=18.18'} + peerDependencies: + eslint: '>=8.56.0' + + eslint-plugin-unused-imports@3.0.0: + resolution: {integrity: sha512-sduiswLJfZHeeBJ+MQaG+xYzSWdRXoSw61DpU13mzWumCkR0ufD0HmO4kdNokjrkluMHpj/7PJeN35pgbhW3kw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^6.0.0 + eslint: ^8.0.0 + peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true + + eslint-plugin-unused-imports@3.2.0: + resolution: {integrity: sha512-6uXyn6xdINEpxE1MtDjxQsyXB37lfyO2yKGVVgtD7WEWQGORSOZjgrD6hBhvGv4/SO+TOlS+UnC6JppRqbuwGQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/eslint-plugin': 6 - 7 + eslint: '8' + peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true + + eslint-plugin-vitest@0.5.4: + resolution: {integrity: sha512-um+odCkccAHU53WdKAw39MY61+1x990uXjSPguUCq3VcEHdqJrOb8OTMrbYlY6f9jAKx7x98kLVlIe3RJeJqoQ==} + engines: {node: ^18.0.0 || >= 20.0.0} + peerDependencies: + '@typescript-eslint/eslint-plugin': '*' + eslint: ^8.57.0 || ^9.0.0 + vitest: '*' + peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true + vitest: + optional: true + + eslint-plugin-vue@9.26.0: + resolution: {integrity: sha512-eTvlxXgd4ijE1cdur850G6KalZqk65k1JKoOI2d1kT3hr8sPD07j1q98FRFdNnpxBELGPWxZmInxeHGF/GxtqQ==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + + eslint-plugin-yml@1.14.0: + resolution: {integrity: sha512-ESUpgYPOcAYQO9czugcX5OqRvn/ydDVwGCPXY4YjPqc09rHaUVUA6IE6HLQys4rXk/S+qx3EwTd1wHCwam/OWQ==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + + eslint-processor-vue-blocks@0.1.2: + resolution: {integrity: sha512-PfpJ4uKHnqeL/fXUnzYkOax3aIenlwewXRX8jFinA1a2yCFnLgMuiH3xvCgvHHUlV2xJWQHbCTdiJWGwb3NqpQ==} + peerDependencies: + '@vue/compiler-sfc': ^3.3.0 + eslint: ^8.50.0 || ^9.0.0 + + eslint-rule-composer@0.3.0: + resolution: {integrity: sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==} + engines: {node: '>=4.0.0'} + + eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-scope@8.0.1: + resolution: {integrity: sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.0.0: + resolution: {integrity: sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@8.50.0: + resolution: {integrity: sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + + eslint@8.57.0: + resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + + eslint@9.3.0: + resolution: {integrity: sha512-5Iv4CsZW030lpUqHBapdPo3MJetAPtejVW8B84GIcIIv8+ohFaddXsrn1Gn8uD9ijDb+kcYKFUVmC8qG8B2ORQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + + espree@10.0.1: + resolution: {integrity: sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esquery@1.5.0: + resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + event-stream@3.3.4: + resolution: {integrity: sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==} + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + eventsource-parser@1.1.2: + resolution: {integrity: sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==} + engines: {node: '>=14.18'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + + expand-tilde@2.0.2: + resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==} + engines: {node: '>=0.10.0'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + + extract-files@11.0.0: + resolution: {integrity: sha512-FuoE1qtbJ4bBVvv94CC7s0oTnKUGvQs+Rjf1L2SJFfS+HTVVjhPFtehPdQ0JiGPqVNfSSZvL5yzHHQq2Z4WNhQ==} + engines: {node: ^12.20 || >= 14.13} + + fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + + fast-deep-equal@2.0.1: + resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-equals@5.0.1: + resolution: {integrity: sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==} + engines: {node: '>=6.0.0'} + + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + + fast-glob@3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + engines: {node: '>=8.6.0'} + + fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-levenshtein@3.0.0: + resolution: {integrity: sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==} + + fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + + fast-redact@3.5.0: + resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} + engines: {node: '>=6'} + + fast-url-parser@1.1.3: + resolution: {integrity: sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==} + + fastest-levenshtein@1.0.16: + resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} + engines: {node: '>= 4.9.1'} + + fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + + fault@1.0.4: + resolution: {integrity: sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==} + + fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + + fbemitter@3.0.0: + resolution: {integrity: sha512-KWKaceCwKQU0+HPoop6gn4eOHk50bBv/VxjJtGMfwmJt3D29JpN4H4eisCtIPA+a8GVBam+ldMMpMjJUvpDyHw==} + + fbjs-css-vars@1.0.2: + resolution: {integrity: sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==} + + fbjs@3.0.5: + resolution: {integrity: sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==} + + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + + fdir@6.4.3: + resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fflate@0.4.8: + resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} + + fflate@0.7.4: + resolution: {integrity: sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==} + + figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + file-stream-rotator@1.0.0: + resolution: {integrity: sha512-qg5mQO7o+vhS7NPqkrkfJS8qqhz0d17Tnewmb5sUTUKwYe27LKaDtbTuRAtQWkBn6jROuFPVIDF5DtckzokFTQ==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + find-up@7.0.0: + resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} + engines: {node: '>=18'} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flat@5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + + flatted@3.3.1: + resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + + flux@4.0.4: + resolution: {integrity: sha512-NCj3XlayA2UsapRpM7va6wU1+9rE5FIL7qoMcmxWHRzbp0yujihMBm9BBHZ1MDIk5h5o2Bl6eGiCe8rYELAmYw==} + peerDependencies: + react: ^15.0.2 || ^16.0.0 || ^17.0.0 + + focus-trap-react@10.2.2: + resolution: {integrity: sha512-ktfVCuRyT2fikb1De8u5bs3afdqFTJu6Hl1iGThwmo++k2+/n78DapitplvHGZ0hkDPlyHvAWGQsR74KF30zxw==} + peerDependencies: + prop-types: ^15.8.1 + react: '>=16.3.0' + react-dom: '>=16.3.0' + + focus-trap@7.5.3: + resolution: {integrity: sha512-7UsT/eSJcTPF0aZp73u7hBRTABz26knRRTJfoTGFCQD5mUImLIIOwWWCrtoQdmWa7dykBi6H+Cp5i3S/kvsMeA==} + + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + + foreground-child@3.1.1: + resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} + engines: {node: '>=14'} + + form-data-encoder@1.7.2: + resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} + + form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + + format@0.2.2: + resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} + engines: {node: '>=0.4.x'} + + formdata-node@4.4.1: + resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} + engines: {node: '>= 12.20'} + + fraction.js@4.3.6: + resolution: {integrity: sha512-n2aZ9tNfYDwaHhvFTkhFErqOMIb8uyzSQ+vGJBjZyanAKZVbGUQ1sngfk9FdkBw7G26O7AgNjLcecLffD1c7eg==} + + fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + + framer-motion@10.17.4: + resolution: {integrity: sha512-CYBSs6cWfzcasAX8aofgKFZootmkQtR4qxbfTOksBLny/lbUfkGbQAFOS3qnl6Uau1N9y8tUpI7mVIrHgkFjLQ==} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + + framer-motion@11.11.9: + resolution: {integrity: sha512-XpdZseuCrZehdHGuW22zZt3SF5g6AHJHJi7JwQIigOznW4Jg1n0oGPMJQheMaKLC+0rp5gxUKMRYI6ytd3q4RQ==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 + react-dom: ^18.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + from@0.1.7: + resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs-extra@11.2.0: + resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} + engines: {node: '>=14.14'} + + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + fuzzysort@3.0.2: + resolution: {integrity: sha512-ZyahVgxvckB1Qosn7YGWLDJJp2XlyaQ2WmZeI+d0AzW0AMqVYnz5N89G6KAKa6m/LOtv+kzJn4lhDF/yVg11Cg==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.2.0: + resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==} + engines: {node: '>=18'} + + get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + + get-installed-path@4.0.8: + resolution: {integrity: sha512-PmANK1xElIHlHH2tXfOoTnSDUjX1X3GvKK6ZyLbUnSCCn1pADwu67eVWttuPzJWrXDDT2MfO6uAaKILOFfitmA==} + engines: {node: '>=6', npm: '>=5', yarn: '>=1'} + + get-intrinsic@1.2.1: + resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} + + get-intrinsic@1.2.4: + resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} + engines: {node: '>= 0.4'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + + get-symbol-description@1.0.0: + resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.7.2: + resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==} + + get-tsconfig@4.7.5: + resolution: {integrity: sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw==} + + giget@1.2.3: + resolution: {integrity: sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA==} + hasBin: true + + git-up@8.0.0: + resolution: {integrity: sha512-uBI8Zdt1OZlrYfGcSVroLJKgyNNXlgusYFzHk614lTasz35yg2PVpL1RMy0LOO2dcvF9msYW3pRfUSmafZNrjg==} + + git-url-parse@16.0.0: + resolution: {integrity: sha512-Y8iAF0AmCaqXc6a5GYgPQW9ESbncNLOL+CeQAJRhmWUOmnPkKpBYeWYp4mFd3LA5j53CdGDdslzX12yEBVHQQg==} + + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + + glob@10.3.15: + resolution: {integrity: sha512-0c6RlJt1TICLyvJYIApxb8GsXoai0KUP7AxKKAtsYXdgJR1mGEUa7DgwShbdk1nly0PYoZj01xd4hzbq3fsjpw==} + engines: {node: '>=16 || 14 >=14.18'} + hasBin: true + + glob@10.3.4: + resolution: {integrity: sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + glob@11.0.0: + resolution: {integrity: sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==} + engines: {node: 20 || >=22} + hasBin: true + + glob@7.1.6: + resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} + deprecated: Glob versions prior to v9 are no longer supported + + glob@7.1.7: + resolution: {integrity: sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==} + deprecated: Glob versions prior to v9 are no longer supported + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + deprecated: Glob versions prior to v9 are no longer supported + + global-modules@1.0.0: + resolution: {integrity: sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==} + engines: {node: '>=0.10.0'} + + global-prefix@1.0.2: + resolution: {integrity: sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==} + engines: {node: '>=0.10.0'} + + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + globals@13.22.0: + resolution: {integrity: sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==} + engines: {node: '>=8'} + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@15.3.0: + resolution: {integrity: sha512-cCdyVjIUVTtX8ZsPkq1oCsOsLmGIswqnjZYMJJTGaNApj1yHtLSymKhwH51ttirREn75z3p4k051clwg7rvNKA==} + engines: {node: '>=18'} + + globalthis@1.0.3: + resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} + engines: {node: '>= 0.4'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + globby@13.2.2: + resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + gradient-string@1.2.0: + resolution: {integrity: sha512-Lxog7IDMMWNjwo4O0KbdBvSewk4vW6kQe5XaLuuPCyCE65AGQ1P8YqKJa5dq8TYf/Ge31F+KjWzPR5mAJvjlAg==} + engines: {node: '>=4'} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + graphql-config@5.0.3: + resolution: {integrity: sha512-BNGZaoxIBkv9yy6Y7omvsaBUHOzfFcII3UN++tpH8MGOKFPFkCPZuwx09ggANMt8FgyWP1Od8SWPmrUEZca4NQ==} + engines: {node: '>= 16.0.0'} + peerDependencies: + cosmiconfig-toml-loader: ^1.0.0 + graphql: ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + peerDependenciesMeta: + cosmiconfig-toml-loader: + optional: true + + graphql-language-service@5.2.1: + resolution: {integrity: sha512-8ewD6otGO43vg2TiEGjoLz3CweTwfaf4ZnqfNREqZXS2JSJGXtsRBOMMknCxMfFVh4x14ql3jyDrXcyAAtbmkQ==} + hasBin: true + peerDependencies: + graphql: ^15.5.0 || ^16.0.0 + + graphql-request@6.1.0: + resolution: {integrity: sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw==} + peerDependencies: + graphql: 14 - 16 + + graphql-tag@2.12.6: + resolution: {integrity: sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==} + engines: {node: '>=10'} + peerDependencies: + graphql: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + graphql-ws@5.16.0: + resolution: {integrity: sha512-Ju2RCU2dQMgSKtArPbEtsK5gNLnsQyTNIo/T7cZNp96niC1x0KdJNZV0TIoilceBPQwfb5itrGl8pkFeOUMl4A==} + engines: {node: '>=10'} + peerDependencies: + graphql: '>=0.11 <=16' + + graphql@16.8.1: + resolution: {integrity: sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + + gunzip-maybe@1.4.2: + resolution: {integrity: sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==} + hasBin: true + + has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.0: + resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + + has-proto@1.0.3: + resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} + engines: {node: '>= 0.4'} + + has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.0: + resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + has@1.0.3: + resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} + engines: {node: '>= 0.4.0'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hast-util-from-parse5@7.1.2: + resolution: {integrity: sha512-Nz7FfPBuljzsN3tCQ4kCBKqdNhQE2l0Tn+X1ubgKBPRoiDIu1mL08Cfw4k7q71+Duyaw7DXDN+VTAp4Vh3oCOw==} + + hast-util-parse-selector@2.2.5: + resolution: {integrity: sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==} + + hast-util-parse-selector@3.1.1: + resolution: {integrity: sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==} + + hast-util-raw@7.2.3: + resolution: {integrity: sha512-RujVQfVsOrxzPOPSzZFiwofMArbQke6DJjnFfceiEbFh7S05CbPt0cYN+A5YeD3pso0JQk6O1aHBnx9+Pm2uqg==} + + hast-util-sanitize@5.0.1: + resolution: {integrity: sha512-IGrgWLuip4O2nq5CugXy4GI2V8kx4sFVy5Hd4vF7AR2gxS0N9s7nEAVUyeMtZKZvzrxVsHt73XdTsno1tClIkQ==} + + hast-util-to-parse5@7.1.0: + resolution: {integrity: sha512-YNRgAJkH2Jky5ySkIqFXTQiaqcAtJyVE+D5lkN6CdtOqrnkLfGYYrEcKuHOJZlp+MwjSwuD3fZuawI+sic/RBw==} + + hast-util-whitespace@2.0.1: + resolution: {integrity: sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==} + + hast@1.0.0: + resolution: {integrity: sha512-vFUqlRV5C+xqP76Wwq2SrM0kipnmpxJm7OfvVXpB35Fp+Fn4MV+ozr+JZr5qFvyR1q/U+Foim2x+3P+x9S1PLA==} + deprecated: Renamed to rehype + + hastscript@6.0.0: + resolution: {integrity: sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==} + + hastscript@7.2.0: + resolution: {integrity: sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + header-case@2.0.4: + resolution: {integrity: sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==} + + hex-rgb@4.3.0: + resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==} + engines: {node: '>=6'} + + highlight.js@10.4.1: + resolution: {integrity: sha512-yR5lWvNz7c85OhVAEAeFhVCc/GV4C30Fjzc/rCP0aCWzc1UUOPUk55dK/qdwTZHBvMZo+eZ2jpk62ndX/xMFlg==} + + highlight.js@10.7.3: + resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + + homedir-polyfill@1.0.3: + resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==} + engines: {node: '>=0.10.0'} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + hosted-git-info@2.8.9: + resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + + hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} + + html-to-text@9.0.5: + resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} + engines: {node: '>=14'} + + html-void-elements@2.0.1: + resolution: {integrity: sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==} + + htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + + http-assert@1.5.0: + resolution: {integrity: sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==} + engines: {node: '>= 0.8'} + + http-errors@1.6.3: + resolution: {integrity: sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==} + engines: {node: '>= 0.6'} + + http-errors@1.8.1: + resolution: {integrity: sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==} + engines: {node: '>= 0.6'} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + http-proxy-agent@4.0.1: + resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==} + engines: {node: '>= 6'} + + http-proxy-agent@7.0.0: + resolution: {integrity: sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==} + engines: {node: '>= 14'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + https-proxy-agent@7.0.2: + resolution: {integrity: sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.4: + resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.5: + resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==} + engines: {node: '>= 14'} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + + humanize-duration@3.31.0: + resolution: {integrity: sha512-fRrehgBG26NNZysRlTq1S+HPtDpp3u+Jzdc/d5A4cEzOD86YLAkDaJyJg8krSdCi7CJ+s7ht3fwRj8Dl+Btd0w==} + + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.1: + resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} + engines: {node: '>= 4'} + + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + + immutable@3.7.6: + resolution: {integrity: sha512-AizQPcaofEtO11RZhPPHBOJRdo/20MKQF9mBLnVkBoyHi1/zXK8fzVdnEpSV9gxqtnh6Qomfp3F0xT5qP/vThw==} + engines: {node: '>=0.8.0'} + + import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + + import-from@4.0.0: + resolution: {integrity: sha512-P9J71vT5nLlDeV8FHs5nNxaLbrpfAV5cF5srvbZfpwpcJoM/xZR3hiv+q+SAnuSmuGbXMWud063iIMx/V/EWZQ==} + engines: {node: '>=12.2'} + + import-meta-resolve@3.1.1: + resolution: {integrity: sha512-qeywsE/KC3w9Fd2ORrRDUw6nS/nLwZpXgfrOc2IILvZYnCaEMd+D56Vfg9k4G29gIeVi3XKql1RQatME8iYsiw==} + + import@0.0.6: + resolution: {integrity: sha512-QPhTdjy9J4wUzmWSG7APkSgMFuPGPw+iJTYUblcfc2AfpqaatbwgCldK1HoLYx+v/+lWvab63GWZtNkcnj9JcQ==} + engines: {node: '>=0.10.0'} + hasBin: true + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + index-to-position@0.1.2: + resolution: {integrity: sha512-MWDKS3AS1bGCHLBA2VLImJz42f7bJh8wQsTGCzI3j519/CASStoDONUBVz2I/VID0MpiX3SGSnbOD2xUalbE5g==} + engines: {node: '>=18'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.3: + resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + inline-style-parser@0.1.1: + resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} + + inquirer@8.2.6: + resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==} + engines: {node: '>=12.0.0'} + + internal-slot@1.0.5: + resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} + engines: {node: '>= 0.4'} + + internal-slot@1.0.7: + resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} + engines: {node: '>= 0.4'} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + interpret@1.4.0: + resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} + engines: {node: '>= 0.10'} + + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + + irregular-plurals@3.5.0: + resolution: {integrity: sha512-1ANGLZ+Nkv1ptFb2pa8oG8Lem4krflKuX/gINiHJHjJUKaJHk/SXk5x6K3J+39/p0h1RQ2saROclJJ+QLvETCQ==} + engines: {node: '>=8'} + + is-absolute@1.0.0: + resolution: {integrity: sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==} + engines: {node: '>=0.10.0'} + + is-alphabetical@1.0.4: + resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} + + is-alphanumerical@1.0.4: + resolution: {integrity: sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==} + + is-arguments@1.1.1: + resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.2: + resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} + + is-array-buffer@3.0.4: + resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} + engines: {node: '>= 0.4'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + + is-async-function@2.0.0: + resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==} + engines: {node: '>= 0.4'} + + is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + + is-buffer@2.0.5: + resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} + engines: {node: '>=4'} + + is-builtin-module@3.2.1: + resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} + engines: {node: '>=6'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-ci@2.0.0: + resolution: {integrity: sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==} + hasBin: true + + is-core-module@2.13.0: + resolution: {integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==} + + is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + + is-decimal@1.0.4: + resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==} + + is-deflate@1.0.0: + resolution: {integrity: sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ==} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-electron@2.2.2: + resolution: {integrity: sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.0.2: + resolution: {integrity: sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-fullwidth-code-point@4.0.0: + resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} + engines: {node: '>=12'} + + is-fullwidth-code-point@5.0.0: + resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==} + engines: {node: '>=18'} + + is-generator-function@1.0.10: + resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-gzip@1.0.0: + resolution: {integrity: sha512-rcfALRIb1YewtnksfRIHGcIY93QnK8BIQ/2c9yDYcG/Y6+vRoJuTWBmmSEbyLLYtXm7q35pHOHbZFQBaLrhlWQ==} + engines: {node: '>=0.10.0'} + + is-hexadecimal@1.0.4: + resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==} + + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + + is-lower-case@2.0.2: + resolution: {integrity: sha512-bVcMJy4X5Og6VZfdOZstSexlEy20Sr0k/p/b2IlQJlfdKAQuMpiv5w2Ccxb8sKdRUNAG1PnHVHjFSdRDVS6NlQ==} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-module@1.0.0: + resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + + is-nan@1.3.2: + resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.2: + resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} + engines: {node: '>= 0.4'} + + is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-plain-obj@2.1.0: + resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} + engines: {node: '>=8'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-reference@1.2.1: + resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + + is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + + is-relative@1.0.0: + resolution: {integrity: sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==} + engines: {node: '>=0.10.0'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.2: + resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} + + is-shared-array-buffer@1.0.3: + resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} + engines: {node: '>= 0.4'} + + is-ssh@1.4.0: + resolution: {integrity: sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ==} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + + is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.12: + resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.13: + resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} + engines: {node: '>= 0.4'} + + is-unc-path@1.0.0: + resolution: {integrity: sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==} + engines: {node: '>=0.10.0'} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + is-upper-case@2.0.2: + resolution: {integrity: sha512-44pxmxAvnnAOwBg4tHPnkfvgjPwbc5QIsSstNU+YcJ1ovxVzCWpSGosPJOZh/a1tdl81fbgnLc9LLv+x2ywbPQ==} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + + is-weakset@2.0.3: + resolution: {integrity: sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==} + engines: {node: '>= 0.4'} + + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isomorphic-fetch@3.0.0: + resolution: {integrity: sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==} + + isomorphic-ws@5.0.0: + resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} + peerDependencies: + ws: '*' + + iterator.prototype@1.1.2: + resolution: {integrity: sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==} + + jackspeak@2.3.6: + resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} + engines: {node: '>=14'} + + jackspeak@4.0.2: + resolution: {integrity: sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==} + engines: {node: 20 || >=22} + + jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + + jiti@1.21.0: + resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} + hasBin: true + + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + + jiti@2.4.2: + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + hasBin: true + + jose@5.1.1: + resolution: {integrity: sha512-bfB+lNxowY49LfrBO0ITUn93JbUhxUN8I11K6oI5hJu/G6PO6fEUddVLjqdD0cQ9SXIHWXuWh7eJYwZF7Z0N/g==} + + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + + js-beautify@1.15.1: + resolution: {integrity: sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==} + engines: {node: '>=14'} + hasBin: true + + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + + js-levenshtein@1.1.6: + resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} + engines: {node: '>=0.10.0'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-tokens@9.0.0: + resolution: {integrity: sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsdoc-type-pratt-parser@4.0.0: + resolution: {integrity: sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==} + engines: {node: '>=12.0.0'} + + jsesc@0.5.0: + resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} + hasBin: true + + jsesc@2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + + jsesc@3.0.2: + resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-better-errors@1.0.2: + resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json-stable-stringify@1.1.0: + resolution: {integrity: sha512-zfA+5SuwYN2VWqN1/5HZaDzQKLJHaBVMZIIM+wuYjdptkaQsqzDdqjqf+lZZJUuJq1aanHiY8LhH8LmH+qBYJA==} + engines: {node: '>= 0.4'} + + json-to-pretty-yaml@1.2.2: + resolution: {integrity: sha512-rvm6hunfCcqegwYaG5T4yKJWxc9FXFgBVrcTZ4XfSVRwa5HA/Xs+vB/Eo9treYYHCeNM0nrSUr82V/M31Urc7A==} + engines: {node: '>= 0.2.0'} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonc-eslint-parser@2.4.0: + resolution: {integrity: sha512-WYDyuc/uFcGp6YtM2H0uKmUwieOuzeE/5YocFJLnLfclZ4inf3mRn8ZVy1s7Hxji7Jxm6Ss8gqpexD/GlKoGgg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + jsonc-parser@3.2.1: + resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==} + + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + + jsonify@0.0.1: + resolution: {integrity: sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==} + + jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + + jwa@1.4.1: + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + + jwa@2.0.0: + resolution: {integrity: sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==} + + jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + + jws@4.0.0: + resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} + + jwt-decode@3.1.2: + resolution: {integrity: sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==} + + jwt-decode@4.0.0: + resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} + engines: {node: '>=18'} + + katex@0.16.8: + resolution: {integrity: sha512-ftuDnJbcbOckGY11OO+zg3OofESlbR5DRl2cmN8HeWeeFIV7wTXvAOx8kEjZjobhA+9wh2fbKeO6cdcA9Mnovg==} + hasBin: true + + keygrip@1.1.0: + resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==} + engines: {node: '>= 0.6'} + + keytar@7.9.0: + resolution: {integrity: sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + knitwork@1.2.0: + resolution: {integrity: sha512-xYSH7AvuQ6nXkq42x0v5S8/Iry+cfulBz/DJQzhIyESdLD7425jXsPy4vn5cCXU+HhRN2kVw51Vd1K6/By4BQg==} + + koa-compose@4.1.0: + resolution: {integrity: sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==} + + koa-convert@2.0.0: + resolution: {integrity: sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==} + engines: {node: '>= 10'} + + koa-morgan@1.0.1: + resolution: {integrity: sha512-JOUdCNlc21G50afBXfErUrr1RKymbgzlrO5KURY+wmDG1Uvd2jmxUJcHgylb/mYXy2SjiNZyYim/ptUBGsIi3A==} + + koa-mount@4.0.0: + resolution: {integrity: sha512-rm71jaA/P+6HeCpoRhmCv8KVBIi0tfGuO/dMKicbQnQW/YJntJ6MnnspkodoA4QstMVEZArsCphmd0bJEtoMjQ==} + engines: {node: '>= 7.6.0'} + + koa-send@5.0.1: + resolution: {integrity: sha512-tmcyQ/wXXuxpDxyNXv5yNNkdAMdFRqwtegBXUaowiQzUKqJehttS0x2j0eOZDQAyloAth5w6wwBImnFzkUz3pQ==} + engines: {node: '>= 8'} + + koa-static@5.0.0: + resolution: {integrity: sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ==} + engines: {node: '>= 7.6.0'} + + koa@2.15.3: + resolution: {integrity: sha512-j/8tY9j5t+GVMLeioLaxweJiKUayFhlGqNTzf2ZGwL0ZCQijd2RLHK0SLW5Tsko8YyyqCZC2cojIb0/s62qTAg==} + engines: {node: ^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4} + + language-subtag-registry@0.3.22: + resolution: {integrity: sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==} + + language-tags@1.0.5: + resolution: {integrity: sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==} + + leac@0.6.0: + resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} + + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + + lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + + lilconfig@3.1.1: + resolution: {integrity: sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==} + engines: {node: '>=14'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + linebreak@1.1.0: + resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + linkify-it@3.0.3: + resolution: {integrity: sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==} + + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + + lint-staged@15.2.4: + resolution: {integrity: sha512-3F9KRQIS2fVDGtCkBp4Bx0jswjX7zUcKx6OF0ZeY1prksUyKPRIIUqZhIUYAstJfvj6i48VFs4dwVIbCYwvTYQ==} + engines: {node: '>=18.12.0'} + hasBin: true + + listr2@4.0.5: + resolution: {integrity: sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA==} + engines: {node: '>=12'} + peerDependencies: + enquirer: '>= 2.3.0 < 3' + peerDependenciesMeta: + enquirer: + optional: true + + listr2@8.2.1: + resolution: {integrity: sha512-irTfvpib/rNiD637xeevjO2l3Z5loZmuaRi0L0YE5LfijwVY96oyVn0DFD3o/teAok7nfobMG1THvvcHh/BP6g==} + engines: {node: '>=18.0.0'} + + load-json-file@4.0.0: + resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} + engines: {node: '>=4'} + + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + loader-runner@4.3.0: + resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} + engines: {node: '>=6.11.5'} + + local-pkg@0.5.0: + resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} + engines: {node: '>=14'} + + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + locate-path@7.2.0: + resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + + lodash.castarray@4.4.0: + resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} + + lodash.curry@4.1.1: + resolution: {integrity: sha512-/u14pXGviLaweY5JI0IUzgzF2J6Ne8INyzAZjImcryjgkZ+ebruBxy2/JaOOkTqScddcYtakjhSaeemV8lR0tA==} + + lodash.flow@3.5.0: + resolution: {integrity: sha512-ff3BX/tSioo+XojX4MOsOMhJw0nZoUEF011LX8g8d3gvjVbxd89cCio4BCXronjxcTUIJUoqKEUA+n4CqvvRPw==} + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + + lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + log-update@4.0.0: + resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==} + engines: {node: '>=10'} + + log-update@6.0.0: + resolution: {integrity: sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw==} + engines: {node: '>=18'} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + loupe@2.3.7: + resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + + lower-case-first@2.0.2: + resolution: {integrity: sha512-EVm/rR94FJTZi3zefZ82fLWab+GX14LJN4HrWBcuo6Evmsl9hEfnqxgcHCKb9q+mNf6EVdsjx/qucYFIIB84pg==} + + lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + + lowlight@1.17.0: + resolution: {integrity: sha512-vmtBgYKD+QVNy7tIa7ulz5d//Il9R4MooOVh4nkOf9R9Cb/Dk5TXMSTieg/vDulkBkIWj59/BIlyFQxT9X1oAQ==} + + lowlight@1.20.0: + resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==} + + lru-cache@10.2.2: + resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==} + engines: {node: 14 || >=16.14} + + lru-cache@11.0.1: + resolution: {integrity: sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + lru-cache@9.1.2: + resolution: {integrity: sha512-ERJq3FOzJTxBbFjZ7iDs+NiK4VI9Wz+RdrrAB8dio1oV+YvdPzUEE4QNiT2VD51DkIbCYRUUzCRkssXCHqSnKQ==} + engines: {node: 14 || >=16.14} + + lucide-react@0.365.0: + resolution: {integrity: sha512-sJYpPyyzGHI4B3pys+XSFnE4qtSWc68rFnDLxbNNKjkLST5XSx9DNn5+1Z3eFgFiw39PphNRiVBSVb+AL3oKwA==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 + + mac-ca@2.0.3: + resolution: {integrity: sha512-FkcNXYdUzfNEElPoYYLDHOXPCQePj4iK7Ir4OYy6Tr7yAUS2bFud4Uh1QdIsATDwRQN/miNUyZEltX2nA/tDAQ==} + + magic-string@0.30.10: + resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + make-dir@1.3.0: + resolution: {integrity: sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==} + engines: {node: '>=4'} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + map-cache@0.2.2: + resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==} + engines: {node: '>=0.10.0'} + + map-stream@0.1.0: + resolution: {integrity: sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==} + + markdown-it@12.3.2: + resolution: {integrity: sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==} + hasBin: true + + markdown-it@14.1.0: + resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + hasBin: true + + markdown-table@3.0.3: + resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==} + + marked@13.0.0: + resolution: {integrity: sha512-VTeDCd9txf4KLLljUZ0nljE/Incb9SrWuueE44QVuU0pkOdh4sfCeW1Z6lPcxyDRSVY6rm8db/0OPaN75RNUmw==} + engines: {node: '>= 18'} + hasBin: true + + marked@7.0.4: + resolution: {integrity: sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ==} + engines: {node: '>= 16'} + hasBin: true + + md-to-react-email@5.0.2: + resolution: {integrity: sha512-x6kkpdzIzUhecda/yahltfEl53mH26QdWu4abUF9+S0Jgam8P//Ciro8cdhyMHnT5MQUJYrIbO6ORM2UxPiNNA==} + peerDependencies: + react: 18.x + + mdast-util-definitions@5.1.2: + resolution: {integrity: sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==} + + mdast-util-find-and-replace@2.2.2: + resolution: {integrity: sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw==} + + mdast-util-from-markdown@0.8.5: + resolution: {integrity: sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==} + + mdast-util-from-markdown@1.3.1: + resolution: {integrity: sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==} + + mdast-util-from-markdown@2.0.2: + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + + mdast-util-gfm-autolink-literal@1.0.3: + resolution: {integrity: sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA==} + + mdast-util-gfm-footnote@1.0.2: + resolution: {integrity: sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ==} + + mdast-util-gfm-strikethrough@1.0.3: + resolution: {integrity: sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ==} + + mdast-util-gfm-table@1.0.7: + resolution: {integrity: sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg==} + + mdast-util-gfm-task-list-item@1.0.2: + resolution: {integrity: sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ==} + + mdast-util-gfm@2.0.2: + resolution: {integrity: sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg==} + + mdast-util-math@2.0.2: + resolution: {integrity: sha512-8gmkKVp9v6+Tgjtq6SYx9kGPpTf6FVYRa53/DLh479aldR9AyP48qeVOgNZ5X7QUK7nOy4yw7vg6mbiGcs9jWQ==} + + mdast-util-phrasing@3.0.1: + resolution: {integrity: sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@12.3.0: + resolution: {integrity: sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==} + + mdast-util-to-markdown@1.5.0: + resolution: {integrity: sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@2.0.0: + resolution: {integrity: sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==} + + mdast-util-to-string@3.2.0: + resolution: {integrity: sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + mdast@3.0.0: + resolution: {integrity: sha512-xySmf8g4fPKMeC07jXGz971EkLbWAJ83s4US2Tj9lEdnZ142UP5grN73H1Xd3HzrdbU5o9GYYP/y8F9ZSwLE9g==} + deprecated: '`mdast` was renamed to `remark`' + + mdn-data@2.0.28: + resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} + + mdn-data@2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + + mdurl@1.0.1: + resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} + + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + memorystream@0.3.1: + resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} + engines: {node: '>= 0.10.0'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + meros@1.3.0: + resolution: {integrity: sha512-2BNGOimxEz5hmjUG2FwoxCt5HN7BXdaWyFqEwxPTrJzVdABtrL4TiHTcsWSFAxPQ/tOnEaQEJh3qWq71QRMY+w==} + engines: {node: '>=13'} + peerDependencies: + '@types/node': '>=13' + peerDependenciesMeta: + '@types/node': + optional: true + + micromark-core-commonmark@1.1.0: + resolution: {integrity: sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==} + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-gfm-autolink-literal@1.0.5: + resolution: {integrity: sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg==} + + micromark-extension-gfm-footnote@1.1.2: + resolution: {integrity: sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q==} + + micromark-extension-gfm-strikethrough@1.0.7: + resolution: {integrity: sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw==} + + micromark-extension-gfm-table@1.0.7: + resolution: {integrity: sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw==} + + micromark-extension-gfm-tagfilter@1.0.2: + resolution: {integrity: sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g==} + + micromark-extension-gfm-task-list-item@1.0.5: + resolution: {integrity: sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ==} + + micromark-extension-gfm@2.0.3: + resolution: {integrity: sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ==} + + micromark-extension-math@2.1.2: + resolution: {integrity: sha512-es0CcOV89VNS9wFmyn+wyFTKweXGW4CEvdaAca6SWRWPyYCbBisnjaHLjWO4Nszuiud84jCpkHsqAJoa768Pvg==} + + micromark-factory-destination@1.1.0: + resolution: {integrity: sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@1.1.0: + resolution: {integrity: sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@1.1.0: + resolution: {integrity: sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@1.1.0: + resolution: {integrity: sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@1.1.0: + resolution: {integrity: sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@1.2.0: + resolution: {integrity: sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@1.1.0: + resolution: {integrity: sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@1.1.0: + resolution: {integrity: sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@1.1.0: + resolution: {integrity: sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@1.1.0: + resolution: {integrity: sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@1.1.0: + resolution: {integrity: sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@1.1.0: + resolution: {integrity: sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@1.2.0: + resolution: {integrity: sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@1.1.0: + resolution: {integrity: sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@1.1.0: + resolution: {integrity: sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@1.2.0: + resolution: {integrity: sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@1.1.0: + resolution: {integrity: sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@1.1.0: + resolution: {integrity: sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@1.1.0: + resolution: {integrity: sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@2.11.4: + resolution: {integrity: sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==} + + micromark@3.2.0: + resolution: {integrity: sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + + micromatch@4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + + micromatch@4.0.6: + resolution: {integrity: sha512-Y4Ypn3oujJYxJcMacVgcs92wofTHxp9FzfDpQON4msDefoC0lb3ETvQLOdLcbhSwU1bz8HrL/1sygfBIHudrkQ==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + minimatch@10.0.1: + resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} + engines: {node: 20 || >=22} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@4.2.3: + resolution: {integrity: sha512-lIUdtK5hdofgCTu3aT0sOaHsYR37viUuIc0rwnnDXImbwFRcumyLMeZaM0t0I/fgxS6s6JMfu0rLD1Wz9pv1ng==} + engines: {node: '>=10'} + + minimatch@5.0.1: + resolution: {integrity: sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==} + engines: {node: '>=10'} + + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + + minimatch@9.0.1: + resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==} + engines: {node: '>=16 || 14 >=14.17'} + + minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimatch@9.0.4: + resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + + minipass@7.1.1: + resolution: {integrity: sha512-UZ7eQ+h8ywIRAW1hIEl2AqdwzJucU/Kp59+8kkZeSvafXhZjul247BvIJjEVFVeON6d7lM46XX1HXCduKAS8VA==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + mkdist@1.6.0: + resolution: {integrity: sha512-nD7J/mx33Lwm4Q4qoPgRBVA9JQNKgyE7fLo5vdPWVDdjz96pXglGERp/fRnGPCTB37Kykfxs5bDdXa9BWOT9nw==} + hasBin: true + peerDependencies: + sass: ^1.78.0 + typescript: '>=5.5.4' + vue-tsc: ^1.8.27 || ^2.0.21 + peerDependenciesMeta: + sass: + optional: true + typescript: + optional: true + vue-tsc: + optional: true + + mlly@1.7.0: + resolution: {integrity: sha512-U9SDaXGEREBYQgfejV97coK0UL1r+qnF2SyO9A3qcI8MzKnsIFKHNVEkrDyNncQTKQQumsasmeq84eNMdBfsNQ==} + + mlly@1.7.4: + resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} + + mocha@10.4.0: + resolution: {integrity: sha512-eqhGB8JKapEYcC4ytX/xrzKforgEc3j1pGlAXVy3eRwrtAy5/nIfT1SvgGzfN0XZZxeLq0aQWkOUAmqIJiv+bA==} + engines: {node: '>= 14.0.0'} + hasBin: true + + moment-timezone@0.5.45: + resolution: {integrity: sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ==} + + moment@2.29.4: + resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==} + + morgan@1.10.0: + resolution: {integrity: sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==} + engines: {node: '>= 0.8.0'} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mute-stream@0.0.8: + resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanoid@4.0.2: + resolution: {integrity: sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==} + engines: {node: ^14 || ^16 || >=18} + hasBin: true + + napi-build-utils@1.0.2: + resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} + + natural-compare-lite@1.4.0: + resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + next-themes@0.2.1: + resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==} + peerDependencies: + next: '*' + react: '*' + react-dom: '*' + + next@13.5.3: + resolution: {integrity: sha512-4Nt4HRLYDW/yRpJ/QR2t1v63UOMS55A38dnWv3UDOWGezuY0ZyFO1ABNbD7mulVzs9qVhgy2+ppjdsANpKP1mg==} + engines: {node: '>=16.14.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + react: ^18.2.0 + react-dom: ^18.2.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + sass: + optional: true + + next@14.1.4: + resolution: {integrity: sha512-1WTaXeSrUwlz/XcnhGTY7+8eiaFvdet5z9u3V2jb+Ek1vFo0VhHKSAIJvDWfQpttWjnyw14kBeq28TPq7bTeEQ==} + engines: {node: '>=18.17.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + react: ^18.2.0 + react-dom: ^18.2.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + sass: + optional: true + + nice-try@1.0.5: + resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} + + no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + + node-abi@3.62.0: + resolution: {integrity: sha512-CPMcGa+y33xuL1E0TcNIu4YyaZCxnnvkVaEXrsosR3FxN+fV8xvb7Mzpb7IgKler10qeMkE6+Dp8qJhpzdq35g==} + engines: {node: '>=10'} + + node-addon-api@4.3.0: + resolution: {integrity: sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==} + + node-addon-api@7.0.0: + resolution: {integrity: sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==} + + node-cleanup@2.1.2: + resolution: {integrity: sha512-qN8v/s2PAJwGUtr1/hYTpNKlD6Y9rc4p8KSmJXyGdYGZsDGKXrGThikLFP9OCHFeLeEpQzPwiAtdIvBLqm//Hw==} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + + node-fetch-native@1.6.4: + resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-forge@1.3.1: + resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} + engines: {node: '>= 6.13.0'} + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + node-releases@2.0.13: + resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} + + node-releases@2.0.14: + resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} + + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + + nopt@7.2.1: + resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + + normalize-package-data@2.5.0: + resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + + normalize-path@2.1.1: + resolution: {integrity: sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==} + engines: {node: '>=0.10.0'} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + + npm-run-all@4.1.5: + resolution: {integrity: sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==} + engines: {node: '>= 4'} + hasBin: true + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + nullthrows@1.1.1: + resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==} + + numeral@2.0.6: + resolution: {integrity: sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA==} + + nypm@0.3.8: + resolution: {integrity: sha512-IGWlC6So2xv6V4cIDmoV0SwwWx7zLG086gyqkyumteH2fIgCAM4nDVFB2iDRszDvmdSVW9xb1N+2KjQ6C7d4og==} + engines: {node: ^14.16.0 || >=16.10.0} + hasBin: true + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + object-inspect@1.12.3: + resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} + + object-inspect@1.13.1: + resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} + + object-is@1.1.6: + resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.4: + resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} + engines: {node: '>= 0.4'} + + object.assign@4.1.5: + resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} + engines: {node: '>= 0.4'} + + object.entries@1.1.7: + resolution: {integrity: sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.7: + resolution: {integrity: sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.1: + resolution: {integrity: sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==} + + object.hasown@1.1.3: + resolution: {integrity: sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA==} + + object.values@1.1.7: + resolution: {integrity: sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==} + engines: {node: '>= 0.4'} + + ohash@1.1.3: + resolution: {integrity: sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + + on-finished@2.3.0: + resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} + engines: {node: '>= 0.8'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + on-headers@1.0.2: + resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + + only@0.0.2: + resolution: {integrity: sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==} + + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + openai-edge@0.5.1: + resolution: {integrity: sha512-/Q/b9wkPKWIzcnsp2o0Of5QW/t4A0XLw+E7rjVSYR3gLVt+pWpFuWCgWPWUVS1UrrLJPvzmndwe1JMpfqLRJIQ==} + engines: {node: '>=12'} + + openai@4.52.7: + resolution: {integrity: sha512-dgxA6UZHary6NXUHEDj5TWt8ogv0+ibH+b4pT5RrWMjiRZVylNwLcw/2ubDrX5n0oUmHX/ZgudMJeemxzOvz7A==} + hasBin: true + + openapi-fetch@0.7.10: + resolution: {integrity: sha512-lDZkHjSxBuSTPXkJuJ9kSpkLxY9jgsVHbKkhS7rukoKi5et5QUlWCEzO/E6PaSHTQkJDPOjXdBJeDOSj2e8QwQ==} + + openapi-typescript-helpers@0.0.4: + resolution: {integrity: sha512-Q0MTapapFAG993+dx8lNw33X6P/6EbFr31yNymJHq56fNc6dODyRm8tWyRnGxuC74lyl1iCRMV6nQCGQsfVNKg==} + + openapi-typescript@7.0.2: + resolution: {integrity: sha512-BBrYEf0YdW31Ernd07cD/qHoalSuiiUQvy+rHvU/1Iz9WbcFpRsIXrnfEnrEuiGTRuKCG6cDQCrxNK/rbwQRLg==} + hasBin: true + peerDependencies: + typescript: ^5.x + + optimist@0.3.7: + resolution: {integrity: sha512-TCx0dXQzVtSCg2OgY/bO9hjM9cV4XYx09TVK+s3+FhkjT6LovsLe+pPMzpWf+6yXK/hUizs2gUoTw3jHM0VaTQ==} + + optionator@0.9.3: + resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} + engines: {node: '>= 0.8.0'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + + orderedmap@2.1.1: + resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} + + os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + + ovsx@0.9.5: + resolution: {integrity: sha512-x8jaFQAA+KLxZ9HAQ8ZBbBxNsrrjjpEnVihfOhb/iuXWCso1n2oKaDJuLbA9O5FtBgtGCy0n23PKf728kOmX8g==} + engines: {node: '>= 20'} + hasBin: true + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + p-limit@5.0.0: + resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} + engines: {node: '>=18'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + pako@0.2.9: + resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + + param-case@3.0.4: + resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-css-color@0.2.1: + resolution: {integrity: sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==} + + parse-entities@2.0.0: + resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==} + + parse-filepath@1.0.2: + resolution: {integrity: sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==} + engines: {node: '>=0.8'} + + parse-gitignore@2.0.0: + resolution: {integrity: sha512-RmVuCHWsfu0QPNW+mraxh/xjQVw/lhUCUru8Zni3Ctq3AoMhpDTq0OVdKS6iesd6Kqb7viCV3isAL43dciOSog==} + engines: {node: '>=14'} + + parse-json@4.0.0: + resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} + engines: {node: '>=4'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse-json@8.1.0: + resolution: {integrity: sha512-rum1bPifK5SSar35Z6EKZuYPJx85pkNaFrxBK3mwdfSJ1/WKbYrjoW/zTPSjRRamfmVX1ACBIdFAO0VRErW/EA==} + engines: {node: '>=18'} + + parse-passwd@1.0.0: + resolution: {integrity: sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==} + engines: {node: '>=0.10.0'} + + parse-path@7.0.0: + resolution: {integrity: sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog==} + + parse-semver@1.1.1: + resolution: {integrity: sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==} + + parse-url@9.2.0: + resolution: {integrity: sha512-bCgsFI+GeGWPAvAiUv63ZorMeif3/U0zaXABGJbOWt5OH2KCaPHF6S+0ok4aqM9RuIPGyZdx9tR9l13PsW4AYQ==} + engines: {node: '>=14.13.0'} + + parse5-htmlparser2-tree-adapter@7.0.0: + resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==} + + parse5@6.0.1: + resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + + parse5@7.1.2: + resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + + parseley@0.12.1: + resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + pascal-case@3.1.2: + resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + + path-case@3.0.4: + resolution: {integrity: sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-exists@5.0.0: + resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@2.0.1: + resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} + engines: {node: '>=4'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-root-regex@0.1.2: + resolution: {integrity: sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==} + engines: {node: '>=0.10.0'} + + path-root@0.1.1: + resolution: {integrity: sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==} + engines: {node: '>=0.10.0'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-scurry@2.0.0: + resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} + engines: {node: 20 || >=22} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + path-type@3.0.0: + resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} + engines: {node: '>=4'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + + pause-stream@0.0.11: + resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==} + + peberminta@0.9.0: + resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} + + peek-stream@1.1.3: + resolution: {integrity: sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + periscopic@3.1.0: + resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} + + picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + + picocolors@1.0.1: + resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + pidtree@0.3.1: + resolution: {integrity: sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==} + engines: {node: '>=0.10'} + hasBin: true + + pidtree@0.6.0: + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} + engines: {node: '>=0.10'} + hasBin: true + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pify@3.0.0: + resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} + engines: {node: '>=4'} + + pino-abstract-transport@1.2.0: + resolution: {integrity: sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==} + + pino-std-serializers@6.2.2: + resolution: {integrity: sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==} + + pino@8.21.0: + resolution: {integrity: sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q==} + hasBin: true + + pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + + pkg-types@1.1.1: + resolution: {integrity: sha512-ko14TjmDuQJ14zsotODv7dBlwxKhUKQEhuhmbqo1uCi9BB0Z2alo/wAXg6q1dTR5TyuqYyWhjtfe/Tsh+X28jQ==} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + playwright-core@1.48.1: + resolution: {integrity: sha512-Yw/t4VAFX/bBr1OzwCuOMZkY1Cnb4z/doAFSwf4huqAGWmf9eMNjmK7NiOljCdLmxeRYcGPPmcDgU0zOlzP0YA==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.48.1: + resolution: {integrity: sha512-j8CiHW/V6HxmbntOfyB4+T/uk08tBy6ph0MpBXwuoofkSnLmlfdYNNkFTYD6ofzzlSqLA1fwH4vwvVFvJgLN0w==} + engines: {node: '>=18'} + hasBin: true + + plur@4.0.0: + resolution: {integrity: sha512-4UGewrYgqDFw9vV6zNV+ADmPAUAfJPKtGvb/VdpQAx25X5f3xXdGdyOEVFwkl8Hl/tl7+xbeHqSEM+D5/TirUg==} + engines: {node: '>=10'} + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + + possible-typed-array-names@1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} + + postcss-calc@10.1.1: + resolution: {integrity: sha512-NYEsLHh8DgG/PRH2+G9BTuUdtf9ViS+vdoQ0YA5OQdGsfN4ztiwtDWNtBl9EKeqNMFnIu8IKZ0cLxEQ5r5KVMw==} + engines: {node: ^18.12 || ^20.9 || >=22.0} + peerDependencies: + postcss: ^8.4.38 + + postcss-colormin@7.0.2: + resolution: {integrity: sha512-YntRXNngcvEvDbEjTdRWGU606eZvB5prmHG4BF0yLmVpamXbpsRJzevyy6MZVyuecgzI2AWAlvFi8DAeCqwpvA==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-convert-values@7.0.4: + resolution: {integrity: sha512-e2LSXPqEHVW6aoGbjV9RsSSNDO3A0rZLCBxN24zvxF25WknMPpX8Dm9UxxThyEbaytzggRuZxaGXqaOhxQ514Q==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-discard-comments@7.0.3: + resolution: {integrity: sha512-q6fjd4WU4afNhWOA2WltHgCbkRhZPgQe7cXF74fuVB/ge4QbM9HEaOIzGSiMvM+g/cOsNAUGdf2JDzqA2F8iLA==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-discard-duplicates@7.0.1: + resolution: {integrity: sha512-oZA+v8Jkpu1ct/xbbrntHRsfLGuzoP+cpt0nJe5ED2FQF8n8bJtn7Bo28jSmBYwqgqnqkuSXJfSUEE7if4nClQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-discard-empty@7.0.0: + resolution: {integrity: sha512-e+QzoReTZ8IAwhnSdp/++7gBZ/F+nBq9y6PomfwORfP7q9nBpK5AMP64kOt0bA+lShBFbBDcgpJ3X4etHg4lzA==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-discard-overridden@7.0.0: + resolution: {integrity: sha512-GmNAzx88u3k2+sBTZrJSDauR0ccpE24omTQCVmaTTZFz1du6AasspjaUPMJ2ud4RslZpoFKyf+6MSPETLojc6w==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.0.1: + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@4.0.1: + resolution: {integrity: sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + + postcss-load-config@4.0.2: + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + + postcss-merge-longhand@7.0.4: + resolution: {integrity: sha512-zer1KoZA54Q8RVHKOY5vMke0cCdNxMP3KBfDerjH/BYHh4nCIh+1Yy0t1pAEQF18ac/4z3OFclO+ZVH8azjR4A==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-merge-rules@7.0.4: + resolution: {integrity: sha512-ZsaamiMVu7uBYsIdGtKJ64PkcQt6Pcpep/uO90EpLS3dxJi6OXamIobTYcImyXGoW0Wpugh7DSD3XzxZS9JCPg==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-minify-font-values@7.0.0: + resolution: {integrity: sha512-2ckkZtgT0zG8SMc5aoNwtm5234eUx1GGFJKf2b1bSp8UflqaeFzR50lid4PfqVI9NtGqJ2J4Y7fwvnP/u1cQog==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-minify-gradients@7.0.0: + resolution: {integrity: sha512-pdUIIdj/C93ryCHew0UgBnL2DtUS3hfFa5XtERrs4x+hmpMYGhbzo6l/Ir5de41O0GaKVpK1ZbDNXSY6GkXvtg==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-minify-params@7.0.2: + resolution: {integrity: sha512-nyqVLu4MFl9df32zTsdcLqCFfE/z2+f8GE1KHPxWOAmegSo6lpV2GNy5XQvrzwbLmiU7d+fYay4cwto1oNdAaQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-minify-selectors@7.0.4: + resolution: {integrity: sha512-JG55VADcNb4xFCf75hXkzc1rNeURhlo7ugf6JjiiKRfMsKlDzN9CXHZDyiG6x/zGchpjQS+UAgb1d4nqXqOpmA==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-nested@6.0.1: + resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-normalize-charset@7.0.0: + resolution: {integrity: sha512-ABisNUXMeZeDNzCQxPxBCkXexvBrUHV+p7/BXOY+ulxkcjUZO0cp8ekGBwvIh2LbCwnWbyMPNJVtBSdyhM2zYQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-display-values@7.0.0: + resolution: {integrity: sha512-lnFZzNPeDf5uGMPYgGOw7v0BfB45+irSRz9gHQStdkkhiM0gTfvWkWB5BMxpn0OqgOQuZG/mRlZyJxp0EImr2Q==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-positions@7.0.0: + resolution: {integrity: sha512-I0yt8wX529UKIGs2y/9Ybs2CelSvItfmvg/DBIjTnoUSrPxSV7Z0yZ8ShSVtKNaV/wAY+m7bgtyVQLhB00A1NQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-repeat-style@7.0.0: + resolution: {integrity: sha512-o3uSGYH+2q30ieM3ppu9GTjSXIzOrRdCUn8UOMGNw7Af61bmurHTWI87hRybrP6xDHvOe5WlAj3XzN6vEO8jLw==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-string@7.0.0: + resolution: {integrity: sha512-w/qzL212DFVOpMy3UGyxrND+Kb0fvCiBBujiaONIihq7VvtC7bswjWgKQU/w4VcRyDD8gpfqUiBQ4DUOwEJ6Qg==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-timing-functions@7.0.0: + resolution: {integrity: sha512-tNgw3YV0LYoRwg43N3lTe3AEWZ66W7Dh7lVEpJbHoKOuHc1sLrzMLMFjP8SNULHaykzsonUEDbKedv8C+7ej6g==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-unicode@7.0.2: + resolution: {integrity: sha512-ztisabK5C/+ZWBdYC+Y9JCkp3M9qBv/XFvDtSw0d/XwfT3UaKeW/YTm/MD/QrPNxuecia46vkfEhewjwcYFjkg==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-url@7.0.0: + resolution: {integrity: sha512-+d7+PpE+jyPX1hDQZYG+NaFD+Nd2ris6r8fPTBAjE8z/U41n/bib3vze8x7rKs5H1uEw5ppe9IojewouHk0klQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-whitespace@7.0.0: + resolution: {integrity: sha512-37/toN4wwZErqohedXYqWgvcHUGlT8O/m2jVkAfAe9Bd4MzRqlBmXrJRePH0e9Wgnz2X7KymTgTOaaFizQe3AQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-ordered-values@7.0.1: + resolution: {integrity: sha512-irWScWRL6nRzYmBOXReIKch75RRhNS86UPUAxXdmW/l0FcAsg0lvAXQCby/1lymxn/o0gVa6Rv/0f03eJOwHxw==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-reduce-initial@7.0.2: + resolution: {integrity: sha512-pOnu9zqQww7dEKf62Nuju6JgsW2V0KRNBHxeKohU+JkHd/GAH5uvoObqFLqkeB2n20mr6yrlWDvo5UBU5GnkfA==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-reduce-transforms@7.0.0: + resolution: {integrity: sha512-pnt1HKKZ07/idH8cpATX/ujMbtOGhUfE+m8gbqwJE05aTaNw8gbo34a2e3if0xc0dlu75sUOiqvwCGY3fzOHew==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-selector-parser@6.0.10: + resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} + engines: {node: '>=4'} + + postcss-selector-parser@6.0.13: + resolution: {integrity: sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==} + engines: {node: '>=4'} + + postcss-selector-parser@6.0.16: + resolution: {integrity: sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==} + engines: {node: '>=4'} + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-selector-parser@7.1.0: + resolution: {integrity: sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==} + engines: {node: '>=4'} + + postcss-svgo@7.0.1: + resolution: {integrity: sha512-0WBUlSL4lhD9rA5k1e5D8EN5wCEyZD6HJk0jIvRxl+FDVOMlJ7DePHYWGGVc5QRqrJ3/06FTXM0bxjmJpmTPSA==} + engines: {node: ^18.12.0 || ^20.9.0 || >= 18} + peerDependencies: + postcss: ^8.4.31 + + postcss-unique-selectors@7.0.3: + resolution: {integrity: sha512-J+58u5Ic5T1QjP/LDV9g3Cx4CNOgB5vz+kM6+OxHHhFACdcDeKhBXjQmB7fnIZM12YSTvsL0Opwco83DmacW2g==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.4.14: + resolution: {integrity: sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.4.38: + resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.3: + resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} + engines: {node: ^10 || ^12 || >=14} + + posthog-js@1.139.0: + resolution: {integrity: sha512-FuYlxQFO0Dq5X1/bFEM8F+NgOqZiVh4fPVHHeOTWMkqVP+pCnODQitbtW0hgT0/EE665w0xpZBk93YavaZRhzQ==} + + preact@10.22.0: + resolution: {integrity: sha512-RRurnSjJPj4rp5K6XoP45Ui33ncb7e4H7WiOHVpjbkvqvA3U+N8Z6Qbo0AE6leGYBV66n8EhEaFixvIu3SkxFw==} + + prebuild-install@7.1.2: + resolution: {integrity: sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==} + engines: {node: '>=10'} + hasBin: true + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + + prettier@3.2.5: + resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==} + engines: {node: '>=14'} + hasBin: true + + pretty-bytes@6.1.1: + resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} + engines: {node: ^14.13.1 || >=16.0.0} + + pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + prism-react-renderer@2.1.0: + resolution: {integrity: sha512-I5cvXHjA1PVGbGm1MsWCpvBCRrYyxEri0MC7/JbfIfYfcXAxHyO5PaUjs3A8H5GW6kJcLhTHxxMaOZZpRZD2iQ==} + peerDependencies: + react: '>=16.0.0' + + prismjs@1.27.0: + resolution: {integrity: sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==} + engines: {node: '>=6'} + + prismjs@1.29.0: + resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} + engines: {node: '>=6'} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process-warning@3.0.0: + resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + promise@7.3.1: + resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + property-information@5.6.0: + resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==} + + property-information@6.3.0: + resolution: {integrity: sha512-gVNZ74nqhRMiIUYWGQdosYetaKc83x8oT41a0LlV3AAFCAZwCpg4vmGkq8t34+cUhp3cnM4XDiU/7xlgK7HGrg==} + + prosemirror-changeset@2.2.1: + resolution: {integrity: sha512-J7msc6wbxB4ekDFj+n9gTW/jav/p53kdlivvuppHsrZXCaQdVgRghoZbSS3kwrRyAstRVQ4/+u5k7YfLgkkQvQ==} + + prosemirror-collab@1.3.1: + resolution: {integrity: sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==} + + prosemirror-commands@1.6.2: + resolution: {integrity: sha512-0nDHH++qcf/BuPLYvmqZTUUsPJUCPBUXt0J1ErTcDIS369CTp773itzLGIgIXG4LJXOlwYCr44+Mh4ii6MP1QA==} + + prosemirror-dropcursor@1.8.1: + resolution: {integrity: sha512-M30WJdJZLyXHi3N8vxN6Zh5O8ZBbQCz0gURTfPmTIBNQ5pxrdU7A58QkNqfa98YEjSAL1HUyyU34f6Pm5xBSGw==} + + prosemirror-gapcursor@1.3.2: + resolution: {integrity: sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==} + + prosemirror-history@1.4.1: + resolution: {integrity: sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==} + + prosemirror-inputrules@1.4.0: + resolution: {integrity: sha512-6ygpPRuTJ2lcOXs9JkefieMst63wVJBgHZGl5QOytN7oSZs3Co/BYbc3Yx9zm9H37Bxw8kVzCnDsihsVsL4yEg==} + + prosemirror-keymap@1.2.2: + resolution: {integrity: sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ==} + + prosemirror-markdown@1.13.1: + resolution: {integrity: sha512-Sl+oMfMtAjWtlcZoj/5L/Q39MpEnVZ840Xo330WJWUvgyhNmLBLN7MsHn07s53nG/KImevWHSE6fEj4q/GihHw==} + + prosemirror-menu@1.2.4: + resolution: {integrity: sha512-S/bXlc0ODQup6aiBbWVsX/eM+xJgCTAfMq/nLqaO5ID/am4wS0tTCIkzwytmao7ypEtjj39i7YbJjAgO20mIqA==} + + prosemirror-model@1.24.1: + resolution: {integrity: sha512-YM053N+vTThzlWJ/AtPtF1j0ebO36nvbmDy4U7qA2XQB8JVaQp1FmB9Jhrps8s+z+uxhhVTny4m20ptUvhk0Mg==} + + prosemirror-schema-basic@1.2.3: + resolution: {integrity: sha512-h+H0OQwZVqMon1PNn0AG9cTfx513zgIG2DY00eJ00Yvgb3UD+GQ/VlWW5rcaxacpCGT1Yx8nuhwXk4+QbXUfJA==} + + prosemirror-schema-list@1.4.1: + resolution: {integrity: sha512-jbDyaP/6AFfDfu70VzySsD75Om2t3sXTOdl5+31Wlxlg62td1haUpty/ybajSfJ1pkGadlOfwQq9kgW5IMo1Rg==} + + prosemirror-state@1.4.3: + resolution: {integrity: sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==} + + prosemirror-tables@1.6.1: + resolution: {integrity: sha512-p8WRJNA96jaNQjhJolmbxTzd6M4huRE5xQ8OxjvMhQUP0Nzpo4zz6TztEiwk6aoqGBhz9lxRWR1yRZLlpQN98w==} + + prosemirror-trailing-node@3.0.0: + resolution: {integrity: sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==} + peerDependencies: + prosemirror-model: ^1.22.1 + prosemirror-state: ^1.4.2 + prosemirror-view: ^1.33.8 + + prosemirror-transform@1.10.2: + resolution: {integrity: sha512-2iUq0wv2iRoJO/zj5mv8uDUriOHWzXRnOTVgCzSXnktS/2iQRa3UUQwVlkBlYZFtygw6Nh1+X4mGqoYBINn5KQ==} + + prosemirror-view@1.37.1: + resolution: {integrity: sha512-MEAnjOdXU1InxEmhjgmEzQAikaS6lF3hD64MveTPpjOGNTl87iRLA1HupC/DEV6YuK7m4Q9DHFNTjwIVtqz5NA==} + + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + + protocols@2.0.1: + resolution: {integrity: sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==} + + ps-tree@1.2.0: + resolution: {integrity: sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==} + engines: {node: '>= 0.10'} + hasBin: true + + pump@2.0.1: + resolution: {integrity: sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==} + + pump@3.0.0: + resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} + + pumpify@1.5.1: + resolution: {integrity: sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==} + + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + + punycode@1.4.1: + resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + pure-color@1.3.0: + resolution: {integrity: sha512-QFADYnsVoBMw1srW7OVKEYjG+MbIa49s54w1MA1EDY6r2r/sTcKKYqRX1f4GYvnXP7eN/Pe9HFcX+hwzmrXRHA==} + + pvtsutils@1.3.5: + resolution: {integrity: sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==} + + pvutils@1.1.3: + resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==} + engines: {node: '>=6.0.0'} + + qs@6.12.1: + resolution: {integrity: sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==} + engines: {node: '>=0.6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + queue-tick@1.0.1: + resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + + rc9@2.1.2: + resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + react-activity-calendar@2.2.8: + resolution: {integrity: sha512-+Zr6UlPC4zLMSkXyg/+bQ0Fd10E8V+nT+AU0FHfCIQi6/dLJe73cR+CkFFf8zPE59XhGO24U7FfUyFgbWqV0Bg==} + peerDependencies: + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + + react-base16-styling@0.6.0: + resolution: {integrity: sha512-yvh/7CArceR/jNATXOKDlvTnPKPmGZz7zsenQ3jUwLzHkNUR0CvY3yGYJbWJ/nnxsL8Sgmt5cO3/SILVuPO6TQ==} + + react-day-picker@8.10.0: + resolution: {integrity: sha512-mz+qeyrOM7++1NCb1ARXmkjMkzWVh2GL9YiPbRjKe0zHccvekk4HE+0MPOZOrosn8r8zTHIIeOUXTmXRqmkRmg==} + peerDependencies: + date-fns: ^2.28.0 || ^3.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + react-dom@18.2.0: + resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} + peerDependencies: + react: ^18.2.0 + + react-email@2.1.3: + resolution: {integrity: sha512-Lm6nxt2IsbTatJM+kSPWKvTPpMYfg3SFLJa8WqnaZ91ZRPz4wedZL++P3i59XxgOHGqejt2kN/a8MYiBZP3Mgw==} + engines: {node: '>=18.0.0'} + hasBin: true + + react-error-boundary@4.0.13: + resolution: {integrity: sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==} + peerDependencies: + react: '>=16.13.1' + + react-hook-form@7.48.2: + resolution: {integrity: sha512-H0T2InFQb1hX7qKtDIZmvpU1Xfn/bdahWBN1fH19gSe4bBEqTfmlr7H3XWTaVtiK4/tpPaI1F3355GPMZYge+A==} + engines: {node: '>=12.22.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 + + react-intersection-observer@9.5.2: + resolution: {integrity: sha512-EmoV66/yvksJcGa1rdW0nDNc4I1RifDWkT50gXSFnPLYQ4xUptuDD4V7k+Rj1OgVAlww628KLGcxPXFlOkkU/Q==} + peerDependencies: + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@18.2.0: + resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + + react-json-view@1.21.3: + resolution: {integrity: sha512-13p8IREj9/x/Ye4WI/JpjhoIwuzEgUAtgJZNBJckfzJt1qyh24BdTm6UQNGnyTq9dapQdrqvquZTo3dz1X6Cjw==} + peerDependencies: + react: ^17.0.0 || ^16.3.0 || ^15.5.4 + react-dom: ^17.0.0 || ^16.3.0 || ^15.5.4 + + react-lazy-load@4.0.1: + resolution: {integrity: sha512-TnXRr79X9rlC9UcmO6iyS28rOPHrgkHIP4+b8yZPfs1tw6k/Rp2DmFY8R20BqWR45ZWkpT+4dqV1f+yci+1ozg==} + peerDependencies: + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + + react-lifecycles-compat@3.0.4: + resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} + + react-markdown@8.0.7: + resolution: {integrity: sha512-bvWbzG4MtOU62XqBx3Xx+zB2raaFFsq4mYiAzfjXJMEz2sixgeAfraA3tvzULF02ZdOMUOKTBFFaZJDDrq+BJQ==} + peerDependencies: + '@types/react': '>=16' + react: '>=16' + + react-nice-avatar@1.5.0: + resolution: {integrity: sha512-sGusqbgWIA4Il6Y0zHEfs4XF+a06etNljhwFYiHIGATDmVVf53Nez7U7GY5EwEz5/xGuUhs6uel5AC5NN/2UPg==} + peerDependencies: + react: '>=16.0.0' + + react-promise-suspense@0.3.4: + resolution: {integrity: sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==} + + react-remove-scroll-bar@2.3.4: + resolution: {integrity: sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.5.5: + resolution: {integrity: sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-resizable-panels@1.0.7: + resolution: {integrity: sha512-CluJkHQheeNqIJly2FYDfri3ME+2h2nCXpf0Y+hTO1K1eVtNxXFA5hVp5cUD6NS70iiufswOmnku9QZiLr1hYg==} + peerDependencies: + react: ^16.14.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 + + react-smooth@4.0.1: + resolution: {integrity: sha512-OE4hm7XqR0jNOq3Qmk9mFLyd6p2+j6bvbPJ7qlB7+oo0eNcL2l7WQzG6MBnT3EXY6xzkLMUBec3AfewJdA0J8w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + + react-style-singleton@2.2.1: + resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-syntax-highlighter@15.5.0: + resolution: {integrity: sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg==} + peerDependencies: + react: '>= 0.14.0' + + react-textarea-autosize@8.5.3: + resolution: {integrity: sha512-XT1024o2pqCuZSuBt9FwHlaDeNtVrtCXu0Rnz88t1jUGheCLa3PhjE1GH8Ctm2axEtvdCl5SUHYschyQ0L5QHQ==} + engines: {node: '>=10'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + react-topbar-progress-indicator@4.1.1: + resolution: {integrity: sha512-Oy3ENNKfymt16zoz5SYy/WOepMurB0oeZEyvuHm8JZ3jrTCe1oAUD7fG6HhYt5sg8Wcg5gdkzSWItaFF6c6VhA==} + peerDependencies: + react: '>=16.8.0' + + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + + react@18.2.0: + resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} + engines: {node: '>=0.10.0'} + + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + + read-pkg-up@7.0.1: + resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} + engines: {node: '>=8'} + + read-pkg@3.0.0: + resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==} + engines: {node: '>=4'} + + read-pkg@5.2.0: + resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} + engines: {node: '>=8'} + + read@1.0.7: + resolution: {integrity: sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==} + engines: {node: '>=0.8'} + + readable-from-web@1.0.0: + resolution: {integrity: sha512-tei03fQhxqLEklpIvocFUR9hO42hiyYvdhwoNHAjJztPAQ8QS1NqF2AhLwzGxIGidPBJ4MCqB48wn7OAFCfhsQ==} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readable-stream@4.5.2: + resolution: {integrity: sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + recharts-scale@0.4.5: + resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} + + recharts@2.12.4: + resolution: {integrity: sha512-dM4skmk4fDKEDjL9MNunxv6zcTxePGVEzRnLDXALRpfJ85JoQ0P0APJ/CoJlmnQI0gPjBlOkjzrwrfQrRST3KA==} + engines: {node: '>=14'} + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 + + rechoir@0.6.2: + resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} + engines: {node: '>= 0.10'} + + refa@0.12.1: + resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + reflect.getprototypeof@1.0.4: + resolution: {integrity: sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==} + engines: {node: '>= 0.4'} + + refractor@3.6.0: + resolution: {integrity: sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==} + + regenerator-runtime@0.14.0: + resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==} + + regexp-ast-analysis@0.7.1: + resolution: {integrity: sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + regexp-tree@0.1.27: + resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} + hasBin: true + + regexp.prototype.flags@1.5.1: + resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.2: + resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==} + engines: {node: '>= 0.4'} + + regjsparser@0.10.0: + resolution: {integrity: sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==} + hasBin: true + + rehype-raw@6.1.1: + resolution: {integrity: sha512-d6AKtisSRtDRX4aSPsJGTfnzrX2ZkHQLE5kiUuGOeEoLpbEulFF4hj0mLPbsa+7vmguDKOVVEQdHKDSwoaIDsQ==} + + rehype-sanitize@6.0.0: + resolution: {integrity: sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==} + + relay-runtime@12.0.0: + resolution: {integrity: sha512-QU6JKr1tMsry22DXNy9Whsq5rmvwr3LSZiiWV/9+DFpuTWvp+WFhobWMc8TC4OjKFfNhEZy7mOiqUAn5atQtug==} + + remark-gfm@3.0.1: + resolution: {integrity: sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==} + + remark-math@5.1.1: + resolution: {integrity: sha512-cE5T2R/xLVtfFI4cCePtiRn+e6jKMtFDR3P8V3qpv8wpKjwvHoBA4eJzvX+nVrnlNy0911bdGmuspCSwetfYHw==} + + remark-parse@10.0.2: + resolution: {integrity: sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@10.1.0: + resolution: {integrity: sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + remark@15.0.1: + resolution: {integrity: sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==} + + remedial@1.0.8: + resolution: {integrity: sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg==} + + remove-trailing-separator@1.1.0: + resolution: {integrity: sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==} + + remove-trailing-spaces@1.0.8: + resolution: {integrity: sha512-O3vsMYfWighyFbTd8hk8VaSj9UAGENxAtX+//ugIst2RMk5e03h6RoIS+0ylsFxY1gvmPuAY/PO4It+gPEeySA==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + + resolve-dir@1.0.1: + resolution: {integrity: sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve-path@1.4.0: + resolution: {integrity: sha512-i1xevIst/Qa+nA9olDxLWnLk8YZbi8R/7JPbCMcgyWaFR6bKWaexgJgEB5oc2PKMjYdrHynyz0NY+if+H98t1w==} + engines: {node: '>= 0.8'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + + resolve@2.0.0-next.4: + resolution: {integrity: sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==} + hasBin: true + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + restore-cursor@4.0.0: + resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.3.0: + resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==} + + rfdc@1.3.1: + resolution: {integrity: sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rimraf@5.0.7: + resolution: {integrity: sha512-nV6YcJo5wbLW77m+8KjH8aB/7/rxQy9SZ0HY5shnwULfS+9nmTtVXAJET5NdZmCzA4fPI/Hm1wo/Po/4mopOdg==} + engines: {node: '>=14.18'} + hasBin: true + + rollup-plugin-dts@6.1.1: + resolution: {integrity: sha512-aSHRcJ6KG2IHIioYlvAOcEq6U99sVtqDDKVhnwt70rW6tsz3tv5OSjEiWcgzfsHdLyGXZ/3b/7b/+Za3Y6r1XA==} + engines: {node: '>=16'} + peerDependencies: + rollup: ^3.29.4 || ^4 + typescript: ^4.5 || ^5.0 + + rollup@3.29.5: + resolution: {integrity: sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==} + engines: {node: '>=14.18.0', npm: '>=8.0.0'} + hasBin: true + + rollup@4.27.4: + resolution: {integrity: sha512-RLKxqHEMjh/RGLsDxAEsaLO3mWgyoU6x9w6n1ikAzet4B3gI2/3yP6PWY2p9QzRTh6MfEIXB3MwsOY0Iv3vNrw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rollup@4.36.0: + resolution: {integrity: sha512-zwATAXNQxUcd40zgtQG0ZafcRK4g004WtEl7kbuhTWPvf07PsfohXl39jVUvPF7jvNAIkKPQ2XrsDlWuxBd++Q==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rope-sequence@1.3.4: + resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} + + run-async@2.4.1: + resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} + engines: {node: '>=0.12.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + + safe-array-concat@1.0.1: + resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==} + engines: {node: '>=0.4'} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-regex-test@1.0.0: + resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} + + safe-stable-stringify@2.4.3: + resolution: {integrity: sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + satori@0.10.8: + resolution: {integrity: sha512-WlPLxgpx5kIyMuOO6qi98DhZGIbRk5XJRqENkt6D6gyDBWGyo3M5kcSl0X/oSFHUH+n4unlzwMejoPM47s4zug==} + engines: {node: '>=16'} + + sax@1.3.0: + resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==} + + scheduler@0.23.0: + resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} + + schema-utils@3.3.0: + resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} + engines: {node: '>= 10.13.0'} + + scslre@0.3.0: + resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==} + engines: {node: ^14.0.0 || >=16.0.0} + + scuid@1.1.0: + resolution: {integrity: sha512-MuCAyrGZcTLfQoH2XoBlQ8C6bzwN88XT/0slOGz0pn8+gIP85BOAfYa44ZXQUTOwRwPU0QvgU+V+OSajl/59Xg==} + + scule@1.3.0: + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + + seedrandom@3.0.5: + resolution: {integrity: sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==} + + selderee@0.11.0: + resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.6.2: + resolution: {integrity: sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==} + engines: {node: '>=10'} + hasBin: true + + semver@7.7.1: + resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} + engines: {node: '>=10'} + hasBin: true + + sentence-case@3.0.4: + resolution: {integrity: sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==} + + serialize-javascript@6.0.0: + resolution: {integrity: sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==} + + serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + set-function-length@1.1.1: + resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==} + engines: {node: '>= 0.4'} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.1: + resolution: {integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + + setprototypeof@1.1.0: + resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shebang-command@1.2.0: + resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} + engines: {node: '>=0.10.0'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@1.0.0: + resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} + engines: {node: '>=0.10.0'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-quote@1.8.1: + resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} + + shelljs@0.8.5: + resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==} + engines: {node: '>=4'} + hasBin: true + + side-channel@1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + + side-channel@1.0.6: + resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + signedsource@1.0.0: + resolution: {integrity: sha512-6+eerH9fEnNmi/hyM1DXcRK3pWdoMQtlkQ+ns0ntzunjKqp5i3sKCc80ym8Fib3iaYhdJUOPdhlJWj1tvge2Ww==} + + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + slash@4.0.0: + resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} + engines: {node: '>=12'} + + slice-ansi@3.0.0: + resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} + engines: {node: '>=8'} + + slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + + slice-ansi@5.0.0: + resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} + engines: {node: '>=12'} + + slice-ansi@7.1.0: + resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} + engines: {node: '>=18'} + + smob@1.5.0: + resolution: {integrity: sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==} + + snake-case@3.0.4: + resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} + + socket.io-adapter@2.5.4: + resolution: {integrity: sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==} + + socket.io-client@4.7.3: + resolution: {integrity: sha512-nU+ywttCyBitXIl9Xe0RSEfek4LneYkJxCeNnKCuhwoH4jGXO1ipIUw/VA/+Vvv2G1MTym11fzFC0SxkrcfXDw==} + engines: {node: '>=10.0.0'} + + socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + + socket.io@4.7.3: + resolution: {integrity: sha512-SE+UIQXBQE+GPG2oszWMlsEmWtHVqw/h1VrYJGK5/MC7CH5p58N448HwIrtREcvR4jfdOJAY4ieQfxMr55qbbw==} + engines: {node: '>=10.2.0'} + + sonic-boom@3.8.1: + resolution: {integrity: sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg==} + + sonner@1.3.1: + resolution: {integrity: sha512-+rOAO56b2eI3q5BtgljERSn2umRk63KFIvgb2ohbZ5X+Eb5u+a/7/0ZgswYqgBMg8dyl7n6OXd9KasA8QF9ToA==} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + + source-map-js@1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} + + source-map-js@1.2.0: + resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} + engines: {node: '>=0.10.0'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.8.0-beta.0: + resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} + engines: {node: '>= 8'} + + space-separated-tokens@1.1.5: + resolution: {integrity: sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + + spdx-exceptions@2.3.0: + resolution: {integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==} + + spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + + spdx-expression-parse@4.0.0: + resolution: {integrity: sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==} + + spdx-license-ids@3.0.16: + resolution: {integrity: sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + split@0.3.3: + resolution: {integrity: sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==} + + split@1.0.1: + resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==} + + sponge-case@1.0.1: + resolution: {integrity: sha512-dblb9Et4DAtiZ5YSUZHLl4XhH4uK80GhAZrVXdN4O2P4gQ40Wa5UIOPUHlA/nFd2PLblBZWUioLMMAVrgpoYcA==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + stacktrace-parser@0.1.10: + resolution: {integrity: sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg==} + engines: {node: '>=6'} + + stats-logscale@1.0.9: + resolution: {integrity: sha512-vT0CmCjqwGO2ZTglgM2VUb4WEKzRKIMCRFsBW1G9rePWiq7jtzpHBUp++UBvG5JftjrBWpVztdHTUr4PG+y26g==} + + statuses@1.5.0: + resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} + engines: {node: '>= 0.6'} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + std-env@3.7.0: + resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + + stop-iteration-iterator@1.0.0: + resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==} + engines: {node: '>= 0.4'} + + stoppable@1.1.0: + resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} + engines: {node: '>=4', npm: '>=6'} + + stream-combiner@0.0.4: + resolution: {integrity: sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==} + + stream-shift@1.0.3: + resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + + streamx@2.20.1: + resolution: {integrity: sha512-uTa0mU6WUC65iUvzKH4X9hEdvSW7rbPxPtwfWiLMSj3qTdQbAiUboZTxauKfpFuGIGa1C2BYijZ7wgdUXICJhA==} + + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + + string-env-interpolation@1.0.1: + resolution: {integrity: sha512-78lwMoCcn0nNu8LszbP1UA7g55OeE4v7rCeWnM5B453rnNr4aq+5it3FEYtZrSEiMvHZOZ9Jlqb0OD0M2VInqg==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string-width@7.1.0: + resolution: {integrity: sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==} + engines: {node: '>=18'} + + string.prototype.codepointat@0.2.1: + resolution: {integrity: sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==} + + string.prototype.matchall@4.0.10: + resolution: {integrity: sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ==} + + string.prototype.padend@3.1.5: + resolution: {integrity: sha512-DOB27b/2UTTD+4myKUFh+/fXWcu/UDyASIXfg+7VzoCNNGOfWvoyU/x5pvVHr++ztyt/oSYI1BcWBBG/hmlNjA==} + engines: {node: '>= 0.4'} + + string.prototype.trim@1.2.8: + resolution: {integrity: sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.7: + resolution: {integrity: sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==} + + string.prototype.trimstart@1.0.7: + resolution: {integrity: sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-literal@2.1.0: + resolution: {integrity: sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==} + + style-mod@4.1.0: + resolution: {integrity: sha512-Ca5ib8HrFn+f+0n4N4ScTIA9iTOQ7MaGS1ylHcoVqW9J7w2w8PzN6g9gKmTYgGEBH8e120+RCmhpje6jC5uGWA==} + + style-mod@4.1.2: + resolution: {integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==} + + style-to-object@0.4.2: + resolution: {integrity: sha512-1JGpfPB3lo42ZX8cuPrheZbfQ6kqPPnPHlKMyeRYtfKD+0jG+QsXgXN57O/dvJlzlB2elI6dGmrPnl5VPQFPaA==} + + styled-jsx@5.1.1: + resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + stylehacks@7.0.4: + resolution: {integrity: sha512-i4zfNrGMt9SB4xRK9L83rlsFCgdGANfeDAYacO1pkqcE7cRHPdWHwnKZVz7WY17Veq/FvyYsRAU++Ga+qDFIww==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + sucrase@3.34.0: + resolution: {integrity: sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==} + engines: {node: '>=8'} + hasBin: true + + sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-color@9.4.0: + resolution: {integrity: sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==} + engines: {node: '>=12'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + svelte@4.2.17: + resolution: {integrity: sha512-N7m1YnoXtRf5wya5Gyx3TWuTddI4nAyayyIWFojiWV5IayDYNV5i2mRp/7qNGol4DtxEYxljmrbgp1HM6hUbmQ==} + engines: {node: '>=16'} + + svgo@3.3.2: + resolution: {integrity: sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==} + engines: {node: '>=14.0.0'} + hasBin: true + + swap-case@2.0.2: + resolution: {integrity: sha512-kc6S2YS/2yXbtkSMunBtKdah4VFETZ8Oh6ONSmSd9bRxhqTrtARUCBUiWXH3xVPpvR7tz2CSnkuXVE42EcGnMw==} + + swr@2.2.4: + resolution: {integrity: sha512-njiZ/4RiIhoOlAaLYDqwz5qH/KZXVilRLvomrx83HjzCWTfa+InyfAjv05PSFxnmLzZkNO9ZfvgoqzAaEI4sGQ==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 + + synckit@0.6.2: + resolution: {integrity: sha512-Vhf+bUa//YSTYKseDiiEuQmhGCoIF3CVBhunm3r/DQnYiGT4JssmnKQc44BIyOZRK2pKjXXAgbhfmbeoC9CJpA==} + engines: {node: '>=12.20'} + + tabbable@6.2.0: + resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} + + tailwind-merge@1.14.0: + resolution: {integrity: sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==} + + tailwind-merge@2.2.0: + resolution: {integrity: sha512-SqqhhaL0T06SW59+JVNfAqKdqLs0497esifRrZ7jOaefP3o64fdFNDMrAQWZFMxTLJPiHVjRLUywT8uFz1xNWQ==} + + tailwindcss-animate@1.0.7: + resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders' + + tailwindcss@3.3.3: + resolution: {integrity: sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==} + engines: {node: '>=14.0.0'} + hasBin: true + + tailwindcss@3.4.0: + resolution: {integrity: sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA==} + engines: {node: '>=14.0.0'} + hasBin: true + + tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + + tar-fs@2.1.1: + resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} + + tar-fs@3.0.6: + resolution: {integrity: sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tar-stream@3.1.7: + resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + + terser-webpack-plugin@5.3.10: + resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@swc/core': '*' + esbuild: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@swc/core': + optional: true + esbuild: + optional: true + uglify-js: + optional: true + + terser@5.31.0: + resolution: {integrity: sha512-Q1JFAoUKE5IMfI4Z/lkE/E6+SwgzO+x4tq4v1AyBLRj8VSYvRO6A/rQrPg1yud4g0En9EKI1TvFRF2tQFcoUkg==} + engines: {node: '>=10'} + hasBin: true + + text-decoder@1.2.1: + resolution: {integrity: sha512-x9v3H/lTKIJKQQe7RPQkLfKAnc9lUTkWDypIQgTzPJAq+5/GCDHonmshfvlsNSj58yyshbIJJDLmU15qNERrXQ==} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + thread-stream@2.7.0: + resolution: {integrity: sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw==} + + through2@2.0.5: + resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tinybench@2.8.0: + resolution: {integrity: sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==} + + tinycolor2@1.6.0: + resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + + tinyglobby@0.2.12: + resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==} + engines: {node: '>=12.0.0'} + + tinygradient@0.4.3: + resolution: {integrity: sha512-tBPYQSs6eWukzzAITBSmqcOwZCKACvRa/XjPPh1mj4mnx4G3Drm51HxyCTU/TKnY8kG4hmTe5QlOh9O82aNtJQ==} + + tinypool@0.8.4: + resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==} + engines: {node: '>=14.0.0'} + + tinyspy@2.2.1: + resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} + engines: {node: '>=14.0.0'} + + tippy.js@6.3.7: + resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} + + title-case@3.0.3: + resolution: {integrity: sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==} + + tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + + tmp@0.2.3: + resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==} + engines: {node: '>=14.14'} + + to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toggle-selection@1.0.6: + resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + toml-eslint-parser@0.9.3: + resolution: {integrity: sha512-moYoCvkNUAPCxSW9jmHmRElhm4tVJpHL8ItC/+uYD0EpPSFXbck7yREz9tNdJVTSpHVod8+HoipcpbQ0oE6gsw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + toml@3.0.0: + resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} + + topbar@0.1.4: + resolution: {integrity: sha512-P3n4WnN4GFd2mQXDo30rQmsAGe4V1bVkggtTreSbNyL50Fyc+eVkW5oatSLeGQmJoan2TLIgoXUZypN+6nw4MQ==} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tr46@1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.1.0: + resolution: {integrity: sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==} + + ts-api-utils@1.3.0: + resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + ts-log@2.2.5: + resolution: {integrity: sha512-PGcnJoTBnVGy6yYNFxWVNkdcAuAMstvutN9MgDJIV6L0oG8fB+ZNNy1T+wJzah8RPGor1mZuPQkVfXNDpy9eHA==} + + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + + tsc-watch@6.2.0: + resolution: {integrity: sha512-2LBhf9kjKXnz7KQ/puLHlozMzzUNHAdYBNMkg3eksQJ9GBAgMg8czznM83T5PmsoUvDnXzfIeQn2lNcIYDr8LA==} + engines: {node: '>=12.12.0'} + hasBin: true + peerDependencies: + typescript: '*' + + tsconfig-paths@3.14.2: + resolution: {integrity: sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==} + + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + + tslib@2.5.3: + resolution: {integrity: sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==} + + tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + + tsscmp@1.0.6: + resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} + engines: {node: '>=0.6.x'} + + tsup@8.0.2: + resolution: {integrity: sha512-NY8xtQXdH7hDUAZwcQdY/Vzlw9johQsaqf7iwZ6g1DOUlFYQ5/AtVAjTvihhEyeRlGo4dLRVHtrRaL35M1daqQ==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + + tsutils@3.21.0: + resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} + engines: {node: '>= 6'} + peerDependencies: + typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + + tsx@4.10.5: + resolution: {integrity: sha512-twDSbf7Gtea4I2copqovUiNTEDrT8XNFXsuHpfGbdpW/z9ZW4fTghzzhAG0WfrCuJmJiOEY1nLIjq4u3oujRWQ==} + engines: {node: '>=18.0.0'} + hasBin: true + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + tunnel@0.0.6: + resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} + engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} + + turbo-darwin-64@1.13.3: + resolution: {integrity: sha512-glup8Qx1qEFB5jerAnXbS8WrL92OKyMmg5Hnd4PleLljAeYmx+cmmnsmLT7tpaVZIN58EAAwu8wHC6kIIqhbWA==} + cpu: [x64] + os: [darwin] + + turbo-darwin-arm64@1.13.3: + resolution: {integrity: sha512-/np2xD+f/+9qY8BVtuOQXRq5f9LehCFxamiQnwdqWm5iZmdjygC5T3uVSYuagVFsZKMvX3ycySwh8dylGTl6lg==} + cpu: [arm64] + os: [darwin] + + turbo-linux-64@1.13.3: + resolution: {integrity: sha512-G+HGrau54iAnbXLfl+N/PynqpDwi/uDzb6iM9hXEDG+yJnSJxaHMShhOkXYJPk9offm9prH33Khx2scXrYVW1g==} + cpu: [x64] + os: [linux] + + turbo-linux-arm64@1.13.3: + resolution: {integrity: sha512-qWwEl5VR02NqRyl68/3pwp3c/olZuSp+vwlwrunuoNTm6JXGLG5pTeme4zoHNnk0qn4cCX7DFrOboArlYxv0wQ==} + cpu: [arm64] + os: [linux] + + turbo-windows-64@1.13.3: + resolution: {integrity: sha512-Nudr4bRChfJzBPzEmpVV85VwUYRCGKecwkBFpbp2a4NtrJ3+UP1VZES653ckqCu2FRyRuS0n03v9euMbAvzH+Q==} + cpu: [x64] + os: [win32] + + turbo-windows-arm64@1.13.3: + resolution: {integrity: sha512-ouJCgsVLd3icjRLmRvHQDDZnmGzT64GBupM1Y+TjtYn2LVaEBoV6hicFy8x5DUpnqdLy+YpCzRMkWlwhmkX7sQ==} + cpu: [arm64] + os: [win32] + + turbo@1.13.3: + resolution: {integrity: sha512-n17HJv4F4CpsYTvKzUJhLbyewbXjq1oLCi90i5tW1TiWDz16ML1eDG7wi5dHaKxzh5efIM56SITnuVbMq5dk4g==} + hasBin: true + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@0.6.0: + resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} + engines: {node: '>=8'} + + type-fest@0.7.1: + resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} + engines: {node: '>=8'} + + type-fest@0.8.1: + resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} + engines: {node: '>=8'} + + type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + + type-fest@3.13.1: + resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==} + engines: {node: '>=14.16'} + + type-fest@4.21.0: + resolution: {integrity: sha512-ADn2w7hVPcK6w1I0uWnM//y1rLXZhzB9mr0a3OirzclKF1Wp6VzevUmzz/NRAWunOT6E8HrnpGY7xOfc6K57fA==} + engines: {node: '>=16'} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + typed-array-buffer@1.0.0: + resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.0: + resolution: {integrity: sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.0: + resolution: {integrity: sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.4: + resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} + + typed-rest-client@1.8.11: + resolution: {integrity: sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==} + + typescript@5.1.6: + resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==} + engines: {node: '>=14.17'} + hasBin: true + + typescript@5.3.3: + resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} + engines: {node: '>=14.17'} + hasBin: true + + typescript@5.4.5: + resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} + engines: {node: '>=14.17'} + hasBin: true + + typescript@5.8.2: + resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} + engines: {node: '>=14.17'} + hasBin: true + + ua-parser-js@1.0.37: + resolution: {integrity: sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==} + + uc.micro@1.0.6: + resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} + + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + + ufo@1.5.3: + resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==} + + ufo@1.5.4: + resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} + + unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + + unbuild@2.0.0: + resolution: {integrity: sha512-JWCUYx3Oxdzvw2J9kTAp+DKE8df/BnH/JTSj6JyA4SH40ECdFu7FoJJcrm8G92B7TjofQ6GZGjJs50TRxoH6Wg==} + hasBin: true + peerDependencies: + typescript: ^5.1.6 + peerDependenciesMeta: + typescript: + optional: true + + unc-path-regex@0.1.2: + resolution: {integrity: sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==} + engines: {node: '>=0.10.0'} + + underscore@1.13.6: + resolution: {integrity: sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==} + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + undici@6.19.2: + resolution: {integrity: sha512-JfjKqIauur3Q6biAtHJ564e3bWa8VvT+7cSiOJHFbX4Erv6CLGDpg8z+Fmg/1OI/47RA+GI2QZaF48SSaLvyBA==} + engines: {node: '>=18.17'} + + unicode-trie@2.0.0: + resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} + + unicorn-magic@0.1.0: + resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} + engines: {node: '>=18'} + + unified@10.1.2: + resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==} + + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-generated@2.0.1: + resolution: {integrity: sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==} + + unist-util-is@5.2.1: + resolution: {integrity: sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==} + + unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + + unist-util-position@4.0.4: + resolution: {integrity: sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@2.0.3: + resolution: {integrity: sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==} + + unist-util-stringify-position@3.0.3: + resolution: {integrity: sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@5.1.3: + resolution: {integrity: sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==} + + unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + + unist-util-visit@4.1.2: + resolution: {integrity: sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unixify@1.0.0: + resolution: {integrity: sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg==} + engines: {node: '>=0.10.0'} + + untyped@1.5.2: + resolution: {integrity: sha512-eL/8PlhLcMmlMDtNPKhyyz9kEBDS3Uk4yMu/ewlkT2WFbtzScjHWPJLdQLmaGPUKjXzwe9MumOtOgc4Fro96Kg==} + hasBin: true + + update-browserslist-db@1.0.13: + resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + upper-case-first@2.0.2: + resolution: {integrity: sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==} + + upper-case@2.0.2: + resolution: {integrity: sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + url-join@4.0.1: + resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} + + urlpattern-polyfill@8.0.2: + resolution: {integrity: sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==} + + urlpattern-polyfill@9.0.0: + resolution: {integrity: sha512-WHN8KDQblxd32odxeIgo83rdVDE2bvdkb86it7bMhYZwWKJz0+O0RK/eZiHYnM+zgt/U7hAHOlCQGfjjvSkw2g==} + + urql@4.0.6: + resolution: {integrity: sha512-meXJ2puOd64uCGKh7Fse2R7gPa8+ZpBOoA62jN7CPXXUt7SVZSdeXWSpB3HvlfzLUkEqsWbvshwrgeWRYNNGaQ==} + peerDependencies: + react: '>= 16.8.0' + + use-callback-ref@1.3.0: + resolution: {integrity: sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + use-composed-ref@1.3.0: + resolution: {integrity: sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + use-isomorphic-layout-effect@1.1.2: + resolution: {integrity: sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + use-latest@1.2.1: + resolution: {integrity: sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + use-local-storage@3.0.0: + resolution: {integrity: sha512-wlPNnBCG3ULIJMr5A+dvWqLiPWCfsN1Kwijq+sAhT5yV4ex0u6XmZuNwP+RerIOfzBuz1pwSZuzhZMiluGQHfQ==} + peerDependencies: + react: '>=16.8.1' + + use-sidecar@1.1.2: + resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.9.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + use-sync-external-store@1.2.0: + resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + use-sync-external-store@1.2.2: + resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + + uvu@0.5.6: + resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==} + engines: {node: '>=8'} + hasBin: true + + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + + value-or-promise@1.0.12: + resolution: {integrity: sha512-Z6Uz+TYwEqE7ZN50gwn+1LCVo9ZVrpxRPOhOLnncYkY1ZzOYtrX8Fwf/rFktZ8R5mJms6EZf5TqNOMeZmnPq9Q==} + engines: {node: '>=12'} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vfile-location@4.1.0: + resolution: {integrity: sha512-YF23YMyASIIJXpktBa4vIGLJ5Gs88UB/XePgqPmTa7cDA+JeO3yclbpheQYCHjVHBn/yePzrXuygIL+xbvRYHw==} + + vfile-message@3.1.4: + resolution: {integrity: sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==} + + vfile-message@4.0.2: + resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + + vfile@5.3.7: + resolution: {integrity: sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + victory-vendor@36.9.2: + resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + + vite-node@1.6.0: + resolution: {integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite@5.2.11: + resolution: {integrity: sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitest@1.6.0: + resolution: {integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 1.6.0 + '@vitest/ui': 1.6.0 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} + engines: {node: '>=14.0.0'} + + vscode-languageclient@9.0.1: + resolution: {integrity: sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==} + engines: {vscode: ^1.82.0} + + vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} + + vscode-languageserver-textdocument@1.0.11: + resolution: {integrity: sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==} + + vscode-languageserver-types@3.17.5: + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + + vscode-languageserver@9.0.1: + resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} + hasBin: true + + vscode-uri@3.0.8: + resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + + vue-eslint-parser@9.4.2: + resolution: {integrity: sha512-Ry9oiGmCAK91HrKMtCrKFWmSFWvYkpGglCeFAIqDdr9zdXmMMpJOmUJS7WWsW7fX81h6mwHmUZCQQ1E0PkSwYQ==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + + watchpack@2.4.0: + resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} + engines: {node: '>=10.13.0'} + + watchpack@2.4.1: + resolution: {integrity: sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==} + engines: {node: '>=10.13.0'} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + + web-streams-polyfill@3.2.1: + resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==} + engines: {node: '>= 8'} + + web-streams-polyfill@4.0.0-beta.3: + resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} + engines: {node: '>= 14'} + + web-tree-sitter@0.20.8: + resolution: {integrity: sha512-weOVgZ3aAARgdnb220GqYuh7+rZU0Ka9k9yfKtGAzEYMa6GgiCzW9JjQRJyCJakvibQW+dfjJdihjInKuuCAUQ==} + + webcrypto-core@1.7.7: + resolution: {integrity: sha512-7FjigXNsBfopEj+5DV2nhNpfic2vumtjjgPmeDKk45z+MJwXKKfhPB7118Pfzrmh4jqOMST6Ch37iPAHoImg5g==} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + + webpack-sources@3.2.3: + resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} + engines: {node: '>=10.13.0'} + + webpack@5.91.0: + resolution: {integrity: sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + + whatwg-fetch@3.6.19: + resolution: {integrity: sha512-d67JP4dHSbm2TrpFj8AbO8DnL1JXL5J9u0Kq2xW6d0TFDbCA3Muhdt8orXC22utleTVj7Prqt82baN6RBvnEgw==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + whatwg-url@7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + + which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + + which-builtin-type@1.1.3: + resolution: {integrity: sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + + which-typed-array@1.1.11: + resolution: {integrity: sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.15: + resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} + engines: {node: '>= 0.4'} + + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.2.2: + resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} + engines: {node: '>=8'} + hasBin: true + + win-ca@3.5.1: + resolution: {integrity: sha512-RNy9gpBS6cxWHjfbqwBA7odaHyT+YQNhtdpJZwYCFoxB/Dq22oeOZ9YCXMwjhLytKpo7JJMnKdJ/ve7N12zzfQ==} + + wonka@6.3.4: + resolution: {integrity: sha512-CjpbqNtBGNAeyNS/9W6q3kSkKE52+FjIj7AkFlLr11s/VWGUu6a2CdYSdGxocIhIVjaW/zchesBQUKPVU69Cqg==} + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wordwrap@0.0.3: + resolution: {integrity: sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==} + engines: {node: '>=0.4.0'} + + workerpool@6.2.1: + resolution: {integrity: sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrap-ansi@9.0.0: + resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} + engines: {node: '>=18'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.11.0: + resolution: {integrity: sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==} + 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 + + ws@8.14.2: + resolution: {integrity: sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==} + 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 + + xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + + xml2js@0.5.0: + resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} + engines: {node: '>=4.0.0'} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + + xmlhttprequest-ssl@2.0.0: + resolution: {integrity: sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==} + engines: {node: '>=0.4.0'} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yaml-ast-parser@0.0.43: + resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==} + + yaml-eslint-parser@1.2.2: + resolution: {integrity: sha512-pEwzfsKbTrB8G3xc/sN7aw1v6A6c/pKxLAkjclnAyo5g5qOh6eL9WGu0o3cSDQZKrTNk4KL4lQSwZW+nBkANEg==} + engines: {node: ^14.17.0 || >=16.0.0} + + yaml@2.3.4: + resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==} + engines: {node: '>= 14'} + + yaml@2.4.2: + resolution: {integrity: sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==} + engines: {node: '>= 14'} + hasBin: true + + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + + yargs-parser@20.2.4: + resolution: {integrity: sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs-unparser@2.0.0: + resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} + engines: {node: '>=10'} + + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + + yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + + yazl@2.5.1: + resolution: {integrity: sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==} + + ylru@1.4.0: + resolution: {integrity: sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==} + engines: {node: '>= 4.0.0'} + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yocto-queue@1.0.0: + resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} + engines: {node: '>=12.20'} + + yoga-wasm-web@0.3.3: + resolution: {integrity: sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==} + + zod@3.21.4: + resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==} + + zod@3.22.4: + resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + + zustand@4.4.6: + resolution: {integrity: sha512-Rb16eW55gqL4W2XZpJh0fnrATxYEG3Apl2gfHTyDSE965x/zxslTikpNch0JgNjJA9zK6gEFW8Fl6d1rTZaqgg==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@0no-co/graphql.web@1.0.4(graphql@16.8.1)': + optionalDependencies: + graphql: 16.8.1 + + '@aashutoshrathi/word-wrap@1.2.6': {} + + '@alloc/quick-lru@5.2.0': {} + + '@ampproject/remapping@2.2.1': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + + '@antfu/eslint-config@2.18.1(@vue/compiler-sfc@3.4.27)(eslint-plugin-react-hooks@4.6.0(eslint@9.3.0))(eslint@9.3.0)(svelte@4.2.17)(typescript@5.4.5)(vitest@1.6.0(@types/node@20.12.12)(terser@5.31.0))': + dependencies: + '@antfu/install-pkg': 0.3.3 + '@clack/prompts': 0.7.0 + '@stylistic/eslint-plugin': 2.1.0(eslint@9.3.0)(typescript@5.4.5) + '@typescript-eslint/eslint-plugin': 7.10.0(@typescript-eslint/parser@7.10.0(eslint@9.3.0)(typescript@5.4.5))(eslint@9.3.0)(typescript@5.4.5) + '@typescript-eslint/parser': 7.10.0(eslint@9.3.0)(typescript@5.4.5) + eslint: 9.3.0 + eslint-config-flat-gitignore: 0.1.5 + eslint-flat-config-utils: 0.2.5 + eslint-merge-processors: 0.1.0(eslint@9.3.0) + eslint-plugin-antfu: 2.2.0(eslint@9.3.0) + eslint-plugin-command: 0.2.3(eslint@9.3.0) + eslint-plugin-eslint-comments: 3.2.0(eslint@9.3.0) + eslint-plugin-import-x: 0.5.0(eslint@9.3.0)(typescript@5.4.5) + eslint-plugin-jsdoc: 48.2.5(eslint@9.3.0) + eslint-plugin-jsonc: 2.15.1(eslint@9.3.0) + eslint-plugin-markdown: 5.0.0(eslint@9.3.0) + eslint-plugin-n: 17.7.0(eslint@9.3.0) + eslint-plugin-no-only-tests: 3.1.0 + eslint-plugin-perfectionist: 2.10.0(eslint@9.3.0)(svelte@4.2.17)(typescript@5.4.5)(vue-eslint-parser@9.4.2(eslint@9.3.0)) + eslint-plugin-regexp: 2.6.0(eslint@9.3.0) + eslint-plugin-toml: 0.11.0(eslint@9.3.0) + eslint-plugin-unicorn: 53.0.0(eslint@9.3.0) + eslint-plugin-unused-imports: 3.2.0(@typescript-eslint/eslint-plugin@7.10.0(@typescript-eslint/parser@7.10.0(eslint@9.3.0)(typescript@5.4.5))(eslint@9.3.0)(typescript@5.4.5))(eslint@9.3.0) + eslint-plugin-vitest: 0.5.4(@typescript-eslint/eslint-plugin@7.10.0(@typescript-eslint/parser@7.10.0(eslint@9.3.0)(typescript@5.4.5))(eslint@9.3.0)(typescript@5.4.5))(eslint@9.3.0)(typescript@5.4.5)(vitest@1.6.0(@types/node@20.12.12)(terser@5.31.0)) + eslint-plugin-vue: 9.26.0(eslint@9.3.0) + eslint-plugin-yml: 1.14.0(eslint@9.3.0) + eslint-processor-vue-blocks: 0.1.2(@vue/compiler-sfc@3.4.27)(eslint@9.3.0) + globals: 15.3.0 + jsonc-eslint-parser: 2.4.0 + local-pkg: 0.5.0 + parse-gitignore: 2.0.0 + picocolors: 1.0.1 + toml-eslint-parser: 0.9.3 + vue-eslint-parser: 9.4.2(eslint@9.3.0) + yaml-eslint-parser: 1.2.2 + yargs: 17.7.2 + optionalDependencies: + eslint-plugin-react-hooks: 4.6.0(eslint@9.3.0) + transitivePeerDependencies: + - '@vue/compiler-sfc' + - supports-color + - svelte + - typescript + - vitest + + '@antfu/install-pkg@0.3.3': + dependencies: + '@jsdevtools/ez-spawn': 3.0.4 + + '@antfu/ni@0.21.12': {} + + '@antfu/utils@0.7.8': {} + + '@ardatan/relay-compiler@12.0.0(encoding@0.1.13)(graphql@16.8.1)': + dependencies: + '@babel/core': 7.23.5 + '@babel/generator': 7.23.5 + '@babel/parser': 7.23.5 + '@babel/runtime': 7.24.4 + '@babel/traverse': 7.23.5 + '@babel/types': 7.23.5 + babel-preset-fbjs: 3.4.0(@babel/core@7.23.5) + chalk: 4.1.2 + fb-watchman: 2.0.2 + fbjs: 3.0.5(encoding@0.1.13) + glob: 7.2.3 + graphql: 16.8.1 + immutable: 3.7.6 + invariant: 2.2.4 + nullthrows: 1.1.1 + relay-runtime: 12.0.0(encoding@0.1.13) + signedsource: 1.0.0 + yargs: 15.4.1 + transitivePeerDependencies: + - encoding + - supports-color + + '@ardatan/sync-fetch@0.0.1(encoding@0.1.13)': + dependencies: + node-fetch: 2.7.0(encoding@0.1.13) + transitivePeerDependencies: + - encoding + + '@azure/abort-controller@1.1.0': + dependencies: + tslib: 2.6.2 + + '@azure/abort-controller@2.1.2': + dependencies: + tslib: 2.6.2 + + '@azure/core-auth@1.7.2': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-util': 1.9.0 + tslib: 2.6.2 + + '@azure/core-client@1.9.2': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.7.2 + '@azure/core-rest-pipeline': 1.16.0 + '@azure/core-tracing': 1.1.2 + '@azure/core-util': 1.9.0 + '@azure/logger': 1.1.2 + tslib: 2.6.2 + transitivePeerDependencies: + - supports-color + + '@azure/core-rest-pipeline@1.16.0': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.7.2 + '@azure/core-tracing': 1.1.2 + '@azure/core-util': 1.9.0 + '@azure/logger': 1.1.2 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.5 + tslib: 2.6.2 + transitivePeerDependencies: + - supports-color + + '@azure/core-tracing@1.1.2': + dependencies: + tslib: 2.6.2 + + '@azure/core-util@1.9.0': + dependencies: + '@azure/abort-controller': 2.1.2 + tslib: 2.6.2 + + '@azure/identity@4.2.0': + dependencies: + '@azure/abort-controller': 1.1.0 + '@azure/core-auth': 1.7.2 + '@azure/core-client': 1.9.2 + '@azure/core-rest-pipeline': 1.16.0 + '@azure/core-tracing': 1.1.2 + '@azure/core-util': 1.9.0 + '@azure/logger': 1.1.2 + '@azure/msal-browser': 3.14.0 + '@azure/msal-node': 2.8.1 + events: 3.3.0 + jws: 4.0.0 + open: 8.4.2 + stoppable: 1.1.0 + tslib: 2.6.2 + transitivePeerDependencies: + - supports-color + + '@azure/logger@1.1.2': + dependencies: + tslib: 2.6.2 + + '@azure/msal-browser@3.14.0': + dependencies: + '@azure/msal-common': 14.10.0 + + '@azure/msal-common@14.10.0': {} + + '@azure/msal-node@2.8.1': + dependencies: + '@azure/msal-common': 14.10.0 + jsonwebtoken: 9.0.2 + uuid: 8.3.2 + + '@babel/code-frame@7.23.5': + dependencies: + '@babel/highlight': 7.23.4 + chalk: 2.4.2 + + '@babel/code-frame@7.24.2': + dependencies: + '@babel/highlight': 7.24.5 + picocolors: 1.0.1 + + '@babel/code-frame@7.26.2': + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.23.3': {} + + '@babel/compat-data@7.26.8': {} + + '@babel/core@7.23.5': + dependencies: + '@ampproject/remapping': 2.2.1 + '@babel/code-frame': 7.23.5 + '@babel/generator': 7.23.5 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.5) + '@babel/helpers': 7.23.5 + '@babel/parser': 7.23.5 + '@babel/template': 7.22.15 + '@babel/traverse': 7.23.5 + '@babel/types': 7.23.5 + convert-source-map: 2.0.0 + debug: 4.3.4 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/core@7.26.10': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.10 + '@babel/helper-compilation-targets': 7.26.5 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.10) + '@babel/helpers': 7.26.10 + '@babel/parser': 7.26.10 + '@babel/template': 7.26.9 + '@babel/traverse': 7.26.10 + '@babel/types': 7.26.10 + convert-source-map: 2.0.0 + debug: 4.3.4(supports-color@9.4.0) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.23.5': + dependencies: + '@babel/types': 7.23.5 + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.20 + jsesc: 2.5.2 + + '@babel/generator@7.26.10': + dependencies: + '@babel/parser': 7.26.10 + '@babel/types': 7.26.10 + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.0.2 + + '@babel/helper-annotate-as-pure@7.22.5': + dependencies: + '@babel/types': 7.23.5 + + '@babel/helper-compilation-targets@7.22.15': + dependencies: + '@babel/compat-data': 7.23.3 + '@babel/helper-validator-option': 7.22.15 + browserslist: 4.22.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-compilation-targets@7.26.5': + dependencies: + '@babel/compat-data': 7.26.8 + '@babel/helper-validator-option': 7.25.9 + browserslist: 4.24.4 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.22.15(@babel/core@7.23.5)': + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-member-expression-to-functions': 7.23.0 + '@babel/helper-optimise-call-expression': 7.22.5 + '@babel/helper-replace-supers': 7.22.20(@babel/core@7.23.5) + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + semver: 6.3.1 + + '@babel/helper-environment-visitor@7.22.20': {} + + '@babel/helper-function-name@7.23.0': + dependencies: + '@babel/template': 7.22.15 + '@babel/types': 7.23.5 + + '@babel/helper-hoist-variables@7.22.5': + dependencies: + '@babel/types': 7.23.5 + + '@babel/helper-member-expression-to-functions@7.23.0': + dependencies: + '@babel/types': 7.23.5 + + '@babel/helper-module-imports@7.22.15': + dependencies: + '@babel/types': 7.23.5 + + '@babel/helper-module-imports@7.25.9': + dependencies: + '@babel/traverse': 7.26.10 + '@babel/types': 7.26.10 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.23.3(@babel/core@7.23.5)': + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-simple-access': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-validator-identifier': 7.22.20 + + '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.26.10 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.22.5': + dependencies: + '@babel/types': 7.23.5 + + '@babel/helper-plugin-utils@7.22.5': {} + + '@babel/helper-replace-supers@7.22.20(@babel/core@7.23.5)': + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-member-expression-to-functions': 7.23.0 + '@babel/helper-optimise-call-expression': 7.22.5 + + '@babel/helper-simple-access@7.22.5': + dependencies: + '@babel/types': 7.23.5 + + '@babel/helper-skip-transparent-expression-wrappers@7.22.5': + dependencies: + '@babel/types': 7.23.5 + + '@babel/helper-split-export-declaration@7.22.6': + dependencies: + '@babel/types': 7.23.5 + + '@babel/helper-string-parser@7.23.4': {} + + '@babel/helper-string-parser@7.24.1': {} + + '@babel/helper-string-parser@7.25.9': {} + + '@babel/helper-validator-identifier@7.22.20': {} + + '@babel/helper-validator-identifier@7.24.5': {} + + '@babel/helper-validator-identifier@7.25.9': {} + + '@babel/helper-validator-option@7.22.15': {} + + '@babel/helper-validator-option@7.25.9': {} + + '@babel/helpers@7.23.5': + dependencies: + '@babel/template': 7.22.15 + '@babel/traverse': 7.23.5 + '@babel/types': 7.23.5 + transitivePeerDependencies: + - supports-color + + '@babel/helpers@7.26.10': + dependencies: + '@babel/template': 7.26.9 + '@babel/types': 7.26.10 + + '@babel/highlight@7.23.4': + dependencies: + '@babel/helper-validator-identifier': 7.22.20 + chalk: 2.4.2 + js-tokens: 4.0.0 + + '@babel/highlight@7.24.5': + dependencies: + '@babel/helper-validator-identifier': 7.24.5 + chalk: 2.4.2 + js-tokens: 4.0.0 + picocolors: 1.0.1 + + '@babel/parser@7.23.5': + dependencies: + '@babel/types': 7.23.5 + + '@babel/parser@7.24.1': + dependencies: + '@babel/types': 7.24.5 + + '@babel/parser@7.26.10': + dependencies: + '@babel/types': 7.26.10 + + '@babel/parser@7.27.0': + dependencies: + '@babel/types': 7.27.0 + + '@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.23.5)': + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.23.5) + '@babel/helper-plugin-utils': 7.22.5 + + '@babel/plugin-proposal-object-rest-spread@7.20.7(@babel/core@7.23.5)': + dependencies: + '@babel/compat-data': 7.23.3 + '@babel/core': 7.23.5 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.23.5) + '@babel/plugin-transform-parameters': 7.23.3(@babel/core@7.23.5) + + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.23.5)': + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + + '@babel/plugin-syntax-flow@7.23.3(@babel/core@7.23.5)': + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + + '@babel/plugin-syntax-import-assertions@7.23.3(@babel/core@7.23.5)': + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + + '@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.23.5)': + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.23.5)': + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + + '@babel/plugin-transform-arrow-functions@7.23.3(@babel/core@7.23.5)': + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + + '@babel/plugin-transform-block-scoped-functions@7.23.3(@babel/core@7.23.5)': + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + + '@babel/plugin-transform-block-scoping@7.23.3(@babel/core@7.23.5)': + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + + '@babel/plugin-transform-classes@7.23.3(@babel/core@7.23.5)': + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-optimise-call-expression': 7.22.5 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-replace-supers': 7.22.20(@babel/core@7.23.5) + '@babel/helper-split-export-declaration': 7.22.6 + globals: 11.12.0 + + '@babel/plugin-transform-computed-properties@7.23.3(@babel/core@7.23.5)': + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/template': 7.22.15 + + '@babel/plugin-transform-destructuring@7.23.3(@babel/core@7.23.5)': + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + + '@babel/plugin-transform-flow-strip-types@7.23.3(@babel/core@7.23.5)': + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-flow': 7.23.3(@babel/core@7.23.5) + + '@babel/plugin-transform-for-of@7.23.3(@babel/core@7.23.5)': + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + + '@babel/plugin-transform-function-name@7.23.3(@babel/core@7.23.5)': + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + + '@babel/plugin-transform-literals@7.23.3(@babel/core@7.23.5)': + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + + '@babel/plugin-transform-member-expression-literals@7.23.3(@babel/core@7.23.5)': + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + + '@babel/plugin-transform-modules-commonjs@7.23.3(@babel/core@7.23.5)': + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.5) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-simple-access': 7.22.5 + + '@babel/plugin-transform-object-super@7.23.3(@babel/core@7.23.5)': + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-replace-supers': 7.22.20(@babel/core@7.23.5) + + '@babel/plugin-transform-parameters@7.23.3(@babel/core@7.23.5)': + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + + '@babel/plugin-transform-property-literals@7.23.3(@babel/core@7.23.5)': + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + + '@babel/plugin-transform-react-display-name@7.23.3(@babel/core@7.23.5)': + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + + '@babel/plugin-transform-react-jsx@7.22.15(@babel/core@7.23.5)': + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-jsx': 7.23.3(@babel/core@7.23.5) + '@babel/types': 7.23.5 + + '@babel/plugin-transform-shorthand-properties@7.23.3(@babel/core@7.23.5)': + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + + '@babel/plugin-transform-spread@7.23.3(@babel/core@7.23.5)': + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + + '@babel/plugin-transform-template-literals@7.23.3(@babel/core@7.23.5)': + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + + '@babel/runtime@7.24.4': + dependencies: + regenerator-runtime: 0.14.0 + + '@babel/standalone@7.26.10': {} + + '@babel/template@7.22.15': + dependencies: + '@babel/code-frame': 7.23.5 + '@babel/parser': 7.23.5 + '@babel/types': 7.23.5 + + '@babel/template@7.26.9': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/parser': 7.26.10 + '@babel/types': 7.26.10 + + '@babel/traverse@7.23.5': + dependencies: + '@babel/code-frame': 7.23.5 + '@babel/generator': 7.23.5 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/parser': 7.23.5 + '@babel/types': 7.23.5 + debug: 4.3.4 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/traverse@7.26.10': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.10 + '@babel/parser': 7.26.10 + '@babel/template': 7.26.9 + '@babel/types': 7.26.10 + debug: 4.3.4(supports-color@9.4.0) + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.23.5': + dependencies: + '@babel/helper-string-parser': 7.23.4 + '@babel/helper-validator-identifier': 7.22.20 + to-fast-properties: 2.0.0 + + '@babel/types@7.24.5': + dependencies: + '@babel/helper-string-parser': 7.24.1 + '@babel/helper-validator-identifier': 7.24.5 + to-fast-properties: 2.0.0 + + '@babel/types@7.26.10': + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + + '@babel/types@7.27.0': + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + + '@clack/core@0.3.4': + dependencies: + picocolors: 1.0.1 + sisteransi: 1.0.5 + + '@clack/prompts@0.7.0': + dependencies: + '@clack/core': 0.3.4 + picocolors: 1.0.1 + sisteransi: 1.0.5 + + '@codemirror/autocomplete@6.11.1(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.0)': + dependencies: + '@codemirror/language': 6.10.1 + '@codemirror/state': 6.4.0 + '@codemirror/view': 6.23.0 + '@lezer/common': 1.2.0 + + '@codemirror/autocomplete@6.11.1(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.23.0)(@lezer/common@1.2.3)': + dependencies: + '@codemirror/language': 6.11.0 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.23.0 + '@lezer/common': 1.2.3 + + '@codemirror/autocomplete@6.11.1(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)(@lezer/common@1.2.3)': + dependencies: + '@codemirror/language': 6.11.0 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.36.4 + '@lezer/common': 1.2.3 + + '@codemirror/lang-angular@0.1.3': + dependencies: + '@codemirror/lang-html': 6.4.7 + '@codemirror/lang-javascript': 6.2.1 + '@codemirror/language': 6.10.1 + '@lezer/common': 1.2.0 + '@lezer/highlight': 1.2.0 + '@lezer/lr': 1.3.14 + + '@codemirror/lang-cpp@6.0.2': + dependencies: + '@codemirror/language': 6.10.1 + '@lezer/cpp': 1.1.2 + + '@codemirror/lang-css@6.2.1(@codemirror/view@6.23.0)': + dependencies: + '@codemirror/autocomplete': 6.11.1(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.0) + '@codemirror/language': 6.10.1 + '@codemirror/state': 6.4.0 + '@lezer/common': 1.2.0 + '@lezer/css': 1.1.6 + transitivePeerDependencies: + - '@codemirror/view' + + '@codemirror/lang-css@6.3.1(@codemirror/view@6.23.0)': + dependencies: + '@codemirror/autocomplete': 6.11.1(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.23.0)(@lezer/common@1.2.3) + '@codemirror/language': 6.11.0 + '@codemirror/state': 6.5.2 + '@lezer/common': 1.2.3 + '@lezer/css': 1.1.10 + transitivePeerDependencies: + - '@codemirror/view' + + '@codemirror/lang-css@6.3.1(@codemirror/view@6.36.4)': + dependencies: + '@codemirror/autocomplete': 6.11.1(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)(@lezer/common@1.2.3) + '@codemirror/language': 6.11.0 + '@codemirror/state': 6.5.2 + '@lezer/common': 1.2.3 + '@lezer/css': 1.1.10 + transitivePeerDependencies: + - '@codemirror/view' + + '@codemirror/lang-html@6.4.7': + dependencies: + '@codemirror/autocomplete': 6.11.1(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.0) + '@codemirror/lang-css': 6.2.1(@codemirror/view@6.23.0) + '@codemirror/lang-javascript': 6.2.1 + '@codemirror/language': 6.10.1 + '@codemirror/state': 6.4.0 + '@codemirror/view': 6.23.0 + '@lezer/common': 1.2.0 + '@lezer/css': 1.1.6 + '@lezer/html': 1.3.8 + + '@codemirror/lang-html@6.4.9': + dependencies: + '@codemirror/autocomplete': 6.11.1(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)(@lezer/common@1.2.3) + '@codemirror/lang-css': 6.3.1(@codemirror/view@6.36.4) + '@codemirror/lang-javascript': 6.2.3 + '@codemirror/language': 6.11.0 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.36.4 + '@lezer/common': 1.2.3 + '@lezer/css': 1.1.10 + '@lezer/html': 1.3.10 + + '@codemirror/lang-java@6.0.1': + dependencies: + '@codemirror/language': 6.10.1 + '@lezer/java': 1.1.1 + + '@codemirror/lang-javascript@6.2.1': + dependencies: + '@codemirror/autocomplete': 6.11.1(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.0) + '@codemirror/language': 6.10.1 + '@codemirror/lint': 6.4.2 + '@codemirror/state': 6.4.0 + '@codemirror/view': 6.23.0 + '@lezer/common': 1.2.0 + '@lezer/javascript': 1.4.12 + + '@codemirror/lang-javascript@6.2.3': + dependencies: + '@codemirror/autocomplete': 6.11.1(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)(@lezer/common@1.2.3) + '@codemirror/language': 6.11.0 + '@codemirror/lint': 6.4.2 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.36.4 + '@lezer/common': 1.2.3 + '@lezer/javascript': 1.4.12 + + '@codemirror/lang-json@6.0.1': + dependencies: + '@codemirror/language': 6.10.1 + '@lezer/json': 1.0.2 + + '@codemirror/lang-less@6.0.2(@codemirror/view@6.23.0)': + dependencies: + '@codemirror/lang-css': 6.2.1(@codemirror/view@6.23.0) + '@codemirror/language': 6.10.1 + '@lezer/common': 1.2.0 + '@lezer/highlight': 1.2.0 + '@lezer/lr': 1.3.14 + transitivePeerDependencies: + - '@codemirror/view' + + '@codemirror/lang-lezer@6.0.1': + dependencies: + '@codemirror/language': 6.10.1 + '@codemirror/state': 6.4.0 + '@lezer/common': 1.2.0 + '@lezer/lezer': 1.1.2 + + '@codemirror/lang-liquid@6.2.0': + dependencies: + '@codemirror/lang-html': 6.4.7 + '@codemirror/language': 6.10.1 + '@lezer/common': 1.2.0 + '@lezer/highlight': 1.2.0 + '@lezer/lr': 1.3.14 + + '@codemirror/lang-markdown@6.2.3': + dependencies: + '@codemirror/autocomplete': 6.11.1(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.0) + '@codemirror/lang-html': 6.4.7 + '@codemirror/language': 6.10.1 + '@codemirror/state': 6.4.0 + '@codemirror/view': 6.23.0 + '@lezer/common': 1.2.0 + '@lezer/markdown': 1.2.0 + + '@codemirror/lang-markdown@6.3.2': + dependencies: + '@codemirror/autocomplete': 6.11.1(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)(@lezer/common@1.2.3) + '@codemirror/lang-html': 6.4.9 + '@codemirror/language': 6.11.0 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.36.4 + '@lezer/common': 1.2.3 + '@lezer/markdown': 1.4.2 + + '@codemirror/lang-php@6.0.1': + dependencies: + '@codemirror/lang-html': 6.4.7 + '@codemirror/language': 6.10.1 + '@codemirror/state': 6.4.0 + '@lezer/common': 1.2.0 + '@lezer/php': 1.0.2 + + '@codemirror/lang-python@6.1.3(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.0)': + dependencies: + '@codemirror/autocomplete': 6.11.1(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.0) + '@codemirror/language': 6.10.1 + '@lezer/python': 1.1.10 + transitivePeerDependencies: + - '@codemirror/state' + - '@codemirror/view' + - '@lezer/common' + + '@codemirror/lang-python@6.1.7(@codemirror/view@6.23.0)': + dependencies: + '@codemirror/autocomplete': 6.11.1(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.23.0)(@lezer/common@1.2.3) + '@codemirror/language': 6.11.0 + '@codemirror/state': 6.5.2 + '@lezer/common': 1.2.3 + '@lezer/python': 1.1.16 + transitivePeerDependencies: + - '@codemirror/view' + + '@codemirror/lang-rust@6.0.1': + dependencies: + '@codemirror/language': 6.10.1 + '@lezer/rust': 1.0.2 + + '@codemirror/lang-sass@6.0.2(@codemirror/view@6.23.0)': + dependencies: + '@codemirror/lang-css': 6.2.1(@codemirror/view@6.23.0) + '@codemirror/language': 6.10.1 + '@codemirror/state': 6.4.0 + '@lezer/common': 1.2.0 + '@lezer/sass': 1.0.4 + transitivePeerDependencies: + - '@codemirror/view' + + '@codemirror/lang-sql@6.5.5(@codemirror/view@6.23.0)': + dependencies: + '@codemirror/autocomplete': 6.11.1(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.0) + '@codemirror/language': 6.10.1 + '@codemirror/state': 6.4.0 + '@lezer/common': 1.2.0 + '@lezer/highlight': 1.2.0 + '@lezer/lr': 1.3.14 + transitivePeerDependencies: + - '@codemirror/view' + + '@codemirror/lang-sql@6.8.0(@codemirror/view@6.23.0)': + dependencies: + '@codemirror/autocomplete': 6.11.1(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.23.0)(@lezer/common@1.2.3) + '@codemirror/language': 6.11.0 + '@codemirror/state': 6.5.2 + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.0 + '@lezer/lr': 1.3.14 + transitivePeerDependencies: + - '@codemirror/view' + + '@codemirror/lang-vue@0.1.3': + dependencies: + '@codemirror/lang-html': 6.4.7 + '@codemirror/lang-javascript': 6.2.1 + '@codemirror/language': 6.10.1 + '@lezer/common': 1.2.0 + '@lezer/highlight': 1.2.0 + '@lezer/lr': 1.3.14 + + '@codemirror/lang-wast@6.0.2': + dependencies: + '@codemirror/language': 6.10.1 + '@lezer/common': 1.2.0 + '@lezer/highlight': 1.2.0 + '@lezer/lr': 1.3.14 + + '@codemirror/lang-xml@6.0.2(@codemirror/view@6.23.0)': + dependencies: + '@codemirror/autocomplete': 6.11.1(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.0) + '@codemirror/language': 6.10.1 + '@codemirror/state': 6.4.0 + '@lezer/common': 1.2.0 + '@lezer/xml': 1.0.4 + transitivePeerDependencies: + - '@codemirror/view' + + '@codemirror/lang-xml@6.1.0': + dependencies: + '@codemirror/autocomplete': 6.11.1(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)(@lezer/common@1.2.3) + '@codemirror/language': 6.11.0 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.36.4 + '@lezer/common': 1.2.3 + '@lezer/xml': 1.0.6 + + '@codemirror/language-data@6.3.1(@codemirror/view@6.23.0)': + dependencies: + '@codemirror/lang-angular': 0.1.3 + '@codemirror/lang-cpp': 6.0.2 + '@codemirror/lang-css': 6.3.1(@codemirror/view@6.23.0) + '@codemirror/lang-html': 6.4.9 + '@codemirror/lang-java': 6.0.1 + '@codemirror/lang-javascript': 6.2.3 + '@codemirror/lang-json': 6.0.1 + '@codemirror/lang-less': 6.0.2(@codemirror/view@6.23.0) + '@codemirror/lang-markdown': 6.3.2 + '@codemirror/lang-php': 6.0.1 + '@codemirror/lang-python': 6.1.7(@codemirror/view@6.23.0) + '@codemirror/lang-rust': 6.0.1 + '@codemirror/lang-sass': 6.0.2(@codemirror/view@6.23.0) + '@codemirror/lang-sql': 6.8.0(@codemirror/view@6.23.0) + '@codemirror/lang-vue': 0.1.3 + '@codemirror/lang-wast': 6.0.2 + '@codemirror/lang-xml': 6.1.0 + '@codemirror/language': 6.11.0 + '@codemirror/legacy-modes': 6.3.3 + transitivePeerDependencies: + - '@codemirror/view' + + '@codemirror/language@6.10.1': + dependencies: + '@codemirror/state': 6.4.0 + '@codemirror/view': 6.23.0 + '@lezer/common': 1.2.0 + '@lezer/highlight': 1.2.0 + '@lezer/lr': 1.3.14 + style-mod: 4.1.0 + + '@codemirror/language@6.11.0': + dependencies: + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.36.4 + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.0 + '@lezer/lr': 1.3.14 + style-mod: 4.1.2 + + '@codemirror/legacy-modes@6.3.3': + dependencies: + '@codemirror/language': 6.11.0 + + '@codemirror/lint@6.4.2': + dependencies: + '@codemirror/state': 6.4.0 + '@codemirror/view': 6.23.0 + crelt: 1.0.6 + + '@codemirror/search@6.5.5': + dependencies: + '@codemirror/state': 6.4.0 + '@codemirror/view': 6.23.0 + crelt: 1.0.6 + + '@codemirror/state@6.4.0': {} + + '@codemirror/state@6.5.2': + dependencies: + '@marijn/find-cluster-break': 1.0.2 + + '@codemirror/theme-one-dark@6.1.2': + dependencies: + '@codemirror/language': 6.10.1 + '@codemirror/state': 6.4.0 + '@codemirror/view': 6.23.0 + '@lezer/highlight': 1.2.0 + + '@codemirror/view@6.23.0': + dependencies: + '@codemirror/state': 6.4.0 + style-mod: 4.1.0 + w3c-keyname: 2.2.8 + + '@codemirror/view@6.36.4': + dependencies: + '@codemirror/state': 6.5.2 + style-mod: 4.1.2 + w3c-keyname: 2.2.8 + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@curvenote/ansi-to-react@7.0.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + anser: 2.1.1 + escape-carriage: 1.3.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + '@emotion/is-prop-valid@0.8.8': + dependencies: + '@emotion/memoize': 0.7.4 + optional: true + + '@emotion/memoize@0.7.4': + optional: true + + '@es-joy/jsdoccomment@0.43.0': + dependencies: + '@types/eslint': 8.56.10 + '@types/estree': 1.0.6 + '@typescript-eslint/types': 7.4.0 + comment-parser: 1.4.1 + esquery: 1.5.0 + jsdoc-type-pratt-parser: 4.0.0 + + '@esbuild/aix-ppc64@0.19.11': + optional: true + + '@esbuild/aix-ppc64@0.19.12': + optional: true + + '@esbuild/aix-ppc64@0.20.2': + optional: true + + '@esbuild/aix-ppc64@0.24.2': + optional: true + + '@esbuild/android-arm64@0.19.11': + optional: true + + '@esbuild/android-arm64@0.19.12': + optional: true + + '@esbuild/android-arm64@0.20.2': + optional: true + + '@esbuild/android-arm64@0.24.2': + optional: true + + '@esbuild/android-arm@0.19.11': + optional: true + + '@esbuild/android-arm@0.19.12': + optional: true + + '@esbuild/android-arm@0.20.2': + optional: true + + '@esbuild/android-arm@0.24.2': + optional: true + + '@esbuild/android-x64@0.19.11': + optional: true + + '@esbuild/android-x64@0.19.12': + optional: true + + '@esbuild/android-x64@0.20.2': + optional: true + + '@esbuild/android-x64@0.24.2': + optional: true + + '@esbuild/darwin-arm64@0.19.11': + optional: true + + '@esbuild/darwin-arm64@0.19.12': + optional: true + + '@esbuild/darwin-arm64@0.20.2': + optional: true + + '@esbuild/darwin-arm64@0.24.2': + optional: true + + '@esbuild/darwin-x64@0.19.11': + optional: true + + '@esbuild/darwin-x64@0.19.12': + optional: true + + '@esbuild/darwin-x64@0.20.2': + optional: true + + '@esbuild/darwin-x64@0.24.2': + optional: true + + '@esbuild/freebsd-arm64@0.19.11': + optional: true + + '@esbuild/freebsd-arm64@0.19.12': + optional: true + + '@esbuild/freebsd-arm64@0.20.2': + optional: true + + '@esbuild/freebsd-arm64@0.24.2': + optional: true + + '@esbuild/freebsd-x64@0.19.11': + optional: true + + '@esbuild/freebsd-x64@0.19.12': + optional: true + + '@esbuild/freebsd-x64@0.20.2': + optional: true + + '@esbuild/freebsd-x64@0.24.2': + optional: true + + '@esbuild/linux-arm64@0.19.11': + optional: true + + '@esbuild/linux-arm64@0.19.12': + optional: true + + '@esbuild/linux-arm64@0.20.2': + optional: true + + '@esbuild/linux-arm64@0.24.2': + optional: true + + '@esbuild/linux-arm@0.19.11': + optional: true + + '@esbuild/linux-arm@0.19.12': + optional: true + + '@esbuild/linux-arm@0.20.2': + optional: true + + '@esbuild/linux-arm@0.24.2': + optional: true + + '@esbuild/linux-ia32@0.19.11': + optional: true + + '@esbuild/linux-ia32@0.19.12': + optional: true + + '@esbuild/linux-ia32@0.20.2': + optional: true + + '@esbuild/linux-ia32@0.24.2': + optional: true + + '@esbuild/linux-loong64@0.19.11': + optional: true + + '@esbuild/linux-loong64@0.19.12': + optional: true + + '@esbuild/linux-loong64@0.20.2': + optional: true + + '@esbuild/linux-loong64@0.24.2': + optional: true + + '@esbuild/linux-mips64el@0.19.11': + optional: true + + '@esbuild/linux-mips64el@0.19.12': + optional: true + + '@esbuild/linux-mips64el@0.20.2': + optional: true + + '@esbuild/linux-mips64el@0.24.2': + optional: true + + '@esbuild/linux-ppc64@0.19.11': + optional: true + + '@esbuild/linux-ppc64@0.19.12': + optional: true + + '@esbuild/linux-ppc64@0.20.2': + optional: true + + '@esbuild/linux-ppc64@0.24.2': + optional: true + + '@esbuild/linux-riscv64@0.19.11': + optional: true + + '@esbuild/linux-riscv64@0.19.12': + optional: true + + '@esbuild/linux-riscv64@0.20.2': + optional: true + + '@esbuild/linux-riscv64@0.24.2': + optional: true + + '@esbuild/linux-s390x@0.19.11': + optional: true + + '@esbuild/linux-s390x@0.19.12': + optional: true + + '@esbuild/linux-s390x@0.20.2': + optional: true + + '@esbuild/linux-s390x@0.24.2': + optional: true + + '@esbuild/linux-x64@0.19.11': + optional: true + + '@esbuild/linux-x64@0.19.12': + optional: true + + '@esbuild/linux-x64@0.20.2': + optional: true + + '@esbuild/linux-x64@0.24.2': + optional: true + + '@esbuild/netbsd-arm64@0.24.2': + optional: true + + '@esbuild/netbsd-x64@0.19.11': + optional: true + + '@esbuild/netbsd-x64@0.19.12': + optional: true + + '@esbuild/netbsd-x64@0.20.2': + optional: true + + '@esbuild/netbsd-x64@0.24.2': + optional: true + + '@esbuild/openbsd-arm64@0.24.2': + optional: true + + '@esbuild/openbsd-x64@0.19.11': + optional: true + + '@esbuild/openbsd-x64@0.19.12': + optional: true + + '@esbuild/openbsd-x64@0.20.2': + optional: true + + '@esbuild/openbsd-x64@0.24.2': + optional: true + + '@esbuild/sunos-x64@0.19.11': + optional: true + + '@esbuild/sunos-x64@0.19.12': + optional: true + + '@esbuild/sunos-x64@0.20.2': + optional: true + + '@esbuild/sunos-x64@0.24.2': + optional: true + + '@esbuild/win32-arm64@0.19.11': + optional: true + + '@esbuild/win32-arm64@0.19.12': + optional: true + + '@esbuild/win32-arm64@0.20.2': + optional: true + + '@esbuild/win32-arm64@0.24.2': + optional: true + + '@esbuild/win32-ia32@0.19.11': + optional: true + + '@esbuild/win32-ia32@0.19.12': + optional: true + + '@esbuild/win32-ia32@0.20.2': + optional: true + + '@esbuild/win32-ia32@0.24.2': + optional: true + + '@esbuild/win32-x64@0.19.11': + optional: true + + '@esbuild/win32-x64@0.19.12': + optional: true + + '@esbuild/win32-x64@0.20.2': + optional: true + + '@esbuild/win32-x64@0.24.2': + optional: true + + '@eslint-community/eslint-utils@4.4.0(eslint@8.50.0)': + dependencies: + eslint: 8.50.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/eslint-utils@4.4.0(eslint@8.57.0)': + dependencies: + eslint: 8.57.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/eslint-utils@4.4.0(eslint@9.3.0)': + dependencies: + eslint: 9.3.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.10.0': {} + + '@eslint/eslintrc@2.1.2': + dependencies: + ajv: 6.12.6 + debug: 4.3.4 + espree: 9.6.1 + globals: 13.22.0 + ignore: 5.3.1 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.12.6 + debug: 4.3.4(supports-color@9.4.0) + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.1 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/eslintrc@3.1.0': + dependencies: + ajv: 6.12.6 + debug: 4.3.4(supports-color@9.4.0) + espree: 10.0.1 + globals: 14.0.0 + ignore: 5.3.1 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.50.0': {} + + '@eslint/js@8.57.0': {} + + '@eslint/js@9.3.0': {} + + '@floating-ui/core@1.5.0': + dependencies: + '@floating-ui/utils': 0.1.4 + + '@floating-ui/dom@1.5.3': + dependencies: + '@floating-ui/core': 1.5.0 + '@floating-ui/utils': 0.1.4 + + '@floating-ui/react-dom@2.0.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@floating-ui/dom': 1.5.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + '@floating-ui/utils@0.1.4': {} + + '@graphql-codegen/add@5.0.0(graphql@16.8.1)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.0.1(graphql@16.8.1) + graphql: 16.8.1 + tslib: 2.5.3 + + '@graphql-codegen/cli@5.0.0(@parcel/watcher@2.3.0)(@types/node@17.0.45)(encoding@0.1.13)(graphql@16.8.1)(typescript@5.8.2)': + dependencies: + '@babel/generator': 7.23.5 + '@babel/template': 7.22.15 + '@babel/types': 7.23.5 + '@graphql-codegen/core': 4.0.0(graphql@16.8.1) + '@graphql-codegen/plugin-helpers': 5.0.1(graphql@16.8.1) + '@graphql-tools/apollo-engine-loader': 8.0.0(encoding@0.1.13)(graphql@16.8.1) + '@graphql-tools/code-file-loader': 8.0.3(graphql@16.8.1) + '@graphql-tools/git-loader': 8.0.3(graphql@16.8.1) + '@graphql-tools/github-loader': 8.0.0(@types/node@17.0.45)(encoding@0.1.13)(graphql@16.8.1) + '@graphql-tools/graphql-file-loader': 8.0.0(graphql@16.8.1) + '@graphql-tools/json-file-loader': 8.0.0(graphql@16.8.1) + '@graphql-tools/load': 8.0.0(graphql@16.8.1) + '@graphql-tools/prisma-loader': 8.0.2(@types/node@17.0.45)(encoding@0.1.13)(graphql@16.8.1) + '@graphql-tools/url-loader': 8.0.0(@types/node@17.0.45)(encoding@0.1.13)(graphql@16.8.1) + '@graphql-tools/utils': 10.0.8(graphql@16.8.1) + '@whatwg-node/fetch': 0.8.8 + chalk: 4.1.2 + cosmiconfig: 8.3.6(typescript@5.8.2) + debounce: 1.2.1 + detect-indent: 6.1.0 + graphql: 16.8.1 + graphql-config: 5.0.3(@types/node@17.0.45)(encoding@0.1.13)(graphql@16.8.1)(typescript@5.8.2) + inquirer: 8.2.6 + is-glob: 4.0.3 + jiti: 1.21.0 + json-to-pretty-yaml: 1.2.2 + listr2: 4.0.5 + log-symbols: 4.1.0 + micromatch: 4.0.5 + shell-quote: 1.8.1 + string-env-interpolation: 1.0.1 + ts-log: 2.2.5 + tslib: 2.6.2 + yaml: 2.3.4 + yargs: 17.7.2 + optionalDependencies: + '@parcel/watcher': 2.3.0 + transitivePeerDependencies: + - '@types/node' + - bufferutil + - cosmiconfig-toml-loader + - encoding + - enquirer + - supports-color + - typescript + - utf-8-validate + + '@graphql-codegen/client-preset@4.1.0(encoding@0.1.13)(graphql@16.8.1)': + dependencies: + '@babel/helper-plugin-utils': 7.22.5 + '@babel/template': 7.22.15 + '@graphql-codegen/add': 5.0.0(graphql@16.8.1) + '@graphql-codegen/gql-tag-operations': 4.0.1(encoding@0.1.13)(graphql@16.8.1) + '@graphql-codegen/plugin-helpers': 5.0.1(graphql@16.8.1) + '@graphql-codegen/typed-document-node': 5.0.1(encoding@0.1.13)(graphql@16.8.1) + '@graphql-codegen/typescript': 4.0.1(encoding@0.1.13)(graphql@16.8.1) + '@graphql-codegen/typescript-operations': 4.0.1(encoding@0.1.13)(graphql@16.8.1) + '@graphql-codegen/visitor-plugin-common': 4.0.1(encoding@0.1.13)(graphql@16.8.1) + '@graphql-tools/documents': 1.0.0(graphql@16.8.1) + '@graphql-tools/utils': 10.0.8(graphql@16.8.1) + '@graphql-typed-document-node/core': 3.2.0(graphql@16.8.1) + graphql: 16.8.1 + tslib: 2.5.3 + transitivePeerDependencies: + - encoding + - supports-color + + '@graphql-codegen/core@4.0.0(graphql@16.8.1)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.0.1(graphql@16.8.1) + '@graphql-tools/schema': 10.0.0(graphql@16.8.1) + '@graphql-tools/utils': 10.0.8(graphql@16.8.1) + graphql: 16.8.1 + tslib: 2.5.3 + + '@graphql-codegen/gql-tag-operations@4.0.1(encoding@0.1.13)(graphql@16.8.1)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.0.1(graphql@16.8.1) + '@graphql-codegen/visitor-plugin-common': 4.0.1(encoding@0.1.13)(graphql@16.8.1) + '@graphql-tools/utils': 10.0.8(graphql@16.8.1) + auto-bind: 4.0.0 + graphql: 16.8.1 + tslib: 2.5.3 + transitivePeerDependencies: + - encoding + - supports-color + + '@graphql-codegen/plugin-helpers@5.0.1(graphql@16.8.1)': + dependencies: + '@graphql-tools/utils': 10.0.8(graphql@16.8.1) + change-case-all: 1.0.15 + common-tags: 1.8.2 + graphql: 16.8.1 + import-from: 4.0.0 + lodash: 4.17.21 + tslib: 2.5.3 + + '@graphql-codegen/schema-ast@4.0.0(graphql@16.8.1)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.0.1(graphql@16.8.1) + '@graphql-tools/utils': 10.0.8(graphql@16.8.1) + graphql: 16.8.1 + tslib: 2.5.3 + + '@graphql-codegen/typed-document-node@5.0.1(encoding@0.1.13)(graphql@16.8.1)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.0.1(graphql@16.8.1) + '@graphql-codegen/visitor-plugin-common': 4.0.1(encoding@0.1.13)(graphql@16.8.1) + auto-bind: 4.0.0 + change-case-all: 1.0.15 + graphql: 16.8.1 + tslib: 2.5.3 + transitivePeerDependencies: + - encoding + - supports-color + + '@graphql-codegen/typescript-operations@4.0.1(encoding@0.1.13)(graphql@16.8.1)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.0.1(graphql@16.8.1) + '@graphql-codegen/typescript': 4.0.1(encoding@0.1.13)(graphql@16.8.1) + '@graphql-codegen/visitor-plugin-common': 4.0.1(encoding@0.1.13)(graphql@16.8.1) + auto-bind: 4.0.0 + graphql: 16.8.1 + tslib: 2.5.3 + transitivePeerDependencies: + - encoding + - supports-color + + '@graphql-codegen/typescript@4.0.1(encoding@0.1.13)(graphql@16.8.1)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.0.1(graphql@16.8.1) + '@graphql-codegen/schema-ast': 4.0.0(graphql@16.8.1) + '@graphql-codegen/visitor-plugin-common': 4.0.1(encoding@0.1.13)(graphql@16.8.1) + auto-bind: 4.0.0 + graphql: 16.8.1 + tslib: 2.5.3 + transitivePeerDependencies: + - encoding + - supports-color + + '@graphql-codegen/visitor-plugin-common@4.0.1(encoding@0.1.13)(graphql@16.8.1)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.0.1(graphql@16.8.1) + '@graphql-tools/optimize': 2.0.0(graphql@16.8.1) + '@graphql-tools/relay-operation-optimizer': 7.0.0(encoding@0.1.13)(graphql@16.8.1) + '@graphql-tools/utils': 10.0.8(graphql@16.8.1) + auto-bind: 4.0.0 + change-case-all: 1.0.15 + dependency-graph: 0.11.0 + graphql: 16.8.1 + graphql-tag: 2.12.6(graphql@16.8.1) + parse-filepath: 1.0.2 + tslib: 2.5.3 + transitivePeerDependencies: + - encoding + - supports-color + + '@graphql-tools/apollo-engine-loader@8.0.0(encoding@0.1.13)(graphql@16.8.1)': + dependencies: + '@ardatan/sync-fetch': 0.0.1(encoding@0.1.13) + '@graphql-tools/utils': 10.0.8(graphql@16.8.1) + '@whatwg-node/fetch': 0.9.14 + graphql: 16.8.1 + tslib: 2.6.2 + transitivePeerDependencies: + - encoding + + '@graphql-tools/batch-execute@9.0.2(graphql@16.8.1)': + dependencies: + '@graphql-tools/utils': 10.0.8(graphql@16.8.1) + dataloader: 2.2.2 + graphql: 16.8.1 + tslib: 2.6.2 + value-or-promise: 1.0.12 + + '@graphql-tools/code-file-loader@8.0.3(graphql@16.8.1)': + dependencies: + '@graphql-tools/graphql-tag-pluck': 8.1.0(graphql@16.8.1) + '@graphql-tools/utils': 10.0.8(graphql@16.8.1) + globby: 11.1.0 + graphql: 16.8.1 + tslib: 2.6.2 + unixify: 1.0.0 + transitivePeerDependencies: + - supports-color + + '@graphql-tools/delegate@10.0.3(graphql@16.8.1)': + dependencies: + '@graphql-tools/batch-execute': 9.0.2(graphql@16.8.1) + '@graphql-tools/executor': 1.2.0(graphql@16.8.1) + '@graphql-tools/schema': 10.0.0(graphql@16.8.1) + '@graphql-tools/utils': 10.0.8(graphql@16.8.1) + dataloader: 2.2.2 + graphql: 16.8.1 + tslib: 2.6.2 + + '@graphql-tools/documents@1.0.0(graphql@16.8.1)': + dependencies: + graphql: 16.8.1 + lodash.sortby: 4.7.0 + tslib: 2.6.2 + + '@graphql-tools/executor-graphql-ws@1.1.0(graphql@16.8.1)': + dependencies: + '@graphql-tools/utils': 10.0.8(graphql@16.8.1) + '@types/ws': 8.5.9 + graphql: 16.8.1 + graphql-ws: 5.16.0(graphql@16.8.1) + isomorphic-ws: 5.0.0(ws@8.14.2) + tslib: 2.6.2 + ws: 8.14.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@graphql-tools/executor-http@1.0.3(@types/node@17.0.45)(graphql@16.8.1)': + dependencies: + '@graphql-tools/utils': 10.0.8(graphql@16.8.1) + '@repeaterjs/repeater': 3.0.5 + '@whatwg-node/fetch': 0.9.14 + extract-files: 11.0.0 + graphql: 16.8.1 + meros: 1.3.0(@types/node@17.0.45) + tslib: 2.6.2 + value-or-promise: 1.0.12 + transitivePeerDependencies: + - '@types/node' + + '@graphql-tools/executor-legacy-ws@1.0.4(graphql@16.8.1)': + dependencies: + '@graphql-tools/utils': 10.0.8(graphql@16.8.1) + '@types/ws': 8.5.9 + graphql: 16.8.1 + isomorphic-ws: 5.0.0(ws@8.14.2) + tslib: 2.6.2 + ws: 8.14.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@graphql-tools/executor@1.2.0(graphql@16.8.1)': + dependencies: + '@graphql-tools/utils': 10.0.8(graphql@16.8.1) + '@graphql-typed-document-node/core': 3.2.0(graphql@16.8.1) + '@repeaterjs/repeater': 3.0.5 + graphql: 16.8.1 + tslib: 2.6.2 + value-or-promise: 1.0.12 + + '@graphql-tools/git-loader@8.0.3(graphql@16.8.1)': + dependencies: + '@graphql-tools/graphql-tag-pluck': 8.1.0(graphql@16.8.1) + '@graphql-tools/utils': 10.0.8(graphql@16.8.1) + graphql: 16.8.1 + is-glob: 4.0.3 + micromatch: 4.0.5 + tslib: 2.6.2 + unixify: 1.0.0 + transitivePeerDependencies: + - supports-color + + '@graphql-tools/github-loader@8.0.0(@types/node@17.0.45)(encoding@0.1.13)(graphql@16.8.1)': + dependencies: + '@ardatan/sync-fetch': 0.0.1(encoding@0.1.13) + '@graphql-tools/executor-http': 1.0.3(@types/node@17.0.45)(graphql@16.8.1) + '@graphql-tools/graphql-tag-pluck': 8.1.0(graphql@16.8.1) + '@graphql-tools/utils': 10.0.8(graphql@16.8.1) + '@whatwg-node/fetch': 0.9.14 + graphql: 16.8.1 + tslib: 2.6.2 + value-or-promise: 1.0.12 + transitivePeerDependencies: + - '@types/node' + - encoding + - supports-color + + '@graphql-tools/graphql-file-loader@8.0.0(graphql@16.8.1)': + dependencies: + '@graphql-tools/import': 7.0.0(graphql@16.8.1) + '@graphql-tools/utils': 10.0.8(graphql@16.8.1) + globby: 11.1.0 + graphql: 16.8.1 + tslib: 2.6.2 + unixify: 1.0.0 + + '@graphql-tools/graphql-tag-pluck@8.1.0(graphql@16.8.1)': + dependencies: + '@babel/core': 7.23.5 + '@babel/parser': 7.23.5 + '@babel/plugin-syntax-import-assertions': 7.23.3(@babel/core@7.23.5) + '@babel/traverse': 7.23.5 + '@babel/types': 7.23.5 + '@graphql-tools/utils': 10.0.8(graphql@16.8.1) + graphql: 16.8.1 + tslib: 2.6.2 + transitivePeerDependencies: + - supports-color + + '@graphql-tools/import@7.0.0(graphql@16.8.1)': + dependencies: + '@graphql-tools/utils': 10.0.8(graphql@16.8.1) + graphql: 16.8.1 + resolve-from: 5.0.0 + tslib: 2.6.2 + + '@graphql-tools/json-file-loader@8.0.0(graphql@16.8.1)': + dependencies: + '@graphql-tools/utils': 10.0.8(graphql@16.8.1) + globby: 11.1.0 + graphql: 16.8.1 + tslib: 2.6.2 + unixify: 1.0.0 + + '@graphql-tools/load@8.0.0(graphql@16.8.1)': + dependencies: + '@graphql-tools/schema': 10.0.0(graphql@16.8.1) + '@graphql-tools/utils': 10.0.8(graphql@16.8.1) + graphql: 16.8.1 + p-limit: 3.1.0 + tslib: 2.6.2 + + '@graphql-tools/merge@9.0.0(graphql@16.8.1)': + dependencies: + '@graphql-tools/utils': 10.0.8(graphql@16.8.1) + graphql: 16.8.1 + tslib: 2.6.2 + + '@graphql-tools/optimize@2.0.0(graphql@16.8.1)': + dependencies: + graphql: 16.8.1 + tslib: 2.6.2 + + '@graphql-tools/prisma-loader@8.0.2(@types/node@17.0.45)(encoding@0.1.13)(graphql@16.8.1)': + dependencies: + '@graphql-tools/url-loader': 8.0.0(@types/node@17.0.45)(encoding@0.1.13)(graphql@16.8.1) + '@graphql-tools/utils': 10.0.8(graphql@16.8.1) + '@types/js-yaml': 4.0.9 + '@types/json-stable-stringify': 1.0.36 + '@whatwg-node/fetch': 0.9.14 + chalk: 4.1.2 + debug: 4.3.4 + dotenv: 16.3.1 + graphql: 16.8.1 + graphql-request: 6.1.0(encoding@0.1.13)(graphql@16.8.1) + http-proxy-agent: 7.0.0 + https-proxy-agent: 7.0.2 + jose: 5.1.1 + js-yaml: 4.1.0 + json-stable-stringify: 1.1.0 + lodash: 4.17.21 + scuid: 1.1.0 + tslib: 2.6.2 + yaml-ast-parser: 0.0.43 + transitivePeerDependencies: + - '@types/node' + - bufferutil + - encoding + - supports-color + - utf-8-validate + + '@graphql-tools/relay-operation-optimizer@7.0.0(encoding@0.1.13)(graphql@16.8.1)': + dependencies: + '@ardatan/relay-compiler': 12.0.0(encoding@0.1.13)(graphql@16.8.1) + '@graphql-tools/utils': 10.0.8(graphql@16.8.1) + graphql: 16.8.1 + tslib: 2.6.2 + transitivePeerDependencies: + - encoding + - supports-color + + '@graphql-tools/schema@10.0.0(graphql@16.8.1)': + dependencies: + '@graphql-tools/merge': 9.0.0(graphql@16.8.1) + '@graphql-tools/utils': 10.0.8(graphql@16.8.1) + graphql: 16.8.1 + tslib: 2.6.2 + value-or-promise: 1.0.12 + + '@graphql-tools/url-loader@8.0.0(@types/node@17.0.45)(encoding@0.1.13)(graphql@16.8.1)': + dependencies: + '@ardatan/sync-fetch': 0.0.1(encoding@0.1.13) + '@graphql-tools/delegate': 10.0.3(graphql@16.8.1) + '@graphql-tools/executor-graphql-ws': 1.1.0(graphql@16.8.1) + '@graphql-tools/executor-http': 1.0.3(@types/node@17.0.45)(graphql@16.8.1) + '@graphql-tools/executor-legacy-ws': 1.0.4(graphql@16.8.1) + '@graphql-tools/utils': 10.0.8(graphql@16.8.1) + '@graphql-tools/wrap': 10.0.1(graphql@16.8.1) + '@types/ws': 8.5.9 + '@whatwg-node/fetch': 0.9.14 + graphql: 16.8.1 + isomorphic-ws: 5.0.0(ws@8.14.2) + tslib: 2.6.2 + value-or-promise: 1.0.12 + ws: 8.14.2 + transitivePeerDependencies: + - '@types/node' + - bufferutil + - encoding + - utf-8-validate + + '@graphql-tools/utils@10.0.8(graphql@16.8.1)': + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.8.1) + cross-inspect: 1.0.0 + dset: 3.1.3 + graphql: 16.8.1 + tslib: 2.6.2 + + '@graphql-tools/wrap@10.0.1(graphql@16.8.1)': + dependencies: + '@graphql-tools/delegate': 10.0.3(graphql@16.8.1) + '@graphql-tools/schema': 10.0.0(graphql@16.8.1) + '@graphql-tools/utils': 10.0.8(graphql@16.8.1) + graphql: 16.8.1 + tslib: 2.6.2 + value-or-promise: 1.0.12 + + '@graphql-typed-document-node/core@3.2.0(graphql@16.8.1)': + dependencies: + graphql: 16.8.1 + + '@hookform/resolvers@3.3.2(react-hook-form@7.48.2(react@18.2.0))': + dependencies: + react-hook-form: 7.48.2(react@18.2.0) + + '@humanwhocodes/config-array@0.11.11': + dependencies: + '@humanwhocodes/object-schema': 1.2.1 + debug: 4.3.4 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/config-array@0.11.14': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.3.4(supports-color@9.4.0) + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/config-array@0.13.0': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.3.4(supports-color@9.4.0) + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@1.2.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@humanwhocodes/retry@0.3.0': {} + + '@ianvs/prettier-plugin-sort-imports@4.1.1(@vue/compiler-sfc@3.4.27)(prettier@2.8.8)': + dependencies: + '@babel/core': 7.23.5 + '@babel/generator': 7.23.5 + '@babel/parser': 7.23.5 + '@babel/traverse': 7.23.5 + '@babel/types': 7.23.5 + prettier: 2.8.8 + semver: 7.6.2 + optionalDependencies: + '@vue/compiler-sfc': 3.4.27 + transitivePeerDependencies: + - supports-color + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jest/schemas@29.6.3': + dependencies: + '@sinclair/typebox': 0.27.8 + + '@jridgewell/gen-mapping@0.3.3': + dependencies: + '@jridgewell/set-array': 1.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.20 + + '@jridgewell/gen-mapping@0.3.5': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/gen-mapping@0.3.8': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.1': {} + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/source-map@0.3.6': + dependencies: + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/sourcemap-codec@1.4.15': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.20': + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@jsdevtools/ez-spawn@3.0.4': + dependencies: + call-me-maybe: 1.0.2 + cross-spawn: 7.0.3 + string-argv: 0.3.2 + type-detect: 4.0.8 + + '@jspm/core@2.0.1': {} + + '@koa/cors@5.0.0': + dependencies: + vary: 1.1.2 + + '@koa/router@13.1.0': + dependencies: + http-errors: 2.0.0 + koa-compose: 4.1.0 + path-to-regexp: 6.3.0 + + '@lezer/common@1.2.0': {} + + '@lezer/common@1.2.3': {} + + '@lezer/cpp@1.1.2': + dependencies: + '@lezer/common': 1.2.0 + '@lezer/highlight': 1.2.0 + '@lezer/lr': 1.3.14 + + '@lezer/css@1.1.10': + dependencies: + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.0 + '@lezer/lr': 1.3.14 + + '@lezer/css@1.1.6': + dependencies: + '@lezer/common': 1.2.0 + '@lezer/highlight': 1.2.0 + '@lezer/lr': 1.3.14 + + '@lezer/highlight@1.2.0': + dependencies: + '@lezer/common': 1.2.0 + + '@lezer/html@1.3.10': + dependencies: + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.0 + '@lezer/lr': 1.3.14 + + '@lezer/html@1.3.8': + dependencies: + '@lezer/common': 1.2.0 + '@lezer/highlight': 1.2.0 + '@lezer/lr': 1.3.14 + + '@lezer/java@1.1.1': + dependencies: + '@lezer/common': 1.2.0 + '@lezer/highlight': 1.2.0 + '@lezer/lr': 1.3.14 + + '@lezer/javascript@1.4.12': + dependencies: + '@lezer/highlight': 1.2.0 + '@lezer/lr': 1.3.14 + + '@lezer/json@1.0.2': + dependencies: + '@lezer/common': 1.2.0 + '@lezer/highlight': 1.2.0 + '@lezer/lr': 1.3.14 + + '@lezer/lezer@1.1.2': + dependencies: + '@lezer/highlight': 1.2.0 + '@lezer/lr': 1.3.14 + + '@lezer/lr@1.3.14': + dependencies: + '@lezer/common': 1.2.0 + + '@lezer/markdown@1.2.0': + dependencies: + '@lezer/common': 1.2.0 + '@lezer/highlight': 1.2.0 + + '@lezer/markdown@1.4.2': + dependencies: + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.0 + + '@lezer/php@1.0.2': + dependencies: + '@lezer/common': 1.2.0 + '@lezer/highlight': 1.2.0 + '@lezer/lr': 1.3.14 + + '@lezer/python@1.1.10': + dependencies: + '@lezer/common': 1.2.0 + '@lezer/highlight': 1.2.0 + '@lezer/lr': 1.3.14 + + '@lezer/python@1.1.16': + dependencies: + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.0 + '@lezer/lr': 1.3.14 + + '@lezer/rust@1.0.2': + dependencies: + '@lezer/common': 1.2.0 + '@lezer/highlight': 1.2.0 + '@lezer/lr': 1.3.14 + + '@lezer/sass@1.0.4': + dependencies: + '@lezer/common': 1.2.0 + '@lezer/highlight': 1.2.0 + '@lezer/lr': 1.3.14 + + '@lezer/xml@1.0.4': + dependencies: + '@lezer/common': 1.2.0 + '@lezer/highlight': 1.2.0 + '@lezer/lr': 1.3.14 + + '@lezer/xml@1.0.6': + dependencies: + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.0 + '@lezer/lr': 1.3.14 + + '@marijn/find-cluster-break@1.0.2': {} + + '@next/env@13.5.3': {} + + '@next/env@14.1.4': {} + + '@next/eslint-plugin-next@13.4.7-canary.1': + dependencies: + glob: 7.1.7 + + '@next/swc-darwin-arm64@13.5.3': + optional: true + + '@next/swc-darwin-arm64@14.1.4': + optional: true + + '@next/swc-darwin-x64@13.5.3': + optional: true + + '@next/swc-darwin-x64@14.1.4': + optional: true + + '@next/swc-linux-arm64-gnu@13.5.3': + optional: true + + '@next/swc-linux-arm64-gnu@14.1.4': + optional: true + + '@next/swc-linux-arm64-musl@13.5.3': + optional: true + + '@next/swc-linux-arm64-musl@14.1.4': + optional: true + + '@next/swc-linux-x64-gnu@13.5.3': + optional: true + + '@next/swc-linux-x64-gnu@14.1.4': + optional: true + + '@next/swc-linux-x64-musl@13.5.3': + optional: true + + '@next/swc-linux-x64-musl@14.1.4': + optional: true + + '@next/swc-win32-arm64-msvc@13.5.3': + optional: true + + '@next/swc-win32-arm64-msvc@14.1.4': + optional: true + + '@next/swc-win32-ia32-msvc@13.5.3': + optional: true + + '@next/swc-win32-ia32-msvc@14.1.4': + optional: true + + '@next/swc-win32-x64-msvc@13.5.3': + optional: true + + '@next/swc-win32-x64-msvc@14.1.4': + optional: true + + '@nextjournal/lang-clojure@1.0.0': + dependencies: + '@codemirror/language': 6.10.1 + '@nextjournal/lezer-clojure': 1.0.0 + + '@nextjournal/lezer-clojure@1.0.0': + dependencies: + '@lezer/lr': 1.3.14 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + + '@one-ini/wasm@0.1.1': {} + + '@orama/orama@2.0.18': {} + + '@parcel/watcher-android-arm64@2.3.0': + optional: true + + '@parcel/watcher-darwin-arm64@2.3.0': + optional: true + + '@parcel/watcher-darwin-x64@2.3.0': + optional: true + + '@parcel/watcher-freebsd-x64@2.3.0': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.3.0': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.3.0': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.3.0': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.3.0': + optional: true + + '@parcel/watcher-linux-x64-musl@2.3.0': + optional: true + + '@parcel/watcher-win32-arm64@2.3.0': + optional: true + + '@parcel/watcher-win32-ia32@2.3.0': + optional: true + + '@parcel/watcher-win32-x64@2.3.0': + optional: true + + '@parcel/watcher@2.3.0': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.5 + node-addon-api: 7.0.0 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.3.0 + '@parcel/watcher-darwin-arm64': 2.3.0 + '@parcel/watcher-darwin-x64': 2.3.0 + '@parcel/watcher-freebsd-x64': 2.3.0 + '@parcel/watcher-linux-arm-glibc': 2.3.0 + '@parcel/watcher-linux-arm64-glibc': 2.3.0 + '@parcel/watcher-linux-arm64-musl': 2.3.0 + '@parcel/watcher-linux-x64-glibc': 2.3.0 + '@parcel/watcher-linux-x64-musl': 2.3.0 + '@parcel/watcher-win32-arm64': 2.3.0 + '@parcel/watcher-win32-ia32': 2.3.0 + '@parcel/watcher-win32-x64': 2.3.0 + + '@peculiar/asn1-schema@2.3.8': + dependencies: + asn1js: 3.0.5 + pvtsutils: 1.3.5 + tslib: 2.6.2 + + '@peculiar/json-schema@1.1.12': + dependencies: + tslib: 2.6.2 + + '@peculiar/webcrypto@1.4.3': + dependencies: + '@peculiar/asn1-schema': 2.3.8 + '@peculiar/json-schema': 1.1.12 + pvtsutils: 1.3.5 + tslib: 2.6.2 + webcrypto-core: 1.7.7 + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@playwright/browser-chromium@1.48.1': + dependencies: + playwright-core: 1.48.1 + + '@popperjs/core@2.11.8': {} + + '@preact/signals-core@1.8.0': {} + + '@quilted/events@2.0.0': {} + + '@quilted/signals@0.2.1': + dependencies: + '@preact/signals-core': 1.8.0 + '@quilted/events': 2.0.0 + + '@radix-ui/colors@1.0.1': {} + + '@radix-ui/number@1.0.1': + dependencies: + '@babel/runtime': 7.24.4 + + '@radix-ui/primitive@1.0.1': + dependencies: + '@babel/runtime': 7.24.4 + + '@radix-ui/primitive@1.1.0': {} + + '@radix-ui/react-accordion@1.1.2(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collapsible': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.8 + + '@radix-ui/react-alert-dialog@1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-dialog': 1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.8 + + '@radix-ui/react-arrow@1.0.3(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.23 + + '@radix-ui/react-arrow@1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.8 + + '@radix-ui/react-avatar@1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/react-context': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.8 + + '@radix-ui/react-checkbox@1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.8 + + '@radix-ui/react-collapsible@1.0.3(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.23 + + '@radix-ui/react-collapsible@1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.8 + + '@radix-ui/react-collection@1.0.3(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.23 + + '@radix-ui/react-collection@1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.8 + + '@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.23)(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.23 + + '@radix-ui/react-compose-refs@1.1.0(@types/react@18.2.23)(react@18.2.0)': + dependencies: + react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.23 + + '@radix-ui/react-context@1.0.1(@types/react@18.2.23)(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.23 + + '@radix-ui/react-dialog@1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-portal': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.23)(react@18.2.0) + aria-hidden: 1.2.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll: 2.5.5(@types/react@18.2.23)(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.8 + + '@radix-ui/react-dialog@1.0.5(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.23)(react@18.2.0) + aria-hidden: 1.2.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll: 2.5.5(@types/react@18.2.23)(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.8 + + '@radix-ui/react-direction@1.0.1(@types/react@18.2.23)(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.23 + + '@radix-ui/react-dismissable-layer@1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.8 + + '@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.23 + + '@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.8 + + '@radix-ui/react-dropdown-menu@2.0.6(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-menu': 2.0.6(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.8 + + '@radix-ui/react-focus-guards@1.0.1(@types/react@18.2.23)(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.23 + + '@radix-ui/react-focus-scope@1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.8 + + '@radix-ui/react-focus-scope@1.0.4(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.23 + + '@radix-ui/react-focus-scope@1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.8 + + '@radix-ui/react-hover-card@1.0.7(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.8 + + '@radix-ui/react-icons@1.3.0(react@18.2.0)': + dependencies: + react: 18.2.0 + + '@radix-ui/react-id@1.0.1(@types/react@18.2.23)(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.23 + + '@radix-ui/react-label@2.0.2(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.8 + + '@radix-ui/react-menu@2.0.6(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.23)(react@18.2.0) + aria-hidden: 1.2.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll: 2.5.5(@types/react@18.2.23)(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.8 + + '@radix-ui/react-popover@1.0.7(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.23)(react@18.2.0) + aria-hidden: 1.2.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll: 2.5.5(@types/react@18.2.23)(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.23 + + '@radix-ui/react-popover@1.0.7(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.23)(react@18.2.0) + aria-hidden: 1.2.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll: 2.5.5(@types/react@18.2.23)(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.8 + + '@radix-ui/react-popper@1.1.2(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@floating-ui/react-dom': 2.0.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-arrow': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-rect': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/rect': 1.0.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.8 + + '@radix-ui/react-popper@1.1.3(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@floating-ui/react-dom': 2.0.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-arrow': 1.0.3(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-rect': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/rect': 1.0.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.23 + + '@radix-ui/react-popper@1.1.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@floating-ui/react-dom': 2.0.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-arrow': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-rect': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/rect': 1.0.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.8 + + '@radix-ui/react-portal@1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.8 + + '@radix-ui/react-portal@1.0.4(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.23 + + '@radix-ui/react-portal@1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.8 + + '@radix-ui/react-presence@1.0.1(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.23 + + '@radix-ui/react-presence@1.0.1(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.8 + + '@radix-ui/react-primitive@1.0.3(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.23 + + '@radix-ui/react-primitive@1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.8 + + '@radix-ui/react-primitive@2.0.0(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@radix-ui/react-slot': 1.1.0(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.8 + + '@radix-ui/react-radio-group@1.1.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.8 + + '@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.23 + + '@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.8 + + '@radix-ui/react-scroll-area@1.0.5(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/number': 1.0.1 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.8 + + '@radix-ui/react-select@1.2.2(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/number': 1.0.1 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-popper': 1.1.2(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-portal': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-visually-hidden': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + aria-hidden: 1.2.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll: 2.5.5(@types/react@18.2.23)(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.8 + + '@radix-ui/react-separator@1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.8 + + '@radix-ui/react-slot@1.0.2(@types/react@18.2.23)(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.23 + + '@radix-ui/react-slot@1.1.0(@types/react@18.2.23)(react@18.2.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.23 + + '@radix-ui/react-switch@1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.8 + + '@radix-ui/react-tabs@1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-context': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.8 + + '@radix-ui/react-toggle-group@1.0.4(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-context': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-toggle': 1.0.3(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.23 + + '@radix-ui/react-toggle@1.0.3(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.23 + + '@radix-ui/react-toggle@1.1.0(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.8 + + '@radix-ui/react-tooltip@1.0.7(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-visually-hidden': 1.0.3(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.23 + + '@radix-ui/react-tooltip@1.0.7(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-visually-hidden': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.8 + + '@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.2.23)(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.23 + + '@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.2.23)(react@18.2.0)': + dependencies: + react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.23 + + '@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.2.23)(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.23 + + '@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.2.23)(react@18.2.0)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.23 + + '@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.2.23)(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.23 + + '@radix-ui/react-use-layout-effect@1.0.1(@types/react@18.2.23)(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.23 + + '@radix-ui/react-use-previous@1.0.1(@types/react@18.2.23)(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.23 + + '@radix-ui/react-use-rect@1.0.1(@types/react@18.2.23)(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/rect': 1.0.1 + react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.23 + + '@radix-ui/react-use-size@1.0.1(@types/react@18.2.23)(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.23 + + '@radix-ui/react-visually-hidden@1.0.3(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.23 + + '@radix-ui/react-visually-hidden@1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + '@types/react-dom': 18.2.8 + + '@radix-ui/rect@1.0.1': + dependencies: + '@babel/runtime': 7.24.4 + + '@react-email/body@0.0.8(react@18.2.0)': + dependencies: + react: 18.2.0 + + '@react-email/button@0.0.15(react@18.2.0)': + dependencies: + react: 18.2.0 + + '@react-email/code-block@0.0.4(react@18.2.0)': + dependencies: + prismjs: 1.29.0 + react: 18.2.0 + + '@react-email/code-inline@0.0.2(react@18.2.0)': + dependencies: + react: 18.2.0 + + '@react-email/column@0.0.10(react@18.2.0)': + dependencies: + react: 18.2.0 + + '@react-email/components@0.0.18(@types/react@18.2.23)(react@18.2.0)': + dependencies: + '@react-email/body': 0.0.8(react@18.2.0) + '@react-email/button': 0.0.15(react@18.2.0) + '@react-email/code-block': 0.0.4(react@18.2.0) + '@react-email/code-inline': 0.0.2(react@18.2.0) + '@react-email/column': 0.0.10(react@18.2.0) + '@react-email/container': 0.0.12(react@18.2.0) + '@react-email/font': 0.0.6(react@18.2.0) + '@react-email/head': 0.0.9(react@18.2.0) + '@react-email/heading': 0.0.12(@types/react@18.2.23)(react@18.2.0) + '@react-email/hr': 0.0.8(react@18.2.0) + '@react-email/html': 0.0.8(react@18.2.0) + '@react-email/img': 0.0.8(react@18.2.0) + '@react-email/link': 0.0.8(react@18.2.0) + '@react-email/markdown': 0.0.10(react@18.2.0) + '@react-email/preview': 0.0.9(react@18.2.0) + '@react-email/render': 0.0.14 + '@react-email/row': 0.0.8(react@18.2.0) + '@react-email/section': 0.0.12(react@18.2.0) + '@react-email/tailwind': 0.0.17(react@18.2.0) + '@react-email/text': 0.0.8(react@18.2.0) + react: 18.2.0 + transitivePeerDependencies: + - '@types/react' + + '@react-email/container@0.0.12(react@18.2.0)': + dependencies: + react: 18.2.0 + + '@react-email/font@0.0.6(react@18.2.0)': + dependencies: + react: 18.2.0 + + '@react-email/head@0.0.9(react@18.2.0)': + dependencies: + react: 18.2.0 + + '@react-email/heading@0.0.12(@types/react@18.2.23)(react@18.2.0)': + dependencies: + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + transitivePeerDependencies: + - '@types/react' + + '@react-email/hr@0.0.8(react@18.2.0)': + dependencies: + react: 18.2.0 + + '@react-email/html@0.0.8(react@18.2.0)': + dependencies: + react: 18.2.0 + + '@react-email/img@0.0.8(react@18.2.0)': + dependencies: + react: 18.2.0 + + '@react-email/link@0.0.8(react@18.2.0)': + dependencies: + react: 18.2.0 + + '@react-email/markdown@0.0.10(react@18.2.0)': + dependencies: + md-to-react-email: 5.0.2(react@18.2.0) + react: 18.2.0 + + '@react-email/preview@0.0.9(react@18.2.0)': + dependencies: + react: 18.2.0 + + '@react-email/render@0.0.14': + dependencies: + html-to-text: 9.0.5 + js-beautify: 1.15.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-promise-suspense: 0.3.4 + + '@react-email/row@0.0.8(react@18.2.0)': + dependencies: + react: 18.2.0 + + '@react-email/section@0.0.12(react@18.2.0)': + dependencies: + react: 18.2.0 + + '@react-email/tailwind@0.0.17(react@18.2.0)': + dependencies: + react: 18.2.0 + + '@react-email/text@0.0.8(react@18.2.0)': + dependencies: + react: 18.2.0 + + '@redocly/ajv@8.11.0': + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + + '@redocly/config@0.6.3': {} + + '@redocly/openapi-core@1.18.0(encoding@0.1.13)(supports-color@9.4.0)': + dependencies: + '@redocly/ajv': 8.11.0 + '@redocly/config': 0.6.3 + colorette: 1.4.0 + https-proxy-agent: 7.0.4(supports-color@9.4.0) + js-levenshtein: 1.1.6 + js-yaml: 4.1.0 + lodash.isequal: 4.5.0 + minimatch: 5.1.6 + node-fetch: 2.7.0(encoding@0.1.13) + pluralize: 8.0.0 + yaml-ast-parser: 0.0.43 + transitivePeerDependencies: + - encoding + - supports-color + + '@remirror/core-constants@3.0.0': {} + + '@repeaterjs/repeater@3.0.5': {} + + '@replit/codemirror-lang-csharp@6.2.0(@codemirror/autocomplete@6.11.1(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.0))(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.0)(@lezer/highlight@1.2.0)(@lezer/lr@1.3.14)': + dependencies: + '@codemirror/autocomplete': 6.11.1(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.0) + '@codemirror/language': 6.10.1 + '@codemirror/state': 6.4.0 + '@codemirror/view': 6.23.0 + '@lezer/common': 1.2.0 + '@lezer/highlight': 1.2.0 + '@lezer/lr': 1.3.14 + + '@replit/codemirror-lang-nix@6.0.1(@codemirror/autocomplete@6.11.1(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.0))(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.0)(@lezer/highlight@1.2.0)(@lezer/lr@1.3.14)': + dependencies: + '@codemirror/autocomplete': 6.11.1(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.0) + '@codemirror/language': 6.10.1 + '@codemirror/state': 6.4.0 + '@codemirror/view': 6.23.0 + '@lezer/common': 1.2.0 + '@lezer/highlight': 1.2.0 + '@lezer/lr': 1.3.14 + + '@replit/codemirror-lang-solidity@6.0.1(@codemirror/language@6.10.1)': + dependencies: + '@codemirror/language': 6.10.1 + + '@replit/codemirror-lang-svelte@6.0.0(@codemirror/autocomplete@6.11.1(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.0))(@codemirror/lang-css@6.2.1(@codemirror/view@6.23.0))(@codemirror/lang-html@6.4.7)(@codemirror/lang-javascript@6.2.1)(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.0)(@lezer/highlight@1.2.0)(@lezer/javascript@1.4.12)(@lezer/lr@1.3.14)': + dependencies: + '@codemirror/autocomplete': 6.11.1(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.0) + '@codemirror/lang-css': 6.2.1(@codemirror/view@6.23.0) + '@codemirror/lang-html': 6.4.7 + '@codemirror/lang-javascript': 6.2.1 + '@codemirror/language': 6.10.1 + '@codemirror/state': 6.4.0 + '@codemirror/view': 6.23.0 + '@lezer/common': 1.2.0 + '@lezer/highlight': 1.2.0 + '@lezer/javascript': 1.4.12 + '@lezer/lr': 1.3.14 + + '@resvg/resvg-wasm@2.4.1': {} + + '@rollup/plugin-alias@5.1.1(rollup@3.29.5)': + optionalDependencies: + rollup: 3.29.5 + + '@rollup/plugin-commonjs@25.0.8(rollup@3.29.5)': + dependencies: + '@rollup/pluginutils': 5.1.4(rollup@3.29.5) + commondir: 1.0.1 + estree-walker: 2.0.2 + glob: 8.1.0 + is-reference: 1.2.1 + magic-string: 0.30.17 + optionalDependencies: + rollup: 3.29.5 + + '@rollup/plugin-commonjs@28.0.3(rollup@4.36.0)': + dependencies: + '@rollup/pluginutils': 5.1.4(rollup@4.36.0) + commondir: 1.0.1 + estree-walker: 2.0.2 + fdir: 6.4.3(picomatch@4.0.2) + is-reference: 1.2.1 + magic-string: 0.30.17 + picomatch: 4.0.2 + optionalDependencies: + rollup: 4.36.0 + + '@rollup/plugin-json@6.1.0(rollup@3.29.5)': + dependencies: + '@rollup/pluginutils': 5.1.4(rollup@3.29.5) + optionalDependencies: + rollup: 3.29.5 + + '@rollup/plugin-node-resolve@15.3.1(rollup@3.29.5)': + dependencies: + '@rollup/pluginutils': 5.1.4(rollup@3.29.5) + '@types/resolve': 1.20.2 + deepmerge: 4.3.1 + is-module: 1.0.0 + resolve: 1.22.8 + optionalDependencies: + rollup: 3.29.5 + + '@rollup/plugin-node-resolve@16.0.0(rollup@4.36.0)': + dependencies: + '@rollup/pluginutils': 5.1.4(rollup@4.36.0) + '@types/resolve': 1.20.2 + deepmerge: 4.3.1 + is-module: 1.0.0 + resolve: 1.22.8 + optionalDependencies: + rollup: 4.36.0 + + '@rollup/plugin-replace@5.0.7(rollup@3.29.5)': + dependencies: + '@rollup/pluginutils': 5.1.4(rollup@3.29.5) + magic-string: 0.30.17 + optionalDependencies: + rollup: 3.29.5 + + '@rollup/plugin-terser@0.4.4(rollup@4.36.0)': + dependencies: + serialize-javascript: 6.0.2 + smob: 1.5.0 + terser: 5.31.0 + optionalDependencies: + rollup: 4.36.0 + + '@rollup/plugin-typescript@12.1.1(rollup@4.27.4)(tslib@2.6.2)(typescript@5.4.5)': + dependencies: + '@rollup/pluginutils': 5.1.4(rollup@4.27.4) + resolve: 1.22.8 + typescript: 5.4.5 + optionalDependencies: + rollup: 4.27.4 + tslib: 2.6.2 + + '@rollup/plugin-typescript@12.1.1(rollup@4.36.0)(tslib@2.6.2)(typescript@5.4.5)': + dependencies: + '@rollup/pluginutils': 5.1.4(rollup@4.36.0) + resolve: 1.22.8 + typescript: 5.4.5 + optionalDependencies: + rollup: 4.36.0 + tslib: 2.6.2 + + '@rollup/pluginutils@5.1.4(rollup@3.29.5)': + dependencies: + '@types/estree': 1.0.6 + estree-walker: 2.0.2 + picomatch: 4.0.2 + optionalDependencies: + rollup: 3.29.5 + + '@rollup/pluginutils@5.1.4(rollup@4.27.4)': + dependencies: + '@types/estree': 1.0.6 + estree-walker: 2.0.2 + picomatch: 4.0.2 + optionalDependencies: + rollup: 4.27.4 + + '@rollup/pluginutils@5.1.4(rollup@4.36.0)': + dependencies: + '@types/estree': 1.0.6 + estree-walker: 2.0.2 + picomatch: 4.0.2 + optionalDependencies: + rollup: 4.36.0 + + '@rollup/rollup-android-arm-eabi@4.27.4': + optional: true + + '@rollup/rollup-android-arm-eabi@4.36.0': + optional: true + + '@rollup/rollup-android-arm64@4.27.4': + optional: true + + '@rollup/rollup-android-arm64@4.36.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.27.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.36.0': + optional: true + + '@rollup/rollup-darwin-x64@4.27.4': + optional: true + + '@rollup/rollup-darwin-x64@4.36.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.27.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.36.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.27.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.36.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.27.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.36.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.27.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.36.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.27.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.36.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.27.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.36.0': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.36.0': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.27.4': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.36.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.27.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.36.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.27.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.36.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.27.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.36.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.27.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.36.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.27.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.36.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.27.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.36.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.27.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.36.0': + optional: true + + '@rushstack/eslint-patch@1.5.0': {} + + '@selderee/plugin-htmlparser2@0.11.0': + dependencies: + domhandler: 5.0.3 + selderee: 0.11.0 + + '@shuding/opentype.js@1.4.0-beta.0': + dependencies: + fflate: 0.7.4 + string.prototype.codepointat: 0.2.1 + + '@sinclair/typebox@0.27.8': {} + + '@sindresorhus/slugify@2.2.1': + dependencies: + '@sindresorhus/transliterate': 1.6.0 + escape-string-regexp: 5.0.0 + + '@sindresorhus/transliterate@1.6.0': + dependencies: + escape-string-regexp: 5.0.0 + + '@socket.io/component-emitter@3.1.2': {} + + '@stylistic/eslint-plugin-js@2.1.0(eslint@9.3.0)': + dependencies: + '@types/eslint': 8.56.10 + acorn: 8.11.3 + eslint: 9.3.0 + eslint-visitor-keys: 4.0.0 + espree: 10.0.1 + + '@stylistic/eslint-plugin-jsx@2.1.0(eslint@9.3.0)': + dependencies: + '@stylistic/eslint-plugin-js': 2.1.0(eslint@9.3.0) + '@types/eslint': 8.56.10 + eslint: 9.3.0 + estraverse: 5.3.0 + picomatch: 4.0.2 + + '@stylistic/eslint-plugin-plus@2.1.0(eslint@9.3.0)(typescript@5.4.5)': + dependencies: + '@types/eslint': 8.56.10 + '@typescript-eslint/utils': 7.10.0(eslint@9.3.0)(typescript@5.4.5) + eslint: 9.3.0 + transitivePeerDependencies: + - supports-color + - typescript + + '@stylistic/eslint-plugin-ts@2.1.0(eslint@9.3.0)(typescript@5.4.5)': + dependencies: + '@stylistic/eslint-plugin-js': 2.1.0(eslint@9.3.0) + '@types/eslint': 8.56.10 + '@typescript-eslint/utils': 7.10.0(eslint@9.3.0)(typescript@5.4.5) + eslint: 9.3.0 + transitivePeerDependencies: + - supports-color + - typescript + + '@stylistic/eslint-plugin@2.1.0(eslint@9.3.0)(typescript@5.4.5)': + dependencies: + '@stylistic/eslint-plugin-js': 2.1.0(eslint@9.3.0) + '@stylistic/eslint-plugin-jsx': 2.1.0(eslint@9.3.0) + '@stylistic/eslint-plugin-plus': 2.1.0(eslint@9.3.0)(typescript@5.4.5) + '@stylistic/eslint-plugin-ts': 2.1.0(eslint@9.3.0)(typescript@5.4.5) + '@types/eslint': 8.56.10 + eslint: 9.3.0 + transitivePeerDependencies: + - supports-color + - typescript + + '@swc/core-darwin-arm64@1.3.101': + optional: true + + '@swc/core-darwin-x64@1.3.101': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.3.101': + optional: true + + '@swc/core-linux-arm64-gnu@1.3.101': + optional: true + + '@swc/core-linux-arm64-musl@1.3.101': + optional: true + + '@swc/core-linux-x64-gnu@1.3.101': + optional: true + + '@swc/core-linux-x64-musl@1.3.101': + optional: true + + '@swc/core-win32-arm64-msvc@1.3.101': + optional: true + + '@swc/core-win32-ia32-msvc@1.3.101': + optional: true + + '@swc/core-win32-x64-msvc@1.3.101': + optional: true + + '@swc/core@1.3.101(@swc/helpers@0.5.2)': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.7 + optionalDependencies: + '@swc/core-darwin-arm64': 1.3.101 + '@swc/core-darwin-x64': 1.3.101 + '@swc/core-linux-arm-gnueabihf': 1.3.101 + '@swc/core-linux-arm64-gnu': 1.3.101 + '@swc/core-linux-arm64-musl': 1.3.101 + '@swc/core-linux-x64-gnu': 1.3.101 + '@swc/core-linux-x64-musl': 1.3.101 + '@swc/core-win32-arm64-msvc': 1.3.101 + '@swc/core-win32-ia32-msvc': 1.3.101 + '@swc/core-win32-x64-msvc': 1.3.101 + '@swc/helpers': 0.5.2 + + '@swc/counter@0.1.3': {} + + '@swc/helpers@0.5.2': + dependencies: + tslib: 2.6.2 + + '@swc/types@0.1.7': + dependencies: + '@swc/counter': 0.1.3 + + '@tailwindcss/typography@0.5.10(tailwindcss@3.3.3(ts-node@10.9.2(@types/node@17.0.45)(typescript@5.8.2)))': + dependencies: + lodash.castarray: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.merge: 4.6.2 + postcss-selector-parser: 6.0.10 + tailwindcss: 3.3.3(ts-node@10.9.2(@types/node@17.0.45)(typescript@5.8.2)) + + '@tiptap/core@2.10.4(@tiptap/pm@2.10.4)': + dependencies: + '@tiptap/pm': 2.10.4 + + '@tiptap/extension-blockquote@2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))': + dependencies: + '@tiptap/core': 2.10.4(@tiptap/pm@2.10.4) + + '@tiptap/extension-bold@2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))': + dependencies: + '@tiptap/core': 2.10.4(@tiptap/pm@2.10.4) + + '@tiptap/extension-bubble-menu@2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))(@tiptap/pm@2.10.4)': + dependencies: + '@tiptap/core': 2.10.4(@tiptap/pm@2.10.4) + '@tiptap/pm': 2.10.4 + tippy.js: 6.3.7 + + '@tiptap/extension-bullet-list@2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))': + dependencies: + '@tiptap/core': 2.10.4(@tiptap/pm@2.10.4) + + '@tiptap/extension-code-block@2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))(@tiptap/pm@2.10.4)': + dependencies: + '@tiptap/core': 2.10.4(@tiptap/pm@2.10.4) + '@tiptap/pm': 2.10.4 + + '@tiptap/extension-code@2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))': + dependencies: + '@tiptap/core': 2.10.4(@tiptap/pm@2.10.4) + + '@tiptap/extension-document@2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))': + dependencies: + '@tiptap/core': 2.10.4(@tiptap/pm@2.10.4) + + '@tiptap/extension-dropcursor@2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))(@tiptap/pm@2.10.4)': + dependencies: + '@tiptap/core': 2.10.4(@tiptap/pm@2.10.4) + '@tiptap/pm': 2.10.4 + + '@tiptap/extension-floating-menu@2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))(@tiptap/pm@2.10.4)': + dependencies: + '@tiptap/core': 2.10.4(@tiptap/pm@2.10.4) + '@tiptap/pm': 2.10.4 + tippy.js: 6.3.7 + + '@tiptap/extension-gapcursor@2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))(@tiptap/pm@2.10.4)': + dependencies: + '@tiptap/core': 2.10.4(@tiptap/pm@2.10.4) + '@tiptap/pm': 2.10.4 + + '@tiptap/extension-hard-break@2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))': + dependencies: + '@tiptap/core': 2.10.4(@tiptap/pm@2.10.4) + + '@tiptap/extension-heading@2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))': + dependencies: + '@tiptap/core': 2.10.4(@tiptap/pm@2.10.4) + + '@tiptap/extension-history@2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))(@tiptap/pm@2.10.4)': + dependencies: + '@tiptap/core': 2.10.4(@tiptap/pm@2.10.4) + '@tiptap/pm': 2.10.4 + + '@tiptap/extension-horizontal-rule@2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))(@tiptap/pm@2.10.4)': + dependencies: + '@tiptap/core': 2.10.4(@tiptap/pm@2.10.4) + '@tiptap/pm': 2.10.4 + + '@tiptap/extension-italic@2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))': + dependencies: + '@tiptap/core': 2.10.4(@tiptap/pm@2.10.4) + + '@tiptap/extension-list-item@2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))': + dependencies: + '@tiptap/core': 2.10.4(@tiptap/pm@2.10.4) + + '@tiptap/extension-mention@2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))(@tiptap/pm@2.10.4)(@tiptap/suggestion@2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))(@tiptap/pm@2.10.4))': + dependencies: + '@tiptap/core': 2.10.4(@tiptap/pm@2.10.4) + '@tiptap/pm': 2.10.4 + '@tiptap/suggestion': 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))(@tiptap/pm@2.10.4) + + '@tiptap/extension-ordered-list@2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))': + dependencies: + '@tiptap/core': 2.10.4(@tiptap/pm@2.10.4) + + '@tiptap/extension-paragraph@2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))': + dependencies: + '@tiptap/core': 2.10.4(@tiptap/pm@2.10.4) + + '@tiptap/extension-placeholder@2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))(@tiptap/pm@2.10.4)': + dependencies: + '@tiptap/core': 2.10.4(@tiptap/pm@2.10.4) + '@tiptap/pm': 2.10.4 + + '@tiptap/extension-strike@2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))': + dependencies: + '@tiptap/core': 2.10.4(@tiptap/pm@2.10.4) + + '@tiptap/extension-text-style@2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))': + dependencies: + '@tiptap/core': 2.10.4(@tiptap/pm@2.10.4) + + '@tiptap/extension-text@2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))': + dependencies: + '@tiptap/core': 2.10.4(@tiptap/pm@2.10.4) + + '@tiptap/pm@2.10.4': + dependencies: + prosemirror-changeset: 2.2.1 + prosemirror-collab: 1.3.1 + prosemirror-commands: 1.6.2 + prosemirror-dropcursor: 1.8.1 + prosemirror-gapcursor: 1.3.2 + prosemirror-history: 1.4.1 + prosemirror-inputrules: 1.4.0 + prosemirror-keymap: 1.2.2 + prosemirror-markdown: 1.13.1 + prosemirror-menu: 1.2.4 + prosemirror-model: 1.24.1 + prosemirror-schema-basic: 1.2.3 + prosemirror-schema-list: 1.4.1 + prosemirror-state: 1.4.3 + prosemirror-tables: 1.6.1 + prosemirror-trailing-node: 3.0.0(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.1) + prosemirror-transform: 1.10.2 + prosemirror-view: 1.37.1 + + '@tiptap/react@2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))(@tiptap/pm@2.10.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@tiptap/core': 2.10.4(@tiptap/pm@2.10.4) + '@tiptap/extension-bubble-menu': 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))(@tiptap/pm@2.10.4) + '@tiptap/extension-floating-menu': 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))(@tiptap/pm@2.10.4) + '@tiptap/pm': 2.10.4 + '@types/use-sync-external-store': 0.0.6 + fast-deep-equal: 3.1.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + use-sync-external-store: 1.2.2(react@18.2.0) + + '@tiptap/starter-kit@2.10.4': + dependencies: + '@tiptap/core': 2.10.4(@tiptap/pm@2.10.4) + '@tiptap/extension-blockquote': 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4)) + '@tiptap/extension-bold': 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4)) + '@tiptap/extension-bullet-list': 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4)) + '@tiptap/extension-code': 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4)) + '@tiptap/extension-code-block': 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))(@tiptap/pm@2.10.4) + '@tiptap/extension-document': 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4)) + '@tiptap/extension-dropcursor': 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))(@tiptap/pm@2.10.4) + '@tiptap/extension-gapcursor': 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))(@tiptap/pm@2.10.4) + '@tiptap/extension-hard-break': 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4)) + '@tiptap/extension-heading': 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4)) + '@tiptap/extension-history': 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))(@tiptap/pm@2.10.4) + '@tiptap/extension-horizontal-rule': 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))(@tiptap/pm@2.10.4) + '@tiptap/extension-italic': 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4)) + '@tiptap/extension-list-item': 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4)) + '@tiptap/extension-ordered-list': 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4)) + '@tiptap/extension-paragraph': 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4)) + '@tiptap/extension-strike': 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4)) + '@tiptap/extension-text': 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4)) + '@tiptap/extension-text-style': 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4)) + '@tiptap/pm': 2.10.4 + + '@tiptap/suggestion@2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))(@tiptap/pm@2.10.4)': + dependencies: + '@tiptap/core': 2.10.4(@tiptap/pm@2.10.4) + '@tiptap/pm': 2.10.4 + + '@tootallnate/once@1.1.2': {} + + '@trysound/sax@0.2.0': {} + + '@tsconfig/node10@1.0.11': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@types/chai@4.3.16': {} + + '@types/chroma-js@2.4.4': {} + + '@types/color-convert@2.0.3': + dependencies: + '@types/color-name': 1.1.4 + + '@types/color-name@1.1.4': {} + + '@types/color@3.0.6': + dependencies: + '@types/color-convert': 2.0.3 + + '@types/cookie@0.4.1': {} + + '@types/cors@2.8.17': + dependencies: + '@types/node': 20.12.12 + + '@types/d3-array@3.2.1': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.0': {} + + '@types/d3-scale@4.0.8': + dependencies: + '@types/d3-time': 3.0.3 + + '@types/d3-shape@3.1.6': + dependencies: + '@types/d3-path': 3.1.0 + + '@types/d3-time@3.0.3': {} + + '@types/d3-timer@3.0.2': {} + + '@types/debug@4.1.9': + dependencies: + '@types/ms': 0.7.32 + + '@types/dedent@0.7.2': {} + + '@types/deep-equal@1.0.4': {} + + '@types/diff@5.2.1': {} + + '@types/dompurify@3.0.5': + dependencies: + '@types/trusted-types': 2.0.7 + + '@types/eslint-scope@3.7.7': + dependencies: + '@types/eslint': 8.56.10 + '@types/estree': 1.0.6 + + '@types/eslint@8.56.10': + dependencies: + '@types/estree': 1.0.6 + '@types/json-schema': 7.0.15 + + '@types/estree@1.0.6': {} + + '@types/estree@1.0.7': + optional: true + + '@types/fast-levenshtein@0.0.4': {} + + '@types/fs-extra@11.0.4': + dependencies: + '@types/jsonfile': 6.1.4 + '@types/node': 20.12.12 + + '@types/git-url-parse@9.0.3': {} + + '@types/glob@7.2.0': + dependencies: + '@types/minimatch': 5.1.2 + '@types/node': 18.19.33 + + '@types/hast@2.3.6': + dependencies: + '@types/unist': 2.0.8 + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/he@1.2.3': {} + + '@types/humanize-duration@3.27.4': {} + + '@types/js-levenshtein@1.1.3': {} + + '@types/js-yaml@4.0.9': {} + + '@types/json-schema@7.0.15': {} + + '@types/json-stable-stringify@1.0.36': {} + + '@types/json5@0.0.29': {} + + '@types/jsonfile@6.1.4': + dependencies: + '@types/node': 18.19.33 + + '@types/katex@0.16.3': {} + + '@types/linkify-it@5.0.0': {} + + '@types/lodash-es@4.17.10': + dependencies: + '@types/lodash': 4.14.200 + + '@types/lodash@4.14.200': {} + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdast@3.0.13': + dependencies: + '@types/unist': 2.0.8 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdurl@2.0.0': {} + + '@types/minimatch@5.1.2': {} + + '@types/mocha@10.0.6': {} + + '@types/ms@0.7.32': {} + + '@types/node-fetch@2.6.6': + dependencies: + '@types/node': 20.12.12 + form-data: 4.0.0 + + '@types/node-forge@1.3.11': + dependencies: + '@types/node': 18.19.33 + + '@types/node@17.0.45': {} + + '@types/node@18.19.33': + dependencies: + undici-types: 5.26.5 + + '@types/node@20.12.12': + dependencies: + undici-types: 5.26.5 + + '@types/normalize-package-data@2.4.4': {} + + '@types/numeral@2.0.5': {} + + '@types/object-hash@3.0.6': {} + + '@types/parse-path@7.0.3': {} + + '@types/parse5@6.0.3': {} + + '@types/prismjs@1.26.4': {} + + '@types/prop-types@15.7.7': {} + + '@types/react-dom@18.2.23': + dependencies: + '@types/react': 18.2.23 + + '@types/react-dom@18.2.8': + dependencies: + '@types/react': 18.2.23 + + '@types/react-syntax-highlighter@15.5.7': + dependencies: + '@types/react': 18.2.23 + + '@types/react@18.2.23': + dependencies: + '@types/prop-types': 15.7.7 + '@types/scheduler': 0.16.4 + csstype: 3.1.3 + + '@types/readable-stream@4.0.14': + dependencies: + '@types/node': 20.12.12 + safe-buffer: 5.1.2 + + '@types/resolve@1.20.2': {} + + '@types/scheduler@0.16.4': {} + + '@types/seedrandom@3.0.8': {} + + '@types/semver@7.5.8': {} + + '@types/tinycolor2@1.4.6': {} + + '@types/trusted-types@2.0.7': {} + + '@types/unist@2.0.8': {} + + '@types/unist@3.0.3': {} + + '@types/use-sync-external-store@0.0.6': {} + + '@types/uuid@9.0.8': {} + + '@types/vscode@1.89.0': {} + + '@types/webpack@5.28.5(@swc/core@1.3.101(@swc/helpers@0.5.2))(esbuild@0.19.11)': + dependencies: + '@types/node': 20.12.12 + tapable: 2.2.1 + webpack: 5.91.0(@swc/core@1.3.101(@swc/helpers@0.5.2))(esbuild@0.19.11) + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + - webpack-cli + + '@types/win-ca@3.5.4': + dependencies: + '@types/node': 18.19.33 + '@types/node-forge': 1.3.11 + + '@types/ws@8.5.9': + dependencies: + '@types/node': 20.12.12 + + '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3)': + dependencies: + '@eslint-community/regexpp': 4.10.0 + '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.3.3) + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.0)(typescript@5.3.3) + '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.3.4(supports-color@9.4.0) + eslint: 8.57.0 + graphemer: 1.4.0 + ignore: 5.3.1 + natural-compare: 1.4.0 + semver: 7.6.2 + ts-api-utils: 1.3.0(typescript@5.3.3) + optionalDependencies: + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)': + dependencies: + '@eslint-community/regexpp': 4.10.0 + '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.3.4(supports-color@9.4.0) + eslint: 8.57.0 + graphemer: 1.4.0 + ignore: 5.3.1 + natural-compare: 1.4.0 + semver: 7.6.2 + ts-api-utils: 1.3.0(typescript@5.4.5) + optionalDependencies: + typescript: 5.4.5 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/eslint-plugin@7.10.0(@typescript-eslint/parser@7.10.0(eslint@9.3.0)(typescript@5.4.5))(eslint@9.3.0)(typescript@5.4.5)': + dependencies: + '@eslint-community/regexpp': 4.10.0 + '@typescript-eslint/parser': 7.10.0(eslint@9.3.0)(typescript@5.4.5) + '@typescript-eslint/scope-manager': 7.10.0 + '@typescript-eslint/type-utils': 7.10.0(eslint@9.3.0)(typescript@5.4.5) + '@typescript-eslint/utils': 7.10.0(eslint@9.3.0)(typescript@5.4.5) + '@typescript-eslint/visitor-keys': 7.10.0 + eslint: 9.3.0 + graphemer: 1.4.0 + ignore: 5.3.1 + natural-compare: 1.4.0 + ts-api-utils: 1.3.0(typescript@5.4.5) + optionalDependencies: + typescript: 5.4.5 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/eslint-plugin@7.4.0(@typescript-eslint/parser@5.62.0(eslint@8.50.0)(typescript@5.8.2))(eslint@8.50.0)(typescript@5.8.2)': + dependencies: + '@eslint-community/regexpp': 4.10.0 + '@typescript-eslint/parser': 5.62.0(eslint@8.50.0)(typescript@5.8.2) + '@typescript-eslint/scope-manager': 7.4.0 + '@typescript-eslint/type-utils': 7.4.0(eslint@8.50.0)(typescript@5.8.2) + '@typescript-eslint/utils': 7.4.0(eslint@8.50.0)(typescript@5.8.2) + '@typescript-eslint/visitor-keys': 7.4.0 + debug: 4.3.4 + eslint: 8.50.0 + graphemer: 1.4.0 + ignore: 5.3.1 + natural-compare: 1.4.0 + semver: 7.6.2 + ts-api-utils: 1.3.0(typescript@5.8.2) + optionalDependencies: + typescript: 5.8.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@5.62.0(eslint@8.50.0)(typescript@5.8.2)': + dependencies: + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.8.2) + debug: 4.3.4 + eslint: 8.50.0 + optionalDependencies: + typescript: 5.8.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3)': + dependencies: + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.3.4(supports-color@9.4.0) + eslint: 8.57.0 + optionalDependencies: + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5)': + dependencies: + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.4.5) + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.3.4(supports-color@9.4.0) + eslint: 8.57.0 + optionalDependencies: + typescript: 5.4.5 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@7.10.0(eslint@9.3.0)(typescript@5.4.5)': + dependencies: + '@typescript-eslint/scope-manager': 7.10.0 + '@typescript-eslint/types': 7.10.0 + '@typescript-eslint/typescript-estree': 7.10.0(typescript@5.4.5) + '@typescript-eslint/visitor-keys': 7.10.0 + debug: 4.3.4(supports-color@9.4.0) + eslint: 9.3.0 + optionalDependencies: + typescript: 5.4.5 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@5.62.0': + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + + '@typescript-eslint/scope-manager@6.21.0': + dependencies: + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/visitor-keys': 6.21.0 + + '@typescript-eslint/scope-manager@7.10.0': + dependencies: + '@typescript-eslint/types': 7.10.0 + '@typescript-eslint/visitor-keys': 7.10.0 + + '@typescript-eslint/scope-manager@7.4.0': + dependencies: + '@typescript-eslint/types': 7.4.0 + '@typescript-eslint/visitor-keys': 7.4.0 + + '@typescript-eslint/type-utils@6.21.0(eslint@8.57.0)(typescript@5.3.3)': + dependencies: + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3) + '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.3.3) + debug: 4.3.4(supports-color@9.4.0) + eslint: 8.57.0 + ts-api-utils: 1.3.0(typescript@5.3.3) + optionalDependencies: + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/type-utils@6.21.0(eslint@8.57.0)(typescript@5.4.5)': + dependencies: + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.4.5) + '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.4.5) + debug: 4.3.4(supports-color@9.4.0) + eslint: 8.57.0 + ts-api-utils: 1.3.0(typescript@5.4.5) + optionalDependencies: + typescript: 5.4.5 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/type-utils@7.10.0(eslint@9.3.0)(typescript@5.4.5)': + dependencies: + '@typescript-eslint/typescript-estree': 7.10.0(typescript@5.4.5) + '@typescript-eslint/utils': 7.10.0(eslint@9.3.0)(typescript@5.4.5) + debug: 4.3.4(supports-color@9.4.0) + eslint: 9.3.0 + ts-api-utils: 1.3.0(typescript@5.4.5) + optionalDependencies: + typescript: 5.4.5 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/type-utils@7.4.0(eslint@8.50.0)(typescript@5.8.2)': + dependencies: + '@typescript-eslint/typescript-estree': 7.4.0(typescript@5.8.2) + '@typescript-eslint/utils': 7.4.0(eslint@8.50.0)(typescript@5.8.2) + debug: 4.3.4 + eslint: 8.50.0 + ts-api-utils: 1.3.0(typescript@5.8.2) + optionalDependencies: + typescript: 5.8.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@5.62.0': {} + + '@typescript-eslint/types@6.21.0': {} + + '@typescript-eslint/types@7.10.0': {} + + '@typescript-eslint/types@7.4.0': {} + + '@typescript-eslint/typescript-estree@5.62.0(typescript@5.8.2)': + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.6.2 + tsutils: 3.21.0(typescript@5.8.2) + optionalDependencies: + typescript: 5.8.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/typescript-estree@6.21.0(typescript@5.3.3)': + dependencies: + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.3.4(supports-color@9.4.0) + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.3 + semver: 7.6.2 + ts-api-utils: 1.3.0(typescript@5.3.3) + optionalDependencies: + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/typescript-estree@6.21.0(typescript@5.4.5)': + dependencies: + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.3.4(supports-color@9.4.0) + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.3 + semver: 7.6.2 + ts-api-utils: 1.3.0(typescript@5.4.5) + optionalDependencies: + typescript: 5.4.5 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/typescript-estree@7.10.0(typescript@5.4.5)': + dependencies: + '@typescript-eslint/types': 7.10.0 + '@typescript-eslint/visitor-keys': 7.10.0 + debug: 4.3.4(supports-color@9.4.0) + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.4 + semver: 7.6.2 + ts-api-utils: 1.3.0(typescript@5.4.5) + optionalDependencies: + typescript: 5.4.5 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/typescript-estree@7.4.0(typescript@5.4.5)': + dependencies: + '@typescript-eslint/types': 7.4.0 + '@typescript-eslint/visitor-keys': 7.4.0 + debug: 4.3.4(supports-color@9.4.0) + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.3 + semver: 7.6.2 + ts-api-utils: 1.3.0(typescript@5.4.5) + optionalDependencies: + typescript: 5.4.5 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/typescript-estree@7.4.0(typescript@5.8.2)': + dependencies: + '@typescript-eslint/types': 7.4.0 + '@typescript-eslint/visitor-keys': 7.4.0 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.3 + semver: 7.6.2 + ts-api-utils: 1.3.0(typescript@5.8.2) + optionalDependencies: + typescript: 5.8.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@6.21.0(eslint@8.57.0)(typescript@5.3.3)': + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.8 + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3) + eslint: 8.57.0 + semver: 7.6.2 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/utils@6.21.0(eslint@8.57.0)(typescript@5.4.5)': + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.8 + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.4.5) + eslint: 8.57.0 + semver: 7.6.2 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/utils@7.10.0(eslint@9.3.0)(typescript@5.4.5)': + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@9.3.0) + '@typescript-eslint/scope-manager': 7.10.0 + '@typescript-eslint/types': 7.10.0 + '@typescript-eslint/typescript-estree': 7.10.0(typescript@5.4.5) + eslint: 9.3.0 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/utils@7.4.0(eslint@8.50.0)(typescript@5.8.2)': + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.50.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.8 + '@typescript-eslint/scope-manager': 7.4.0 + '@typescript-eslint/types': 7.4.0 + '@typescript-eslint/typescript-estree': 7.4.0(typescript@5.8.2) + eslint: 8.50.0 + semver: 7.6.2 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/utils@7.4.0(eslint@9.3.0)(typescript@5.4.5)': + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@9.3.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.8 + '@typescript-eslint/scope-manager': 7.4.0 + '@typescript-eslint/types': 7.4.0 + '@typescript-eslint/typescript-estree': 7.4.0(typescript@5.4.5) + eslint: 9.3.0 + semver: 7.6.2 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/visitor-keys@5.62.0': + dependencies: + '@typescript-eslint/types': 5.62.0 + eslint-visitor-keys: 3.4.3 + + '@typescript-eslint/visitor-keys@6.21.0': + dependencies: + '@typescript-eslint/types': 6.21.0 + eslint-visitor-keys: 3.4.3 + + '@typescript-eslint/visitor-keys@7.10.0': + dependencies: + '@typescript-eslint/types': 7.10.0 + eslint-visitor-keys: 3.4.3 + + '@typescript-eslint/visitor-keys@7.4.0': + dependencies: + '@typescript-eslint/types': 7.4.0 + eslint-visitor-keys: 3.4.3 + + '@uidotdev/usehooks@2.4.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + '@uiw/codemirror-extensions-langs@4.21.21(@codemirror/autocomplete@6.11.1(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.0))(@codemirror/language-data@6.3.1(@codemirror/view@6.23.0))(@codemirror/language@6.10.1)(@codemirror/legacy-modes@6.3.3)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.0)(@lezer/highlight@1.2.0)(@lezer/javascript@1.4.12)(@lezer/lr@1.3.14)': + dependencies: + '@codemirror/lang-angular': 0.1.3 + '@codemirror/lang-cpp': 6.0.2 + '@codemirror/lang-css': 6.2.1(@codemirror/view@6.23.0) + '@codemirror/lang-html': 6.4.7 + '@codemirror/lang-java': 6.0.1 + '@codemirror/lang-javascript': 6.2.1 + '@codemirror/lang-json': 6.0.1 + '@codemirror/lang-less': 6.0.2(@codemirror/view@6.23.0) + '@codemirror/lang-lezer': 6.0.1 + '@codemirror/lang-liquid': 6.2.0 + '@codemirror/lang-markdown': 6.2.3 + '@codemirror/lang-php': 6.0.1 + '@codemirror/lang-python': 6.1.3(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.0) + '@codemirror/lang-rust': 6.0.1 + '@codemirror/lang-sass': 6.0.2(@codemirror/view@6.23.0) + '@codemirror/lang-sql': 6.5.5(@codemirror/view@6.23.0) + '@codemirror/lang-vue': 0.1.3 + '@codemirror/lang-wast': 6.0.2 + '@codemirror/lang-xml': 6.0.2(@codemirror/view@6.23.0) + '@codemirror/language-data': 6.3.1(@codemirror/view@6.23.0) + '@codemirror/legacy-modes': 6.3.3 + '@nextjournal/lang-clojure': 1.0.0 + '@replit/codemirror-lang-csharp': 6.2.0(@codemirror/autocomplete@6.11.1(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.0))(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.0)(@lezer/highlight@1.2.0)(@lezer/lr@1.3.14) + '@replit/codemirror-lang-nix': 6.0.1(@codemirror/autocomplete@6.11.1(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.0))(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.0)(@lezer/highlight@1.2.0)(@lezer/lr@1.3.14) + '@replit/codemirror-lang-solidity': 6.0.1(@codemirror/language@6.10.1) + '@replit/codemirror-lang-svelte': 6.0.0(@codemirror/autocomplete@6.11.1(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.0))(@codemirror/lang-css@6.2.1(@codemirror/view@6.23.0))(@codemirror/lang-html@6.4.7)(@codemirror/lang-javascript@6.2.1)(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.0)(@lezer/highlight@1.2.0)(@lezer/javascript@1.4.12)(@lezer/lr@1.3.14) + codemirror-lang-mermaid: 0.5.0 + transitivePeerDependencies: + - '@codemirror/autocomplete' + - '@codemirror/language' + - '@codemirror/state' + - '@codemirror/view' + - '@lezer/common' + - '@lezer/highlight' + - '@lezer/javascript' + - '@lezer/lr' + + '@ungap/structured-clone@1.2.0': {} + + '@upstash/redis@1.22.0(encoding@0.1.13)': + dependencies: + isomorphic-fetch: 3.0.0(encoding@0.1.13) + transitivePeerDependencies: + - encoding + + '@urql/core@4.2.3(graphql@16.8.1)': + dependencies: + '@0no-co/graphql.web': 1.0.4(graphql@16.8.1) + wonka: 6.3.4 + transitivePeerDependencies: + - graphql + + '@urql/exchange-auth@2.1.6(graphql@16.8.1)': + dependencies: + '@urql/core': 4.2.3(graphql@16.8.1) + wonka: 6.3.4 + transitivePeerDependencies: + - graphql + + '@urql/exchange-graphcache@6.4.0(graphql@16.8.1)': + dependencies: + '@0no-co/graphql.web': 1.0.4(graphql@16.8.1) + '@urql/core': 4.2.3(graphql@16.8.1) + wonka: 6.3.4 + transitivePeerDependencies: + - graphql + + '@vercel/analytics@1.0.2': {} + + '@vercel/kv@0.2.3(encoding@0.1.13)': + dependencies: + '@upstash/redis': 1.22.0(encoding@0.1.13) + transitivePeerDependencies: + - encoding + + '@vercel/og@0.5.17': + dependencies: + '@resvg/resvg-wasm': 2.4.1 + satori: 0.10.8 + yoga-wasm-web: 0.3.3 + + '@vitest/expect@1.6.0': + dependencies: + '@vitest/spy': 1.6.0 + '@vitest/utils': 1.6.0 + chai: 4.4.1 + + '@vitest/runner@1.6.0': + dependencies: + '@vitest/utils': 1.6.0 + p-limit: 5.0.0 + pathe: 1.1.2 + + '@vitest/snapshot@1.6.0': + dependencies: + magic-string: 0.30.10 + pathe: 1.1.2 + pretty-format: 29.7.0 + + '@vitest/spy@1.6.0': + dependencies: + tinyspy: 2.2.1 + + '@vitest/utils@1.6.0': + dependencies: + diff-sequences: 29.6.3 + estree-walker: 3.0.3 + loupe: 2.3.7 + pretty-format: 29.7.0 + + '@vscode/test-electron@2.3.10': + dependencies: + http-proxy-agent: 4.0.1 + https-proxy-agent: 5.0.1 + jszip: 3.10.1 + semver: 7.6.2 + transitivePeerDependencies: + - supports-color + + '@vscode/test-web@0.0.63': + dependencies: + '@koa/cors': 5.0.0 + '@koa/router': 13.1.0 + '@playwright/browser-chromium': 1.48.1 + glob: 11.0.0 + gunzip-maybe: 1.4.2 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.5 + koa: 2.15.3 + koa-morgan: 1.0.1 + koa-mount: 4.0.0 + koa-static: 5.0.0 + minimist: 1.2.8 + playwright: 1.48.1 + tar-fs: 3.0.6 + vscode-uri: 3.0.8 + transitivePeerDependencies: + - supports-color + + '@vscode/vsce-sign-alpine-arm64@2.0.2': + optional: true + + '@vscode/vsce-sign-alpine-x64@2.0.2': + optional: true + + '@vscode/vsce-sign-darwin-arm64@2.0.2': + optional: true + + '@vscode/vsce-sign-darwin-x64@2.0.2': + optional: true + + '@vscode/vsce-sign-linux-arm64@2.0.2': + optional: true + + '@vscode/vsce-sign-linux-arm@2.0.2': + optional: true + + '@vscode/vsce-sign-linux-x64@2.0.2': + optional: true + + '@vscode/vsce-sign-win32-arm64@2.0.2': + optional: true + + '@vscode/vsce-sign-win32-x64@2.0.2': + optional: true + + '@vscode/vsce-sign@2.0.4': + optionalDependencies: + '@vscode/vsce-sign-alpine-arm64': 2.0.2 + '@vscode/vsce-sign-alpine-x64': 2.0.2 + '@vscode/vsce-sign-darwin-arm64': 2.0.2 + '@vscode/vsce-sign-darwin-x64': 2.0.2 + '@vscode/vsce-sign-linux-arm': 2.0.2 + '@vscode/vsce-sign-linux-arm64': 2.0.2 + '@vscode/vsce-sign-linux-x64': 2.0.2 + '@vscode/vsce-sign-win32-arm64': 2.0.2 + '@vscode/vsce-sign-win32-x64': 2.0.2 + + '@vscode/vsce@2.26.1': + dependencies: + '@azure/identity': 4.2.0 + azure-devops-node-api: 12.5.0 + chalk: 2.4.2 + cheerio: 1.0.0-rc.12 + cockatiel: 3.1.3 + commander: 6.2.1 + form-data: 4.0.0 + glob: 7.2.3 + hosted-git-info: 4.1.0 + jsonc-parser: 3.2.1 + leven: 3.1.0 + markdown-it: 12.3.2 + mime: 1.6.0 + minimatch: 3.1.2 + parse-semver: 1.1.1 + read: 1.0.7 + semver: 7.6.2 + tmp: 0.2.3 + typed-rest-client: 1.8.11 + url-join: 4.0.1 + xml2js: 0.5.0 + yauzl: 2.10.0 + yazl: 2.5.1 + optionalDependencies: + keytar: 7.9.0 + transitivePeerDependencies: + - supports-color + + '@vscode/vsce@3.1.1': + dependencies: + '@azure/identity': 4.2.0 + '@vscode/vsce-sign': 2.0.4 + azure-devops-node-api: 12.5.0 + chalk: 2.4.2 + cheerio: 1.0.0-rc.12 + cockatiel: 3.1.3 + commander: 6.2.1 + form-data: 4.0.0 + glob: 11.0.0 + hosted-git-info: 4.1.0 + jsonc-parser: 3.2.1 + leven: 3.1.0 + markdown-it: 14.1.0 + mime: 1.6.0 + minimatch: 3.1.2 + parse-semver: 1.1.1 + read: 1.0.7 + semver: 7.6.2 + tmp: 0.2.3 + typed-rest-client: 1.8.11 + url-join: 4.0.1 + xml2js: 0.5.0 + yauzl: 2.10.0 + yazl: 2.5.1 + optionalDependencies: + keytar: 7.9.0 + transitivePeerDependencies: + - supports-color + + '@vue/compiler-core@3.4.27': + dependencies: + '@babel/parser': 7.27.0 + '@vue/shared': 3.4.27 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.4.27': + dependencies: + '@vue/compiler-core': 3.4.27 + '@vue/shared': 3.4.27 + + '@vue/compiler-sfc@3.4.27': + dependencies: + '@babel/parser': 7.27.0 + '@vue/compiler-core': 3.4.27 + '@vue/compiler-dom': 3.4.27 + '@vue/compiler-ssr': 3.4.27 + '@vue/shared': 3.4.27 + estree-walker: 2.0.2 + magic-string: 0.30.17 + postcss: 8.5.3 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.4.27': + dependencies: + '@vue/compiler-dom': 3.4.27 + '@vue/shared': 3.4.27 + + '@vue/shared@3.4.27': {} + + '@webassemblyjs/ast@1.12.1': + dependencies: + '@webassemblyjs/helper-numbers': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + + '@webassemblyjs/floating-point-hex-parser@1.11.6': {} + + '@webassemblyjs/helper-api-error@1.11.6': {} + + '@webassemblyjs/helper-buffer@1.12.1': {} + + '@webassemblyjs/helper-numbers@1.11.6': + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.11.6 + '@webassemblyjs/helper-api-error': 1.11.6 + '@xtuc/long': 4.2.2 + + '@webassemblyjs/helper-wasm-bytecode@1.11.6': {} + + '@webassemblyjs/helper-wasm-section@1.12.1': + dependencies: + '@webassemblyjs/ast': 1.12.1 + '@webassemblyjs/helper-buffer': 1.12.1 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/wasm-gen': 1.12.1 + + '@webassemblyjs/ieee754@1.11.6': + dependencies: + '@xtuc/ieee754': 1.2.0 + + '@webassemblyjs/leb128@1.11.6': + dependencies: + '@xtuc/long': 4.2.2 + + '@webassemblyjs/utf8@1.11.6': {} + + '@webassemblyjs/wasm-edit@1.12.1': + dependencies: + '@webassemblyjs/ast': 1.12.1 + '@webassemblyjs/helper-buffer': 1.12.1 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/helper-wasm-section': 1.12.1 + '@webassemblyjs/wasm-gen': 1.12.1 + '@webassemblyjs/wasm-opt': 1.12.1 + '@webassemblyjs/wasm-parser': 1.12.1 + '@webassemblyjs/wast-printer': 1.12.1 + + '@webassemblyjs/wasm-gen@1.12.1': + dependencies: + '@webassemblyjs/ast': 1.12.1 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/ieee754': 1.11.6 + '@webassemblyjs/leb128': 1.11.6 + '@webassemblyjs/utf8': 1.11.6 + + '@webassemblyjs/wasm-opt@1.12.1': + dependencies: + '@webassemblyjs/ast': 1.12.1 + '@webassemblyjs/helper-buffer': 1.12.1 + '@webassemblyjs/wasm-gen': 1.12.1 + '@webassemblyjs/wasm-parser': 1.12.1 + + '@webassemblyjs/wasm-parser@1.12.1': + dependencies: + '@webassemblyjs/ast': 1.12.1 + '@webassemblyjs/helper-api-error': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/ieee754': 1.11.6 + '@webassemblyjs/leb128': 1.11.6 + '@webassemblyjs/utf8': 1.11.6 + + '@webassemblyjs/wast-printer@1.12.1': + dependencies: + '@webassemblyjs/ast': 1.12.1 + '@xtuc/long': 4.2.2 + + '@whatwg-node/events@0.0.3': {} + + '@whatwg-node/events@0.1.1': {} + + '@whatwg-node/fetch@0.8.8': + dependencies: + '@peculiar/webcrypto': 1.4.3 + '@whatwg-node/node-fetch': 0.3.6 + busboy: 1.6.0 + urlpattern-polyfill: 8.0.2 + web-streams-polyfill: 3.2.1 + + '@whatwg-node/fetch@0.9.14': + dependencies: + '@whatwg-node/node-fetch': 0.5.0 + urlpattern-polyfill: 9.0.0 + + '@whatwg-node/node-fetch@0.3.6': + dependencies: + '@whatwg-node/events': 0.0.3 + busboy: 1.6.0 + fast-querystring: 1.1.2 + fast-url-parser: 1.1.3 + tslib: 2.6.2 + + '@whatwg-node/node-fetch@0.5.0': + dependencies: + '@whatwg-node/events': 0.1.1 + busboy: 1.6.0 + fast-querystring: 1.1.2 + fast-url-parser: 1.1.3 + tslib: 2.6.2 + + '@xtuc/ieee754@1.2.0': {} + + '@xtuc/long@4.2.2': {} + + abbrev@2.0.0: {} + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + acorn-import-assertions@1.9.0(acorn@8.11.3): + dependencies: + acorn: 8.11.3 + + acorn-jsx@5.3.2(acorn@8.11.3): + dependencies: + acorn: 8.11.3 + + acorn-walk@8.3.2: {} + + acorn@8.11.3: {} + + acorn@8.14.1: {} + + agent-base@6.0.2: + dependencies: + debug: 4.3.4(supports-color@9.4.0) + transitivePeerDependencies: + - supports-color + + agent-base@7.1.0: + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + agent-base@7.1.1(supports-color@9.4.0): + dependencies: + debug: 4.3.4(supports-color@9.4.0) + transitivePeerDependencies: + - supports-color + + agentkeepalive@4.5.0: + dependencies: + humanize-ms: 1.2.1 + + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + + ajv-keywords@3.5.2(ajv@6.12.6): + dependencies: + ajv: 6.12.6 + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + anser@2.1.1: {} + + ansi-colors@4.1.1: {} + + ansi-colors@4.1.3: {} + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-escapes@6.2.1: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.0.1: {} + + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + ansi-styles@6.2.1: {} + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + are-docs-informative@0.0.2: {} + + arg@4.1.3: {} + + arg@5.0.2: {} + + argparse@2.0.1: {} + + aria-hidden@1.2.3: + dependencies: + tslib: 2.6.2 + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: + optional: true + + array-buffer-byte-length@1.0.0: + dependencies: + call-bind: 1.0.5 + is-array-buffer: 3.0.2 + + array-buffer-byte-length@1.0.1: + dependencies: + call-bind: 1.0.7 + is-array-buffer: 3.0.4 + + array-includes@3.1.7: + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.2 + get-intrinsic: 1.2.1 + is-string: 1.0.7 + + array-union@2.1.0: {} + + array.prototype.findlastindex@1.2.3: + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.2 + es-shim-unscopables: 1.0.0 + get-intrinsic: 1.2.1 + + array.prototype.flat@1.3.2: + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.2 + es-shim-unscopables: 1.0.0 + + array.prototype.flatmap@1.3.2: + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.2 + es-shim-unscopables: 1.0.0 + + array.prototype.tosorted@1.1.2: + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.2 + es-shim-unscopables: 1.0.0 + get-intrinsic: 1.2.1 + + arraybuffer.prototype.slice@1.0.2: + dependencies: + array-buffer-byte-length: 1.0.0 + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.2 + get-intrinsic: 1.2.1 + is-array-buffer: 3.0.2 + is-shared-array-buffer: 1.0.2 + + asap@2.0.6: {} + + asn1js@3.0.5: + dependencies: + pvtsutils: 1.3.5 + pvutils: 1.1.3 + tslib: 2.6.2 + + assert@2.1.0: + dependencies: + call-bind: 1.0.7 + is-nan: 1.3.2 + object-is: 1.1.6 + object.assign: 4.1.5 + util: 0.12.5 + + assertion-error@1.1.0: {} + + ast-types-flow@0.0.7: {} + + astral-regex@2.0.0: {} + + asynciterator.prototype@1.0.0: + dependencies: + has-symbols: 1.0.3 + + asynckit@0.4.0: {} + + atomic-sleep@1.0.0: {} + + auto-bind@4.0.0: {} + + autoprefixer@10.4.14(postcss@8.4.38): + dependencies: + browserslist: 4.23.0 + caniuse-lite: 1.0.30001620 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.0.1 + postcss: 8.4.38 + postcss-value-parser: 4.2.0 + + autoprefixer@10.4.16(postcss@8.4.31): + dependencies: + browserslist: 4.22.1 + caniuse-lite: 1.0.30001541 + fraction.js: 4.3.6 + normalize-range: 0.1.2 + picocolors: 1.0.0 + postcss: 8.4.31 + postcss-value-parser: 4.2.0 + + autoprefixer@10.4.21(postcss@8.5.3): + dependencies: + browserslist: 4.24.4 + caniuse-lite: 1.0.30001706 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.1.1 + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + available-typed-arrays@1.0.5: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.0.0 + + axe-core@4.8.2: {} + + axobject-query@3.2.1: + dependencies: + dequal: 2.0.3 + + axobject-query@4.1.0: + optional: true + + azure-devops-node-api@12.5.0: + dependencies: + tunnel: 0.0.6 + typed-rest-client: 1.8.11 + + b4a@1.6.7: {} + + babel-plugin-syntax-trailing-function-commas@7.0.0-beta.0: {} + + babel-preset-fbjs@3.4.0(@babel/core@7.23.5): + dependencies: + '@babel/core': 7.23.5 + '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.23.5) + '@babel/plugin-proposal-object-rest-spread': 7.20.7(@babel/core@7.23.5) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.23.5) + '@babel/plugin-syntax-flow': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-syntax-jsx': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.23.5) + '@babel/plugin-transform-arrow-functions': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-block-scoped-functions': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-block-scoping': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-classes': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-computed-properties': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-destructuring': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-flow-strip-types': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-for-of': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-function-name': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-literals': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-member-expression-literals': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-object-super': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-parameters': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-property-literals': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-react-display-name': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-react-jsx': 7.22.15(@babel/core@7.23.5) + '@babel/plugin-transform-shorthand-properties': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-spread': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-template-literals': 7.23.3(@babel/core@7.23.5) + babel-plugin-syntax-trailing-function-commas: 7.0.0-beta.0 + + babel-walk@3.0.0: + dependencies: + '@babel/types': 7.24.5 + + bail@2.0.2: {} + + balanced-match@1.0.2: {} + + bare-events@2.5.0: + optional: true + + bare-fs@2.3.5: + dependencies: + bare-events: 2.5.0 + bare-path: 2.1.3 + bare-stream: 2.3.2 + optional: true + + bare-os@2.4.4: + optional: true + + bare-path@2.1.3: + dependencies: + bare-os: 2.4.4 + optional: true + + bare-stream@2.3.2: + dependencies: + streamx: 2.20.1 + optional: true + + base16@1.0.0: {} + + base64-js@0.0.8: {} + + base64-js@1.5.1: {} + + base64id@2.0.0: {} + + basic-auth@2.0.1: + dependencies: + safe-buffer: 5.1.2 + + binary-extensions@2.3.0: {} + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + boolbase@1.0.0: {} + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browser-stdout@1.3.1: {} + + browserify-zlib@0.1.4: + dependencies: + pako: 0.2.9 + + browserslist@4.22.1: + dependencies: + caniuse-lite: 1.0.30001541 + electron-to-chromium: 1.4.537 + node-releases: 2.0.13 + update-browserslist-db: 1.0.13(browserslist@4.22.1) + + browserslist@4.23.0: + dependencies: + caniuse-lite: 1.0.30001620 + electron-to-chromium: 1.4.777 + node-releases: 2.0.14 + update-browserslist-db: 1.0.13(browserslist@4.23.0) + + browserslist@4.24.4: + dependencies: + caniuse-lite: 1.0.30001706 + electron-to-chromium: 1.5.123 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.24.4) + + bser@2.1.1: + dependencies: + node-int64: 0.4.0 + + buffer-crc32@0.2.13: {} + + buffer-equal-constant-time@1.0.1: {} + + buffer-from@1.1.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + builtin-modules@3.3.0: {} + + bumpp@9.4.1: + dependencies: + '@jsdevtools/ez-spawn': 3.0.4 + c12: 1.10.0 + cac: 6.7.14 + escalade: 3.1.2 + fast-glob: 3.3.2 + js-yaml: 4.1.0 + prompts: 2.4.2 + semver: 7.6.2 + + bundle-require@4.1.0(esbuild@0.19.12): + dependencies: + esbuild: 0.19.12 + load-tsconfig: 0.2.5 + + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + + c12@1.10.0: + dependencies: + chokidar: 3.6.0 + confbox: 0.1.7 + defu: 6.1.4 + dotenv: 16.4.5 + giget: 1.2.3 + jiti: 1.21.0 + mlly: 1.7.0 + ohash: 1.1.3 + pathe: 1.1.2 + perfect-debounce: 1.0.0 + pkg-types: 1.1.1 + rc9: 2.1.2 + + cac@6.7.14: {} + + cache-content-type@1.0.1: + dependencies: + mime-types: 2.1.35 + ylru: 1.4.0 + + call-bind@1.0.5: + dependencies: + function-bind: 1.1.2 + get-intrinsic: 1.2.1 + set-function-length: 1.1.1 + + call-bind@1.0.7: + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + set-function-length: 1.2.2 + + call-me-maybe@1.0.2: {} + + callsites@3.1.0: {} + + camel-case@4.1.2: + dependencies: + pascal-case: 3.1.2 + tslib: 2.6.2 + + camelcase-css@2.0.1: {} + + camelcase@5.3.1: {} + + camelcase@6.3.0: {} + + camelize@1.0.1: {} + + caniuse-api@3.0.0: + dependencies: + browserslist: 4.24.4 + caniuse-lite: 1.0.30001706 + lodash.memoize: 4.1.2 + lodash.uniq: 4.5.0 + + caniuse-lite@1.0.30001541: {} + + caniuse-lite@1.0.30001620: {} + + caniuse-lite@1.0.30001706: {} + + capital-case@1.0.4: + dependencies: + no-case: 3.0.4 + tslib: 2.6.2 + upper-case-first: 2.0.2 + + ccount@2.0.1: {} + + chai@4.4.1: + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.3 + deep-eql: 4.1.3 + get-func-name: 2.0.2 + loupe: 2.3.7 + pathval: 1.1.1 + type-detect: 4.0.8 + + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + chalk@3.0.0: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.3.0: {} + + change-case-all@1.0.15: + dependencies: + change-case: 4.1.2 + is-lower-case: 2.0.2 + is-upper-case: 2.0.2 + lower-case: 2.0.2 + lower-case-first: 2.0.2 + sponge-case: 1.0.1 + swap-case: 2.0.2 + title-case: 3.0.3 + upper-case: 2.0.2 + upper-case-first: 2.0.2 + + change-case@4.1.2: + dependencies: + camel-case: 4.1.2 + capital-case: 1.0.4 + constant-case: 3.0.4 + dot-case: 3.0.4 + header-case: 2.0.4 + no-case: 3.0.4 + param-case: 3.0.4 + pascal-case: 3.1.2 + path-case: 3.0.4 + sentence-case: 3.0.4 + snake-case: 3.0.4 + tslib: 2.6.2 + + character-entities-legacy@1.1.4: {} + + character-entities@1.2.4: {} + + character-entities@2.0.2: {} + + character-reference-invalid@1.1.4: {} + + chardet@0.7.0: {} + + check-error@1.0.3: + dependencies: + get-func-name: 2.0.2 + + cheerio-select@2.1.0: + dependencies: + boolbase: 1.0.0 + css-select: 5.1.0 + css-what: 6.1.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + + cheerio@1.0.0-rc.12: + dependencies: + cheerio-select: 2.1.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.1.0 + htmlparser2: 8.0.2 + parse5: 7.1.2 + parse5-htmlparser2-tree-adapter: 7.0.0 + + chokidar@3.5.3: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chownr@1.1.4: + optional: true + + chownr@2.0.0: {} + + chroma-js@2.4.2: {} + + chrome-trace-event@1.0.3: {} + + ci-info@2.0.0: {} + + ci-info@4.0.0: {} + + citty@0.1.6: + dependencies: + consola: 3.2.3 + + class-variance-authority@0.4.0(typescript@5.8.2): + optionalDependencies: + typescript: 5.8.2 + + clean-regexp@1.0.0: + dependencies: + escape-string-regexp: 1.0.5 + + clean-stack@2.2.0: {} + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-cursor@4.0.0: + dependencies: + restore-cursor: 4.0.0 + + cli-spinners@2.9.1: {} + + cli-truncate@2.1.0: + dependencies: + slice-ansi: 3.0.0 + string-width: 4.2.3 + + cli-truncate@4.0.0: + dependencies: + slice-ansi: 5.0.0 + string-width: 7.1.0 + + cli-width@3.0.0: {} + + client-only@0.0.1: {} + + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone@1.0.4: {} + + clsx@1.2.1: {} + + clsx@2.1.0: {} + + cm6-graphql@0.0.15(@codemirror/autocomplete@6.11.1(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.0))(@codemirror/language@6.10.1)(@codemirror/lint@6.4.2)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/highlight@1.2.0)(graphql@16.8.1): + dependencies: + '@codemirror/autocomplete': 6.11.1(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.0) + '@codemirror/language': 6.10.1 + '@codemirror/lint': 6.4.2 + '@codemirror/state': 6.4.0 + '@codemirror/view': 6.23.0 + '@lezer/highlight': 1.2.0 + graphql: 16.8.1 + graphql-language-service: 5.2.1(graphql@16.8.1) + + cmdk@1.0.0(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + + co@4.6.0: {} + + cockatiel@3.1.3: {} + + code-red@1.0.4: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + '@types/estree': 1.0.7 + acorn: 8.14.1 + estree-walker: 3.0.3 + periscopic: 3.1.0 + optional: true + + codemirror-lang-mermaid@0.5.0: + dependencies: + '@codemirror/language': 6.10.1 + '@lezer/highlight': 1.2.0 + '@lezer/lr': 1.3.14 + + codiff@0.1.2: {} + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.3: {} + + color-name@1.1.4: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + + colord@2.9.3: {} + + colorette@1.4.0: {} + + colorette@2.0.20: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + comma-separated-tokens@1.0.8: {} + + comma-separated-tokens@2.0.3: {} + + commander@10.0.1: {} + + commander@11.1.0: {} + + commander@12.1.0: {} + + commander@2.20.3: {} + + commander@4.1.1: {} + + commander@6.2.1: {} + + commander@7.2.0: {} + + commander@8.3.0: {} + + comment-parser@1.4.1: {} + + common-tags@1.8.2: {} + + commondir@1.0.1: {} + + compare-versions@6.1.0: {} + + compute-scroll-into-view@3.1.0: {} + + concat-map@0.0.1: {} + + confbox@0.1.7: {} + + confbox@0.1.8: {} + + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + + consola@3.2.3: {} + + consola@3.4.2: {} + + constant-case@3.0.4: + dependencies: + no-case: 3.0.4 + tslib: 2.6.2 + upper-case: 2.0.2 + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + convert-source-map@2.0.0: {} + + cookie@0.4.2: {} + + cookies@0.9.1: + dependencies: + depd: 2.0.0 + keygrip: 1.1.0 + + copy-to-clipboard@3.3.3: + dependencies: + toggle-selection: 1.0.6 + + core-js-compat@3.37.1: + dependencies: + browserslist: 4.23.0 + + core-util-is@1.0.3: {} + + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cosmiconfig@8.3.6(typescript@5.8.2): + dependencies: + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + path-type: 4.0.0 + optionalDependencies: + typescript: 5.8.2 + + create-require@1.1.1: {} + + crelt@1.0.6: {} + + cross-fetch@3.1.8(encoding@0.1.13): + dependencies: + node-fetch: 2.7.0(encoding@0.1.13) + transitivePeerDependencies: + - encoding + + cross-inspect@1.0.0: + dependencies: + tslib: 2.6.2 + + cross-spawn@6.0.5: + dependencies: + nice-try: 1.0.5 + path-key: 2.0.1 + semver: 5.7.2 + shebang-command: 1.2.0 + which: 1.3.1 + + cross-spawn@7.0.3: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + crypto-random-string@5.0.0: + dependencies: + type-fest: 2.19.0 + + css-background-parser@0.1.0: {} + + css-box-shadow@1.0.0-3: {} + + css-color-keywords@1.0.0: {} + + css-declaration-sorter@7.2.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + + css-select@5.1.0: + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 5.0.3 + domutils: 3.1.0 + nth-check: 2.1.1 + + css-to-react-native@3.2.0: + dependencies: + camelize: 1.0.1 + css-color-keywords: 1.0.0 + postcss-value-parser: 4.2.0 + + css-tree@2.2.1: + dependencies: + mdn-data: 2.0.28 + source-map-js: 1.2.1 + + css-tree@2.3.1: + dependencies: + mdn-data: 2.0.30 + source-map-js: 1.2.1 + + css-what@6.1.0: {} + + cssesc@3.0.0: {} + + cssnano-preset-default@7.0.6(postcss@8.5.3): + dependencies: + browserslist: 4.24.4 + css-declaration-sorter: 7.2.0(postcss@8.5.3) + cssnano-utils: 5.0.0(postcss@8.5.3) + postcss: 8.5.3 + postcss-calc: 10.1.1(postcss@8.5.3) + postcss-colormin: 7.0.2(postcss@8.5.3) + postcss-convert-values: 7.0.4(postcss@8.5.3) + postcss-discard-comments: 7.0.3(postcss@8.5.3) + postcss-discard-duplicates: 7.0.1(postcss@8.5.3) + postcss-discard-empty: 7.0.0(postcss@8.5.3) + postcss-discard-overridden: 7.0.0(postcss@8.5.3) + postcss-merge-longhand: 7.0.4(postcss@8.5.3) + postcss-merge-rules: 7.0.4(postcss@8.5.3) + postcss-minify-font-values: 7.0.0(postcss@8.5.3) + postcss-minify-gradients: 7.0.0(postcss@8.5.3) + postcss-minify-params: 7.0.2(postcss@8.5.3) + postcss-minify-selectors: 7.0.4(postcss@8.5.3) + postcss-normalize-charset: 7.0.0(postcss@8.5.3) + postcss-normalize-display-values: 7.0.0(postcss@8.5.3) + postcss-normalize-positions: 7.0.0(postcss@8.5.3) + postcss-normalize-repeat-style: 7.0.0(postcss@8.5.3) + postcss-normalize-string: 7.0.0(postcss@8.5.3) + postcss-normalize-timing-functions: 7.0.0(postcss@8.5.3) + postcss-normalize-unicode: 7.0.2(postcss@8.5.3) + postcss-normalize-url: 7.0.0(postcss@8.5.3) + postcss-normalize-whitespace: 7.0.0(postcss@8.5.3) + postcss-ordered-values: 7.0.1(postcss@8.5.3) + postcss-reduce-initial: 7.0.2(postcss@8.5.3) + postcss-reduce-transforms: 7.0.0(postcss@8.5.3) + postcss-svgo: 7.0.1(postcss@8.5.3) + postcss-unique-selectors: 7.0.3(postcss@8.5.3) + + cssnano-utils@5.0.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + + cssnano@7.0.6(postcss@8.5.3): + dependencies: + cssnano-preset-default: 7.0.6(postcss@8.5.3) + lilconfig: 3.1.3 + postcss: 8.5.3 + + csso@5.0.5: + dependencies: + css-tree: 2.2.1 + + csstype@3.1.2: {} + + csstype@3.1.3: {} + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-ease@3.0.1: {} + + d3-format@3.1.0: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + damerau-levenshtein@1.0.8: {} + + dataloader@2.2.2: {} + + date-fns@3.6.0: {} + + debounce@1.2.1: {} + + debounce@2.0.0: {} + + debounce@2.2.0: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.3.4: + dependencies: + ms: 2.1.2 + + debug@4.3.4(supports-color@8.1.1): + dependencies: + ms: 2.1.2 + optionalDependencies: + supports-color: 8.1.1 + + debug@4.3.4(supports-color@9.4.0): + dependencies: + ms: 2.1.2 + optionalDependencies: + supports-color: 9.4.0 + + decamelize@1.2.0: {} + + decamelize@4.0.0: {} + + decimal.js-light@2.5.1: {} + + decode-named-character-reference@1.0.2: + dependencies: + character-entities: 2.0.2 + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + optional: true + + dedent@0.7.0: {} + + deep-eql@4.1.3: + dependencies: + type-detect: 4.0.8 + + deep-equal@1.0.1: {} + + deep-equal@2.2.3: + dependencies: + array-buffer-byte-length: 1.0.1 + call-bind: 1.0.7 + es-get-iterator: 1.1.3 + get-intrinsic: 1.2.4 + is-arguments: 1.1.1 + is-array-buffer: 3.0.4 + is-date-object: 1.0.5 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.3 + isarray: 2.0.5 + object-is: 1.1.6 + object-keys: 1.1.1 + object.assign: 4.1.5 + regexp.prototype.flags: 1.5.2 + side-channel: 1.0.6 + which-boxed-primitive: 1.0.2 + which-collection: 1.0.2 + which-typed-array: 1.1.15 + + deep-extend@0.6.0: + optional: true + + deep-is@0.1.4: {} + + deepmerge-ts@5.1.0: {} + + deepmerge@4.3.1: {} + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + define-data-property@1.1.1: + dependencies: + get-intrinsic: 1.2.1 + gopd: 1.0.1 + has-property-descriptors: 1.0.0 + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + gopd: 1.0.1 + + define-lazy-prop@2.0.0: {} + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + defu@6.1.4: {} + + delayed-stream@1.0.0: {} + + delegates@1.0.0: {} + + depd@1.1.2: {} + + depd@2.0.0: {} + + dependency-graph@0.11.0: {} + + dequal@2.0.3: {} + + destr@2.0.3: {} + + destroy@1.2.0: {} + + detect-indent@6.1.0: {} + + detect-libc@1.0.3: {} + + detect-libc@2.0.3: + optional: true + + detect-node-es@1.1.0: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + didyoumean@1.2.2: {} + + diff-sequences@29.6.3: {} + + diff@4.0.2: {} + + diff@5.0.0: {} + + diff@5.2.0: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + dlv@1.1.3: {} + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.24.4 + csstype: 3.1.2 + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + dompurify@3.1.5: {} + + domutils@3.1.0: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dot-case@3.0.4: + dependencies: + no-case: 3.0.4 + tslib: 2.6.2 + + dot-prop@8.0.2: + dependencies: + type-fest: 3.13.1 + + dotenv@16.0.3: {} + + dotenv@16.3.1: {} + + dotenv@16.4.5: {} + + downshift@8.2.2(react@18.2.0): + dependencies: + '@babel/runtime': 7.24.4 + compute-scroll-into-view: 3.1.0 + prop-types: 15.8.1 + react: 18.2.0 + react-is: 18.2.0 + tslib: 2.6.2 + + dset@3.1.3: {} + + duplexer@0.1.2: {} + + duplexify@3.7.1: + dependencies: + end-of-stream: 1.4.4 + inherits: 2.0.4 + readable-stream: 2.3.8 + stream-shift: 1.0.3 + + eastasianwidth@0.2.0: {} + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + editorconfig@1.0.4: + dependencies: + '@one-ini/wasm': 0.1.1 + commander: 10.0.1 + minimatch: 9.0.1 + semver: 7.6.2 + + ee-first@1.1.1: {} + + electron-to-chromium@1.4.537: {} + + electron-to-chromium@1.4.777: {} + + electron-to-chromium@1.5.123: {} + + embla-carousel-autoplay@8.5.2(embla-carousel@8.5.2): + dependencies: + embla-carousel: 8.5.2 + + embla-carousel-react@8.5.2(react@18.2.0): + dependencies: + embla-carousel: 8.5.2 + embla-carousel-reactive-utils: 8.5.2(embla-carousel@8.5.2) + react: 18.2.0 + + embla-carousel-reactive-utils@8.5.2(embla-carousel@8.5.2): + dependencies: + embla-carousel: 8.5.2 + + embla-carousel@8.5.2: {} + + emoji-regex@10.3.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + emphasize@4.2.0: + dependencies: + chalk: 4.1.2 + highlight.js: 10.4.1 + lowlight: 1.17.0 + + encodeurl@1.0.2: {} + + encoding@0.1.13: + dependencies: + iconv-lite: 0.6.3 + + end-of-stream@1.4.4: + dependencies: + once: 1.4.0 + + engine.io-client@6.5.3: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.4(supports-color@9.4.0) + engine.io-parser: 5.2.2 + ws: 8.11.0 + xmlhttprequest-ssl: 2.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + engine.io-parser@5.2.2: {} + + engine.io@6.5.4: + dependencies: + '@types/cookie': 0.4.1 + '@types/cors': 2.8.17 + '@types/node': 20.12.12 + accepts: 1.3.8 + base64id: 2.0.0 + cookie: 0.4.2 + cors: 2.8.5 + debug: 4.3.4(supports-color@9.4.0) + engine.io-parser: 5.2.2 + ws: 8.11.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + enhanced-resolve@5.15.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + + enhanced-resolve@5.16.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + + entities@2.1.0: {} + + entities@4.5.0: {} + + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + + es-abstract@1.22.2: + dependencies: + array-buffer-byte-length: 1.0.0 + arraybuffer.prototype.slice: 1.0.2 + available-typed-arrays: 1.0.5 + call-bind: 1.0.5 + es-set-tostringtag: 2.0.1 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.1 + get-symbol-description: 1.0.0 + globalthis: 1.0.3 + gopd: 1.0.1 + has: 1.0.3 + has-property-descriptors: 1.0.0 + has-proto: 1.0.1 + has-symbols: 1.0.3 + internal-slot: 1.0.5 + is-array-buffer: 3.0.2 + is-callable: 1.2.7 + is-negative-zero: 2.0.2 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + is-string: 1.0.7 + is-typed-array: 1.1.12 + is-weakref: 1.0.2 + object-inspect: 1.12.3 + object-keys: 1.1.1 + object.assign: 4.1.4 + regexp.prototype.flags: 1.5.1 + safe-array-concat: 1.0.1 + safe-regex-test: 1.0.0 + string.prototype.trim: 1.2.8 + string.prototype.trimend: 1.0.7 + string.prototype.trimstart: 1.0.7 + typed-array-buffer: 1.0.0 + typed-array-byte-length: 1.0.0 + typed-array-byte-offset: 1.0.0 + typed-array-length: 1.0.4 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.11 + + es-define-property@1.0.0: + dependencies: + get-intrinsic: 1.2.4 + + es-errors@1.3.0: {} + + es-get-iterator@1.1.3: + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + has-symbols: 1.0.3 + is-arguments: 1.1.1 + is-map: 2.0.3 + is-set: 2.0.3 + is-string: 1.0.7 + isarray: 2.0.5 + stop-iteration-iterator: 1.0.0 + + es-iterator-helpers@1.0.15: + dependencies: + asynciterator.prototype: 1.0.0 + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.2 + es-set-tostringtag: 2.0.1 + function-bind: 1.1.2 + get-intrinsic: 1.2.1 + globalthis: 1.0.3 + has-property-descriptors: 1.0.0 + has-proto: 1.0.1 + has-symbols: 1.0.3 + internal-slot: 1.0.5 + iterator.prototype: 1.1.2 + safe-array-concat: 1.0.1 + + es-module-lexer@1.5.3: {} + + es-set-tostringtag@2.0.1: + dependencies: + get-intrinsic: 1.2.1 + has: 1.0.3 + has-tostringtag: 1.0.0 + + es-shim-unscopables@1.0.0: + dependencies: + has: 1.0.3 + + es-to-primitive@1.2.1: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + + esbuild-plugin-copy@2.1.1(esbuild@0.24.2): + dependencies: + chalk: 4.1.2 + chokidar: 3.6.0 + esbuild: 0.24.2 + fs-extra: 10.1.0 + globby: 11.1.0 + + esbuild-plugin-polyfill-node@0.3.0(esbuild@0.19.12): + dependencies: + '@jspm/core': 2.0.1 + esbuild: 0.19.12 + import-meta-resolve: 3.1.1 + + esbuild-plugin-polyfill-node@0.3.0(esbuild@0.24.2): + dependencies: + '@jspm/core': 2.0.1 + esbuild: 0.24.2 + import-meta-resolve: 3.1.1 + + esbuild@0.19.11: + optionalDependencies: + '@esbuild/aix-ppc64': 0.19.11 + '@esbuild/android-arm': 0.19.11 + '@esbuild/android-arm64': 0.19.11 + '@esbuild/android-x64': 0.19.11 + '@esbuild/darwin-arm64': 0.19.11 + '@esbuild/darwin-x64': 0.19.11 + '@esbuild/freebsd-arm64': 0.19.11 + '@esbuild/freebsd-x64': 0.19.11 + '@esbuild/linux-arm': 0.19.11 + '@esbuild/linux-arm64': 0.19.11 + '@esbuild/linux-ia32': 0.19.11 + '@esbuild/linux-loong64': 0.19.11 + '@esbuild/linux-mips64el': 0.19.11 + '@esbuild/linux-ppc64': 0.19.11 + '@esbuild/linux-riscv64': 0.19.11 + '@esbuild/linux-s390x': 0.19.11 + '@esbuild/linux-x64': 0.19.11 + '@esbuild/netbsd-x64': 0.19.11 + '@esbuild/openbsd-x64': 0.19.11 + '@esbuild/sunos-x64': 0.19.11 + '@esbuild/win32-arm64': 0.19.11 + '@esbuild/win32-ia32': 0.19.11 + '@esbuild/win32-x64': 0.19.11 + + esbuild@0.19.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.19.12 + '@esbuild/android-arm': 0.19.12 + '@esbuild/android-arm64': 0.19.12 + '@esbuild/android-x64': 0.19.12 + '@esbuild/darwin-arm64': 0.19.12 + '@esbuild/darwin-x64': 0.19.12 + '@esbuild/freebsd-arm64': 0.19.12 + '@esbuild/freebsd-x64': 0.19.12 + '@esbuild/linux-arm': 0.19.12 + '@esbuild/linux-arm64': 0.19.12 + '@esbuild/linux-ia32': 0.19.12 + '@esbuild/linux-loong64': 0.19.12 + '@esbuild/linux-mips64el': 0.19.12 + '@esbuild/linux-ppc64': 0.19.12 + '@esbuild/linux-riscv64': 0.19.12 + '@esbuild/linux-s390x': 0.19.12 + '@esbuild/linux-x64': 0.19.12 + '@esbuild/netbsd-x64': 0.19.12 + '@esbuild/openbsd-x64': 0.19.12 + '@esbuild/sunos-x64': 0.19.12 + '@esbuild/win32-arm64': 0.19.12 + '@esbuild/win32-ia32': 0.19.12 + '@esbuild/win32-x64': 0.19.12 + + esbuild@0.20.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.20.2 + '@esbuild/android-arm': 0.20.2 + '@esbuild/android-arm64': 0.20.2 + '@esbuild/android-x64': 0.20.2 + '@esbuild/darwin-arm64': 0.20.2 + '@esbuild/darwin-x64': 0.20.2 + '@esbuild/freebsd-arm64': 0.20.2 + '@esbuild/freebsd-x64': 0.20.2 + '@esbuild/linux-arm': 0.20.2 + '@esbuild/linux-arm64': 0.20.2 + '@esbuild/linux-ia32': 0.20.2 + '@esbuild/linux-loong64': 0.20.2 + '@esbuild/linux-mips64el': 0.20.2 + '@esbuild/linux-ppc64': 0.20.2 + '@esbuild/linux-riscv64': 0.20.2 + '@esbuild/linux-s390x': 0.20.2 + '@esbuild/linux-x64': 0.20.2 + '@esbuild/netbsd-x64': 0.20.2 + '@esbuild/openbsd-x64': 0.20.2 + '@esbuild/sunos-x64': 0.20.2 + '@esbuild/win32-arm64': 0.20.2 + '@esbuild/win32-ia32': 0.20.2 + '@esbuild/win32-x64': 0.20.2 + + esbuild@0.24.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.24.2 + '@esbuild/android-arm': 0.24.2 + '@esbuild/android-arm64': 0.24.2 + '@esbuild/android-x64': 0.24.2 + '@esbuild/darwin-arm64': 0.24.2 + '@esbuild/darwin-x64': 0.24.2 + '@esbuild/freebsd-arm64': 0.24.2 + '@esbuild/freebsd-x64': 0.24.2 + '@esbuild/linux-arm': 0.24.2 + '@esbuild/linux-arm64': 0.24.2 + '@esbuild/linux-ia32': 0.24.2 + '@esbuild/linux-loong64': 0.24.2 + '@esbuild/linux-mips64el': 0.24.2 + '@esbuild/linux-ppc64': 0.24.2 + '@esbuild/linux-riscv64': 0.24.2 + '@esbuild/linux-s390x': 0.24.2 + '@esbuild/linux-x64': 0.24.2 + '@esbuild/netbsd-arm64': 0.24.2 + '@esbuild/netbsd-x64': 0.24.2 + '@esbuild/openbsd-arm64': 0.24.2 + '@esbuild/openbsd-x64': 0.24.2 + '@esbuild/sunos-x64': 0.24.2 + '@esbuild/win32-arm64': 0.24.2 + '@esbuild/win32-ia32': 0.24.2 + '@esbuild/win32-x64': 0.24.2 + + escalade@3.1.2: {} + + escalade@3.2.0: {} + + escape-carriage@1.3.1: {} + + escape-html@1.0.3: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@4.0.0: {} + + escape-string-regexp@5.0.0: {} + + eslint-compat-utils@0.5.0(eslint@9.3.0): + dependencies: + eslint: 9.3.0 + semver: 7.6.2 + + eslint-config-flat-gitignore@0.1.5: + dependencies: + find-up: 7.0.0 + parse-gitignore: 2.0.0 + + eslint-config-next@13.4.7-canary.1(eslint@8.50.0)(typescript@5.8.2): + dependencies: + '@next/eslint-plugin-next': 13.4.7-canary.1 + '@rushstack/eslint-patch': 1.5.0 + '@typescript-eslint/parser': 5.62.0(eslint@8.50.0)(typescript@5.8.2) + eslint: 8.50.0 + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@5.62.0(eslint@8.50.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.28.1)(eslint@8.50.0) + eslint-plugin-import: 2.28.1(@typescript-eslint/parser@5.62.0(eslint@8.50.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0) + eslint-plugin-jsx-a11y: 6.7.1(eslint@8.50.0) + eslint-plugin-react: 7.33.2(eslint@8.50.0) + eslint-plugin-react-hooks: 4.6.0(eslint@8.50.0) + optionalDependencies: + typescript: 5.8.2 + transitivePeerDependencies: + - eslint-import-resolver-webpack + - supports-color + + eslint-config-prettier@8.10.0(eslint@8.50.0): + dependencies: + eslint: 8.50.0 + + eslint-config-prettier@9.0.0(eslint@9.3.0): + dependencies: + eslint: 9.3.0 + + eslint-config-prettier@9.1.0(eslint@8.57.0): + dependencies: + eslint: 8.57.0 + + eslint-config-turbo@1.10.12(eslint@9.3.0): + dependencies: + eslint: 9.3.0 + eslint-plugin-turbo: 1.10.12(eslint@9.3.0) + + eslint-flat-config-utils@0.2.5: + dependencies: + '@types/eslint': 8.56.10 + pathe: 1.1.2 + + eslint-formatter-mo@1.2.0: + dependencies: + chalk: 3.0.0 + emphasize: 4.2.0 + gradient-string: 1.2.0 + plur: 4.0.0 + string-width: 4.2.3 + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.13.0 + resolve: 1.22.8 + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@5.62.0(eslint@8.50.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.28.1)(eslint@8.50.0): + dependencies: + debug: 4.3.4 + enhanced-resolve: 5.15.0 + eslint: 8.50.0 + eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0(eslint@8.50.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@5.62.0(eslint@8.50.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.28.1)(eslint@8.50.0))(eslint@8.50.0) + eslint-plugin-import: 2.28.1(@typescript-eslint/parser@5.62.0(eslint@8.50.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0) + fast-glob: 3.3.1 + get-tsconfig: 4.7.2 + is-core-module: 2.13.0 + is-glob: 4.0.3 + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint-import-resolver-node + - eslint-import-resolver-webpack + - supports-color + + eslint-merge-processors@0.1.0(eslint@9.3.0): + dependencies: + eslint: 9.3.0 + + eslint-module-utils@2.8.0(@typescript-eslint/parser@5.62.0(eslint@8.50.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@5.62.0(eslint@8.50.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.28.1)(eslint@8.50.0))(eslint@8.50.0): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 5.62.0(eslint@8.50.0)(typescript@5.8.2) + eslint: 8.50.0 + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@5.62.0(eslint@8.50.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.28.1)(eslint@8.50.0) + transitivePeerDependencies: + - supports-color + + eslint-plugin-antfu@2.2.0(eslint@9.3.0): + dependencies: + '@antfu/utils': 0.7.8 + eslint: 9.3.0 + + eslint-plugin-command@0.2.3(eslint@9.3.0): + dependencies: + '@es-joy/jsdoccomment': 0.43.0 + eslint: 9.3.0 + + eslint-plugin-es-x@7.6.0(eslint@9.3.0): + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@9.3.0) + '@eslint-community/regexpp': 4.10.0 + eslint: 9.3.0 + eslint-compat-utils: 0.5.0(eslint@9.3.0) + + eslint-plugin-eslint-comments@3.2.0(eslint@9.3.0): + dependencies: + escape-string-regexp: 1.0.5 + eslint: 9.3.0 + ignore: 5.3.1 + + eslint-plugin-import-x@0.5.0(eslint@9.3.0)(typescript@5.4.5): + dependencies: + '@typescript-eslint/utils': 7.4.0(eslint@9.3.0)(typescript@5.4.5) + debug: 4.3.4(supports-color@9.4.0) + doctrine: 3.0.0 + eslint: 9.3.0 + eslint-import-resolver-node: 0.3.9 + get-tsconfig: 4.7.5 + is-glob: 4.0.3 + minimatch: 9.0.4 + semver: 7.6.2 + transitivePeerDependencies: + - supports-color + - typescript + + eslint-plugin-import@2.28.1(@typescript-eslint/parser@5.62.0(eslint@8.50.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0): + dependencies: + array-includes: 3.1.7 + array.prototype.findlastindex: 1.2.3 + array.prototype.flat: 1.3.2 + array.prototype.flatmap: 1.3.2 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 8.50.0 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0(eslint@8.50.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@5.62.0(eslint@8.50.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.28.1)(eslint@8.50.0))(eslint@8.50.0) + has: 1.0.3 + is-core-module: 2.13.0 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.7 + object.groupby: 1.0.1 + object.values: 1.1.7 + semver: 6.3.1 + tsconfig-paths: 3.14.2 + optionalDependencies: + '@typescript-eslint/parser': 5.62.0(eslint@8.50.0)(typescript@5.8.2) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-jsdoc@48.2.5(eslint@9.3.0): + dependencies: + '@es-joy/jsdoccomment': 0.43.0 + are-docs-informative: 0.0.2 + comment-parser: 1.4.1 + debug: 4.3.4(supports-color@9.4.0) + escape-string-regexp: 4.0.0 + eslint: 9.3.0 + esquery: 1.5.0 + is-builtin-module: 3.2.1 + semver: 7.6.2 + spdx-expression-parse: 4.0.0 + transitivePeerDependencies: + - supports-color + + eslint-plugin-jsonc@2.15.1(eslint@9.3.0): + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@9.3.0) + eslint: 9.3.0 + eslint-compat-utils: 0.5.0(eslint@9.3.0) + espree: 9.6.1 + graphemer: 1.4.0 + jsonc-eslint-parser: 2.4.0 + natural-compare: 1.4.0 + synckit: 0.6.2 + + eslint-plugin-jsx-a11y@6.7.1(eslint@8.50.0): + dependencies: + '@babel/runtime': 7.24.4 + aria-query: 5.3.0 + array-includes: 3.1.7 + array.prototype.flatmap: 1.3.2 + ast-types-flow: 0.0.7 + axe-core: 4.8.2 + axobject-query: 3.2.1 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + eslint: 8.50.0 + has: 1.0.3 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.5 + minimatch: 3.1.2 + object.entries: 1.1.7 + object.fromentries: 2.0.7 + semver: 6.3.1 + + eslint-plugin-markdown@5.0.0(eslint@9.3.0): + dependencies: + eslint: 9.3.0 + mdast-util-from-markdown: 0.8.5 + transitivePeerDependencies: + - supports-color + + eslint-plugin-n@17.7.0(eslint@9.3.0): + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@9.3.0) + enhanced-resolve: 5.15.0 + eslint: 9.3.0 + eslint-plugin-es-x: 7.6.0(eslint@9.3.0) + get-tsconfig: 4.7.2 + globals: 15.3.0 + ignore: 5.3.1 + minimatch: 9.0.4 + semver: 7.6.2 + + eslint-plugin-no-only-tests@3.1.0: {} + + eslint-plugin-perfectionist@2.10.0(eslint@9.3.0)(svelte@4.2.17)(typescript@5.4.5)(vue-eslint-parser@9.4.2(eslint@9.3.0)): + dependencies: + '@typescript-eslint/utils': 7.4.0(eslint@9.3.0)(typescript@5.4.5) + eslint: 9.3.0 + minimatch: 9.0.4 + natural-compare-lite: 1.4.0 + optionalDependencies: + svelte: 4.2.17 + vue-eslint-parser: 9.4.2(eslint@9.3.0) + transitivePeerDependencies: + - supports-color + - typescript + + eslint-plugin-react-hooks@4.6.0(eslint@8.50.0): + dependencies: + eslint: 8.50.0 + + eslint-plugin-react-hooks@4.6.0(eslint@9.3.0): + dependencies: + eslint: 9.3.0 + optional: true + + eslint-plugin-react@7.33.2(eslint@8.50.0): + dependencies: + array-includes: 3.1.7 + array.prototype.flatmap: 1.3.2 + array.prototype.tosorted: 1.1.2 + doctrine: 2.1.0 + es-iterator-helpers: 1.0.15 + eslint: 8.50.0 + estraverse: 5.3.0 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.7 + object.fromentries: 2.0.7 + object.hasown: 1.1.3 + object.values: 1.1.7 + prop-types: 15.8.1 + resolve: 2.0.0-next.4 + semver: 6.3.1 + string.prototype.matchall: 4.0.10 + + eslint-plugin-regexp@2.6.0(eslint@9.3.0): + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@9.3.0) + '@eslint-community/regexpp': 4.10.0 + comment-parser: 1.4.1 + eslint: 9.3.0 + jsdoc-type-pratt-parser: 4.0.0 + refa: 0.12.1 + regexp-ast-analysis: 0.7.1 + scslre: 0.3.0 + + eslint-plugin-tailwindcss@3.13.0(tailwindcss@3.3.3(ts-node@10.9.2(@types/node@17.0.45)(typescript@5.8.2))): + dependencies: + fast-glob: 3.3.1 + postcss: 8.4.31 + tailwindcss: 3.3.3(ts-node@10.9.2(@types/node@17.0.45)(typescript@5.8.2)) + + eslint-plugin-toml@0.11.0(eslint@9.3.0): + dependencies: + debug: 4.3.4(supports-color@9.4.0) + eslint: 9.3.0 + eslint-compat-utils: 0.5.0(eslint@9.3.0) + lodash: 4.17.21 + toml-eslint-parser: 0.9.3 + transitivePeerDependencies: + - supports-color + + eslint-plugin-turbo@1.10.12(eslint@9.3.0): + dependencies: + dotenv: 16.0.3 + eslint: 9.3.0 + + eslint-plugin-unicorn@53.0.0(eslint@9.3.0): + dependencies: + '@babel/helper-validator-identifier': 7.24.5 + '@eslint-community/eslint-utils': 4.4.0(eslint@9.3.0) + '@eslint/eslintrc': 3.1.0 + ci-info: 4.0.0 + clean-regexp: 1.0.0 + core-js-compat: 3.37.1 + eslint: 9.3.0 + esquery: 1.5.0 + indent-string: 4.0.0 + is-builtin-module: 3.2.1 + jsesc: 3.0.2 + pluralize: 8.0.0 + read-pkg-up: 7.0.1 + regexp-tree: 0.1.27 + regjsparser: 0.10.0 + semver: 7.6.2 + strip-indent: 3.0.0 + transitivePeerDependencies: + - supports-color + + eslint-plugin-unused-imports@3.0.0(@typescript-eslint/eslint-plugin@7.4.0(@typescript-eslint/parser@5.62.0(eslint@8.50.0)(typescript@5.8.2))(eslint@8.50.0)(typescript@5.8.2))(eslint@8.50.0): + dependencies: + eslint: 8.50.0 + eslint-rule-composer: 0.3.0 + optionalDependencies: + '@typescript-eslint/eslint-plugin': 7.4.0(@typescript-eslint/parser@5.62.0(eslint@8.50.0)(typescript@5.8.2))(eslint@8.50.0)(typescript@5.8.2) + + eslint-plugin-unused-imports@3.2.0(@typescript-eslint/eslint-plugin@7.10.0(@typescript-eslint/parser@7.10.0(eslint@9.3.0)(typescript@5.4.5))(eslint@9.3.0)(typescript@5.4.5))(eslint@9.3.0): + dependencies: + eslint: 9.3.0 + eslint-rule-composer: 0.3.0 + optionalDependencies: + '@typescript-eslint/eslint-plugin': 7.10.0(@typescript-eslint/parser@7.10.0(eslint@9.3.0)(typescript@5.4.5))(eslint@9.3.0)(typescript@5.4.5) + + eslint-plugin-vitest@0.5.4(@typescript-eslint/eslint-plugin@7.10.0(@typescript-eslint/parser@7.10.0(eslint@9.3.0)(typescript@5.4.5))(eslint@9.3.0)(typescript@5.4.5))(eslint@9.3.0)(typescript@5.4.5)(vitest@1.6.0(@types/node@20.12.12)(terser@5.31.0)): + dependencies: + '@typescript-eslint/utils': 7.10.0(eslint@9.3.0)(typescript@5.4.5) + eslint: 9.3.0 + optionalDependencies: + '@typescript-eslint/eslint-plugin': 7.10.0(@typescript-eslint/parser@7.10.0(eslint@9.3.0)(typescript@5.4.5))(eslint@9.3.0)(typescript@5.4.5) + vitest: 1.6.0(@types/node@20.12.12)(terser@5.31.0) + transitivePeerDependencies: + - supports-color + - typescript + + eslint-plugin-vue@9.26.0(eslint@9.3.0): + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@9.3.0) + eslint: 9.3.0 + globals: 13.24.0 + natural-compare: 1.4.0 + nth-check: 2.1.1 + postcss-selector-parser: 6.0.16 + semver: 7.6.2 + vue-eslint-parser: 9.4.2(eslint@9.3.0) + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - supports-color + + eslint-plugin-yml@1.14.0(eslint@9.3.0): + dependencies: + debug: 4.3.4(supports-color@9.4.0) + eslint: 9.3.0 + eslint-compat-utils: 0.5.0(eslint@9.3.0) + lodash: 4.17.21 + natural-compare: 1.4.0 + yaml-eslint-parser: 1.2.2 + transitivePeerDependencies: + - supports-color + + eslint-processor-vue-blocks@0.1.2(@vue/compiler-sfc@3.4.27)(eslint@9.3.0): + dependencies: + '@vue/compiler-sfc': 3.4.27 + eslint: 9.3.0 + + eslint-rule-composer@0.3.0: {} + + eslint-scope@5.1.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-scope@8.0.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.0.0: {} + + eslint@8.50.0: + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.50.0) + '@eslint-community/regexpp': 4.10.0 + '@eslint/eslintrc': 2.1.2 + '@eslint/js': 8.50.0 + '@humanwhocodes/config-array': 0.11.11 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.5.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.22.0 + graphemer: 1.4.0 + ignore: 5.3.1 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.3 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + eslint@8.57.0: + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@eslint-community/regexpp': 4.10.0 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.0 + '@humanwhocodes/config-array': 0.11.14 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.2.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4(supports-color@9.4.0) + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.5.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.1 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + eslint@9.3.0: + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@9.3.0) + '@eslint-community/regexpp': 4.10.0 + '@eslint/eslintrc': 3.1.0 + '@eslint/js': 9.3.0 + '@humanwhocodes/config-array': 0.13.0 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.3.0 + '@nodelib/fs.walk': 1.2.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4(supports-color@9.4.0) + escape-string-regexp: 4.0.0 + eslint-scope: 8.0.1 + eslint-visitor-keys: 4.0.0 + espree: 10.0.1 + esquery: 1.5.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.1 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + espree@10.0.1: + dependencies: + acorn: 8.11.3 + acorn-jsx: 5.3.2(acorn@8.11.3) + eslint-visitor-keys: 4.0.0 + + espree@9.6.1: + dependencies: + acorn: 8.11.3 + acorn-jsx: 5.3.2(acorn@8.11.3) + eslint-visitor-keys: 3.4.3 + + esquery@1.5.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@4.3.0: {} + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.6 + + esutils@2.0.3: {} + + event-stream@3.3.4: + dependencies: + duplexer: 0.1.2 + from: 0.1.7 + map-stream: 0.1.0 + pause-stream: 0.0.11 + split: 0.3.3 + stream-combiner: 0.0.4 + through: 2.3.8 + + event-target-shim@5.0.1: {} + + eventemitter3@4.0.7: {} + + eventemitter3@5.0.1: {} + + events@3.3.0: {} + + eventsource-parser@1.1.2: {} + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.3 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + execa@8.0.1: + dependencies: + cross-spawn: 7.0.3 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + + expand-template@2.0.3: + optional: true + + expand-tilde@2.0.2: + dependencies: + homedir-polyfill: 1.0.3 + + extend@3.0.2: {} + + external-editor@3.1.0: + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + + extract-files@11.0.0: {} + + fast-decode-uri-component@1.0.1: {} + + fast-deep-equal@2.0.1: {} + + fast-deep-equal@3.1.3: {} + + fast-equals@5.0.1: {} + + fast-fifo@1.3.2: {} + + fast-glob@3.3.1: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + + fast-glob@3.3.2: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.6 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-levenshtein@3.0.0: + dependencies: + fastest-levenshtein: 1.0.16 + + fast-querystring@1.1.2: + dependencies: + fast-decode-uri-component: 1.0.1 + + fast-redact@3.5.0: {} + + fast-url-parser@1.1.3: + dependencies: + punycode: 1.4.1 + + fastest-levenshtein@1.0.16: {} + + fastq@1.17.1: + dependencies: + reusify: 1.0.4 + + fault@1.0.4: + dependencies: + format: 0.2.2 + + fb-watchman@2.0.2: + dependencies: + bser: 2.1.1 + + fbemitter@3.0.0(encoding@0.1.13): + dependencies: + fbjs: 3.0.5(encoding@0.1.13) + transitivePeerDependencies: + - encoding + + fbjs-css-vars@1.0.2: {} + + fbjs@3.0.5(encoding@0.1.13): + dependencies: + cross-fetch: 3.1.8(encoding@0.1.13) + fbjs-css-vars: 1.0.2 + loose-envify: 1.4.0 + object-assign: 4.1.1 + promise: 7.3.1 + setimmediate: 1.0.5 + ua-parser-js: 1.0.37 + transitivePeerDependencies: + - encoding + + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + + fdir@6.4.3(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + + fflate@0.4.8: {} + + fflate@0.7.4: {} + + figures@3.2.0: + dependencies: + escape-string-regexp: 1.0.5 + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + file-stream-rotator@1.0.0: {} + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + find-up@7.0.0: + dependencies: + locate-path: 7.2.0 + path-exists: 5.0.0 + unicorn-magic: 0.1.0 + + flat-cache@3.2.0: + dependencies: + flatted: 3.3.1 + keyv: 4.5.4 + rimraf: 3.0.2 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.1 + keyv: 4.5.4 + + flat@5.0.2: {} + + flatted@3.3.1: {} + + flux@4.0.4(encoding@0.1.13)(react@18.2.0): + dependencies: + fbemitter: 3.0.0(encoding@0.1.13) + fbjs: 3.0.5(encoding@0.1.13) + react: 18.2.0 + transitivePeerDependencies: + - encoding + + focus-trap-react@10.2.2(prop-types@15.8.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + focus-trap: 7.5.3 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + tabbable: 6.2.0 + + focus-trap@7.5.3: + dependencies: + tabbable: 6.2.0 + + follow-redirects@1.15.9: {} + + for-each@0.3.3: + dependencies: + is-callable: 1.2.7 + + foreground-child@3.1.1: + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + + form-data-encoder@1.7.2: {} + + form-data@4.0.0: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + + format@0.2.2: {} + + formdata-node@4.4.1: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 4.0.0-beta.3 + + fraction.js@4.3.6: {} + + fraction.js@4.3.7: {} + + framer-motion@10.17.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + tslib: 2.6.2 + optionalDependencies: + '@emotion/is-prop-valid': 0.8.8 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + framer-motion@11.11.9(@emotion/is-prop-valid@0.8.8)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + tslib: 2.6.2 + optionalDependencies: + '@emotion/is-prop-valid': 0.8.8 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + fresh@0.5.2: {} + + from@0.1.7: {} + + fs-constants@1.0.0: + optional: true + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs-extra@11.2.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + + fs.realpath@1.0.0: {} + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.1.6: + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.2 + functions-have-names: 1.2.3 + + functions-have-names@1.2.3: {} + + fuzzysort@3.0.2: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.2.0: {} + + get-func-name@2.0.2: {} + + get-installed-path@4.0.8: + dependencies: + global-modules: 1.0.0 + + get-intrinsic@1.2.1: + dependencies: + function-bind: 1.1.2 + has: 1.0.3 + has-proto: 1.0.1 + has-symbols: 1.0.3 + + get-intrinsic@1.2.4: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + + get-nonce@1.0.1: {} + + get-stream@6.0.1: {} + + get-stream@8.0.1: {} + + get-symbol-description@1.0.0: + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.1 + + get-tsconfig@4.7.2: + dependencies: + resolve-pkg-maps: 1.0.0 + + get-tsconfig@4.7.5: + dependencies: + resolve-pkg-maps: 1.0.0 + + giget@1.2.3: + dependencies: + citty: 0.1.6 + consola: 3.2.3 + defu: 6.1.4 + node-fetch-native: 1.6.4 + nypm: 0.3.8 + ohash: 1.1.3 + pathe: 1.1.2 + tar: 6.2.1 + + git-up@8.0.0: + dependencies: + is-ssh: 1.4.0 + parse-url: 9.2.0 + + git-url-parse@16.0.0: + dependencies: + git-up: 8.0.0 + + github-from-package@0.0.0: + optional: true + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob-to-regexp@0.4.1: {} + + glob@10.3.15: + dependencies: + foreground-child: 3.1.1 + jackspeak: 2.3.6 + minimatch: 9.0.4 + minipass: 7.1.1 + path-scurry: 1.11.1 + + glob@10.3.4: + dependencies: + foreground-child: 3.1.1 + jackspeak: 2.3.6 + minimatch: 9.0.4 + minipass: 7.1.1 + path-scurry: 1.11.1 + + glob@11.0.0: + dependencies: + foreground-child: 3.1.1 + jackspeak: 4.0.2 + minimatch: 10.0.1 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.0 + + glob@7.1.6: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + glob@7.1.7: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + glob@8.1.0: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.6 + once: 1.4.0 + + global-modules@1.0.0: + dependencies: + global-prefix: 1.0.2 + is-windows: 1.0.2 + resolve-dir: 1.0.1 + + global-prefix@1.0.2: + dependencies: + expand-tilde: 2.0.2 + homedir-polyfill: 1.0.3 + ini: 1.3.8 + is-windows: 1.0.2 + which: 1.3.1 + + globals@11.12.0: {} + + globals@13.22.0: + dependencies: + type-fest: 0.20.2 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globals@14.0.0: {} + + globals@15.3.0: {} + + globalthis@1.0.3: + dependencies: + define-properties: 1.2.1 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.2 + ignore: 5.3.1 + merge2: 1.4.1 + slash: 3.0.0 + + globby@13.2.2: + dependencies: + dir-glob: 3.0.1 + fast-glob: 3.3.2 + ignore: 5.3.1 + merge2: 1.4.1 + slash: 4.0.0 + + gopd@1.0.1: + dependencies: + get-intrinsic: 1.2.4 + + graceful-fs@4.2.11: {} + + gradient-string@1.2.0: + dependencies: + chalk: 2.4.2 + tinygradient: 0.4.3 + + graphemer@1.4.0: {} + + graphql-config@5.0.3(@types/node@17.0.45)(encoding@0.1.13)(graphql@16.8.1)(typescript@5.8.2): + dependencies: + '@graphql-tools/graphql-file-loader': 8.0.0(graphql@16.8.1) + '@graphql-tools/json-file-loader': 8.0.0(graphql@16.8.1) + '@graphql-tools/load': 8.0.0(graphql@16.8.1) + '@graphql-tools/merge': 9.0.0(graphql@16.8.1) + '@graphql-tools/url-loader': 8.0.0(@types/node@17.0.45)(encoding@0.1.13)(graphql@16.8.1) + '@graphql-tools/utils': 10.0.8(graphql@16.8.1) + cosmiconfig: 8.3.6(typescript@5.8.2) + graphql: 16.8.1 + jiti: 1.21.0 + minimatch: 4.2.3 + string-env-interpolation: 1.0.1 + tslib: 2.6.2 + transitivePeerDependencies: + - '@types/node' + - bufferutil + - encoding + - typescript + - utf-8-validate + + graphql-language-service@5.2.1(graphql@16.8.1): + dependencies: + graphql: 16.8.1 + nullthrows: 1.1.1 + vscode-languageserver-types: 3.17.5 + + graphql-request@6.1.0(encoding@0.1.13)(graphql@16.8.1): + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.8.1) + cross-fetch: 3.1.8(encoding@0.1.13) + graphql: 16.8.1 + transitivePeerDependencies: + - encoding + + graphql-tag@2.12.6(graphql@16.8.1): + dependencies: + graphql: 16.8.1 + tslib: 2.6.2 + + graphql-ws@5.16.0(graphql@16.8.1): + dependencies: + graphql: 16.8.1 + + graphql@16.8.1: {} + + gunzip-maybe@1.4.2: + dependencies: + browserify-zlib: 0.1.4 + is-deflate: 1.0.0 + is-gzip: 1.0.0 + peek-stream: 1.1.3 + pumpify: 1.5.1 + through2: 2.0.5 + + has-bigints@1.0.2: {} + + has-flag@3.0.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.0: + dependencies: + get-intrinsic: 1.2.1 + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.0 + + has-proto@1.0.1: {} + + has-proto@1.0.3: {} + + has-symbols@1.0.3: {} + + has-tostringtag@1.0.0: + dependencies: + has-symbols: 1.0.3 + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.0.3 + + has@1.0.3: + dependencies: + function-bind: 1.1.2 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hast-util-from-parse5@7.1.2: + dependencies: + '@types/hast': 2.3.6 + '@types/unist': 2.0.8 + hastscript: 7.2.0 + property-information: 6.3.0 + vfile: 5.3.7 + vfile-location: 4.1.0 + web-namespaces: 2.0.1 + + hast-util-parse-selector@2.2.5: {} + + hast-util-parse-selector@3.1.1: + dependencies: + '@types/hast': 2.3.6 + + hast-util-raw@7.2.3: + dependencies: + '@types/hast': 2.3.6 + '@types/parse5': 6.0.3 + hast-util-from-parse5: 7.1.2 + hast-util-to-parse5: 7.1.0 + html-void-elements: 2.0.1 + parse5: 6.0.1 + unist-util-position: 4.0.4 + unist-util-visit: 4.1.2 + vfile: 5.3.7 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-sanitize@5.0.1: + dependencies: + '@types/hast': 3.0.4 + '@ungap/structured-clone': 1.2.0 + unist-util-position: 5.0.0 + + hast-util-to-parse5@7.1.0: + dependencies: + '@types/hast': 2.3.6 + comma-separated-tokens: 2.0.3 + property-information: 6.3.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-whitespace@2.0.1: {} + + hast@1.0.0: {} + + hastscript@6.0.0: + dependencies: + '@types/hast': 2.3.6 + comma-separated-tokens: 1.0.8 + hast-util-parse-selector: 2.2.5 + property-information: 5.6.0 + space-separated-tokens: 1.1.5 + + hastscript@7.2.0: + dependencies: + '@types/hast': 2.3.6 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 3.1.1 + property-information: 6.3.0 + space-separated-tokens: 2.0.2 + + he@1.2.0: {} + + header-case@2.0.4: + dependencies: + capital-case: 1.0.4 + tslib: 2.6.2 + + hex-rgb@4.3.0: {} + + highlight.js@10.4.1: {} + + highlight.js@10.7.3: {} + + homedir-polyfill@1.0.3: + dependencies: + parse-passwd: 1.0.0 + + hookable@5.5.3: {} + + hosted-git-info@2.8.9: {} + + hosted-git-info@4.1.0: + dependencies: + lru-cache: 6.0.0 + + html-to-text@9.0.5: + dependencies: + '@selderee/plugin-htmlparser2': 0.11.0 + deepmerge: 4.3.1 + dom-serializer: 2.0.0 + htmlparser2: 8.0.2 + selderee: 0.11.0 + + html-void-elements@2.0.1: {} + + htmlparser2@8.0.2: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + entities: 4.5.0 + + http-assert@1.5.0: + dependencies: + deep-equal: 1.0.1 + http-errors: 1.8.1 + + http-errors@1.6.3: + dependencies: + depd: 1.1.2 + inherits: 2.0.3 + setprototypeof: 1.1.0 + statuses: 1.5.0 + + http-errors@1.8.1: + dependencies: + depd: 1.1.2 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 1.5.0 + toidentifier: 1.0.1 + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + http-proxy-agent@4.0.1: + dependencies: + '@tootallnate/once': 1.1.2 + agent-base: 6.0.2 + debug: 4.3.4(supports-color@9.4.0) + transitivePeerDependencies: + - supports-color + + http-proxy-agent@7.0.0: + dependencies: + agent-base: 7.1.0 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.1(supports-color@9.4.0) + debug: 4.3.4(supports-color@9.4.0) + transitivePeerDependencies: + - supports-color + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.3.4(supports-color@9.4.0) + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.0 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.4(supports-color@9.4.0): + dependencies: + agent-base: 7.1.1(supports-color@9.4.0) + debug: 4.3.4(supports-color@9.4.0) + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.5: + dependencies: + agent-base: 7.1.1(supports-color@9.4.0) + debug: 4.3.4(supports-color@9.4.0) + transitivePeerDependencies: + - supports-color + + human-signals@2.1.0: {} + + human-signals@5.0.0: {} + + humanize-duration@3.31.0: {} + + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + ignore@5.3.1: {} + + immediate@3.0.6: {} + + immutable@3.7.6: {} + + import-fresh@3.3.0: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-from@4.0.0: {} + + import-meta-resolve@3.1.1: {} + + import@0.0.6: + dependencies: + optimist: 0.3.7 + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + index-to-position@0.1.2: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.3: {} + + inherits@2.0.4: {} + + ini@1.3.8: {} + + inline-style-parser@0.1.1: {} + + inquirer@8.2.6: + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.17.21 + mute-stream: 0.0.8 + ora: 5.4.1 + run-async: 2.4.1 + rxjs: 7.8.1 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + wrap-ansi: 6.2.0 + + internal-slot@1.0.5: + dependencies: + get-intrinsic: 1.2.1 + has: 1.0.3 + side-channel: 1.0.4 + + internal-slot@1.0.7: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.0.6 + + internmap@2.0.3: {} + + interpret@1.4.0: {} + + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + + irregular-plurals@3.5.0: {} + + is-absolute@1.0.0: + dependencies: + is-relative: 1.0.0 + is-windows: 1.0.2 + + is-alphabetical@1.0.4: {} + + is-alphanumerical@1.0.4: + dependencies: + is-alphabetical: 1.0.4 + is-decimal: 1.0.4 + + is-arguments@1.1.1: + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + + is-array-buffer@3.0.2: + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.1 + is-typed-array: 1.1.12 + + is-array-buffer@3.0.4: + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + + is-arrayish@0.2.1: {} + + is-arrayish@0.3.2: {} + + is-async-function@2.0.0: + dependencies: + has-tostringtag: 1.0.2 + + is-bigint@1.0.4: + dependencies: + has-bigints: 1.0.2 + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-boolean-object@1.1.2: + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + + is-buffer@2.0.5: {} + + is-builtin-module@3.2.1: + dependencies: + builtin-modules: 3.3.0 + + is-callable@1.2.7: {} + + is-ci@2.0.0: + dependencies: + ci-info: 2.0.0 + + is-core-module@2.13.0: + dependencies: + has: 1.0.3 + + is-date-object@1.0.5: + dependencies: + has-tostringtag: 1.0.2 + + is-decimal@1.0.4: {} + + is-deflate@1.0.0: {} + + is-docker@2.2.1: {} + + is-electron@2.2.2: {} + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.0.2: + dependencies: + call-bind: 1.0.7 + + is-fullwidth-code-point@3.0.0: {} + + is-fullwidth-code-point@4.0.0: {} + + is-fullwidth-code-point@5.0.0: + dependencies: + get-east-asian-width: 1.2.0 + + is-generator-function@1.0.10: + dependencies: + has-tostringtag: 1.0.2 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-gzip@1.0.0: {} + + is-hexadecimal@1.0.4: {} + + is-interactive@1.0.0: {} + + is-lower-case@2.0.2: + dependencies: + tslib: 2.6.2 + + is-map@2.0.3: {} + + is-module@1.0.0: {} + + is-nan@1.3.2: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + + is-negative-zero@2.0.2: {} + + is-number-object@1.0.7: + dependencies: + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-path-inside@3.0.3: {} + + is-plain-obj@2.1.0: {} + + is-plain-obj@4.1.0: {} + + is-reference@1.2.1: + dependencies: + '@types/estree': 1.0.6 + + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.7 + optional: true + + is-regex@1.1.4: + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + + is-relative@1.0.0: + dependencies: + is-unc-path: 1.0.0 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.2: + dependencies: + call-bind: 1.0.5 + + is-shared-array-buffer@1.0.3: + dependencies: + call-bind: 1.0.7 + + is-ssh@1.4.0: + dependencies: + protocols: 2.0.1 + + is-stream@2.0.1: {} + + is-stream@3.0.0: {} + + is-string@1.0.7: + dependencies: + has-tostringtag: 1.0.2 + + is-symbol@1.0.4: + dependencies: + has-symbols: 1.0.3 + + is-typed-array@1.1.12: + dependencies: + which-typed-array: 1.1.11 + + is-typed-array@1.1.13: + dependencies: + which-typed-array: 1.1.15 + + is-unc-path@1.0.0: + dependencies: + unc-path-regex: 0.1.2 + + is-unicode-supported@0.1.0: {} + + is-upper-case@2.0.2: + dependencies: + tslib: 2.6.2 + + is-weakmap@2.0.2: {} + + is-weakref@1.0.2: + dependencies: + call-bind: 1.0.5 + + is-weakset@2.0.3: + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + + is-windows@1.0.2: {} + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + isarray@1.0.0: {} + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + isomorphic-fetch@3.0.0(encoding@0.1.13): + dependencies: + node-fetch: 2.7.0(encoding@0.1.13) + whatwg-fetch: 3.6.19 + transitivePeerDependencies: + - encoding + + isomorphic-ws@5.0.0(ws@8.14.2): + dependencies: + ws: 8.14.2 + + iterator.prototype@1.1.2: + dependencies: + define-properties: 1.2.1 + get-intrinsic: 1.2.1 + has-symbols: 1.0.3 + reflect.getprototypeof: 1.0.4 + set-function-name: 2.0.1 + + jackspeak@2.3.6: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jackspeak@4.0.2: + dependencies: + '@isaacs/cliui': 8.0.2 + + jest-worker@27.5.1: + dependencies: + '@types/node': 20.12.12 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jiti@1.21.0: {} + + jiti@1.21.7: {} + + jiti@2.4.2: {} + + jose@5.1.1: {} + + joycon@3.1.1: {} + + js-beautify@1.15.1: + dependencies: + config-chain: 1.1.13 + editorconfig: 1.0.4 + glob: 10.3.15 + js-cookie: 3.0.5 + nopt: 7.2.1 + + js-cookie@3.0.5: {} + + js-levenshtein@1.1.6: {} + + js-tokens@4.0.0: {} + + js-tokens@9.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsdoc-type-pratt-parser@4.0.0: {} + + jsesc@0.5.0: {} + + jsesc@2.5.2: {} + + jsesc@3.0.2: {} + + json-buffer@3.0.1: {} + + json-parse-better-errors@1.0.2: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json-stable-stringify@1.1.0: + dependencies: + call-bind: 1.0.5 + isarray: 2.0.5 + jsonify: 0.0.1 + object-keys: 1.1.1 + + json-to-pretty-yaml@1.2.2: + dependencies: + remedial: 1.0.8 + remove-trailing-spaces: 1.0.8 + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + json5@2.2.3: {} + + jsonc-eslint-parser@2.4.0: + dependencies: + acorn: 8.11.3 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + semver: 7.6.2 + + jsonc-parser@3.2.1: {} + + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsonify@0.0.1: {} + + jsonwebtoken@9.0.2: + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.6.2 + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.7 + array.prototype.flat: 1.3.2 + object.assign: 4.1.4 + object.values: 1.1.7 + + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + + jwa@1.4.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jwa@2.0.0: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@3.2.2: + dependencies: + jwa: 1.4.1 + safe-buffer: 5.2.1 + + jws@4.0.0: + dependencies: + jwa: 2.0.0 + safe-buffer: 5.2.1 + + jwt-decode@3.1.2: {} + + jwt-decode@4.0.0: {} + + katex@0.16.8: + dependencies: + commander: 8.3.0 + + keygrip@1.1.0: + dependencies: + tsscmp: 1.0.6 + + keytar@7.9.0: + dependencies: + node-addon-api: 4.3.0 + prebuild-install: 7.1.2 + optional: true + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kleur@3.0.3: {} + + kleur@4.1.5: {} + + knitwork@1.2.0: {} + + koa-compose@4.1.0: {} + + koa-convert@2.0.0: + dependencies: + co: 4.6.0 + koa-compose: 4.1.0 + + koa-morgan@1.0.1: + dependencies: + morgan: 1.10.0 + transitivePeerDependencies: + - supports-color + + koa-mount@4.0.0: + dependencies: + debug: 4.3.4(supports-color@9.4.0) + koa-compose: 4.1.0 + transitivePeerDependencies: + - supports-color + + koa-send@5.0.1: + dependencies: + debug: 4.3.4(supports-color@9.4.0) + http-errors: 1.8.1 + resolve-path: 1.4.0 + transitivePeerDependencies: + - supports-color + + koa-static@5.0.0: + dependencies: + debug: 3.2.7 + koa-send: 5.0.1 + transitivePeerDependencies: + - supports-color + + koa@2.15.3: + dependencies: + accepts: 1.3.8 + cache-content-type: 1.0.1 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookies: 0.9.1 + debug: 4.3.4(supports-color@9.4.0) + delegates: 1.0.0 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + fresh: 0.5.2 + http-assert: 1.5.0 + http-errors: 1.8.1 + is-generator-function: 1.0.10 + koa-compose: 4.1.0 + koa-convert: 2.0.0 + on-finished: 2.4.1 + only: 0.0.2 + parseurl: 1.3.3 + statuses: 1.5.0 + type-is: 1.6.18 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + language-subtag-registry@0.3.22: {} + + language-tags@1.0.5: + dependencies: + language-subtag-registry: 0.3.22 + + leac@0.6.0: {} + + leven@3.1.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lie@3.3.0: + dependencies: + immediate: 3.0.6 + + lilconfig@2.1.0: {} + + lilconfig@3.1.1: {} + + lilconfig@3.1.3: {} + + linebreak@1.1.0: + dependencies: + base64-js: 0.0.8 + unicode-trie: 2.0.0 + + lines-and-columns@1.2.4: {} + + linkify-it@3.0.3: + dependencies: + uc.micro: 1.0.6 + + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + + lint-staged@15.2.4: + dependencies: + chalk: 5.3.0 + commander: 12.1.0 + debug: 4.3.4(supports-color@9.4.0) + execa: 8.0.1 + lilconfig: 3.1.1 + listr2: 8.2.1 + micromatch: 4.0.6 + pidtree: 0.6.0 + string-argv: 0.3.2 + yaml: 2.4.2 + transitivePeerDependencies: + - supports-color + + listr2@4.0.5: + dependencies: + cli-truncate: 2.1.0 + colorette: 2.0.20 + log-update: 4.0.0 + p-map: 4.0.0 + rfdc: 1.3.0 + rxjs: 7.8.1 + through: 2.3.8 + wrap-ansi: 7.0.0 + + listr2@8.2.1: + dependencies: + cli-truncate: 4.0.0 + colorette: 2.0.20 + eventemitter3: 5.0.1 + log-update: 6.0.0 + rfdc: 1.3.1 + wrap-ansi: 9.0.0 + + load-json-file@4.0.0: + dependencies: + graceful-fs: 4.2.11 + parse-json: 4.0.0 + pify: 3.0.0 + strip-bom: 3.0.0 + + load-tsconfig@0.2.5: {} + + loader-runner@4.3.0: {} + + local-pkg@0.5.0: + dependencies: + mlly: 1.7.0 + pkg-types: 1.1.1 + + locate-character@3.0.0: + optional: true + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + locate-path@7.2.0: + dependencies: + p-locate: 6.0.0 + + lodash-es@4.17.21: {} + + lodash.castarray@4.4.0: {} + + lodash.curry@4.1.1: {} + + lodash.flow@3.5.0: {} + + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isequal@4.5.0: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + + lodash.memoize@4.1.2: {} + + lodash.merge@4.6.2: {} + + lodash.once@4.1.1: {} + + lodash.sortby@4.7.0: {} + + lodash.uniq@4.5.0: {} + + lodash@4.17.21: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + log-update@4.0.0: + dependencies: + ansi-escapes: 4.3.2 + cli-cursor: 3.1.0 + slice-ansi: 4.0.0 + wrap-ansi: 6.2.0 + + log-update@6.0.0: + dependencies: + ansi-escapes: 6.2.1 + cli-cursor: 4.0.0 + slice-ansi: 7.1.0 + strip-ansi: 7.1.0 + wrap-ansi: 9.0.0 + + longest-streak@3.1.0: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + loupe@2.3.7: + dependencies: + get-func-name: 2.0.2 + + lower-case-first@2.0.2: + dependencies: + tslib: 2.6.2 + + lower-case@2.0.2: + dependencies: + tslib: 2.6.2 + + lowlight@1.17.0: + dependencies: + fault: 1.0.4 + highlight.js: 10.4.1 + + lowlight@1.20.0: + dependencies: + fault: 1.0.4 + highlight.js: 10.7.3 + + lru-cache@10.2.2: {} + + lru-cache@11.0.1: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + + lru-cache@9.1.2: {} + + lucide-react@0.365.0(react@18.2.0): + dependencies: + react: 18.2.0 + + mac-ca@2.0.3: + dependencies: + node-forge: 1.3.1 + + magic-string@0.30.10: + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + make-dir@1.3.0: + dependencies: + pify: 3.0.0 + + make-error@1.3.6: {} + + map-cache@0.2.2: {} + + map-stream@0.1.0: {} + + markdown-it@12.3.2: + dependencies: + argparse: 2.0.1 + entities: 2.1.0 + linkify-it: 3.0.3 + mdurl: 1.0.1 + uc.micro: 1.0.6 + + markdown-it@14.1.0: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + + markdown-table@3.0.3: {} + + marked@13.0.0: {} + + marked@7.0.4: {} + + md-to-react-email@5.0.2(react@18.2.0): + dependencies: + marked: 7.0.4 + react: 18.2.0 + + mdast-util-definitions@5.1.2: + dependencies: + '@types/mdast': 3.0.13 + '@types/unist': 2.0.8 + unist-util-visit: 4.1.2 + + mdast-util-find-and-replace@2.2.2: + dependencies: + '@types/mdast': 3.0.13 + escape-string-regexp: 5.0.0 + unist-util-is: 5.2.1 + unist-util-visit-parents: 5.1.3 + + mdast-util-from-markdown@0.8.5: + dependencies: + '@types/mdast': 3.0.13 + mdast-util-to-string: 2.0.0 + micromark: 2.11.4 + parse-entities: 2.0.0 + unist-util-stringify-position: 2.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-from-markdown@1.3.1: + dependencies: + '@types/mdast': 3.0.13 + '@types/unist': 2.0.8 + decode-named-character-reference: 1.0.2 + mdast-util-to-string: 3.2.0 + micromark: 3.2.0 + micromark-util-decode-numeric-character-reference: 1.1.0 + micromark-util-decode-string: 1.1.0 + micromark-util-normalize-identifier: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + unist-util-stringify-position: 3.0.3 + uvu: 0.5.6 + transitivePeerDependencies: + - supports-color + + mdast-util-from-markdown@2.0.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@1.0.3: + dependencies: + '@types/mdast': 3.0.13 + ccount: 2.0.1 + mdast-util-find-and-replace: 2.2.2 + micromark-util-character: 1.2.0 + + mdast-util-gfm-footnote@1.0.2: + dependencies: + '@types/mdast': 3.0.13 + mdast-util-to-markdown: 1.5.0 + micromark-util-normalize-identifier: 1.1.0 + + mdast-util-gfm-strikethrough@1.0.3: + dependencies: + '@types/mdast': 3.0.13 + mdast-util-to-markdown: 1.5.0 + + mdast-util-gfm-table@1.0.7: + dependencies: + '@types/mdast': 3.0.13 + markdown-table: 3.0.3 + mdast-util-from-markdown: 1.3.1 + mdast-util-to-markdown: 1.5.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@1.0.2: + dependencies: + '@types/mdast': 3.0.13 + mdast-util-to-markdown: 1.5.0 + + mdast-util-gfm@2.0.2: + dependencies: + mdast-util-from-markdown: 1.3.1 + mdast-util-gfm-autolink-literal: 1.0.3 + mdast-util-gfm-footnote: 1.0.2 + mdast-util-gfm-strikethrough: 1.0.3 + mdast-util-gfm-table: 1.0.7 + mdast-util-gfm-task-list-item: 1.0.2 + mdast-util-to-markdown: 1.5.0 + transitivePeerDependencies: + - supports-color + + mdast-util-math@2.0.2: + dependencies: + '@types/mdast': 3.0.13 + longest-streak: 3.1.0 + mdast-util-to-markdown: 1.5.0 + + mdast-util-phrasing@3.0.1: + dependencies: + '@types/mdast': 3.0.13 + unist-util-is: 5.2.1 + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.0 + + mdast-util-to-hast@12.3.0: + dependencies: + '@types/hast': 2.3.6 + '@types/mdast': 3.0.13 + mdast-util-definitions: 5.1.2 + micromark-util-sanitize-uri: 1.2.0 + trim-lines: 3.0.1 + unist-util-generated: 2.0.1 + unist-util-position: 4.0.4 + unist-util-visit: 4.1.2 + + mdast-util-to-markdown@1.5.0: + dependencies: + '@types/mdast': 3.0.13 + '@types/unist': 2.0.8 + longest-streak: 3.1.0 + mdast-util-phrasing: 3.0.1 + mdast-util-to-string: 3.2.0 + micromark-util-decode-string: 1.1.0 + unist-util-visit: 4.1.2 + zwitch: 2.0.4 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + + mdast-util-to-string@2.0.0: {} + + mdast-util-to-string@3.2.0: + dependencies: + '@types/mdast': 3.0.13 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + mdast@3.0.0: {} + + mdn-data@2.0.28: {} + + mdn-data@2.0.30: {} + + mdurl@1.0.1: {} + + mdurl@2.0.0: {} + + media-typer@0.3.0: {} + + memorystream@0.3.1: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + meros@1.3.0(@types/node@17.0.45): + optionalDependencies: + '@types/node': 17.0.45 + + micromark-core-commonmark@1.1.0: + dependencies: + decode-named-character-reference: 1.0.2 + micromark-factory-destination: 1.1.0 + micromark-factory-label: 1.1.0 + micromark-factory-space: 1.1.0 + micromark-factory-title: 1.1.0 + micromark-factory-whitespace: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-chunked: 1.1.0 + micromark-util-classify-character: 1.1.0 + micromark-util-html-tag-name: 1.2.0 + micromark-util-normalize-identifier: 1.1.0 + micromark-util-resolve-all: 1.1.0 + micromark-util-subtokenize: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@1.0.5: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-sanitize-uri: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-extension-gfm-footnote@1.1.2: + dependencies: + micromark-core-commonmark: 1.1.0 + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-normalize-identifier: 1.1.0 + micromark-util-sanitize-uri: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + + micromark-extension-gfm-strikethrough@1.0.7: + dependencies: + micromark-util-chunked: 1.1.0 + micromark-util-classify-character: 1.1.0 + micromark-util-resolve-all: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + + micromark-extension-gfm-table@1.0.7: + dependencies: + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + + micromark-extension-gfm-tagfilter@1.0.2: + dependencies: + micromark-util-types: 1.1.0 + + micromark-extension-gfm-task-list-item@1.0.5: + dependencies: + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + + micromark-extension-gfm@2.0.3: + dependencies: + micromark-extension-gfm-autolink-literal: 1.0.5 + micromark-extension-gfm-footnote: 1.1.2 + micromark-extension-gfm-strikethrough: 1.0.7 + micromark-extension-gfm-table: 1.0.7 + micromark-extension-gfm-tagfilter: 1.0.2 + micromark-extension-gfm-task-list-item: 1.0.5 + micromark-util-combine-extensions: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-extension-math@2.1.2: + dependencies: + '@types/katex': 0.16.3 + katex: 0.16.8 + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + + micromark-factory-destination@1.1.0: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@1.1.0: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@1.1.0: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-types: 1.1.0 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@1.1.0: + dependencies: + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@1.1.0: + dependencies: + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@1.2.0: + dependencies: + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@1.1.0: + dependencies: + micromark-util-symbol: 1.1.0 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@1.1.0: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@1.1.0: + dependencies: + micromark-util-chunked: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@1.1.0: + dependencies: + micromark-util-symbol: 1.1.0 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@1.1.0: + dependencies: + decode-named-character-reference: 1.0.2 + micromark-util-character: 1.2.0 + micromark-util-decode-numeric-character-reference: 1.1.0 + micromark-util-symbol: 1.1.0 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.0.2 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@1.1.0: {} + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@1.2.0: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@1.1.0: + dependencies: + micromark-util-symbol: 1.1.0 + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@1.1.0: + dependencies: + micromark-util-types: 1.1.0 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@1.2.0: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-encode: 1.1.0 + micromark-util-symbol: 1.1.0 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@1.1.0: + dependencies: + micromark-util-chunked: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@1.1.0: {} + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@1.1.0: {} + + micromark-util-types@2.0.2: {} + + micromark@2.11.4: + dependencies: + debug: 4.3.4(supports-color@9.4.0) + parse-entities: 2.0.0 + transitivePeerDependencies: + - supports-color + + micromark@3.2.0: + dependencies: + '@types/debug': 4.1.9 + debug: 4.3.4 + decode-named-character-reference: 1.0.2 + micromark-core-commonmark: 1.1.0 + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-chunked: 1.1.0 + micromark-util-combine-extensions: 1.1.0 + micromark-util-decode-numeric-character-reference: 1.1.0 + micromark-util-encode: 1.1.0 + micromark-util-normalize-identifier: 1.1.0 + micromark-util-resolve-all: 1.1.0 + micromark-util-sanitize-uri: 1.2.0 + micromark-util-subtokenize: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + transitivePeerDependencies: + - supports-color + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.9 + debug: 4.3.4 + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + + micromatch@4.0.5: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + micromatch@4.0.6: + dependencies: + braces: 3.0.3 + picomatch: 4.0.2 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@1.6.0: {} + + mimic-fn@2.1.0: {} + + mimic-fn@4.0.0: {} + + mimic-response@3.1.0: + optional: true + + min-indent@1.0.1: {} + + minimatch@10.0.1: + dependencies: + brace-expansion: 2.0.1 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minimatch@4.2.3: + dependencies: + brace-expansion: 1.1.11 + + minimatch@5.0.1: + dependencies: + brace-expansion: 2.0.1 + + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.1 + + minimatch@9.0.1: + dependencies: + brace-expansion: 2.0.1 + + minimatch@9.0.3: + dependencies: + brace-expansion: 2.0.1 + + minimatch@9.0.4: + dependencies: + brace-expansion: 2.0.1 + + minimist@1.2.8: {} + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@5.0.0: {} + + minipass@7.1.1: {} + + minipass@7.1.2: {} + + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + + mitt@3.0.1: {} + + mkdirp-classic@0.5.3: + optional: true + + mkdirp@1.0.4: {} + + mkdist@1.6.0(typescript@5.4.5): + dependencies: + autoprefixer: 10.4.21(postcss@8.5.3) + citty: 0.1.6 + cssnano: 7.0.6(postcss@8.5.3) + defu: 6.1.4 + esbuild: 0.24.2 + jiti: 1.21.7 + mlly: 1.7.4 + pathe: 1.1.2 + pkg-types: 1.3.1 + postcss: 8.5.3 + postcss-nested: 6.2.0(postcss@8.5.3) + semver: 7.7.1 + tinyglobby: 0.2.12 + optionalDependencies: + typescript: 5.4.5 + + mlly@1.7.0: + dependencies: + acorn: 8.11.3 + pathe: 1.1.2 + pkg-types: 1.1.1 + ufo: 1.5.3 + + mlly@1.7.4: + dependencies: + acorn: 8.14.1 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.5.4 + + mocha@10.4.0: + dependencies: + ansi-colors: 4.1.1 + browser-stdout: 1.3.1 + chokidar: 3.5.3 + debug: 4.3.4(supports-color@8.1.1) + diff: 5.0.0 + escape-string-regexp: 4.0.0 + find-up: 5.0.0 + glob: 8.1.0 + he: 1.2.0 + js-yaml: 4.1.0 + log-symbols: 4.1.0 + minimatch: 5.0.1 + ms: 2.1.3 + serialize-javascript: 6.0.0 + strip-json-comments: 3.1.1 + supports-color: 8.1.1 + workerpool: 6.2.1 + yargs: 16.2.0 + yargs-parser: 20.2.4 + yargs-unparser: 2.0.0 + + moment-timezone@0.5.45: + dependencies: + moment: 2.29.4 + + moment@2.29.4: {} + + morgan@1.10.0: + dependencies: + basic-auth: 2.0.1 + debug: 2.6.9 + depd: 2.0.0 + on-finished: 2.3.0 + on-headers: 1.0.2 + transitivePeerDependencies: + - supports-color + + mri@1.2.0: {} + + ms@2.0.0: {} + + ms@2.1.2: {} + + ms@2.1.3: {} + + mute-stream@0.0.8: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.11: {} + + nanoid@3.3.7: {} + + nanoid@4.0.2: {} + + napi-build-utils@1.0.2: + optional: true + + natural-compare-lite@1.4.0: {} + + natural-compare@1.4.0: {} + + negotiator@0.6.3: {} + + neo-async@2.6.2: {} + + next-themes@0.2.1(next@13.5.3(@babel/core@7.23.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + next: 13.5.3(@babel/core@7.23.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + next@13.5.3(@babel/core@7.23.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@next/env': 13.5.3 + '@swc/helpers': 0.5.2 + busboy: 1.6.0 + caniuse-lite: 1.0.30001541 + postcss: 8.4.14 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + styled-jsx: 5.1.1(@babel/core@7.23.5)(react@18.2.0) + watchpack: 2.4.0 + zod: 3.21.4 + optionalDependencies: + '@next/swc-darwin-arm64': 13.5.3 + '@next/swc-darwin-x64': 13.5.3 + '@next/swc-linux-arm64-gnu': 13.5.3 + '@next/swc-linux-arm64-musl': 13.5.3 + '@next/swc-linux-x64-gnu': 13.5.3 + '@next/swc-linux-x64-musl': 13.5.3 + '@next/swc-win32-arm64-msvc': 13.5.3 + '@next/swc-win32-ia32-msvc': 13.5.3 + '@next/swc-win32-x64-msvc': 13.5.3 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + next@14.1.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@next/env': 14.1.4 + '@swc/helpers': 0.5.2 + busboy: 1.6.0 + caniuse-lite: 1.0.30001620 + graceful-fs: 4.2.11 + postcss: 8.4.31 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + styled-jsx: 5.1.1(@babel/core@7.23.5)(react@18.2.0) + optionalDependencies: + '@next/swc-darwin-arm64': 14.1.4 + '@next/swc-darwin-x64': 14.1.4 + '@next/swc-linux-arm64-gnu': 14.1.4 + '@next/swc-linux-arm64-musl': 14.1.4 + '@next/swc-linux-x64-gnu': 14.1.4 + '@next/swc-linux-x64-musl': 14.1.4 + '@next/swc-win32-arm64-msvc': 14.1.4 + '@next/swc-win32-ia32-msvc': 14.1.4 + '@next/swc-win32-x64-msvc': 14.1.4 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + nice-try@1.0.5: {} + + no-case@3.0.4: + dependencies: + lower-case: 2.0.2 + tslib: 2.6.2 + + node-abi@3.62.0: + dependencies: + semver: 7.6.2 + optional: true + + node-addon-api@4.3.0: + optional: true + + node-addon-api@7.0.0: {} + + node-cleanup@2.1.2: {} + + node-domexception@1.0.0: {} + + node-fetch-native@1.6.4: {} + + node-fetch@2.7.0(encoding@0.1.13): + dependencies: + whatwg-url: 5.0.0 + optionalDependencies: + encoding: 0.1.13 + + node-forge@1.3.1: {} + + node-int64@0.4.0: {} + + node-releases@2.0.13: {} + + node-releases@2.0.14: {} + + node-releases@2.0.19: {} + + nopt@7.2.1: + dependencies: + abbrev: 2.0.0 + + normalize-package-data@2.5.0: + dependencies: + hosted-git-info: 2.8.9 + resolve: 1.22.8 + semver: 5.7.2 + validate-npm-package-license: 3.0.4 + + normalize-path@2.1.1: + dependencies: + remove-trailing-separator: 1.1.0 + + normalize-path@3.0.0: {} + + normalize-range@0.1.2: {} + + npm-run-all@4.1.5: + dependencies: + ansi-styles: 3.2.1 + chalk: 2.4.2 + cross-spawn: 6.0.5 + memorystream: 0.3.1 + minimatch: 3.1.2 + pidtree: 0.3.1 + read-pkg: 3.0.0 + shell-quote: 1.8.1 + string.prototype.padend: 3.1.5 + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + nullthrows@1.1.1: {} + + numeral@2.0.6: {} + + nypm@0.3.8: + dependencies: + citty: 0.1.6 + consola: 3.2.3 + execa: 8.0.1 + pathe: 1.1.2 + ufo: 1.5.3 + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + object-inspect@1.12.3: {} + + object-inspect@1.13.1: {} + + object-is@1.1.6: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + + object-keys@1.1.1: {} + + object.assign@4.1.4: + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + + object.assign@4.1.5: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + + object.entries@1.1.7: + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.2 + + object.fromentries@2.0.7: + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.2 + + object.groupby@1.0.1: + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.2 + get-intrinsic: 1.2.1 + + object.hasown@1.1.3: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.22.2 + + object.values@1.1.7: + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.2 + + ohash@1.1.3: {} + + on-exit-leak-free@2.1.2: {} + + on-finished@2.3.0: + dependencies: + ee-first: 1.1.1 + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + on-headers@1.0.2: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + + only@0.0.2: {} + + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + + openai-edge@0.5.1: {} + + openai@4.52.7(encoding@0.1.13): + dependencies: + '@types/node': 18.19.33 + '@types/node-fetch': 2.6.6 + abort-controller: 3.0.0 + agentkeepalive: 4.5.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0(encoding@0.1.13) + web-streams-polyfill: 3.2.1 + transitivePeerDependencies: + - encoding + + openapi-fetch@0.7.10: + dependencies: + openapi-typescript-helpers: 0.0.4 + + openapi-typescript-helpers@0.0.4: {} + + openapi-typescript@7.0.2(encoding@0.1.13)(typescript@5.4.5): + dependencies: + '@redocly/openapi-core': 1.18.0(encoding@0.1.13)(supports-color@9.4.0) + ansi-colors: 4.1.3 + parse-json: 8.1.0 + supports-color: 9.4.0 + typescript: 5.4.5 + yargs-parser: 21.1.1 + transitivePeerDependencies: + - encoding + + optimist@0.3.7: + dependencies: + wordwrap: 0.0.3 + + optionator@0.9.3: + dependencies: + '@aashutoshrathi/word-wrap': 1.2.6 + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.1 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + orderedmap@2.1.1: {} + + os-tmpdir@1.0.2: {} + + ovsx@0.9.5: + dependencies: + '@vscode/vsce': 3.1.1 + commander: 6.2.1 + follow-redirects: 1.15.9 + is-ci: 2.0.0 + leven: 3.1.0 + semver: 7.6.2 + tmp: 0.2.3 + transitivePeerDependencies: + - debug + - supports-color + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-limit@4.0.0: + dependencies: + yocto-queue: 1.0.0 + + p-limit@5.0.0: + dependencies: + yocto-queue: 1.0.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-locate@6.0.0: + dependencies: + p-limit: 4.0.0 + + p-map@4.0.0: + dependencies: + aggregate-error: 3.1.0 + + p-try@2.2.0: {} + + package-json-from-dist@1.0.1: {} + + pako@0.2.9: {} + + pako@1.0.11: {} + + param-case@3.0.4: + dependencies: + dot-case: 3.0.4 + tslib: 2.6.2 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-css-color@0.2.1: + dependencies: + color-name: 1.1.4 + hex-rgb: 4.3.0 + + parse-entities@2.0.0: + dependencies: + character-entities: 1.2.4 + character-entities-legacy: 1.1.4 + character-reference-invalid: 1.1.4 + is-alphanumerical: 1.0.4 + is-decimal: 1.0.4 + is-hexadecimal: 1.0.4 + + parse-filepath@1.0.2: + dependencies: + is-absolute: 1.0.0 + map-cache: 0.2.2 + path-root: 0.1.1 + + parse-gitignore@2.0.0: {} + + parse-json@4.0.0: + dependencies: + error-ex: 1.3.2 + json-parse-better-errors: 1.0.2 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.23.5 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse-json@8.1.0: + dependencies: + '@babel/code-frame': 7.24.2 + index-to-position: 0.1.2 + type-fest: 4.21.0 + + parse-passwd@1.0.0: {} + + parse-path@7.0.0: + dependencies: + protocols: 2.0.1 + + parse-semver@1.1.1: + dependencies: + semver: 5.7.2 + + parse-url@9.2.0: + dependencies: + '@types/parse-path': 7.0.3 + parse-path: 7.0.0 + + parse5-htmlparser2-tree-adapter@7.0.0: + dependencies: + domhandler: 5.0.3 + parse5: 7.1.2 + + parse5@6.0.1: {} + + parse5@7.1.2: + dependencies: + entities: 4.5.0 + + parseley@0.12.1: + dependencies: + leac: 0.6.0 + peberminta: 0.9.0 + + parseurl@1.3.3: {} + + pascal-case@3.1.2: + dependencies: + no-case: 3.0.4 + tslib: 2.6.2 + + path-case@3.0.4: + dependencies: + dot-case: 3.0.4 + tslib: 2.6.2 + + path-exists@4.0.0: {} + + path-exists@5.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@2.0.1: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-parse@1.0.7: {} + + path-root-regex@0.1.2: {} + + path-root@0.1.1: + dependencies: + path-root-regex: 0.1.2 + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.2.2 + minipass: 7.1.1 + + path-scurry@2.0.0: + dependencies: + lru-cache: 11.0.1 + minipass: 7.1.2 + + path-to-regexp@6.3.0: {} + + path-type@3.0.0: + dependencies: + pify: 3.0.0 + + path-type@4.0.0: {} + + pathe@1.1.2: {} + + pathe@2.0.3: {} + + pathval@1.1.1: {} + + pause-stream@0.0.11: + dependencies: + through: 2.3.8 + + peberminta@0.9.0: {} + + peek-stream@1.1.3: + dependencies: + buffer-from: 1.1.2 + duplexify: 3.7.1 + through2: 2.0.5 + + pend@1.2.0: {} + + perfect-debounce@1.0.0: {} + + periscopic@3.1.0: + dependencies: + '@types/estree': 1.0.7 + estree-walker: 3.0.3 + is-reference: 3.0.3 + optional: true + + picocolors@1.0.0: {} + + picocolors@1.0.1: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.2: {} + + pidtree@0.3.1: {} + + pidtree@0.6.0: {} + + pify@2.3.0: {} + + pify@3.0.0: {} + + pino-abstract-transport@1.2.0: + dependencies: + readable-stream: 4.5.2 + split2: 4.2.0 + + pino-std-serializers@6.2.2: {} + + pino@8.21.0: + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.5.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 1.2.0 + pino-std-serializers: 6.2.2 + process-warning: 3.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.4.3 + sonic-boom: 3.8.1 + thread-stream: 2.7.0 + + pirates@4.0.6: {} + + pkg-types@1.1.1: + dependencies: + confbox: 0.1.7 + mlly: 1.7.0 + pathe: 1.1.2 + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.7.4 + pathe: 2.0.3 + + playwright-core@1.48.1: {} + + playwright@1.48.1: + dependencies: + playwright-core: 1.48.1 + optionalDependencies: + fsevents: 2.3.2 + + plur@4.0.0: + dependencies: + irregular-plurals: 3.5.0 + + pluralize@8.0.0: {} + + possible-typed-array-names@1.0.0: {} + + postcss-calc@10.1.1(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-selector-parser: 7.1.0 + postcss-value-parser: 4.2.0 + + postcss-colormin@7.0.2(postcss@8.5.3): + dependencies: + browserslist: 4.24.4 + caniuse-api: 3.0.0 + colord: 2.9.3 + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-convert-values@7.0.4(postcss@8.5.3): + dependencies: + browserslist: 4.24.4 + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-discard-comments@7.0.3(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-selector-parser: 6.1.2 + + postcss-discard-duplicates@7.0.1(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + + postcss-discard-empty@7.0.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + + postcss-discard-overridden@7.0.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + + postcss-import@15.1.0(postcss@8.4.31): + dependencies: + postcss: 8.4.31 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.8 + + postcss-import@15.1.0(postcss@8.4.38): + dependencies: + postcss: 8.4.38 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.8 + + postcss-js@4.0.1(postcss@8.4.31): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.4.31 + + postcss-js@4.0.1(postcss@8.4.38): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.4.38 + + postcss-load-config@4.0.1(postcss@8.4.31)(ts-node@10.9.2(@types/node@17.0.45)(typescript@5.8.2)): + dependencies: + lilconfig: 2.1.0 + yaml: 2.3.4 + optionalDependencies: + postcss: 8.4.31 + ts-node: 10.9.2(@types/node@17.0.45)(typescript@5.8.2) + + postcss-load-config@4.0.2(postcss@8.4.38)(ts-node@10.9.2(@swc/core@1.3.101(@swc/helpers@0.5.2))(@types/node@20.12.12)(typescript@5.8.2)): + dependencies: + lilconfig: 3.1.1 + yaml: 2.4.2 + optionalDependencies: + postcss: 8.4.38 + ts-node: 10.9.2(@swc/core@1.3.101(@swc/helpers@0.5.2))(@types/node@20.12.12)(typescript@5.8.2) + + postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.3.101(@swc/helpers@0.5.2))(@types/node@18.19.33)(typescript@5.3.3)): + dependencies: + lilconfig: 3.1.1 + yaml: 2.4.2 + optionalDependencies: + postcss: 8.5.3 + ts-node: 10.9.2(@swc/core@1.3.101(@swc/helpers@0.5.2))(@types/node@18.19.33)(typescript@5.3.3) + + postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.3.101(@swc/helpers@0.5.2))(@types/node@18.19.33)(typescript@5.4.5)): + dependencies: + lilconfig: 3.1.1 + yaml: 2.4.2 + optionalDependencies: + postcss: 8.5.3 + ts-node: 10.9.2(@swc/core@1.3.101(@swc/helpers@0.5.2))(@types/node@18.19.33)(typescript@5.4.5) + + postcss-merge-longhand@7.0.4(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + stylehacks: 7.0.4(postcss@8.5.3) + + postcss-merge-rules@7.0.4(postcss@8.5.3): + dependencies: + browserslist: 4.24.4 + caniuse-api: 3.0.0 + cssnano-utils: 5.0.0(postcss@8.5.3) + postcss: 8.5.3 + postcss-selector-parser: 6.1.2 + + postcss-minify-font-values@7.0.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-minify-gradients@7.0.0(postcss@8.5.3): + dependencies: + colord: 2.9.3 + cssnano-utils: 5.0.0(postcss@8.5.3) + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-minify-params@7.0.2(postcss@8.5.3): + dependencies: + browserslist: 4.24.4 + cssnano-utils: 5.0.0(postcss@8.5.3) + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-minify-selectors@7.0.4(postcss@8.5.3): + dependencies: + cssesc: 3.0.0 + postcss: 8.5.3 + postcss-selector-parser: 6.1.2 + + postcss-nested@6.0.1(postcss@8.4.31): + dependencies: + postcss: 8.4.31 + postcss-selector-parser: 6.0.13 + + postcss-nested@6.0.1(postcss@8.4.38): + dependencies: + postcss: 8.4.38 + postcss-selector-parser: 6.0.13 + + postcss-nested@6.2.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-selector-parser: 6.1.2 + + postcss-normalize-charset@7.0.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + + postcss-normalize-display-values@7.0.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-normalize-positions@7.0.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-normalize-repeat-style@7.0.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-normalize-string@7.0.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-normalize-timing-functions@7.0.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-normalize-unicode@7.0.2(postcss@8.5.3): + dependencies: + browserslist: 4.24.4 + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-normalize-url@7.0.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-normalize-whitespace@7.0.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-ordered-values@7.0.1(postcss@8.5.3): + dependencies: + cssnano-utils: 5.0.0(postcss@8.5.3) + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-reduce-initial@7.0.2(postcss@8.5.3): + dependencies: + browserslist: 4.24.4 + caniuse-api: 3.0.0 + postcss: 8.5.3 + + postcss-reduce-transforms@7.0.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-selector-parser@6.0.10: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-selector-parser@6.0.13: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-selector-parser@6.0.16: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-selector-parser@7.1.0: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-svgo@7.0.1(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + svgo: 3.3.2 + + postcss-unique-selectors@7.0.3(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-selector-parser: 6.1.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.4.14: + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.1 + source-map-js: 1.0.2 + + postcss@8.4.31: + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.0 + source-map-js: 1.0.2 + + postcss@8.4.38: + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.1 + source-map-js: 1.2.0 + + postcss@8.5.3: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + posthog-js@1.139.0: + dependencies: + fflate: 0.4.8 + preact: 10.22.0 + + preact@10.22.0: {} + + prebuild-install@7.1.2: + dependencies: + detect-libc: 2.0.3 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 1.0.2 + node-abi: 3.62.0 + pump: 3.0.0 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.1 + tunnel-agent: 0.6.0 + optional: true + + prelude-ls@1.2.1: {} + + prettier@2.8.8: {} + + prettier@3.2.5: {} + + pretty-bytes@6.1.1: {} + + pretty-format@29.7.0: + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.2.0 + + prism-react-renderer@2.1.0(react@18.2.0): + dependencies: + '@types/prismjs': 1.26.4 + clsx: 1.2.1 + react: 18.2.0 + + prismjs@1.27.0: {} + + prismjs@1.29.0: {} + + process-nextick-args@2.0.1: {} + + process-warning@3.0.0: {} + + process@0.11.10: {} + + promise@7.3.1: + dependencies: + asap: 2.0.6 + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + property-information@5.6.0: + dependencies: + xtend: 4.0.2 + + property-information@6.3.0: {} + + prosemirror-changeset@2.2.1: + dependencies: + prosemirror-transform: 1.10.2 + + prosemirror-collab@1.3.1: + dependencies: + prosemirror-state: 1.4.3 + + prosemirror-commands@1.6.2: + dependencies: + prosemirror-model: 1.24.1 + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.2 + + prosemirror-dropcursor@1.8.1: + dependencies: + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.2 + prosemirror-view: 1.37.1 + + prosemirror-gapcursor@1.3.2: + dependencies: + prosemirror-keymap: 1.2.2 + prosemirror-model: 1.24.1 + prosemirror-state: 1.4.3 + prosemirror-view: 1.37.1 + + prosemirror-history@1.4.1: + dependencies: + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.2 + prosemirror-view: 1.37.1 + rope-sequence: 1.3.4 + + prosemirror-inputrules@1.4.0: + dependencies: + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.2 + + prosemirror-keymap@1.2.2: + dependencies: + prosemirror-state: 1.4.3 + w3c-keyname: 2.2.8 + + prosemirror-markdown@1.13.1: + dependencies: + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.0 + prosemirror-model: 1.24.1 + + prosemirror-menu@1.2.4: + dependencies: + crelt: 1.0.6 + prosemirror-commands: 1.6.2 + prosemirror-history: 1.4.1 + prosemirror-state: 1.4.3 + + prosemirror-model@1.24.1: + dependencies: + orderedmap: 2.1.1 + + prosemirror-schema-basic@1.2.3: + dependencies: + prosemirror-model: 1.24.1 + + prosemirror-schema-list@1.4.1: + dependencies: + prosemirror-model: 1.24.1 + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.2 + + prosemirror-state@1.4.3: + dependencies: + prosemirror-model: 1.24.1 + prosemirror-transform: 1.10.2 + prosemirror-view: 1.37.1 + + prosemirror-tables@1.6.1: + dependencies: + prosemirror-keymap: 1.2.2 + prosemirror-model: 1.24.1 + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.2 + prosemirror-view: 1.37.1 + + prosemirror-trailing-node@3.0.0(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.1): + dependencies: + '@remirror/core-constants': 3.0.0 + escape-string-regexp: 4.0.0 + prosemirror-model: 1.24.1 + prosemirror-state: 1.4.3 + prosemirror-view: 1.37.1 + + prosemirror-transform@1.10.2: + dependencies: + prosemirror-model: 1.24.1 + + prosemirror-view@1.37.1: + dependencies: + prosemirror-model: 1.24.1 + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.2 + + proto-list@1.2.4: {} + + protocols@2.0.1: {} + + ps-tree@1.2.0: + dependencies: + event-stream: 3.3.4 + + pump@2.0.1: + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + + pump@3.0.0: + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + + pumpify@1.5.1: + dependencies: + duplexify: 3.7.1 + inherits: 2.0.4 + pump: 2.0.1 + + punycode.js@2.3.1: {} + + punycode@1.4.1: {} + + punycode@2.3.1: {} + + pure-color@1.3.0: {} + + pvtsutils@1.3.5: + dependencies: + tslib: 2.6.2 + + pvutils@1.1.3: {} + + qs@6.12.1: + dependencies: + side-channel: 1.0.6 + + queue-microtask@1.2.3: {} + + queue-tick@1.0.1: {} + + quick-format-unescaped@4.0.4: {} + + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + + rc9@2.1.2: + dependencies: + defu: 6.1.4 + destr: 2.0.3 + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + optional: true + + react-activity-calendar@2.2.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@types/chroma-js': 2.4.4 + chroma-js: 2.4.2 + date-fns: 3.6.0 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + react-base16-styling@0.6.0: + dependencies: + base16: 1.0.0 + lodash.curry: 4.1.1 + lodash.flow: 3.5.0 + pure-color: 1.3.0 + + react-day-picker@8.10.0(date-fns@3.6.0)(react@18.2.0): + dependencies: + date-fns: 3.6.0 + react: 18.2.0 + + react-dom@18.2.0(react@18.2.0): + dependencies: + loose-envify: 1.4.0 + react: 18.2.0 + scheduler: 0.23.0 + + react-email@2.1.3(@swc/helpers@0.5.2)(eslint@9.3.0)(ts-node@10.9.2(@swc/core@1.3.101(@swc/helpers@0.5.2))(@types/node@20.12.12)(typescript@5.8.2)): + dependencies: + '@babel/parser': 7.24.1 + '@radix-ui/colors': 1.0.1 + '@radix-ui/react-collapsible': 1.0.3(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-popover': 1.0.7(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-toggle-group': 1.0.4(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-tooltip': 1.0.7(@types/react-dom@18.2.23)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@swc/core': 1.3.101(@swc/helpers@0.5.2) + '@types/react': 18.2.23 + '@types/react-dom': 18.2.23 + '@types/webpack': 5.28.5(@swc/core@1.3.101(@swc/helpers@0.5.2))(esbuild@0.19.11) + autoprefixer: 10.4.14(postcss@8.4.38) + babel-walk: 3.0.0 + chalk: 4.1.2 + chokidar: 3.5.3 + clsx: 2.1.0 + commander: 11.1.0 + debounce: 2.0.0 + esbuild: 0.19.11 + eslint-config-prettier: 9.0.0(eslint@9.3.0) + eslint-config-turbo: 1.10.12(eslint@9.3.0) + framer-motion: 10.17.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + glob: 10.3.4 + log-symbols: 4.1.0 + mime-types: 2.1.35 + next: 14.1.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + normalize-path: 3.0.0 + ora: 5.4.1 + postcss: 8.4.38 + prism-react-renderer: 2.1.0(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + shelljs: 0.8.5 + socket.io: 4.7.3 + socket.io-client: 4.7.3 + sonner: 1.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + source-map-js: 1.0.2 + stacktrace-parser: 0.1.10 + tailwind-merge: 2.2.0 + tailwindcss: 3.4.0(ts-node@10.9.2(@swc/core@1.3.101(@swc/helpers@0.5.2))(@types/node@20.12.12)(typescript@5.8.2)) + typescript: 5.1.6 + transitivePeerDependencies: + - '@babel/core' + - '@opentelemetry/api' + - '@swc/helpers' + - babel-plugin-macros + - bufferutil + - eslint + - sass + - supports-color + - ts-node + - uglify-js + - utf-8-validate + - webpack-cli + + react-error-boundary@4.0.13(react@18.2.0): + dependencies: + '@babel/runtime': 7.24.4 + react: 18.2.0 + + react-hook-form@7.48.2(react@18.2.0): + dependencies: + react: 18.2.0 + + react-intersection-observer@9.5.2(react@18.2.0): + dependencies: + react: 18.2.0 + + react-is@16.13.1: {} + + react-is@18.2.0: {} + + react-json-view@1.21.3(@types/react@18.2.23)(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + flux: 4.0.4(encoding@0.1.13)(react@18.2.0) + react: 18.2.0 + react-base16-styling: 0.6.0 + react-dom: 18.2.0(react@18.2.0) + react-lifecycles-compat: 3.0.4 + react-textarea-autosize: 8.5.3(@types/react@18.2.23)(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + - encoding + + react-lazy-load@4.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + react-lifecycles-compat@3.0.4: {} + + react-markdown@8.0.7(@types/react@18.2.23)(react@18.2.0): + dependencies: + '@types/hast': 2.3.6 + '@types/prop-types': 15.7.7 + '@types/react': 18.2.23 + '@types/unist': 2.0.8 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 2.0.1 + prop-types: 15.8.1 + property-information: 6.3.0 + react: 18.2.0 + react-is: 18.2.0 + remark-parse: 10.0.2 + remark-rehype: 10.1.0 + space-separated-tokens: 2.0.2 + style-to-object: 0.4.2 + unified: 10.1.2 + unist-util-visit: 4.1.2 + vfile: 5.3.7 + transitivePeerDependencies: + - supports-color + + react-nice-avatar@1.5.0(react@18.2.0): + dependencies: + '@babel/runtime': 7.24.4 + chroma-js: 2.4.2 + prop-types: 15.8.1 + react: 18.2.0 + + react-promise-suspense@0.3.4: + dependencies: + fast-deep-equal: 2.0.1 + + react-remove-scroll-bar@2.3.4(@types/react@18.2.23)(react@18.2.0): + dependencies: + react: 18.2.0 + react-style-singleton: 2.2.1(@types/react@18.2.23)(react@18.2.0) + tslib: 2.6.2 + optionalDependencies: + '@types/react': 18.2.23 + + react-remove-scroll@2.5.5(@types/react@18.2.23)(react@18.2.0): + dependencies: + react: 18.2.0 + react-remove-scroll-bar: 2.3.4(@types/react@18.2.23)(react@18.2.0) + react-style-singleton: 2.2.1(@types/react@18.2.23)(react@18.2.0) + tslib: 2.6.2 + use-callback-ref: 1.3.0(@types/react@18.2.23)(react@18.2.0) + use-sidecar: 1.1.2(@types/react@18.2.23)(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + + react-resizable-panels@1.0.7(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + react-smooth@4.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + fast-equals: 5.0.1 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-transition-group: 4.4.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + + react-style-singleton@2.2.1(@types/react@18.2.23)(react@18.2.0): + dependencies: + get-nonce: 1.0.1 + invariant: 2.2.4 + react: 18.2.0 + tslib: 2.6.2 + optionalDependencies: + '@types/react': 18.2.23 + + react-syntax-highlighter@15.5.0(react@18.2.0): + dependencies: + '@babel/runtime': 7.24.4 + highlight.js: 10.7.3 + lowlight: 1.20.0 + prismjs: 1.29.0 + react: 18.2.0 + refractor: 3.6.0 + + react-textarea-autosize@8.5.3(@types/react@18.2.23)(react@18.2.0): + dependencies: + '@babel/runtime': 7.24.4 + react: 18.2.0 + use-composed-ref: 1.3.0(react@18.2.0) + use-latest: 1.2.1(@types/react@18.2.23)(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + + react-topbar-progress-indicator@4.1.1(react@18.2.0): + dependencies: + react: 18.2.0 + topbar: 0.1.4 + + react-transition-group@4.4.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.24.4 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + react@18.2.0: + dependencies: + loose-envify: 1.4.0 + + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + + read-pkg-up@7.0.1: + dependencies: + find-up: 4.1.0 + read-pkg: 5.2.0 + type-fest: 0.8.1 + + read-pkg@3.0.0: + dependencies: + load-json-file: 4.0.0 + normalize-package-data: 2.5.0 + path-type: 3.0.0 + + read-pkg@5.2.0: + dependencies: + '@types/normalize-package-data': 2.4.4 + normalize-package-data: 2.5.0 + parse-json: 5.2.0 + type-fest: 0.6.0 + + read@1.0.7: + dependencies: + mute-stream: 0.0.8 + + readable-from-web@1.0.0: + dependencies: + '@types/readable-stream': 4.0.14 + readable-stream: 4.5.2 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readable-stream@4.5.2: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + real-require@0.2.0: {} + + recharts-scale@0.4.5: + dependencies: + decimal.js-light: 2.5.1 + + recharts@2.12.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + clsx: 2.1.0 + eventemitter3: 4.0.7 + lodash: 4.17.21 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-is: 16.13.1 + react-smooth: 4.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + recharts-scale: 0.4.5 + tiny-invariant: 1.3.3 + victory-vendor: 36.9.2 + + rechoir@0.6.2: + dependencies: + resolve: 1.22.8 + + refa@0.12.1: + dependencies: + '@eslint-community/regexpp': 4.10.0 + + reflect.getprototypeof@1.0.4: + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.2 + get-intrinsic: 1.2.1 + globalthis: 1.0.3 + which-builtin-type: 1.1.3 + + refractor@3.6.0: + dependencies: + hastscript: 6.0.0 + parse-entities: 2.0.0 + prismjs: 1.27.0 + + regenerator-runtime@0.14.0: {} + + regexp-ast-analysis@0.7.1: + dependencies: + '@eslint-community/regexpp': 4.10.0 + refa: 0.12.1 + + regexp-tree@0.1.27: {} + + regexp.prototype.flags@1.5.1: + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + set-function-name: 2.0.1 + + regexp.prototype.flags@1.5.2: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-errors: 1.3.0 + set-function-name: 2.0.2 + + regjsparser@0.10.0: + dependencies: + jsesc: 0.5.0 + + rehype-raw@6.1.1: + dependencies: + '@types/hast': 2.3.6 + hast-util-raw: 7.2.3 + unified: 10.1.2 + + rehype-sanitize@6.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-sanitize: 5.0.1 + + relay-runtime@12.0.0(encoding@0.1.13): + dependencies: + '@babel/runtime': 7.24.4 + fbjs: 3.0.5(encoding@0.1.13) + invariant: 2.2.4 + transitivePeerDependencies: + - encoding + + remark-gfm@3.0.1: + dependencies: + '@types/mdast': 3.0.13 + mdast-util-gfm: 2.0.2 + micromark-extension-gfm: 2.0.3 + unified: 10.1.2 + transitivePeerDependencies: + - supports-color + + remark-math@5.1.1: + dependencies: + '@types/mdast': 3.0.13 + mdast-util-math: 2.0.2 + micromark-extension-math: 2.1.2 + unified: 10.1.2 + + remark-parse@10.0.2: + dependencies: + '@types/mdast': 3.0.13 + mdast-util-from-markdown: 1.3.1 + unified: 10.1.2 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@10.1.0: + dependencies: + '@types/hast': 2.3.6 + '@types/mdast': 3.0.13 + mdast-util-to-hast: 12.3.0 + unified: 10.1.2 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + + remark@15.0.1: + dependencies: + '@types/mdast': 4.0.4 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remedial@1.0.8: {} + + remove-trailing-separator@1.1.0: {} + + remove-trailing-spaces@1.0.8: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + require-main-filename@2.0.0: {} + + resolve-dir@1.0.1: + dependencies: + expand-tilde: 2.0.2 + global-modules: 1.0.0 + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve-path@1.4.0: + dependencies: + http-errors: 1.6.3 + path-is-absolute: 1.0.1 + + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.8: + dependencies: + is-core-module: 2.13.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + resolve@2.0.0-next.4: + dependencies: + is-core-module: 2.13.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + restore-cursor@4.0.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + reusify@1.0.4: {} + + rfdc@1.3.0: {} + + rfdc@1.3.1: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + rimraf@5.0.7: + dependencies: + glob: 10.3.15 + + rollup-plugin-dts@6.1.1(rollup@3.29.5)(typescript@5.4.5): + dependencies: + magic-string: 0.30.17 + rollup: 3.29.5 + typescript: 5.4.5 + optionalDependencies: + '@babel/code-frame': 7.24.2 + + rollup@3.29.5: + optionalDependencies: + fsevents: 2.3.3 + + rollup@4.27.4: + dependencies: + '@types/estree': 1.0.6 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.27.4 + '@rollup/rollup-android-arm64': 4.27.4 + '@rollup/rollup-darwin-arm64': 4.27.4 + '@rollup/rollup-darwin-x64': 4.27.4 + '@rollup/rollup-freebsd-arm64': 4.27.4 + '@rollup/rollup-freebsd-x64': 4.27.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.27.4 + '@rollup/rollup-linux-arm-musleabihf': 4.27.4 + '@rollup/rollup-linux-arm64-gnu': 4.27.4 + '@rollup/rollup-linux-arm64-musl': 4.27.4 + '@rollup/rollup-linux-powerpc64le-gnu': 4.27.4 + '@rollup/rollup-linux-riscv64-gnu': 4.27.4 + '@rollup/rollup-linux-s390x-gnu': 4.27.4 + '@rollup/rollup-linux-x64-gnu': 4.27.4 + '@rollup/rollup-linux-x64-musl': 4.27.4 + '@rollup/rollup-win32-arm64-msvc': 4.27.4 + '@rollup/rollup-win32-ia32-msvc': 4.27.4 + '@rollup/rollup-win32-x64-msvc': 4.27.4 + fsevents: 2.3.3 + + rollup@4.36.0: + dependencies: + '@types/estree': 1.0.6 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.36.0 + '@rollup/rollup-android-arm64': 4.36.0 + '@rollup/rollup-darwin-arm64': 4.36.0 + '@rollup/rollup-darwin-x64': 4.36.0 + '@rollup/rollup-freebsd-arm64': 4.36.0 + '@rollup/rollup-freebsd-x64': 4.36.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.36.0 + '@rollup/rollup-linux-arm-musleabihf': 4.36.0 + '@rollup/rollup-linux-arm64-gnu': 4.36.0 + '@rollup/rollup-linux-arm64-musl': 4.36.0 + '@rollup/rollup-linux-loongarch64-gnu': 4.36.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.36.0 + '@rollup/rollup-linux-riscv64-gnu': 4.36.0 + '@rollup/rollup-linux-s390x-gnu': 4.36.0 + '@rollup/rollup-linux-x64-gnu': 4.36.0 + '@rollup/rollup-linux-x64-musl': 4.36.0 + '@rollup/rollup-win32-arm64-msvc': 4.36.0 + '@rollup/rollup-win32-ia32-msvc': 4.36.0 + '@rollup/rollup-win32-x64-msvc': 4.36.0 + fsevents: 2.3.3 + + rope-sequence@1.3.4: {} + + run-async@2.4.1: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + rxjs@7.8.1: + dependencies: + tslib: 2.6.2 + + sade@1.8.1: + dependencies: + mri: 1.2.0 + + safe-array-concat@1.0.1: + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.1 + has-symbols: 1.0.3 + isarray: 2.0.5 + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safe-regex-test@1.0.0: + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.1 + is-regex: 1.1.4 + + safe-stable-stringify@2.4.3: {} + + safer-buffer@2.1.2: {} + + satori@0.10.8: + dependencies: + '@shuding/opentype.js': 1.4.0-beta.0 + css-background-parser: 0.1.0 + css-box-shadow: 1.0.0-3 + css-to-react-native: 3.2.0 + emoji-regex: 10.3.0 + escape-html: 1.0.3 + linebreak: 1.1.0 + parse-css-color: 0.2.1 + postcss-value-parser: 4.2.0 + yoga-wasm-web: 0.3.3 + + sax@1.3.0: {} + + scheduler@0.23.0: + dependencies: + loose-envify: 1.4.0 + + schema-utils@3.3.0: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) + + scslre@0.3.0: + dependencies: + '@eslint-community/regexpp': 4.10.0 + refa: 0.12.1 + regexp-ast-analysis: 0.7.1 + + scuid@1.1.0: {} + + scule@1.3.0: {} + + seedrandom@3.0.5: {} + + selderee@0.11.0: + dependencies: + parseley: 0.12.1 + + semver@5.7.2: {} + + semver@6.3.1: {} + + semver@7.6.2: {} + + semver@7.7.1: {} + + sentence-case@3.0.4: + dependencies: + no-case: 3.0.4 + tslib: 2.6.2 + upper-case-first: 2.0.2 + + serialize-javascript@6.0.0: + dependencies: + randombytes: 2.1.0 + + serialize-javascript@6.0.2: + dependencies: + randombytes: 2.1.0 + + set-blocking@2.0.0: {} + + set-function-length@1.1.1: + dependencies: + define-data-property: 1.1.1 + get-intrinsic: 1.2.1 + gopd: 1.0.1 + has-property-descriptors: 1.0.0 + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.1: + dependencies: + define-data-property: 1.1.1 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.0 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + setimmediate@1.0.5: {} + + setprototypeof@1.1.0: {} + + setprototypeof@1.2.0: {} + + shebang-command@1.2.0: + dependencies: + shebang-regex: 1.0.0 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@1.0.0: {} + + shebang-regex@3.0.0: {} + + shell-quote@1.8.1: {} + + shelljs@0.8.5: + dependencies: + glob: 7.2.3 + interpret: 1.4.0 + rechoir: 0.6.2 + + side-channel@1.0.4: + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.1 + object-inspect: 1.12.3 + + side-channel@1.0.6: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + object-inspect: 1.13.1 + + siginfo@2.0.0: {} + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + signedsource@1.0.0: {} + + simple-concat@1.0.1: + optional: true + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + optional: true + + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + + sisteransi@1.0.5: {} + + slash@3.0.0: {} + + slash@4.0.0: {} + + slice-ansi@3.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + slice-ansi@4.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + slice-ansi@5.0.0: + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 4.0.0 + + slice-ansi@7.1.0: + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 5.0.0 + + smob@1.5.0: {} + + snake-case@3.0.4: + dependencies: + dot-case: 3.0.4 + tslib: 2.6.2 + + socket.io-adapter@2.5.4: + dependencies: + debug: 4.3.4(supports-color@9.4.0) + ws: 8.11.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-client@4.7.3: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.4(supports-color@9.4.0) + engine.io-client: 6.5.3 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-parser@4.2.4: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.4(supports-color@9.4.0) + transitivePeerDependencies: + - supports-color + + socket.io@4.7.3: + dependencies: + accepts: 1.3.8 + base64id: 2.0.0 + cors: 2.8.5 + debug: 4.3.4(supports-color@9.4.0) + engine.io: 6.5.4 + socket.io-adapter: 2.5.4 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + sonic-boom@3.8.1: + dependencies: + atomic-sleep: 1.0.0 + + sonner@1.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + source-map-js@1.0.2: {} + + source-map-js@1.2.0: {} + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + source-map@0.8.0-beta.0: + dependencies: + whatwg-url: 7.1.0 + + space-separated-tokens@1.1.5: {} + + space-separated-tokens@2.0.2: {} + + spdx-correct@3.2.0: + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.16 + + spdx-exceptions@2.3.0: {} + + spdx-expression-parse@3.0.1: + dependencies: + spdx-exceptions: 2.3.0 + spdx-license-ids: 3.0.16 + + spdx-expression-parse@4.0.0: + dependencies: + spdx-exceptions: 2.3.0 + spdx-license-ids: 3.0.16 + + spdx-license-ids@3.0.16: {} + + split2@4.2.0: {} + + split@0.3.3: + dependencies: + through: 2.3.8 + + split@1.0.1: + dependencies: + through: 2.3.8 + + sponge-case@1.0.1: + dependencies: + tslib: 2.6.2 + + stackback@0.0.2: {} + + stacktrace-parser@0.1.10: + dependencies: + type-fest: 0.7.1 + + stats-logscale@1.0.9: {} + + statuses@1.5.0: {} + + statuses@2.0.1: {} + + std-env@3.7.0: {} + + stop-iteration-iterator@1.0.0: + dependencies: + internal-slot: 1.0.7 + + stoppable@1.1.0: {} + + stream-combiner@0.0.4: + dependencies: + duplexer: 0.1.2 + + stream-shift@1.0.3: {} + + streamsearch@1.1.0: {} + + streamx@2.20.1: + dependencies: + fast-fifo: 1.3.2 + queue-tick: 1.0.1 + text-decoder: 1.2.1 + optionalDependencies: + bare-events: 2.5.0 + + string-argv@0.3.2: {} + + string-env-interpolation@1.0.1: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + string-width@7.1.0: + dependencies: + emoji-regex: 10.3.0 + get-east-asian-width: 1.2.0 + strip-ansi: 7.1.0 + + string.prototype.codepointat@0.2.1: {} + + string.prototype.matchall@4.0.10: + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.2 + get-intrinsic: 1.2.1 + has-symbols: 1.0.3 + internal-slot: 1.0.5 + regexp.prototype.flags: 1.5.1 + set-function-name: 2.0.1 + side-channel: 1.0.4 + + string.prototype.padend@3.1.5: + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.2 + + string.prototype.trim@1.2.8: + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.2 + + string.prototype.trimend@1.0.7: + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.2 + + string.prototype.trimstart@1.0.7: + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.2 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.0.1 + + strip-bom@3.0.0: {} + + strip-final-newline@2.0.0: {} + + strip-final-newline@3.0.0: {} + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + strip-json-comments@2.0.1: + optional: true + + strip-json-comments@3.1.1: {} + + strip-literal@2.1.0: + dependencies: + js-tokens: 9.0.0 + + style-mod@4.1.0: {} + + style-mod@4.1.2: {} + + style-to-object@0.4.2: + dependencies: + inline-style-parser: 0.1.1 + + styled-jsx@5.1.1(@babel/core@7.23.5)(react@18.2.0): + dependencies: + client-only: 0.0.1 + react: 18.2.0 + optionalDependencies: + '@babel/core': 7.23.5 + + stylehacks@7.0.4(postcss@8.5.3): + dependencies: + browserslist: 4.24.4 + postcss: 8.5.3 + postcss-selector-parser: 6.1.2 + + sucrase@3.34.0: + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + commander: 4.1.1 + glob: 7.1.6 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.6 + ts-interface-checker: 0.1.13 + + sucrase@3.35.0: + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + commander: 4.1.1 + glob: 10.3.15 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.6 + ts-interface-checker: 0.1.13 + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-color@9.4.0: {} + + supports-preserve-symlinks-flag@1.0.0: {} + + svelte@4.2.17: + dependencies: + '@ampproject/remapping': 2.3.0 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + '@types/estree': 1.0.7 + acorn: 8.14.1 + aria-query: 5.3.2 + axobject-query: 4.1.0 + code-red: 1.0.4 + css-tree: 2.3.1 + estree-walker: 3.0.3 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.17 + periscopic: 3.1.0 + optional: true + + svgo@3.3.2: + dependencies: + '@trysound/sax': 0.2.0 + commander: 7.2.0 + css-select: 5.1.0 + css-tree: 2.3.1 + css-what: 6.1.0 + csso: 5.0.5 + picocolors: 1.1.1 + + swap-case@2.0.2: + dependencies: + tslib: 2.6.2 + + swr@2.2.4(react@18.2.0): + dependencies: + client-only: 0.0.1 + react: 18.2.0 + use-sync-external-store: 1.2.0(react@18.2.0) + + synckit@0.6.2: + dependencies: + tslib: 2.6.2 + + tabbable@6.2.0: {} + + tailwind-merge@1.14.0: {} + + tailwind-merge@2.2.0: + dependencies: + '@babel/runtime': 7.24.4 + + tailwindcss-animate@1.0.7(tailwindcss@3.3.3(ts-node@10.9.2(@types/node@17.0.45)(typescript@5.8.2))): + dependencies: + tailwindcss: 3.3.3(ts-node@10.9.2(@types/node@17.0.45)(typescript@5.8.2)) + + tailwindcss@3.3.3(ts-node@10.9.2(@types/node@17.0.45)(typescript@5.8.2)): + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.5.3 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.1 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.0 + lilconfig: 2.1.0 + micromatch: 4.0.5 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.0.0 + postcss: 8.4.31 + postcss-import: 15.1.0(postcss@8.4.31) + postcss-js: 4.0.1(postcss@8.4.31) + postcss-load-config: 4.0.1(postcss@8.4.31)(ts-node@10.9.2(@types/node@17.0.45)(typescript@5.8.2)) + postcss-nested: 6.0.1(postcss@8.4.31) + postcss-selector-parser: 6.0.13 + resolve: 1.22.8 + sucrase: 3.34.0 + transitivePeerDependencies: + - ts-node + + tailwindcss@3.4.0(ts-node@10.9.2(@swc/core@1.3.101(@swc/helpers@0.5.2))(@types/node@20.12.12)(typescript@5.8.2)): + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.2 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.0 + lilconfig: 2.1.0 + micromatch: 4.0.6 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.0.1 + postcss: 8.4.38 + postcss-import: 15.1.0(postcss@8.4.38) + postcss-js: 4.0.1(postcss@8.4.38) + postcss-load-config: 4.0.2(postcss@8.4.38)(ts-node@10.9.2(@swc/core@1.3.101(@swc/helpers@0.5.2))(@types/node@20.12.12)(typescript@5.8.2)) + postcss-nested: 6.0.1(postcss@8.4.38) + postcss-selector-parser: 6.0.16 + resolve: 1.22.8 + sucrase: 3.35.0 + transitivePeerDependencies: + - ts-node + + tapable@2.2.1: {} + + tar-fs@2.1.1: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.0 + tar-stream: 2.2.0 + optional: true + + tar-fs@3.0.6: + dependencies: + pump: 3.0.0 + tar-stream: 3.1.7 + optionalDependencies: + bare-fs: 2.3.5 + bare-path: 2.1.3 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.4 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + optional: true + + tar-stream@3.1.7: + dependencies: + b4a: 1.6.7 + fast-fifo: 1.3.2 + streamx: 2.20.1 + + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + + terser-webpack-plugin@5.3.10(@swc/core@1.3.101(@swc/helpers@0.5.2))(esbuild@0.19.11)(webpack@5.91.0(@swc/core@1.3.101(@swc/helpers@0.5.2))(esbuild@0.19.11)): + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + jest-worker: 27.5.1 + schema-utils: 3.3.0 + serialize-javascript: 6.0.2 + terser: 5.31.0 + webpack: 5.91.0(@swc/core@1.3.101(@swc/helpers@0.5.2))(esbuild@0.19.11) + optionalDependencies: + '@swc/core': 1.3.101(@swc/helpers@0.5.2) + esbuild: 0.19.11 + + terser@5.31.0: + dependencies: + '@jridgewell/source-map': 0.3.6 + acorn: 8.14.1 + commander: 2.20.3 + source-map-support: 0.5.21 + + text-decoder@1.2.1: {} + + text-table@0.2.0: {} + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + thread-stream@2.7.0: + dependencies: + real-require: 0.2.0 + + through2@2.0.5: + dependencies: + readable-stream: 2.3.8 + xtend: 4.0.2 + + through@2.3.8: {} + + tiny-inflate@1.0.3: {} + + tiny-invariant@1.3.3: {} + + tinybench@2.8.0: {} + + tinycolor2@1.6.0: {} + + tinyglobby@0.2.12: + dependencies: + fdir: 6.4.3(picomatch@4.0.2) + picomatch: 4.0.2 + + tinygradient@0.4.3: + dependencies: + '@types/tinycolor2': 1.4.6 + tinycolor2: 1.6.0 + + tinypool@0.8.4: {} + + tinyspy@2.2.1: {} + + tippy.js@6.3.7: + dependencies: + '@popperjs/core': 2.11.8 + + title-case@3.0.3: + dependencies: + tslib: 2.6.2 + + tmp@0.0.33: + dependencies: + os-tmpdir: 1.0.2 + + tmp@0.2.3: {} + + to-fast-properties@2.0.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toggle-selection@1.0.6: {} + + toidentifier@1.0.1: {} + + toml-eslint-parser@0.9.3: + dependencies: + eslint-visitor-keys: 3.4.3 + + toml@3.0.0: {} + + topbar@0.1.4: {} + + tr46@0.0.3: {} + + tr46@1.0.1: + dependencies: + punycode: 2.3.1 + + tree-kill@1.2.2: {} + + trim-lines@3.0.1: {} + + trough@2.1.0: {} + + ts-api-utils@1.3.0(typescript@5.3.3): + dependencies: + typescript: 5.3.3 + + ts-api-utils@1.3.0(typescript@5.4.5): + dependencies: + typescript: 5.4.5 + + ts-api-utils@1.3.0(typescript@5.8.2): + dependencies: + typescript: 5.8.2 + + ts-interface-checker@0.1.13: {} + + ts-log@2.2.5: {} + + ts-node@10.9.2(@swc/core@1.3.101(@swc/helpers@0.5.2))(@types/node@18.19.33)(typescript@5.3.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 18.19.33 + acorn: 8.11.3 + acorn-walk: 8.3.2 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.3.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optionalDependencies: + '@swc/core': 1.3.101(@swc/helpers@0.5.2) + + ts-node@10.9.2(@swc/core@1.3.101(@swc/helpers@0.5.2))(@types/node@18.19.33)(typescript@5.4.5): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 18.19.33 + acorn: 8.11.3 + acorn-walk: 8.3.2 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.4.5 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optionalDependencies: + '@swc/core': 1.3.101(@swc/helpers@0.5.2) + optional: true + + ts-node@10.9.2(@swc/core@1.3.101(@swc/helpers@0.5.2))(@types/node@20.12.12)(typescript@5.8.2): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.12.12 + acorn: 8.11.3 + acorn-walk: 8.3.2 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.8.2 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optionalDependencies: + '@swc/core': 1.3.101(@swc/helpers@0.5.2) + optional: true + + ts-node@10.9.2(@types/node@17.0.45)(typescript@5.8.2): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 17.0.45 + acorn: 8.14.1 + acorn-walk: 8.3.2 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.8.2 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optional: true + + tsc-watch@6.2.0(typescript@5.3.3): + dependencies: + cross-spawn: 7.0.3 + node-cleanup: 2.1.2 + ps-tree: 1.2.0 + string-argv: 0.3.2 + typescript: 5.3.3 + + tsc-watch@6.2.0(typescript@5.4.5): + dependencies: + cross-spawn: 7.0.3 + node-cleanup: 2.1.2 + ps-tree: 1.2.0 + string-argv: 0.3.2 + typescript: 5.4.5 + + tsconfig-paths@3.14.2: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@1.14.1: {} + + tslib@2.5.3: {} + + tslib@2.6.2: {} + + tsscmp@1.0.6: {} + + tsup@8.0.2(@swc/core@1.3.101(@swc/helpers@0.5.2))(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.3.101(@swc/helpers@0.5.2))(@types/node@18.19.33)(typescript@5.3.3))(typescript@5.3.3): + dependencies: + bundle-require: 4.1.0(esbuild@0.19.12) + cac: 6.7.14 + chokidar: 3.6.0 + debug: 4.3.4(supports-color@9.4.0) + esbuild: 0.19.12 + execa: 5.1.1 + globby: 11.1.0 + joycon: 3.1.1 + postcss-load-config: 4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.3.101(@swc/helpers@0.5.2))(@types/node@18.19.33)(typescript@5.3.3)) + resolve-from: 5.0.0 + rollup: 4.36.0 + source-map: 0.8.0-beta.0 + sucrase: 3.35.0 + tree-kill: 1.2.2 + optionalDependencies: + '@swc/core': 1.3.101(@swc/helpers@0.5.2) + postcss: 8.5.3 + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + - ts-node + + tsup@8.0.2(@swc/core@1.3.101(@swc/helpers@0.5.2))(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.3.101(@swc/helpers@0.5.2))(@types/node@18.19.33)(typescript@5.4.5))(typescript@5.4.5): + dependencies: + bundle-require: 4.1.0(esbuild@0.19.12) + cac: 6.7.14 + chokidar: 3.6.0 + debug: 4.3.4(supports-color@9.4.0) + esbuild: 0.19.12 + execa: 5.1.1 + globby: 11.1.0 + joycon: 3.1.1 + postcss-load-config: 4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.3.101(@swc/helpers@0.5.2))(@types/node@18.19.33)(typescript@5.4.5)) + resolve-from: 5.0.0 + rollup: 4.36.0 + source-map: 0.8.0-beta.0 + sucrase: 3.35.0 + tree-kill: 1.2.2 + optionalDependencies: + '@swc/core': 1.3.101(@swc/helpers@0.5.2) + postcss: 8.5.3 + typescript: 5.4.5 + transitivePeerDependencies: + - supports-color + - ts-node + + tsutils@3.21.0(typescript@5.8.2): + dependencies: + tslib: 1.14.1 + typescript: 5.8.2 + + tsx@4.10.5: + dependencies: + esbuild: 0.20.2 + get-tsconfig: 4.7.5 + optionalDependencies: + fsevents: 2.3.3 + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + optional: true + + tunnel@0.0.6: {} + + turbo-darwin-64@1.13.3: + optional: true + + turbo-darwin-arm64@1.13.3: + optional: true + + turbo-linux-64@1.13.3: + optional: true + + turbo-linux-arm64@1.13.3: + optional: true + + turbo-windows-64@1.13.3: + optional: true + + turbo-windows-arm64@1.13.3: + optional: true + + turbo@1.13.3: + optionalDependencies: + turbo-darwin-64: 1.13.3 + turbo-darwin-arm64: 1.13.3 + turbo-linux-64: 1.13.3 + turbo-linux-arm64: 1.13.3 + turbo-windows-64: 1.13.3 + turbo-windows-arm64: 1.13.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-detect@4.0.8: {} + + type-fest@0.20.2: {} + + type-fest@0.21.3: {} + + type-fest@0.6.0: {} + + type-fest@0.7.1: {} + + type-fest@0.8.1: {} + + type-fest@2.19.0: {} + + type-fest@3.13.1: {} + + type-fest@4.21.0: {} + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + typed-array-buffer@1.0.0: + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.1 + is-typed-array: 1.1.12 + + typed-array-byte-length@1.0.0: + dependencies: + call-bind: 1.0.5 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + + typed-array-byte-offset@1.0.0: + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.5 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + + typed-array-length@1.0.4: + dependencies: + call-bind: 1.0.5 + for-each: 0.3.3 + is-typed-array: 1.1.12 + + typed-rest-client@1.8.11: + dependencies: + qs: 6.12.1 + tunnel: 0.0.6 + underscore: 1.13.6 + + typescript@5.1.6: {} + + typescript@5.3.3: {} + + typescript@5.4.5: {} + + typescript@5.8.2: {} + + ua-parser-js@1.0.37: {} + + uc.micro@1.0.6: {} + + uc.micro@2.1.0: {} + + ufo@1.5.3: {} + + ufo@1.5.4: {} + + unbox-primitive@1.0.2: + dependencies: + call-bind: 1.0.5 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + + unbuild@2.0.0(typescript@5.4.5): + dependencies: + '@rollup/plugin-alias': 5.1.1(rollup@3.29.5) + '@rollup/plugin-commonjs': 25.0.8(rollup@3.29.5) + '@rollup/plugin-json': 6.1.0(rollup@3.29.5) + '@rollup/plugin-node-resolve': 15.3.1(rollup@3.29.5) + '@rollup/plugin-replace': 5.0.7(rollup@3.29.5) + '@rollup/pluginutils': 5.1.4(rollup@3.29.5) + chalk: 5.3.0 + citty: 0.1.6 + consola: 3.4.2 + defu: 6.1.4 + esbuild: 0.19.12 + globby: 13.2.2 + hookable: 5.5.3 + jiti: 1.21.7 + magic-string: 0.30.17 + mkdist: 1.6.0(typescript@5.4.5) + mlly: 1.7.4 + pathe: 1.1.2 + pkg-types: 1.3.1 + pretty-bytes: 6.1.1 + rollup: 3.29.5 + rollup-plugin-dts: 6.1.1(rollup@3.29.5)(typescript@5.4.5) + scule: 1.3.0 + untyped: 1.5.2 + optionalDependencies: + typescript: 5.4.5 + transitivePeerDependencies: + - sass + - supports-color + - vue-tsc + + unc-path-regex@0.1.2: {} + + underscore@1.13.6: {} + + undici-types@5.26.5: {} + + undici@6.19.2: {} + + unicode-trie@2.0.0: + dependencies: + pako: 0.2.9 + tiny-inflate: 1.0.3 + + unicorn-magic@0.1.0: {} + + unified@10.1.2: + dependencies: + '@types/unist': 2.0.8 + bail: 2.0.2 + extend: 3.0.2 + is-buffer: 2.0.5 + is-plain-obj: 4.1.0 + trough: 2.1.0 + vfile: 5.3.7 + + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.1.0 + vfile: 6.0.3 + + unist-util-generated@2.0.1: {} + + unist-util-is@5.2.1: + dependencies: + '@types/unist': 2.0.8 + + unist-util-is@6.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@4.0.4: + dependencies: + '@types/unist': 2.0.8 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@2.0.3: + dependencies: + '@types/unist': 2.0.8 + + unist-util-stringify-position@3.0.3: + dependencies: + '@types/unist': 2.0.8 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@5.1.3: + dependencies: + '@types/unist': 2.0.8 + unist-util-is: 5.2.1 + + unist-util-visit-parents@6.0.1: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-visit@4.1.2: + dependencies: + '@types/unist': 2.0.8 + unist-util-is: 5.2.1 + unist-util-visit-parents: 5.1.3 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + universalify@2.0.1: {} + + unixify@1.0.0: + dependencies: + normalize-path: 2.1.1 + + untyped@1.5.2: + dependencies: + '@babel/core': 7.26.10 + '@babel/standalone': 7.26.10 + '@babel/types': 7.26.10 + citty: 0.1.6 + defu: 6.1.4 + jiti: 2.4.2 + knitwork: 1.2.0 + scule: 1.3.0 + transitivePeerDependencies: + - supports-color + + update-browserslist-db@1.0.13(browserslist@4.22.1): + dependencies: + browserslist: 4.22.1 + escalade: 3.1.2 + picocolors: 1.0.0 + + update-browserslist-db@1.0.13(browserslist@4.23.0): + dependencies: + browserslist: 4.23.0 + escalade: 3.1.2 + picocolors: 1.0.0 + + update-browserslist-db@1.1.3(browserslist@4.24.4): + dependencies: + browserslist: 4.24.4 + escalade: 3.2.0 + picocolors: 1.1.1 + + upper-case-first@2.0.2: + dependencies: + tslib: 2.6.2 + + upper-case@2.0.2: + dependencies: + tslib: 2.6.2 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + url-join@4.0.1: {} + + urlpattern-polyfill@8.0.2: {} + + urlpattern-polyfill@9.0.0: {} + + urql@4.0.6(graphql@16.8.1)(react@18.2.0): + dependencies: + '@urql/core': 4.2.3(graphql@16.8.1) + react: 18.2.0 + wonka: 6.3.4 + transitivePeerDependencies: + - graphql + + use-callback-ref@1.3.0(@types/react@18.2.23)(react@18.2.0): + dependencies: + react: 18.2.0 + tslib: 2.6.2 + optionalDependencies: + '@types/react': 18.2.23 + + use-composed-ref@1.3.0(react@18.2.0): + dependencies: + react: 18.2.0 + + use-isomorphic-layout-effect@1.1.2(@types/react@18.2.23)(react@18.2.0): + dependencies: + react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.23 + + use-latest@1.2.1(@types/react@18.2.23)(react@18.2.0): + dependencies: + react: 18.2.0 + use-isomorphic-layout-effect: 1.1.2(@types/react@18.2.23)(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + + use-local-storage@3.0.0(react@18.2.0): + dependencies: + react: 18.2.0 + + use-sidecar@1.1.2(@types/react@18.2.23)(react@18.2.0): + dependencies: + detect-node-es: 1.1.0 + react: 18.2.0 + tslib: 2.6.2 + optionalDependencies: + '@types/react': 18.2.23 + + use-sync-external-store@1.2.0(react@18.2.0): + dependencies: + react: 18.2.0 + + use-sync-external-store@1.2.2(react@18.2.0): + dependencies: + react: 18.2.0 + + util-deprecate@1.0.2: {} + + util@0.12.5: + dependencies: + inherits: 2.0.4 + is-arguments: 1.1.1 + is-generator-function: 1.0.10 + is-typed-array: 1.1.13 + which-typed-array: 1.1.15 + + uuid@8.3.2: {} + + uuid@9.0.1: {} + + uvu@0.5.6: + dependencies: + dequal: 2.0.3 + diff: 5.2.0 + kleur: 4.1.5 + sade: 1.8.1 + + v8-compile-cache-lib@3.0.1: {} + + validate-npm-package-license@3.0.4: + dependencies: + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + + value-or-promise@1.0.12: {} + + vary@1.1.2: {} + + vfile-location@4.1.0: + dependencies: + '@types/unist': 2.0.8 + vfile: 5.3.7 + + vfile-message@3.1.4: + dependencies: + '@types/unist': 2.0.8 + unist-util-stringify-position: 3.0.3 + + vfile-message@4.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@5.3.7: + dependencies: + '@types/unist': 2.0.8 + is-buffer: 2.0.5 + unist-util-stringify-position: 3.0.3 + vfile-message: 3.1.4 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.2 + + victory-vendor@36.9.2: + dependencies: + '@types/d3-array': 3.2.1 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.8 + '@types/d3-shape': 3.1.6 + '@types/d3-time': 3.0.3 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + + vite-node@1.6.0(@types/node@17.0.45)(terser@5.31.0): + dependencies: + cac: 6.7.14 + debug: 4.3.4 + pathe: 1.1.2 + picocolors: 1.0.1 + vite: 5.2.11(@types/node@17.0.45)(terser@5.31.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + + vite-node@1.6.0(@types/node@20.12.12)(terser@5.31.0): + dependencies: + cac: 6.7.14 + debug: 4.3.4(supports-color@9.4.0) + pathe: 1.1.2 + picocolors: 1.0.1 + vite: 5.2.11(@types/node@20.12.12)(terser@5.31.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + + vite@5.2.11(@types/node@17.0.45)(terser@5.31.0): + dependencies: + esbuild: 0.20.2 + postcss: 8.4.38 + rollup: 4.36.0 + optionalDependencies: + '@types/node': 17.0.45 + fsevents: 2.3.3 + terser: 5.31.0 + + vite@5.2.11(@types/node@20.12.12)(terser@5.31.0): + dependencies: + esbuild: 0.20.2 + postcss: 8.4.38 + rollup: 4.36.0 + optionalDependencies: + '@types/node': 20.12.12 + fsevents: 2.3.3 + terser: 5.31.0 + + vitest@1.6.0(@types/node@17.0.45)(terser@5.31.0): + dependencies: + '@vitest/expect': 1.6.0 + '@vitest/runner': 1.6.0 + '@vitest/snapshot': 1.6.0 + '@vitest/spy': 1.6.0 + '@vitest/utils': 1.6.0 + acorn-walk: 8.3.2 + chai: 4.4.1 + debug: 4.3.4 + execa: 8.0.1 + local-pkg: 0.5.0 + magic-string: 0.30.10 + pathe: 1.1.2 + picocolors: 1.0.1 + std-env: 3.7.0 + strip-literal: 2.1.0 + tinybench: 2.8.0 + tinypool: 0.8.4 + vite: 5.2.11(@types/node@17.0.45)(terser@5.31.0) + vite-node: 1.6.0(@types/node@17.0.45)(terser@5.31.0) + why-is-node-running: 2.2.2 + optionalDependencies: + '@types/node': 17.0.45 + transitivePeerDependencies: + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + + vitest@1.6.0(@types/node@20.12.12)(terser@5.31.0): + dependencies: + '@vitest/expect': 1.6.0 + '@vitest/runner': 1.6.0 + '@vitest/snapshot': 1.6.0 + '@vitest/spy': 1.6.0 + '@vitest/utils': 1.6.0 + acorn-walk: 8.3.2 + chai: 4.4.1 + debug: 4.3.4(supports-color@9.4.0) + execa: 8.0.1 + local-pkg: 0.5.0 + magic-string: 0.30.10 + pathe: 1.1.2 + picocolors: 1.0.1 + std-env: 3.7.0 + strip-literal: 2.1.0 + tinybench: 2.8.0 + tinypool: 0.8.4 + vite: 5.2.11(@types/node@20.12.12)(terser@5.31.0) + vite-node: 1.6.0(@types/node@20.12.12)(terser@5.31.0) + why-is-node-running: 2.2.2 + optionalDependencies: + '@types/node': 20.12.12 + transitivePeerDependencies: + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + + vscode-jsonrpc@8.2.0: {} + + vscode-languageclient@9.0.1: + dependencies: + minimatch: 5.1.6 + semver: 7.6.2 + vscode-languageserver-protocol: 3.17.5 + + vscode-languageserver-protocol@3.17.5: + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + + vscode-languageserver-textdocument@1.0.11: {} + + vscode-languageserver-types@3.17.5: {} + + vscode-languageserver@9.0.1: + dependencies: + vscode-languageserver-protocol: 3.17.5 + + vscode-uri@3.0.8: {} + + vue-eslint-parser@9.4.2(eslint@9.3.0): + dependencies: + debug: 4.3.4(supports-color@9.4.0) + eslint: 9.3.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.5.0 + lodash: 4.17.21 + semver: 7.6.2 + transitivePeerDependencies: + - supports-color + + w3c-keyname@2.2.8: {} + + watchpack@2.4.0: + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + + watchpack@2.4.1: + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + web-namespaces@2.0.1: {} + + web-streams-polyfill@3.2.1: {} + + web-streams-polyfill@4.0.0-beta.3: {} + + web-tree-sitter@0.20.8: {} + + webcrypto-core@1.7.7: + dependencies: + '@peculiar/asn1-schema': 2.3.8 + '@peculiar/json-schema': 1.1.12 + asn1js: 3.0.5 + pvtsutils: 1.3.5 + tslib: 2.6.2 + + webidl-conversions@3.0.1: {} + + webidl-conversions@4.0.2: {} + + webpack-sources@3.2.3: {} + + webpack@5.91.0(@swc/core@1.3.101(@swc/helpers@0.5.2))(esbuild@0.19.11): + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.6 + '@webassemblyjs/ast': 1.12.1 + '@webassemblyjs/wasm-edit': 1.12.1 + '@webassemblyjs/wasm-parser': 1.12.1 + acorn: 8.11.3 + acorn-import-assertions: 1.9.0(acorn@8.11.3) + browserslist: 4.23.0 + chrome-trace-event: 1.0.3 + enhanced-resolve: 5.16.1 + es-module-lexer: 1.5.3 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.0 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 3.3.0 + tapable: 2.2.1 + terser-webpack-plugin: 5.3.10(@swc/core@1.3.101(@swc/helpers@0.5.2))(esbuild@0.19.11)(webpack@5.91.0(@swc/core@1.3.101(@swc/helpers@0.5.2))(esbuild@0.19.11)) + watchpack: 2.4.1 + webpack-sources: 3.2.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + + whatwg-fetch@3.6.19: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + whatwg-url@7.1.0: + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + + which-boxed-primitive@1.0.2: + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + + which-builtin-type@1.1.3: + dependencies: + function.prototype.name: 1.1.6 + has-tostringtag: 1.0.0 + is-async-function: 2.0.0 + is-date-object: 1.0.5 + is-finalizationregistry: 1.0.2 + is-generator-function: 1.0.10 + is-regex: 1.1.4 + is-weakref: 1.0.2 + isarray: 2.0.5 + which-boxed-primitive: 1.0.2 + which-collection: 1.0.2 + which-typed-array: 1.1.15 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.3 + + which-module@2.0.1: {} + + which-typed-array@1.1.11: + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.5 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + + which-typed-array@1.1.15: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.2 + + which@1.3.1: + dependencies: + isexe: 2.0.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.2.2: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + win-ca@3.5.1: + dependencies: + is-electron: 2.2.2 + make-dir: 1.3.0 + node-forge: 1.3.1 + split: 1.0.1 + + wonka@6.3.4: {} + + word-wrap@1.2.5: {} + + wordwrap@0.0.3: {} + + workerpool@6.2.1: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + + wrap-ansi@9.0.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 7.1.0 + strip-ansi: 7.1.0 + + wrappy@1.0.2: {} + + ws@8.11.0: {} + + ws@8.14.2: {} + + xml-name-validator@4.0.0: {} + + xml2js@0.5.0: + dependencies: + sax: 1.3.0 + xmlbuilder: 11.0.1 + + xmlbuilder@11.0.1: {} + + xmlhttprequest-ssl@2.0.0: {} + + xtend@4.0.2: {} + + y18n@4.0.3: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yallist@4.0.0: {} + + yaml-ast-parser@0.0.43: {} + + yaml-eslint-parser@1.2.2: + dependencies: + eslint-visitor-keys: 3.4.3 + lodash: 4.17.21 + yaml: 2.4.2 + + yaml@2.3.4: {} + + yaml@2.4.2: {} + + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + + yargs-parser@20.2.4: {} + + yargs-parser@21.1.1: {} + + yargs-unparser@2.0.0: + dependencies: + camelcase: 6.3.0 + decamelize: 4.0.0 + flat: 5.0.2 + is-plain-obj: 2.1.0 + + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + + yargs@16.2.0: + dependencies: + cliui: 7.0.4 + escalade: 3.1.2 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.4 + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.1.2 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + + yazl@2.5.1: + dependencies: + buffer-crc32: 0.2.13 + + ylru@1.4.0: {} + + yn@3.1.1: {} + + yocto-queue@0.1.0: {} + + yocto-queue@1.0.0: {} + + yoga-wasm-web@0.3.3: {} + + zod@3.21.4: {} + + zod@3.22.4: {} + + zustand@4.4.6(@types/react@18.2.23)(react@18.2.0): + dependencies: + use-sync-external-store: 1.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.23 + react: 18.2.0 + + zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 000000000000..6595736d1597 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +packages: + - clients/* + - ee/tabby-ui + - ee/tabby-email \ No newline at end of file diff --git a/python/tabby-eval/modal/predict.py b/python/tabby-eval/modal/predict.py index 8f288400cd65..d85b27639655 100644 --- a/python/tabby-eval/modal/predict.py +++ b/python/tabby-eval/modal/predict.py @@ -160,7 +160,7 @@ def read_dataframe_from_file(language: str, file: str) -> pd.DataFrame: @stub.local_entrypoint() async def main(language: str, files: str): - #Multiple files seperated by ',' + #Multiple files separated by ',' model = Model() diff --git a/rules/do-not-use-logkit-crate.yml b/rules/do-not-use-logkit-crate.yml new file mode 100644 index 000000000000..4d013e8eb40c --- /dev/null +++ b/rules/do-not-use-logkit-crate.yml @@ -0,0 +1,8 @@ +id: do-not-use-logkit-crate +message: Don't use logkit crate with use statement to avoid conflicts with the tracing crate. logkit crate is only used for background job logging to enrich the jobs output in admin UI. +severity: error +language: rust +files: +- ./** +rule: + pattern: use logkit::$$$; \ No newline at end of file diff --git a/rules/do-not-use-next-pages.yml b/rules/do-not-use-next-pages.yml new file mode 100644 index 000000000000..c3e700f872c5 --- /dev/null +++ b/rules/do-not-use-next-pages.yml @@ -0,0 +1,8 @@ +id: do-not-use-next-pages +message: Don't use next pages routing as we're fully commited to app router. +severity: error +language: typescript +files: +- ./ee/tabby-ui/** +rule: + pattern: import $$$ from 'next/router' \ No newline at end of file diff --git a/rules/only-dao-and-policy-can-depend-tabby-db.yml b/rules/only-dao-and-policy-can-depend-tabby-db.yml new file mode 100644 index 000000000000..358f9d55d6bc --- /dev/null +++ b/rules/only-dao-and-policy-can-depend-tabby-db.yml @@ -0,0 +1,11 @@ +id: only-dao-can-depend-tabby-db +message: Only dao can depend on tabby-db +severity: error +language: rust +files: +- ./ee/tabby-schema/src/** +ignores: +- ./ee/tabby-schema/src/dao.rs +- ./ee/tabby-schema/src/policy.rs +rule: + pattern: tabby_db diff --git a/rules/use-basic-job.yml b/rules/use-basic-job.yml new file mode 100644 index 000000000000..63a4bec7c525 --- /dev/null +++ b/rules/use-basic-job.yml @@ -0,0 +1,10 @@ +id: use-basic-job +message: Use BasicJob / CronJob for worker creation. +severity: error +language: rust +files: +- ./ee/tabby-webserver/src/service/background_job/** +ignores: +- ./ee/tabby-webserver/src/service/background_job/helper/mod.rs +rule: + pattern: WorkerBuilder \ No newline at end of file diff --git a/rules/use-schema-result.yml b/rules/use-schema-result.yml new file mode 100644 index 000000000000..d178a19ec6c6 --- /dev/null +++ b/rules/use-schema-result.yml @@ -0,0 +1,17 @@ +id: use-schema-result +message: Use schema::Result as API interface +severity: error +language: rust +files: +- ./ee/tabby-schema/src/** +ignores: +- ./ee/tabby-schema/src/lib.rs +- ./ee/tabby-schema/src/dao.rs +rule: + any: + - pattern: anyhow + not: + inside: + kind: enum_variant + stopBy: end + - pattern: FieldResult \ No newline at end of file diff --git a/rules/validate-requires-code.yml b/rules/validate-requires-code.yml new file mode 100644 index 000000000000..ca74dfffd2ea --- /dev/null +++ b/rules/validate-requires-code.yml @@ -0,0 +1,29 @@ +id: validate-requires-code +message: Validations requires code / message being set for frontend error display +severity: error +language: rust +files: + - ./ee/tabby-webserver/src/** + - ./ee/tabby-schema/src/** +rule: + all: + - pattern: "#[validate]" + - not: + all: + - has: + stopBy: end + pattern: code + - has: + stopBy: end + pattern: message + - not: + any: + - has: + stopBy: end + pattern: custom + - has: + stopBy: end + pattern: nested + - has: + stopBy: end + pattern: schema diff --git a/sgconfig.yml b/sgconfig.yml new file mode 100644 index 000000000000..790229a4a44d --- /dev/null +++ b/sgconfig.yml @@ -0,0 +1,2 @@ +ruleDirs: +- rules \ No newline at end of file diff --git a/turbo.json b/turbo.json new file mode 100644 index 000000000000..1173dd8708c9 --- /dev/null +++ b/turbo.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://turbo.build/schema.json", + "pipeline": { + "build": { + "dependsOn": [ + "^build" + ], + "outputs": [ + "out/**", + "dist/**", + ".next/**", + "!.next/cache/**" + ] + }, + "lint": {}, + "lint:fix": {}, + "test": {}, + "vscode:dev": {} + } +} \ No newline at end of file diff --git a/website/blog/2023-08-31-first-stable-release.md b/website/blog/2023-08-31-first-stable-release.md index ff17c9b7444a..ff8444311708 100644 --- a/website/blog/2023-08-31-first-stable-release.md +++ b/website/blog/2023-08-31-first-stable-release.md @@ -1,5 +1,4 @@ --- -slug: first-stable-release title: "Introducing First Stable Release: v0.0.1" authors: [ meng ] tags: [release] diff --git a/website/blog/2023-09-05-deploy-tabby-to-huggingface-space/index.md b/website/blog/2023-09-05-deploy-tabby-to-huggingface-space/index.md index a87ac42bdf81..67ec7bc4a786 100644 --- a/website/blog/2023-09-05-deploy-tabby-to-huggingface-space/index.md +++ b/website/blog/2023-09-05-deploy-tabby-to-huggingface-space/index.md @@ -1,5 +1,5 @@ --- -slug: deploy-tabby-to-huggingface-space.md +slug: /2023/09/05/deploy-tabby-to-huggingface-space title: Deploying a Tabby Instance in Hugging Face Spaces authors: - name: Rand Xie @@ -25,7 +25,7 @@ Let’s firstly take a look at what steps are needed to deploy a Tabby instance **Step 3:** After space is built, you will be able to send requests to the APIs. -That's it! With the hosted APIs, now you can connect Tabby's [IDE extensions](/docs/getting-started) to the API endpoint. Next, we will deep dive into each step with screenshots!! +That's it! With the hosted APIs, now you can connect Tabby's [IDE extensions](/docs/extensions/installation/vscode) to the API endpoint. Next, we will deep dive into each step with screenshots!! **Everything will be done in the Hugging Face UI. No local setup is needed.** :::tip @@ -82,7 +82,7 @@ If the App is up successfully, you should see this page: #### Call code completion API -Now, you are able to call the completion API. The full URL is https://{YOUR-ACCOUNT-NAME}-tabbyml.hf.space/v1/completions. In this post, the URL is https://randxie-tabbyml.hf.space/v1/completions. +Now, you are able to call the completion API. The full URL is https://YOUR-ACCOUNT-NAME-tabbyml.hf.space/v1/completions. In this post, the URL is https://randxie-tabbyml.hf.space/v1/completions. To test if your APIs are up and running, use [this online tool](https://reqbin.com/curl) to send curl commands: diff --git a/website/blog/2023-10-14-seed-round-release-0-3-0-RAG.md b/website/blog/2023-10-14-seed-round-release-0-3-0-RAG.md index ecf54f497eed..91d384f97d29 100644 --- a/website/blog/2023-10-14-seed-round-release-0-3-0-RAG.md +++ b/website/blog/2023-10-14-seed-round-release-0-3-0-RAG.md @@ -2,6 +2,7 @@ authors: [ meng, gyxlucy ] tags: [release] +slug: /2023/10/14/seed-round-release-0-3-0 --- # Announcing our $3.2M seed round, and the long-awaited RAG release in Tabby v0.3.0 diff --git a/website/blog/2023-10-16-repository-context-for-code-completion/index.md b/website/blog/2023-10-16-repository-context-for-code-completion/index.md index 12cef22ed447..1d2fecbc8f6f 100644 --- a/website/blog/2023-10-16-repository-context-for-code-completion/index.md +++ b/website/blog/2023-10-16-repository-context-for-code-completion/index.md @@ -10,7 +10,7 @@ image: ./whiteboard.jpg
    ![Whiteboard](./whiteboard.jpg) -*Credit: [Elon Mask's tweet](https://twitter.com/elonmusk/status/1593899029531803649)* +*Credit: [Elon Musk's tweet](https://twitter.com/elonmusk/status/1593899029531803649)*
    @@ -159,6 +159,5 @@ We are incredibly enthusiastic about the potential for enhancing the quality and ## Give it a try To use this repository context feature: -1. Installing [tabby](/docs/installation/). -2. Navigate to the [Configuration](/docs/configuration#repository-context-for-code-completion) page and set up your `~/.tabby/config.toml` -3. Finally, run `tabby scheduler` to build an index and unlock the full potential of this innovative feature! 🛠️ +1. Installing [tabby](/docs/quick-start/installation/docker). +2. Navigate to the [Repository Context](/docs/administration/context) page and follow the instructions to set it up. \ No newline at end of file diff --git a/website/blog/2023-10-21-incremental-decoding/index.md b/website/blog/2023-10-21-incremental-decoding/index.md index 790932d7bbff..048adfb72abb 100644 --- a/website/blog/2023-10-21-incremental-decoding/index.md +++ b/website/blog/2023-10-21-incremental-decoding/index.md @@ -51,7 +51,7 @@ However, as it's now an undeterministic approach, sometimes the models could gen ## Era of Streaming for LLM Latency is key in user experience for many LLM applications. In order to minimize the idle time for users, **streaming response** is commonly adopted. In LLM streaming, we start decoding the response as soon as it's available, instead of waiting for the entire response to be returned. -Considering streaming process in LLM decoding, although greedy decoding often produces sub-optimal results compared to beam decoding or sampling-methods methods, it wins with its fast and parallelizable computation. Most LLM applications today (e.g. ChatGPT, Bard, Anthropic, etc.) have adopted greedy decoding with certain samplings and carefully tuned them for different tasks: creative tasks such as chatbots or writing articles receives diverse responses from samplings; input-grounded tasks such as translation or coding benefit from greedy decoding to get the immediate "correct" result. (Indeed, ⌨️ coding tasks emphasize more on the consistency with given context - lines of code you just wrote, than the variations of possible responses.😆) +Considering streaming process in LLM decoding, although greedy decoding often produces sub-optimal results compared to beam decoding or sampling-based methods, it wins with its fast and parallelizable computation. Most LLM applications today (e.g. ChatGPT, Bard, Anthropic, etc.) have adopted greedy decoding with certain samplings and carefully tuned them for different tasks: creative tasks such as chatbots or writing articles receives diverse responses from samplings; input-grounded tasks such as translation or coding benefit from greedy decoding to get the immediate "correct" result. (Indeed, ⌨️ coding tasks emphasize more on the consistency with given context - lines of code you just wrote, than the variations of possible responses.😆) ### Incremental Decoding ⏩ However, often times decoding a sequence of tokens one-by-one without considering previous decoded results could produce undesired results. For example, diff --git a/website/blog/2023-11-23-coding-llm-leaderboard/index.mdx b/website/blog/2023-11-23-coding-llm-leaderboard/index.mdx index 3f0be8c07c68..2ea8a95d69f8 100644 --- a/website/blog/2023-11-23-coding-llm-leaderboard/index.mdx +++ b/website/blog/2023-11-23-coding-llm-leaderboard/index.mdx @@ -9,7 +9,7 @@ image: ./leaderboard.png # Introducing the Coding LLM Leaderboard In our previous post on *Cracking the Coding Evaluation*, we shed light on the limitations of relying on HumanEval pass@1 as a code completion benchmark. -In response, we've launched the [Coding LLMs Leaderboard](http://leaderboard.tabbyml.com), embracing **Next Line Accuracy** as a metric inspired by academic works such as [RepoCoder](https://arxiv.org/abs/2303.12570), [RepoBench](https://arxiv.org/abs/2306.03091), and [CCEval](https://arxiv.org/abs/2310.11248). +In response, we've launched the [Coding LLMs Leaderboard](https://leaderboard.tabbyml.com), embracing **Next Line Accuracy** as a metric inspired by academic works such as [RepoCoder](https://arxiv.org/abs/2303.12570), [RepoBench](https://arxiv.org/abs/2306.03091), and [CCEval](https://arxiv.org/abs/2310.11248). ![Leaderboard](./leaderboard.png) diff --git a/website/blog/2024-01-24-running-tabby-locally-with-rocm/index.md b/website/blog/2024-01-24-running-tabby-locally-with-rocm/index.md index f9f887485612..b67abacfe1be 100644 --- a/website/blog/2024-01-24-running-tabby-locally-with-rocm/index.md +++ b/website/blog/2024-01-24-running-tabby-locally-with-rocm/index.md @@ -6,7 +6,7 @@ tags: [deployment] :::info -Tabby's ROCm support is currently only in our [nightly builds](https://github.com/TabbyML/tabby/releases/tag/nightly). It will become stable in version 0.8. +Tabby's ROCm support is currently only in our [nightly builds](https://github.com/TabbyML/tabby/releases/tag/nightly). It will become stable in version 0.9. ::: @@ -22,9 +22,25 @@ Before starting, please make sure you are on a supported system and have ROCm in ![ROCm installed on Arch Linux](./rocm-packages.png) -## Install and run Tabby +## Deploy Tabby with ROCm from Docker -Once you have installed ROCm, you can [download the precompiled binary for Tabby](https://github.com/TabbyML/tabby/releases/download/nightly/tabby_x86_64-manylinux2014-rocm57) with ROCm, or you can [compile it yourself](https://github.com/TabbyML/tabby/blob/main/CONTRIBUTING.md#local-setup). If compiling yourself, make sure to use the flag `--features rocm` to enable it. ROCm is currently supported in Tabby's nightly builds only, but will be stable with 0.8.8. +Once you've installed ROCm, you're ready to start using Tabby! Simply use the following command to run the container with GPU passthrough: + +``` +docker run \ + --device=/dev/kfd --device=/dev/dri --security-opt seccomp=unconfined --group-add video \ + -p 8080:8080 -v $HOME/.tabby:/data \ + tabbyml/tabby-rocm \ + serve --device rocm --model StarCoder-1B +``` + +The command output should look similar to the below: + +![Tabby running inside Docker](./tabby-rocm-docker.png) + +## Build Tabby with ROCm locally + +If you would rather run Tabby directly on your machine, you can [compile Tabby yourself](https://github.com/TabbyML/tabby/blob/main/CONTRIBUTING.md#local-setup). If compiling yourself, make sure to use the flag `--features rocm` to enable it. Once you have a compiled binary, you can run it with this command: diff --git a/website/blog/2024-01-24-running-tabby-locally-with-rocm/tabby-rocm-docker.png b/website/blog/2024-01-24-running-tabby-locally-with-rocm/tabby-rocm-docker.png new file mode 100644 index 000000000000..c07518fc629e --- /dev/null +++ b/website/blog/2024-01-24-running-tabby-locally-with-rocm/tabby-rocm-docker.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:08b6cb60d4992266a26156545dc20fdef05bdb190c167702057e7c9341396bdf +size 78331 diff --git a/website/blog/2024-02-05-create-tabby-extension-with-language-server-protocol/coc-tabby-completion.png b/website/blog/2024-02-05-create-tabby-extension-with-language-server-protocol/coc-tabby-completion.png new file mode 100644 index 000000000000..4caaa3b5b515 --- /dev/null +++ b/website/blog/2024-02-05-create-tabby-extension-with-language-server-protocol/coc-tabby-completion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6e69e961ee0cd8f8b79265e6bcf37944acc7e80b6105098a4905725ab66ac46 +size 102790 diff --git a/website/blog/2024-02-05-create-tabby-extension-with-language-server-protocol/index.md b/website/blog/2024-02-05-create-tabby-extension-with-language-server-protocol/index.md new file mode 100644 index 000000000000..fa0ba4a332ac --- /dev/null +++ b/website/blog/2024-02-05-create-tabby-extension-with-language-server-protocol/index.md @@ -0,0 +1,93 @@ +--- +title: "Create Tabby extension with Language Server Protocol" +authors: [icycodes] +tags: [editor integration] +--- + +Excited to share that [Tabby Agent](https://github.com/tabbyml/tabby/tree/main/clients/tabby-agent/) now available on [npm](https://www.npmjs.com/package/tabby-agent) and supports running as a [language server](https://microsoft.github.io/language-server-protocol/) 🎉. This new feature provides a uniform protocol to easily integrate Tabby into different text editors. Let's dive deeper together to unfold the stories behind! + +## What is Tabby Agent + +[Tabby Agent](https://github.com/tabbyml/tabby/tree/main/clients/tabby-agent/) is a Node.js package that communicates with the Tabby server and implements several essential features for code completion, including: + +- **Debouncing**: Tabby Agent handles code completion requests by implementing an appropriate debouncing mechanism. This ensures that requests are not sent too frequently, reducing server load and improving performance, as the automatic inline code completion often listens for text input which can be very frequent. +- **Caching**: Tabby Agent prevents redundant completion requests with KV caching. When a completion is dismissed but requested again at the same location, the cached content is used directly. Cached completions are also matched when the prefix of a request aligns with a previously cached completion, no need for additional server requests. This is especially useful when users type the same text as the ghost text suggestions. +- **Post-processing**: Tabby Agent enhances completion results through post-processings including filtering out low-quality completions, removing duplicate suggestions, and limiting the length of suggestions to the focused scope. All of these post-processings help users focus on the most relevant suggestions. + +These features were initially developed as part of the [Tabby VSCode extension](https://github.com/tabbyml/tabby/tree/main/clients/vscode/). Now it's desirable to reuse the client code logic as Tabby expands to more text editors. Therefore, we are building Tabby Agent as a standalone Node.js package that can be used by other editors to communicate with Tabby. + +![tabby-agent](./tabby-agent.png) + +## Why Language Server + +Tabby Agent previously utilized a customized protocol based on JSON Lines, designed to be compatible with VIM's JSON mode channel. However, this protocol was not widely adopted, making it hard to integrate Tabby Agent to different editors. With a more universal protocol, we can offer a more flexible and streamlined experience in creating Tabby plugins for various editors. + +The [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) defines a standardized protocol for communication between a language server and its clients. It provides methods to implement a wide range of features, including code completion. + +![tabby-agent-lsp](./tabby-agent-lsp.png) + +Running Tabby as a Language Server provides code completion with the standard `textDocument/completion` protocol. It can suggest code completions based on the context of the code, whether it's a line or a block, rather than just a single word. + +I'm also looking forward to the proposed `textDocument/inlineCompletion` feature in the upcoming version 3.18 of the LSP Specification. It will provide better support for multi-line code completions. Stay tuned for more updates on this topic in the future! + +## Running Tabby as a Language Server + +To run Tabby as a language server, follow these steps: +1. Set up your **Tabby server** following [this documentation](https://tabby.tabbyml.com/docs/installation/). +2. Make sure you have **[Node.js](https://nodejs.org/) version 18 or above** installed on your system. +3. Run the following command in your terminal: + ```bash + npx tabby-agent --lsp --stdio + ``` + Follow the instructions displayed in the console. Once the installation is complete, the Tabby agent will start listening for requests on `StdIO`. If there are no error messages, you can assume that the Tabby Agent script is running correctly. You can stop it by pressing `Ctrl+C`. + ![npx-run-tabby-agent](./npx-run-tabby-agent.gif) + + Alternatively, you can install `tabby-agent` as a global package and run it by using the following command: + ```bash + # Install tabby-agent as a global package + npm install --global tabby-agent + # Run the agent as a language server + tabby-agent --lsp --stdio + # Press `Ctrl+C` to stop the agent + ``` +4. You can configure the agent's settings by editing the config file located at `~/.tabby-client/agent/config.toml`. If your Tabby server uses a different port or requires authentication, modify these settings accordingly: + ```toml + [server] + endpoint = "http://127.0.0.1:8080" # Replace with your server's endpoint + token = "your_token" + ``` + For more details on configuration options, refer to [this documentation](https://tabby.tabbyml.com/docs/extensions/configurations). + +## Connect Your Editor to Tabby + +Most text editors support built-in LSP clients or popular LSP client plugins, making it easy to connect them to the Tabby agent language server. Let's take NeoVim and [coc.nvim](https://github.com/neoclide/coc.nvim) as an example to show you how to configure your editor to connect to Tabby. + +1. Install [coc.nvim](https://github.com/neoclide/coc.nvim) by following the [guide](https://github.com/neoclide/coc.nvim?tab=readme-ov-file#quick-start) +2. Start NeoVim, and use the ``:CocConfig`` command to open the configuration file. Add the following configuration: + ```json + { + "languageserver": { + "tabby-agent": { + "command": "npx", + "args": ["tabby-agent", "--lsp", "--stdio"], + "filetypes": ["*"] + } + } + } + ``` + The "filetypes": ["*"] setting enables Tabby for all filetypes. You can modify it according to your needs. + +3. Save the configuration file, and restart NeoVim. +4. Open a file and start typing code to see code completion suggestions from Tabby. + +![coc-tabby-completion](coc-tabby-completion.png) + +For more examples of connecting Tabby to other editors, refer to the [Tabby Agent documentation](https://github.com/tabbyml/tabby/tree/main/clients/tabby-agent/). If you have configurations for your favorite editors that you'd like to share, feel free to submit a pull request! + +## Create a Plugin for a New Editor + +In the previous examples, Tabby completions are displayed in the dropdown completion list. However, this method may not be very convenient for displaying multi-line code completions. As most LSP clients do not yet support [inline completion](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#textDocument_inlineCompletion), you may want to create a plugin for an editor that provides inline completion. To demonstrate how to communicate with Tabby via LSP, we have provided an example project [here](https://github.com/tabbyml/tabby/tree/main/clients/example-vscode-lsp). + +Please note that language server support is still in its early stages, and your feedback will be invaluable in making it even better. If you have any ideas or suggestions, feel free to create an issue or join our [Slack community](https://links.tabbyml.com/join-slack). + +Happy coding with Tabby! 🐱💻 diff --git a/website/blog/2024-02-05-create-tabby-extension-with-language-server-protocol/npx-run-tabby-agent.gif b/website/blog/2024-02-05-create-tabby-extension-with-language-server-protocol/npx-run-tabby-agent.gif new file mode 100644 index 000000000000..969c33599eed Binary files /dev/null and b/website/blog/2024-02-05-create-tabby-extension-with-language-server-protocol/npx-run-tabby-agent.gif differ diff --git a/website/blog/2024-02-05-create-tabby-extension-with-language-server-protocol/tabby-agent-lsp.png b/website/blog/2024-02-05-create-tabby-extension-with-language-server-protocol/tabby-agent-lsp.png new file mode 100644 index 000000000000..0b8a39d840ef --- /dev/null +++ b/website/blog/2024-02-05-create-tabby-extension-with-language-server-protocol/tabby-agent-lsp.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25dee621e1830ba3d1505584666ab2c897932de29a790b831e8929363df5b0c1 +size 157054 diff --git a/website/blog/2024-02-05-create-tabby-extension-with-language-server-protocol/tabby-agent.png b/website/blog/2024-02-05-create-tabby-extension-with-language-server-protocol/tabby-agent.png new file mode 100644 index 000000000000..77389bef68c5 --- /dev/null +++ b/website/blog/2024-02-05-create-tabby-extension-with-language-server-protocol/tabby-agent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f48c62977b81503c25fac0498820a143ccf08ad57f909d445530941e244b2511 +size 139107 diff --git a/website/blog/2024-03-25-deploy-tabby-in-air-gapped-environment-with-docker/cli-output.png b/website/blog/2024-03-25-deploy-tabby-in-air-gapped-environment-with-docker/cli-output.png new file mode 100644 index 000000000000..6a7954da36e8 --- /dev/null +++ b/website/blog/2024-03-25-deploy-tabby-in-air-gapped-environment-with-docker/cli-output.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:245f523814627d4265aa58c484b5b700f279ec209b51ebb52490711444655157 +size 97443 diff --git a/website/blog/2024-03-25-deploy-tabby-in-air-gapped-environment-with-docker/index.md b/website/blog/2024-03-25-deploy-tabby-in-air-gapped-environment-with-docker/index.md new file mode 100644 index 000000000000..349952055ab2 --- /dev/null +++ b/website/blog/2024-03-25-deploy-tabby-in-air-gapped-environment-with-docker/index.md @@ -0,0 +1,76 @@ +--- +authors: [wwayne] +tags: [deployment] +--- + +import noInternetImg from './no-internet.png'; + +# Deploy Tabby in Air-Gapped Environment with Docker + +
    + No internet access +
    + +Are you working in an air-gapped environment, and wondering if you can still deploy Tabby? Fear not, because the answer is YES! 🐱📣 + +## Prerequisite📋 + +* Docker installed on both the internet-connected computer and the offline computer. + +## Offline Deployment Guide🐾 + +Here's how we'll deploy Tabby in an offline environment: + +* Create a Docker image on a computer with internet access. +* Transfer the image to your offline computer. +* Run the Docker image and let Tabby work its magic! ✨ + +Now, let's dive into the detailed steps: + +1. Create a new **Dockerfile** on a computer with internet access. + +```docker +FROM tabbyml/tabby + +ENV TABBY_MODEL_CACHE_ROOT=/models + +RUN /opt/tabby/bin/tabby-cpu download --model StarCoder-1B +RUN /opt/tabby/bin/tabby-cpu download --model Nomic-Embed-Text +``` + +The **TABBY_MODEL_CACHE_ROOT** env var sets the directory for saving downloaded models. By setting `ENV TABBY_MODEL_CACHE_ROOT=/models`, we instruct Tabby to save the downloaded model files in the `/models` directory within the Docker container during the build process. + +2. Build the Docker image which containing the model + +```bash +docker build -t tabby-offline . +``` + +3. Save the Docker image to a tar file: + +```bash +docker save -o tabby-offline.tar tabby-offline +``` + +4. Copy the `tabby-offline.tar` file to the computer without internet access. + +5. Load the Docker image from the tar file: + +```bash +docker load -i tabby-offline.tar +``` + +6. Run the Tabby container + +```bash +docker run -it \ + --gpus all -p 8080:8080 -v $HOME/.tabby:/data \ + tabby-offline \ + serve --model StarCoder-1B --device cuda +``` + +Once the container is running successfully, you should see the CLI output similar to the screenshot below: + +![Tabby Cli Output](./cli-output.png) + +If you encounter any further issues or have questions, consider join our [slack community](https://links.tabbyml.com/join-slack). Our friendly Tabby enthusiasts are always ready to lend a helping paw and guide you to the answers you seek! 😸💡 diff --git a/website/blog/2024-03-25-deploy-tabby-in-air-gapped-environment-with-docker/no-internet.png b/website/blog/2024-03-25-deploy-tabby-in-air-gapped-environment-with-docker/no-internet.png new file mode 100644 index 000000000000..321ba754acbe --- /dev/null +++ b/website/blog/2024-03-25-deploy-tabby-in-air-gapped-environment-with-docker/no-internet.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1175d1728a20f363218876c33b23370040e3deddb747892c87571c3aaef73942 +size 134732 diff --git a/website/blog/2024-03-26-tabby-with-replicas-behind-reverse-proxy/Caddyfile b/website/blog/2024-03-26-tabby-with-replicas-behind-reverse-proxy/Caddyfile new file mode 100644 index 000000000000..50d68183cfe1 --- /dev/null +++ b/website/blog/2024-03-26-tabby-with-replicas-behind-reverse-proxy/Caddyfile @@ -0,0 +1,5 @@ +http://*:8080 { + handle_path /* { + reverse_proxy worker-0:8080 worker-1:8080 + } +} diff --git a/website/blog/2024-03-26-tabby-with-replicas-behind-reverse-proxy/docker-compose.yml b/website/blog/2024-03-26-tabby-with-replicas-behind-reverse-proxy/docker-compose.yml new file mode 100644 index 000000000000..0b641daa126c --- /dev/null +++ b/website/blog/2024-03-26-tabby-with-replicas-behind-reverse-proxy/docker-compose.yml @@ -0,0 +1,37 @@ +version: '3.5' + +services: + worker-0: + restart: always + image: tabbyml/tabby + command: serve --model TabbyML/StarCoder-1B --device cuda --no-webserver + volumes: + - "$HOME/.tabby:/data" + deploy: + resources: + reservations: + devices: + - driver: nvidia + device_ids: ["0"] + capabilities: [gpu] + + worker-1: + restart: always + image: tabbyml/tabby + command: serve --model TabbyML/StarCoder-1B --device cuda --no-webserver + volumes: + - "$HOME/.tabby:/data" + deploy: + resources: + reservations: + devices: + - driver: nvidia + device_ids: ["1"] + capabilities: [gpu] + + web: + image: caddy + volumes: + - "./Caddyfile:/etc/caddy/Caddyfile:ro" + ports: + - "8080:8080" diff --git a/website/blog/2024-03-26-tabby-with-replicas-behind-reverse-proxy/index.mdx b/website/blog/2024-03-26-tabby-with-replicas-behind-reverse-proxy/index.mdx new file mode 100644 index 000000000000..8cef754c3be5 --- /dev/null +++ b/website/blog/2024-03-26-tabby-with-replicas-behind-reverse-proxy/index.mdx @@ -0,0 +1,88 @@ +--- +authors: [meng] +tags: [deployment, reverse proxy] +--- + +import CodeBlock from '@theme/CodeBlock'; +import Caddyfile from "raw-loader!./Caddyfile" +import DockerComposeYaml from "raw-loader!./docker-compose.yml" + +# Tabby with Replicas and a Reverse Proxy + +Tabby operates as a single process, typically utilizing resources from a single GPU.This setup is usually sufficient for a team of ~50 engineers. +However, if you wish to scale this for a larger team, you'll need to harness compute resources from multiple GPUs. +One approach to achieve this is by creating additional replicas of the Tabby service and employing a reverse proxy to distribute traffic among these replicas. + +This guide assumes that you have a Linux machine with Docker, CUDA drivers, and the [nvidia-container-toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) already installed. + +Let's dive in! + +## Creating the Caddyfile + +Before configuring our services, we need to create a `Caddyfile` that will define how Caddy should handle incoming requests and reverse proxy them to Tabby: + + +{Caddyfile} + + +Note that we are assuming we have two GPUs in the machine; therefore, we should redirect traffic to two worker nodes. + +## Preparing the Model File + +Now, execute the following Docker command to pre-download the model file: + +```bash +docker run --entrypoint /opt/tabby/bin/tabby-cpu \ + -v $HOME/.tabby:/data tabbyml/tabby \ + download --model StarCoder-1B +``` + +Since we are only downloading the model file, we override the entrypoint to `tabby-cpu` to avoid the need for a GPU + +## Creating the Docker Compose File + +Next, create a `docker-compose.yml` file to orchestrate the Tabby and Caddy services. Here is the configuration for both services: + + +{DockerComposeYaml} + + +Note that we have two worker nodes, and we are using the same model for both of them, with each assigned to a different GPU (0 and 1, respectively). If you have more GPUs, you can add more worker nodes and assign them to the available GPUs (remember to update the `Caddyfile` accordingly!). + +## Starting the Services + +With the `docker-compose.yml` and `Caddyfile` configured, start the services using Docker Compose: + +```bash +docker-compose up -d +``` + +## Verifying the Setup + +To ensure that Tabby is running correctly behind Caddy, execute a curl command against the health endpoint: + +```bash +curl -L 'http://localhost:8080/v1/completions' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-d '{ + "language": "python", + "segments": { + "prefix": "def fib(n):\n ", + "suffix": "\n return fib(n - 1) + fib(n - 2)" + } +}' +``` + +The response should indicate that Tabby is healthy and ready to assist you with your coding tasks. + +## Securing Your Setup (Optional) + +For those interested in securing their setup, consider using Caddy directives like `forward_auth` and integrating with a service like [Authelia](https://www.authelia.com/). For more details on this, refer to the [Caddy documentation on forward_auth](https://caddyserver.com/docs/caddyfile/directives/forward_auth#authelia). + +--- + +And there you have it! You've successfully set up Tabby with Caddy as a reverse proxy. Happy coding with your new AI assistant! + +As an additional note, since the release of v0.9.0, Tabby enterprise edition now includes the built-in account management system. +For more information, refer to the [official documentation](/) for details. \ No newline at end of file diff --git a/website/blog/2024-04-08-connect-private-github-repository-to-tabby/code-search-preview.png b/website/blog/2024-04-08-connect-private-github-repository-to-tabby/code-search-preview.png new file mode 100644 index 000000000000..2cc10448e041 --- /dev/null +++ b/website/blog/2024-04-08-connect-private-github-repository-to-tabby/code-search-preview.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0dbe8e47fbc4ef0e96d2cd6144c6d3038668b8d40410997547d678f7170c9324 +size 367526 diff --git a/website/blog/2024-04-08-connect-private-github-repository-to-tabby/github-pat-contents-access.png b/website/blog/2024-04-08-connect-private-github-repository-to-tabby/github-pat-contents-access.png new file mode 100644 index 000000000000..8041d00a7abe --- /dev/null +++ b/website/blog/2024-04-08-connect-private-github-repository-to-tabby/github-pat-contents-access.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c82c756b5604523cc8af5a312f4543721029b0c081cf095c05648eca4d388df +size 95729 diff --git a/website/blog/2024-04-08-connect-private-github-repository-to-tabby/github-pat-filling-info.png b/website/blog/2024-04-08-connect-private-github-repository-to-tabby/github-pat-filling-info.png new file mode 100644 index 000000000000..1cb717c9611e --- /dev/null +++ b/website/blog/2024-04-08-connect-private-github-repository-to-tabby/github-pat-filling-info.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:37e49d2adaa48cf10f783a9d01ca4138c5cc31d76f3a6a3e046d7f07c358d6aa +size 222024 diff --git a/website/blog/2024-04-08-connect-private-github-repository-to-tabby/github-pat-generate-new-token.png b/website/blog/2024-04-08-connect-private-github-repository-to-tabby/github-pat-generate-new-token.png new file mode 100644 index 000000000000..1e641f04313e --- /dev/null +++ b/website/blog/2024-04-08-connect-private-github-repository-to-tabby/github-pat-generate-new-token.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34e78a3c3f07e04444f4603e9be7fa2af6dcecd90cba41fe7b5f2783e859a7b9 +size 132068 diff --git a/website/blog/2024-04-08-connect-private-github-repository-to-tabby/github-pat-generate-token.png b/website/blog/2024-04-08-connect-private-github-repository-to-tabby/github-pat-generate-token.png new file mode 100644 index 000000000000..920ebaf7a719 --- /dev/null +++ b/website/blog/2024-04-08-connect-private-github-repository-to-tabby/github-pat-generate-token.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cf8ef5342c5c3242f205af858b1fe47d5a362bf3b10e01a7a81bc13d46f5ec3 +size 50474 diff --git a/website/blog/2024-04-08-connect-private-github-repository-to-tabby/index.md b/website/blog/2024-04-08-connect-private-github-repository-to-tabby/index.md new file mode 100644 index 000000000000..8418af005d3e --- /dev/null +++ b/website/blog/2024-04-08-connect-private-github-repository-to-tabby/index.md @@ -0,0 +1,119 @@ +--- +authors: [icycodes] +tags: [deployment, repository context] +--- + +# Connect Private GitHub Repository to Tabby + +A few months ago, we published a blog [Repository context for LLM assisted code completion](https://tabby.tabbyml.com/blog/2023/10/16/repository-context-for-code-completion), introducing the Repository Context feature in Tabby. This feature has been widely embraced by many users to incorporate repository-level knowledge into Tabby, thus improving the relevance of code completion suggestions within the working project. + +In this blog, I will guide you through the steps of setting up a Tabby server configured with **a private Git repositories** context, aiming to simplify and streamline the integration process. + +## Generating a Personal Access Token + +In order to provide the Tabby server with access to your private Git repositories, it is essential to create a **Personal Access Token (PAT)** specific to your Git provider. The following steps outline the process with GitHub as a reference: + +1. Visit [GitHub Personal Access Tokens Settings](https://github.com/settings/tokens?type=beta) and select `Generate new token`. + ![GitHub PAT Generate New Token](./github-pat-generate-new-token.png) +2. Enter the `Token name`, specify an `Expiration` date, an optional `Description`, and select the repositories you wish to grant access to. + ![GitHub PAT Filling Info](./github-pat-filling-info.png) +3. Within the `Permissions` section, ensure that `Contents` is configured for `Read-only` access. + ![GitHub PAT Contents Access](./github-pat-contents-access.png) +4. Click `Generate token` to generate the new PAT. Remember to make a copy of the PAT before closing the webpage. + ![GitHub PAT Generate Token](./github-pat-generate-token.png) + +For additional information, please refer to the documentation on [Managing your personal access tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens). + +**Note**: For users of GitLab, guidance on creating a personal access token can be found in the documentation [Personal access tokens - GitLab](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#create-a-personal-access-token). + +## Configuration + +To configure the Tabby server with your private Git repositories, you need to provide the required settings in a TOML file. Create and edit a configuration file located at `~/.tabby/config.toml`: + +```toml +## Add the private repository +[[repositories]] +name = "my_private_project" +git_url = "https://@github.com/icycodes/my_private_project.git" + +## More repositories can be added like this +[[repositories]] +name = "another_project" +git_url = "https://@github.com/icycodes/another_project.git" +``` + +For more detailed about the configuration file, you can refer to the [configuration documentation](https://tabby.tabbyml.com/docs/configuration). + +**Note:** The URL format for GitLab repositories may vary, you can check the [official documentation](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#clone-repository-using-personal-access-token) for specific guidelines. + +## Building the Index + +In the process of building the index, we will parse the repository and extract code components for indexing, using the parser [tree-sitter](https://tree-sitter.github.io/tree-sitter/). This will allow for quick retrieval of related code snippets before generating code completions, thereby enhancing the context for suggestion generation. + +:::tip +The commands provided in this section are based on a Linux environment and assume the pre-installation of Docker with CUDA drivers. Adjust the commands as necessary if you are running Tabby on a different setup. +::: + +Once the configuration file is set up, proceed with running the `scheduler` to synchronize git repositories and construct the index. In this scenario, utilizing the `tabby-cpu` entrypoint will avoid the requirement for GPU resources. + +```bash +docker run -it --entrypoint /opt/tabby/bin/tabby-cpu -v $HOME/.tabby:/data tabbyml/tabby scheduler --now +``` + +The expected output looks like this: + +```console +icy@Icys-Ubuntu:~$ docker run -it --entrypoint /opt/tabby/bin/tabby-cpu -v $HOME/.tabby:/data tabbyml/tabby scheduler --now +Syncing 1 repositories... +Cloning into '/data/repositories/my_private_project'... +remote: Enumerating objects: 51, done. +remote: Total 51 (delta 0), reused 0 (delta 0), pack-reused 51 +Receiving objects: 100% (51/51), 7.16 KiB | 2.38 MiB/s, done. +Resolving deltas: 100% (18/18), done. +Building dataset... +100%|████████████████████████████████████████| 12/12 [00:00<00:00, 55.56it/s] +Indexing repositories... +100%|████████████████████████████████████████| 12/12 [00:00<00:00, 73737.70it/s] +``` + +Subsequently, launch the server using the following command: + +```bash +docker run -it --gpus all -p 8080:8080 -v $HOME/.tabby:/data tabbyml/tabby serve --model StarCoder-1B --device cuda +``` + +The expected output upon successful initiation of the server should like this: + +```console +icy@Icys-Ubuntu:~$ docker run -it --gpus all -p 8080:8080 -v $HOME/.tabby:/data tabbyml/tabby serve --model StarCoder-1B --device cuda +2024-03-21T16:16:47.189632Z INFO tabby::serve: crates/tabby/src/serve.rs:118: Starting server, this might take a few minutes... +2024-03-21T16:16:47.190764Z INFO tabby::services::code: crates/tabby/src/services/code.rs:53: Index is ready, enabling server... +ggml_init_cublas: GGML_CUDA_FORCE_MMQ: no +ggml_init_cublas: CUDA_USE_TENSOR_CORES: yes +ggml_init_cublas: found 1 CUDA devices: + Device 0: NVIDIA GeForce RTX 4090, compute capability 8.9, VMM: yes +2024-03-21T16:16:52.464116Z INFO tabby::routes: crates/tabby/src/routes/mod.rs:35: Listening at 0.0.0.0:8080 +``` + +Notably, the line `Index is ready, enabling server...` signifies that the server has been successfully launched with the constructed index. + +## Verifying Indexing Results + +To confirm that the code completion is effectively utilizing the built index, you can employ the code search feature to validate the indexing process: + +1. Access the Swagger UI page at [http://localhost:8080/swagger-ui/#/v1beta/search](http://localhost:8080/swagger-ui/#/v1beta/search). +2. Click on the `Try it out` button, and input the query parameter `q` with a symbol to search for. +3. Click the `Execute` button to trigger the search and see if there are any relevant code snippets was found. + +In the screenshot below, we use `CodeSearch` as the query string and find some code snippets related in the Tabby repository: + +![Code Search Preview](./code-search-preview.png) + +Alternatively, if you have utilized the code completion with the constructed index, you can examine the server log located in `~/.tabby/events` to inspect how the prompt is enhanced during code completion. + +## Additional Notes + +Starting from version v0.9, Tabby offers a web UI to manage your git repository contexts. Additionally, a scheduler job management system has been integrated, streamlining the process of monitoring scheduler job statuses. With these enhancements, you can save a lot of effort in maintaining yaml config files and docker compose configurations. Furthermore, users can easily monitor visualized indexing results through the built-in code browser. +In the upcoming v0.11, a new feature will be introduced that enables a direct connection to GitHub, simplifying and securing your access to private GitHub repositories. + +For further details and guidance, please refer to [administration documents](https://tabby.tabbyml.com/docs/administration/). diff --git a/website/blog/2024-05-01-vulkan-support/completion.png b/website/blog/2024-05-01-vulkan-support/completion.png new file mode 100644 index 000000000000..55d1e313cd72 --- /dev/null +++ b/website/blog/2024-05-01-vulkan-support/completion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4aa971d90eca2da311b6f07d59130c2969790999e2ff1023d481df4aeece19d4 +size 6127 diff --git a/website/blog/2024-05-01-vulkan-support/index.md b/website/blog/2024-05-01-vulkan-support/index.md new file mode 100644 index 000000000000..f6f561c2c94d --- /dev/null +++ b/website/blog/2024-05-01-vulkan-support/index.md @@ -0,0 +1,61 @@ +--- +title: 'Vulkan Support: LLMs for Everyone' +authors: [boxbeam] +tags: [deployment] +--- + +It has long been the case that machine learning models are run on the GPU to improve their performance. +The GPU is far more effective at the kinds of computations needed for AI than the CPU, and so GPU compute libraries +such as Cuda and ROCm are typically used. + +However, requiring the support of these libraries can restrict which graphics cards are compatible, leaving many +with older or less popular cards unable to run LLMs efficiently. + +Tabby is happy to announce that we now support Vulkan, a graphics library created primarily for games. Its original purpose +means that it is designed to work on a very broad range of cards, and leveraging it to host LLMs means that we can now +offer GPU acceleration to people whose cards are not supported by Cuda and ROCm. + +Vulkan works on basically any GPU, so if you have previously been forced to host local models on your CPU, now is the time +to see what Tabby with Vulkan can do for you! + +## Vulkan Installation + +To begin, first make sure that you have Vulkan installed. + +For Windows users, Vulkan may be natively supported. Otherwise, the Vulkan SDK can be downloaded at https://vulkan.lunarg.com/sdk/home#windows. + +For Linux users, Vulkan can be installed through your package manager: +- Arch Linux: vulkan-icd-loader (universal), and also install vulkan-radeon (for AMD) or vulkan-nouveau (for Nvidia) +- Debian Linux: libvulkan1 + +![Vulkan installed on Arch Linux](./vulkan-installed-on-arch.png) + +## Tabby Installation + +To start using Tabby with Vulkan, first download one of the pre-built Vulkan binaries for your platform: +- Linux: https://github.com/TabbyML/tabby/releases/download/v0.10.0/tabby_x86_64-manylinux2014-vulkan +- Windows: https://github.com/TabbyML/tabby/releases/download/v0.10.0/tabby_x86_64-windows-msvc-vulkan.exe + +## Running + +Once you've installed the appropriate binary, you can simply run it from the command line: + +For Windows, open a command prompt and navigate to the download folder, then run: + +``` +tabby_x86_64-windows-msvc-vulkan serve --model StarCoder-1B --device vulkan +``` + +For Linux: + +``` +./tabby_x86_64-manylinux2014-vulkan serve --model StarCoder-1B --device vulkan +``` + +When it starts, you should see a printout indicating that Vulkan has found your card and is working properly: + +![Vulkan running on Linux](./vulkan-running.png) + +Now enjoy your speedy completions! + +![Completion example](./completion.png) diff --git a/website/blog/2024-05-01-vulkan-support/vulkan-installed-on-arch.png b/website/blog/2024-05-01-vulkan-support/vulkan-installed-on-arch.png new file mode 100644 index 000000000000..bc8025163c2d --- /dev/null +++ b/website/blog/2024-05-01-vulkan-support/vulkan-installed-on-arch.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b3eba27c4b08d17a753e71d9ed08dc59fb2460db5a54fc759a95b91da7618c30 +size 16694 diff --git a/website/blog/2024-05-01-vulkan-support/vulkan-running.png b/website/blog/2024-05-01-vulkan-support/vulkan-running.png new file mode 100644 index 000000000000..de5d13672472 --- /dev/null +++ b/website/blog/2024-05-01-vulkan-support/vulkan-running.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ed0a074e8cb4ea35a8f3174257527c46fdbdbd6f64ac912ff71102e36e1a46c +size 46399 diff --git a/website/blog/2024-06-11-rank-fusion-in-tabby-code-completion/index.md b/website/blog/2024-06-11-rank-fusion-in-tabby-code-completion/index.md new file mode 100644 index 000000000000..c5edd95a471a --- /dev/null +++ b/website/blog/2024-06-11-rank-fusion-in-tabby-code-completion/index.md @@ -0,0 +1,47 @@ +--- +authors: [meng] +tags: [quality, vector embedding] +--- + +# Rank Fusion for improved Code Context in Tabby + +## Introduction + +Tabby has made significant advancements in its code context understanding with the introduction of a semantic relevance score (via vector embedding) and rank fusion in version [0.12](https://github.com/TabbyML/tabby/releases/tag/v0.12.0). These enhancements have transformed the way Tabby ranks source code context, resulting in more accurate context for feeding into LLM. + +## From BM25 to Rank Fusion + +Tabby's initial approach to ranking involved the use of the BM25 algorithm, as described in [Repository context for LLM assisted code completion](/blog/2023/10/16/repository-context-for-code-completion/). This algorithm indexed source code in chunks, which served as the basis for code completion and Q&A. In the latest release, Tabby has augmented this approach with a semantic relevance score calculated from embedding vector distances. This dual scoring system necessitated the implementation of a rank fusion technique to effectively combine these disparate ranks. + +## The Mechanics of Reciprocal Rank Fusion + +The RRF method adopted by Tabby is a well-established technique in information retrieval. It merges multiple rank lists to produce a single, more accurate ranking. In Tabby, the RRF is applied as follows: + +```python title="derived from https://www.elastic.co/guide/en/elasticsearch/reference/current/rrf.html" +score = 0.0 +for q in queries: + if d in result(q): + score += 1.0 / ( k + rank( result(q), d ) ) +return score + +# where: +# k is a constant, currently set to 60 in Tabby +# q is a query within the set of queries +# d is a document found in the result set of q +# result(q) is the result set for query q +# rank( result(q), d ) is the ordinal rank of document d within result(q) +``` + +By introducing the semantic relevance score and rank fusion, Tabby can now provide more accurate code suggestions that are contextually relevant to the user's current work. + +For developers using Tabby, the enhanced ranking system requires no additional configuration beyond the repository context setup in the admin UI. The indexing process now includes the computation of embedding vectors, which, while slightly extending the initial indexing time, is mitigated by caching vectors between commits to optimize performance. + +*Try **Explain Code** in [Code Browser](https://demo.tabbyml.com)* + +![Repository Context Triggered](./repository-context-triggered.png) + +## Conclusion + +By leveraging a combination of BM25 and semantic relevance scores, Tabby delivers more accurate and contextually appropriate suggestions, streamlining the development process. + +As Tabby continues to evolve, users can anticipate ongoing improvements designed to bolster productivity and enrich the coding experience. diff --git a/website/blog/2024-06-11-rank-fusion-in-tabby-code-completion/repository-context-triggered.png b/website/blog/2024-06-11-rank-fusion-in-tabby-code-completion/repository-context-triggered.png new file mode 100644 index 000000000000..5aeaf48e5228 --- /dev/null +++ b/website/blog/2024-06-11-rank-fusion-in-tabby-code-completion/repository-context-triggered.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7953d75736098fd36d919a8cc159634d1bdebde0f1df5910a78fadf7bcba536d +size 341126 diff --git a/website/blog/2024-07-09-tabby-codestral/codestral_api.jpeg b/website/blog/2024-07-09-tabby-codestral/codestral_api.jpeg new file mode 100644 index 000000000000..603e23ce80c8 Binary files /dev/null and b/website/blog/2024-07-09-tabby-codestral/codestral_api.jpeg differ diff --git a/website/blog/2024-07-09-tabby-codestral/index.mdx b/website/blog/2024-07-09-tabby-codestral/index.mdx new file mode 100644 index 000000000000..2e4c92ab5941 --- /dev/null +++ b/website/blog/2024-07-09-tabby-codestral/index.mdx @@ -0,0 +1,52 @@ +--- +authors: [gyxlucy] +tags: [partnership, product] +image: ./tabby-mistral.png + +--- + +import CodestralVideoUrl from './tabby_codestral.mp4' + + +# Introducing the Codestral Integration in Tabby + +![Tabby x Mistral](./tabby-mistral.png) + +We are excited to share the integration of ***Codestral*** model into Tabby! +Codestral has demonstrated exceptional performance across various coding tasks, and we are delighted to support our users in both self-hosting and accessing Codestral through its cloud API. +This addition aims to enhance your coding experience with precision and efficiency, setting a new standard for AI-enhanced developer experience. + +