mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
chore: merge jbbrown/marketplace squashed
Detailed commits:
Author: Dicha Zelianivan Arkana <51877647+elianiva@users.noreply.github.com>
Date: Wed May 21 00:36:02 2025 +0700
feat: better UI for marketplace item list (#11)
* feat: better UI for marketplace item list
* feat: better source config UI
* refactor: change how we fetch items
* fix: update state more optimistically
* fix: incorrect tags filter
* fix: better tags filtering
* feat: more consistent UI
* feat: marketplace animation
* fix: remove cache, it's fast enough
* refactor: make the UI more consistent across themes
* feat: integrate install metadata to UI
* feat: add translation files
* test: add marketplace UI
commit 1a11665672
Author: Trung Dang <dangquoctrung123@gmail.com>
Date: Mon May 19 23:08:25 2025 +0700
feat: `installedMetadata` MVP (#10)
* feat: `installedMetadata` MVP
* feat: removal support for installed items (+ general refactors)
commit e25b3e77c0
Author: NamesMT <dangquoctrung123@gmail.com>
Date: Tue May 6 08:40:19 2025 +0000
refactor: minor: cleanup marketplaceHandler passing
commit 3dcb4534f8
Author: NamesMT <dangquoctrung123@gmail.com>
Date: Tue May 6 07:53:43 2025 +0000
chore: add reusable `globalContext` util
commit b7fae4a325
Author: NamesMT <dangquoctrung123@gmail.com>
Date: Tue May 6 06:55:33 2025 +0000
chore(cline_docs/marketplace): align with `Roo-Code-Marketplace`
commit f89c11ec67
Author: Trung Dang <dangquoctrung123@gmail.com>
Date: Mon May 5 14:35:19 2025 +0700
feat(marketplace): UI form for configurable install (#9)
* feat(wip): installation UI
* chore: rebase, refactor and fixes
---------
Co-authored-by: elianiva <51877647+elianiva@users.noreply.github.com>
commit bbd790f4bd
Author: Trung Dang <dangquoctrung123@gmail.com>
Date: Sun May 4 12:36:52 2025 +0700
chore: bump `roo-rocket` and `config-rocket` version (#7)
This is a breaking change that reword a lot of thing, revise the config props, and adds some more features.
commit 562c34b9b7
Author: NamesMT <dangquoctrung123@gmail.com>
Date: Sat May 3 14:57:53 2025 +0000
chore(MM/roo-rocket): shorten and pin CLI version
commit a25ed006d6
Author: NamesMT <dangquoctrung123@gmail.com>
Date: Fri May 2 16:48:39 2025 +0000
chore: adjust `registry` dir find & validate logic
commit 065c8af320
Author: NamesMT <dangquoctrung123@gmail.com>
Date: Fri May 2 16:27:47 2025 +0000
chore: `showInstallButton` for `package` item
commit 563b327007
Author: Trung Dang <dangquoctrung123@gmail.com>
Date: Fri May 2 23:16:26 2025 +0700
chore: marketplace wordings, cleanup, and document refresh (#6)
* chore: reword all `PackageManager` => `Marketplace`
* chore: document refresh, `mcp-server` => `mcp`
* chore: correct word
* chore: remove unnecessary condition
* chore: remove unnecessary bits
* chore: add note for failed test
commit c0638798d8
Author: NamesMT <dangquoctrung123@gmail.com>
Date: Fri May 2 05:13:35 2025 +0000
tests: prepare required mocks for `MarketplaceItemActionsMenu`
commit 39b2638c6a
Author: NamesMT <dangquoctrung123@gmail.com>
Date: Fri May 2 05:12:55 2025 +0000
chore: remove tests specific to `MarketplaceItemActionsMenu` from ItemCard test file
commit 03974e82b2
Author: NamesMT <dangquoctrung123@gmail.com>
Date: Fri May 2 05:10:35 2025 +0000
refactor: move `isValidUrl` to global util, prop/scope cleaning
commit 8ea74f77dd
Author: NamesMT <dangquoctrung123@gmail.com>
Date: Fri May 2 04:28:35 2025 +0000
chore: bump `roo-rocket`, `config-rocket` (fix tests)
commit bbfbaa1b30
Author: NamesMT <dangquoctrung123@gmail.com>
Date: Wed Apr 30 19:47:08 2025 +0000
fix: use relative path import instead
commit 7274092a6b
Author: NamesMT <dangquoctrung123@gmail.com>
Date: Wed Apr 30 19:30:57 2025 +0000
fix: should display N/A instead of `All` for unknown types
commit 8fbc2d95a2
Author: NamesMT <dangquoctrung123@gmail.com>
Date: Wed Apr 30 16:58:16 2025 +0000
style: minor indent & import combine
commit 765764790a
Author: NamesMT <dangquoctrung123@gmail.com>
Date: Wed Apr 30 16:56:48 2025 +0000
perf: verify binary hash right after download
commit bc60b426b5
Author: NamesMT <dangquoctrung123@gmail.com>
Date: Wed Apr 30 16:54:27 2025 +0000
fix: resolve lint problems
commit 9354a80b1a
Author: NamesMT <dangquoctrung123@gmail.com>
Date: Wed Apr 30 16:48:39 2025 +0000
fix: bump version and lockfile, adjust import
commit 6a4dc46c80
Merge: 29d73dce 324be54e
Author: JB Brown <jb.brown@smartsheet.com>
Date: Tue Apr 29 06:57:25 2025 -0700
Merge pull request #5 from NamesMT/marketplace/roo-rocket
feat: marketplace install MVP
commit 324be54e4e
Author: NamesMT <dangquoctrung123@gmail.com>
Date: Mon Apr 28 15:02:59 2025 +0000
chore: minor rewording
commit 086523655b
Author: NamesMT <dangquoctrung123@gmail.com>
Date: Mon Apr 28 14:48:52 2025 +0000
feat: working CLI interactive install mode
commit 3ac4bf61f2
Author: NamesMT <dangquoctrung123@gmail.com>
Date: Mon Apr 28 12:16:35 2025 +0000
feat: binary configurable pack detection, auto-select installation method
commit 3539550b0c
Author: NamesMT <dangquoctrung123@gmail.com>
Date: Mon Apr 28 10:52:04 2025 +0000
feat: delegate the hooks declaration to `roo-rocket`, prepare for CLI feat
commit 26be7b3ac7
Author: NamesMT <dangquoctrung123@gmail.com>
Date: Mon Apr 28 07:54:10 2025 +0000
fix: should not require workspace folder for `global` install (+ formatting)
commit 74a0c44d59
Author: NamesMT <dangquoctrung123@gmail.com>
Date: Mon Apr 28 06:57:39 2025 +0000
feat: marketplace supports global install
commit 04d7360da9
Author: NamesMT <dangquoctrung123@gmail.com>
Date: Sun Apr 27 20:41:07 2025 +0000
fix: correct error message
commit 52b653f271
Author: NamesMT <dangquoctrung123@gmail.com>
Date: Sun Apr 27 20:24:07 2025 +0000
feat: marketplace install MVP
commit 29d73dce4c
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Sun Apr 27 00:11:00 2025 -0400
Knip fixes
commit 0e7a14e68a
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Sun Apr 27 00:02:10 2025 -0400
Shouldn't need to change this
commit 1a6a41d8c0
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Sat Apr 26 23:54:16 2025 -0400
Revert unrelated changes
commit c9449ca2fe
Merge: 4f5b04b6 1924e10e
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Sat Apr 26 23:51:13 2025 -0400
Merge remote-tracking branch 'origin/main' into jbbrown/marketplace
commit 1924e10e72
Author: Chris Estreich <cestreich@gmail.com>
Date: Sat Apr 26 09:45:26 2025 -0700
Fix all linter errors (and fix the lint scripts too) (#2958)
commit 418791a743
Author: Julio Navarro <julioocz@gmail.com>
Date: Sat Apr 26 08:46:05 2025 -0400
Fix: custom modes export import (#2810)
* Enhance export method in ContextProxy to filter out project custom modes, ensuring only global settings are included in the export.
* Fix issue of customModes not being imported when importing settings.
The problem was that the Custom modes are managed by the CustomModesManager, not by the context proxy.
* * Fix tests
* Update webviewMessageHandler to provide customModeManager to importSettings
commit 42c1f5f882
Author: R00-B0T <110429663+R00-B0T@users.noreply.github.com>
Date: Fri Apr 25 21:03:17 2025 -0700
Changeset version bump (#2957)
* changeset version bump
* Update package.json
* Update package-lock.json
* Update CHANGELOG.md
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
commit d6860e1114
Author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Date: Fri Apr 25 23:53:45 2025 -0400
Update contributors list (#2946)
docs: update contributors list [skip ci]
Co-authored-by: mrubens <mrubens@users.noreply.github.com>
commit 5b99e4d50d
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Fri Apr 25 23:51:13 2025 -0400
Updated tips (#2961)
* Updated tips
* Changeset
commit 83133c6e6d
Author: Sacha Sayan <sacha.sayan@gmail.com>
Date: Fri Apr 25 22:39:11 2025 -0400
UI top down homescreen (#2951)
* isotrUpdate welcome screen UI components and fix unused variable
* Save collapsibility.
* Add persistence, locales.
* Top layout, expanded tips.
* Minimal 'tasks' language.
* Adjust announcement positioning/dimensions.
* Fix tests, defaults.
* Fix translations.
* Compromise-compromise.
* Final tweaks.
* More tweaks.
* More tweaks.
* Issues fix.
* Update translations, remove debug.
* Fix transparency issue.
---------
Co-authored-by: hannesrudolph <hrudolph@gmail.com>
commit de22566ea2
Author: Tony Zhang <157202938+zhangtony239@users.noreply.github.com>
Date: Sat Apr 26 08:13:13 2025 +0800
Fix: word wrapping in Roo message title (#2948)
commit e3d55a36ac
Author: shariqriazz <ishariqriaz@gmail.com>
Date: Fri Apr 25 15:49:19 2025 -0700
docs: fix file paths in settings.md (#2930)
commit c8b5cdf7b2
Author: Chris Estreich <cestreich@gmail.com>
Date: Fri Apr 25 15:24:03 2025 -0700
Omit reasoning params for non-reasoning models (#2932)
commit cb29e9d56f
Author: Chris Estreich <cestreich@gmail.com>
Date: Fri Apr 25 15:23:25 2025 -0700
Remove ModelInfo objects from settings (#2939)
commit a354c01b0e
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Fri Apr 25 16:06:50 2025 -0400
Add Boomerang as a default mode (#2934)
* Add Boomerang as a default mode
* Rename boomerang, add emoji
commit 547874eed7
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Fri Apr 25 16:04:38 2025 -0400
Revert "Fix: Preserve editor state and prevent tab unpinning during diffs" (#2956)
Revert "Fix: Preserve editor state and prevent tab unpinning during diffs (#2…"
This reverts commit c2dd743aeb.
commit 06db547308
Author: Chris Estreich <cestreich@gmail.com>
Date: Fri Apr 25 10:32:35 2025 -0700
Use a WASM-based tiktoken implementation (#2859)
* Use a WASM-based tiktoken implementation
* Clean up imports
commit 7e76736e13
Author: pugazhendhi-m <132246623+pugazhendhi-m@users.noreply.github.com>
Date: Fri Apr 25 17:06:25 2025 +0530
Updates default model for Unbound (#2944)
* Updates default model for Unbound
* Adds changeset
---------
Co-authored-by: Pugazhendhi <pugazhendhi@unboundsecurity.ai>
commit 586e43bd55
Author: R00-B0T <110429663+R00-B0T@users.noreply.github.com>
Date: Thu Apr 24 16:41:26 2025 -0700
Changeset version bump (#2926)
* changeset version bump
* Update CHANGELOG.md
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Chris Estreich <cestreich@gmail.com>
commit 0dfbae64f3
Author: Chris Estreich <cestreich@gmail.com>
Date: Thu Apr 24 16:34:22 2025 -0700
Allow users to toggle Gemini caching on / off for OpenRouter (#2927)
commit 5c2511e355
Author: KJ7LNW <93454819+KJ7LNW@users.noreply.github.com>
Date: Thu Apr 24 16:28:09 2025 -0700
feat: compress terminal output with backspace characters (#2907)
Follow-up to #2562 adding support for backspace character compression.
Optimizes terminal output by handling backspace characters similar to
carriage returns, improving readability of progress spinners and other
terminal output that uses backspace for animation.
- Added processBackspaces function using efficient indexOf approach
- Added comprehensive test suite for backspace handling
- Integrated with terminal output compression pipeline
Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
Co-authored-by: Eric Wheeler <roo-code@z.ewheeler.org>
commit 23aa3b636b
Author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Date: Thu Apr 24 19:26:00 2025 -0400
Update contributors list (#2903)
docs: update contributors list [skip ci]
Co-authored-by: cte <cte@users.noreply.github.com>
commit a3f1a3f3ad
Author: Chris Estreich <cestreich@gmail.com>
Date: Thu Apr 24 14:20:09 2025 -0700
Gemini caching improvements (#2925)
commit 7f99c0691e
Author: asychin <asychin@crypton.studio>
Date: Fri Apr 25 03:03:30 2025 +0700
feat/add-russian-lang (#2909)
* rus
* sad
* fix lang
* Add ru to translate mode instructions
* Add ru to evals types
* Add link to READMEs
* Bring back smart quotes
* Add prompt caching translations
---------
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
commit b75379bed3
Author: Chris Estreich <cestreich@gmail.com>
Date: Thu Apr 24 12:29:36 2025 -0700
Improve OpenRouter model fetching (#2922)
commit ad4782b766
Author: Chris Estreich <cestreich@gmail.com>
Date: Thu Apr 24 12:28:43 2025 -0700
Add an option to enable prompt caching (#2924)
commit 31600ed3c9
Author: R00-B0T <110429663+R00-B0T@users.noreply.github.com>
Date: Thu Apr 24 09:27:32 2025 -0700
Changeset version bump (#2919)
* changeset version bump
* Update CHANGELOG.md
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Chris Estreich <cestreich@gmail.com>
commit d46a9b484c
Author: Chris Estreich <cestreich@gmail.com>
Date: Thu Apr 24 09:24:56 2025 -0700
v3.14.1 (#2920)
commit fb91836203
Author: Chris Estreich <cestreich@gmail.com>
Date: Thu Apr 24 09:15:56 2025 -0700
Revert Gemini caching, fix OR supports cache issue (#2918)
commit 9b73965867
Author: R00-B0T <110429663+R00-B0T@users.noreply.github.com>
Date: Wed Apr 23 21:51:24 2025 -0700
Changeset version bump (#2877)
* changeset version bump
* Update CHANGELOG.md
* Update CHANGELOG.md
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
commit 2e9b1f1824
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Thu Apr 24 00:44:24 2025 -0400
v3.14.0 (#2902)
commit 37a8a442ac
Author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Date: Thu Apr 24 00:31:27 2025 -0400
Update contributors list (#2871)
docs: update contributors list [skip ci]
Co-authored-by: mrubens <mrubens@users.noreply.github.com>
commit b5ffaf1ba2
Author: Yikai Liao <lyk-boya@outlook.com>
Date: Thu Apr 24 12:22:47 2025 +0800
Fix Terminal Carriage Return Handling for Correct Progress Bar Display (#2562)
* fix(terminal): Ensure correct handling of carriage returns for progress bars
This commit refines the tests for `TerminalProcess` to ensure the correct interpretation of terminal output containing carriage returns (`\\r`), which is essential for properly handling dynamic elements like progress bars (e.g., `tqdm`).
- Validated the `processCarriageReturns` method's behavior in simulating terminal line overwrites caused by `\\r`.
- Corrected the expectation in the `handles carriage returns in mixed content` test to accurately reflect the method's output (final line content + preserved escape sequences), confirming the logic works as intended for progress-bar-like updates.
- Fixed a minor Jest `toBe` syntax error in a related test case.
- Suppressed an expected `console.warn` in the non-shell-integration test for cleaner logs.
By ensuring `processCarriageReturns` is correctly tested, we increase confidence that the component responsible for pre-processing terminal output handles progress bars appropriately before the output is potentially used elsewhere (e.g., sent to an LLM).
* fix(test): Make TerminalProcess integration test reliable
This commit fixes the flaky test case `integrates with getUnretrievedOutput to handle progress bars` in `TerminalProcess.test.ts`.
The test previously failed intermittently due to:
1. Relying on a fixed `setTimeout` duration to wait for asynchronous stream processing, which created a race condition.
2. Incorrectly assuming that `await terminalProcess.run(...)` would return the final output directly via its resolved value.
The fix addresses these issues by:
- Removing the unreliable intermediate check based on `setTimeout`.
- Modifying the test to correctly obtain the final output by listening for the `completed` event emitted by `TerminalProcess`, which is the intended way to receive the result.
This ensures the test accurately reflects the behavior of `TerminalProcess` and is no longer prone to timing-related failures.
* Add changeset for terminal carriage return fix
* Implement terminal compress progress bar feature
This commit introduces a new feature to compress terminal output by processing carriage returns. The `processCarriageReturns` function has been integrated into the `Terminal` class to handle progress bar updates effectively, ensuring only the final state is displayed.
Additionally, the `terminalCompressProgressBar` setting has been added to the global settings schema, allowing users to enable or disable this feature.
Tests have been updated to validate the new functionality and ensure correct behavior in various scenarios.
A Benchmark is also added to test the performance.
Not that there is still no i18n support for this.
* Add i18n support for compressProgressBar setting in multiple languages
* Optimize processCarriageReturns function for performance and multi-byte character handling
This commit enhances the `processCarriageReturns` function by implementing in-place string operations to improve performance, especially with large outputs. Key features include:
- Line-by-line processing to maximize chunk handling.
- Use of string indexes and substring operations instead of arrays.
- Single-pass traversal of input for efficiency.
- Special handling for multi-byte characters to prevent corruption during overwrites.
Additionally, tests have been updated to validate the new functionality, ensuring correct behavior with various character sets, including emojis and non-ASCII text.
Highly Density CR case is added to Benchmark
* slight performance improvement by caching several variable
* Optimize multi-byte character handling in processCarriageReturns
Refactor the logic within the `processCarriageReturns` function to simplify the detection of partially overwritten multi-byte characters (e.g., emojis).
Removed redundant checks and clarified the conditions for identifying potential character corruption during carriage return processing. This improves code readability and maintainability while preserving the original functionality of replacing potentially corrupted characters with a space.
Also enforced consistent use of semicolons for improved code style.
* docs: standardize carriage return (\r) and line feed (\n) terminology
Improve code clarity by consistently adding escape sequence notation to all
references of carriage returns and line feeds throughout documentation and tests.
This makes the code more readable and avoids ambiguity when discussing these special characters.
* feat: Improve terminal output processing clarity and settings UI
- Add detailed comments to `processCarriageReturns` explaining line feed handling.
- Relocate `terminalCompressProgressBar` setting below `terminalOutputLineLimit` for better context in UI.
* Fix: Compress Progress Bar Setting Checkbox
---------
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
commit 3b65023d0b
Author: axb <uaxb@hotmail.com>
Date: Thu Apr 24 12:12:07 2025 +0800
feat(diff): improve progress indicator for apply_diff tool (#2758)
Add animated dots to progress indicator based on content length
Optimize when progress updates are shown (every 10 characters)
Move searchBlockCount calculation inside conditional blocks
Skip unnecessary ask operations when toolProgressStatus is empty
commit c228e638b9
Author: Hannes Rudolph <hrudolph@gmail.com>
Date: Wed Apr 23 15:43:36 2025 -0600
Update insert_content tool description for clarity and detail (#2892)
commit 41db2cad43
Author: Hannes Rudolph <hrudolph@gmail.com>
Date: Wed Apr 23 15:42:59 2025 -0600
Update search_and_replace tool description for clarity and detail (#2891)
commit a53f604e3a
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Wed Apr 23 16:46:52 2025 -0400
Disable OpenRouter Gemini caching for now (#2890)
commit 03925d25b0
Author: Chris Estreich <cestreich@gmail.com>
Date: Wed Apr 23 13:26:59 2025 -0700
Fix code cli flag in evals (#2889)
commit 1543713c32
Author: Chris Estreich <cestreich@gmail.com>
Date: Wed Apr 23 13:24:22 2025 -0700
Don't immediately show an model ID error when changing API providers (#2888)
commit 2c40224f6f
Author: Chris Estreich <cestreich@gmail.com>
Date: Wed Apr 23 12:20:13 2025 -0700
Fix task size cache TTL (#2887)
commit 72962b3c76
Author: Chris Estreich <cestreich@gmail.com>
Date: Wed Apr 23 12:09:24 2025 -0700
Split api and chat message persistence into a separate module (#2866)
commit 230e953e5d
Author: Chris Estreich <cestreich@gmail.com>
Date: Wed Apr 23 11:42:22 2025 -0700
Throttle calls to calculate task folder size (#2885)
commit b421636a2c
Author: Chris Estreich <cestreich@gmail.com>
Date: Wed Apr 23 11:41:06 2025 -0700
Properly hide cache section of task header (#2884)
commit a08461a655
Author: Chris Estreich <cestreich@gmail.com>
Date: Wed Apr 23 11:35:50 2025 -0700
Gemini prompt caching (#2827)
commit 31d4b656d0
Author: Chris Estreich <cestreich@gmail.com>
Date: Wed Apr 23 11:21:35 2025 -0700
Package material icons in vsix (#2882)
* Package material icons in vsix
* Add changeset
commit a77be28a95
Author: Chris Estreich <cestreich@gmail.com>
Date: Wed Apr 23 11:16:20 2025 -0700
Use formatLargeNumber on token counts in task header (#2883)
* Format large numbers
* Add changeset
commit 970ccd7879
Author: Daniel <57051444+daniel-lxs@users.noreply.github.com>
Date: Wed Apr 23 12:21:32 2025 -0500
feat: add other useful variables to the custom system prompt (#2879)
commit 4606e9a6cc
Author: Dicha Zelianivan Arkana <51877647+elianiva@users.noreply.github.com>
Date: Wed Apr 23 22:57:50 2025 +0700
fix(chat): better loading feedback (#2750)
* fix(chat): better loading feedback
* fix(chat): missing loading aria role
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
* refactor(chat): use vscode loading for more consistency
---------
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
commit 80912d0919
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Wed Apr 23 09:47:19 2025 -0400
v13.3.3 (#2876)
commit a9ca17717c
Author: Chris Estreich <cestreich@gmail.com>
Date: Wed Apr 23 06:45:57 2025 -0700
OpenRouter Gemini caching (#2847)
* OpenRouter Gemini caching
* Fix tests
* Remove unsupported models
* Clean up the task header a bit
* Update src/api/providers/openrouter.ts
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
* Remove model that doesn't seem to work
---------
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
commit e53d299acf
Author: mlopezr <mlopezr@users.noreply.github.com>
Date: Wed Apr 23 15:36:50 2025 +0200
Allow Amazon Bedrock Marketplace ARNs (#2874)
* Update validate.ts
Allow ARNs from Bedrock Marketplace, which are different because models are deployed using SageMaker Inference behind the scenes.
* Update bedrock.ts
Allow ARNs from Bedrock Marketplace, which are different because models are deployed using SageMaker Inference behind the scenes.
commit 844753e0d9
Author: Dominik Oswald <6849456+d-oit@users.noreply.github.com>
Date: Wed Apr 23 15:34:32 2025 +0200
Remove unnecessary cost calculation from vscode-lm.ts (#2875)
* feat: Removed unnecessary cost calculation
* Update vscode-lm.ts
---------
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
commit 74faacd69d
Author: Wojciech Kordalski <wkordalski@pm.me>
Date: Wed Apr 23 10:42:06 2025 +0200
FakeAI "controller" object must not be copied (#2463)
The FakeAI object passed by the user must be exactly the same object
that is passed to FakeAIHandler via API configuration.
Unfortunatelly, as the VSCode global state is used as configuration
storage, we lose this property (VSCode global state creates copies of
the object). Also the class of the stored object is lost, so methods
of the object are unavailable.
Therefore, we store the original objects in global variable and
use ID field of FakeAI object to identify the original object.
commit e403a96e66
Author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Date: Wed Apr 23 03:14:08 2025 -0400
Update contributors list (#2867)
docs: update contributors list [skip ci]
Co-authored-by: mrubens <mrubens@users.noreply.github.com>
commit 01a7a66ca0
Author: Trung Dang <dangquoctrung123@gmail.com>
Date: Wed Apr 23 14:13:04 2025 +0700
feat: add `injectEnv` util, support env ref in mcp config (#2679)
* feat: support environment variables reference in mcp `env` config
* tests(src/utils/config): add test for `injectEnv`
* fix(injectEnv): use `env == null` and `??` check instead of `!env`, `||`
* refactor: remove unnecessary type declare
* chore!: simplify regexp, remove replacement for env vars with dots
commit 3df9eac5b7
Author: Dicha Zelianivan Arkana <51877647+elianiva@users.noreply.github.com>
Date: Wed Apr 23 14:09:29 2025 +0700
fix(mention): conditionally remove aftercursor content (#2732)
commit 33ee5d6c34
Author: hongzio <11085613+hongzio@users.noreply.github.com>
Date: Wed Apr 23 16:06:48 2025 +0900
Fix: focusInput open roo code panel (#2626) (#2817)
* Fix: focusInput open roo code panel (#2626)
* Fix: `roo-cline.focusInput` open roo code panel
* fixup! Fix: focusInput open roo code panel (#2626)
commit 3a5913ffca
Author: Alfredo Medrano <alfredomedranosanchez@gmail.com>
Date: Wed Apr 23 01:05:47 2025 -0600
Bugfix/fix vscodellm model information (#2832)
* feat: initialize VS Code Language Model client in constructor
* feat: add VS Code LLM models and configuration
* feat: integrate VS Code LLM models into API configuration normalization
* Fix tests
---------
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
commit c2dd743aeb
Author: seedlord <boklu@hotmail.de>
Date: Wed Apr 23 08:45:35 2025 +0200
Fix: Preserve editor state and prevent tab unpinning during diffs (#2857)
- Maintains editor view column state when closing and reopening files during diff operations, ensuring tabs stay opened in their original position.
- Prevents closing the original editor tab when opening the diff view, preserving pinned status when applying changes via write_to_file or apply_diff.
- Updates VSCode workspace launch flag from -n to -W for compatibility.
commit 3d129e8a89
Author: Hannes Rudolph <hrudolph@gmail.com>
Date: Wed Apr 23 00:35:10 2025 -0600
fix: allow opening files without workspace root (#1054)
* fix: allow opening files without workspace root
The openFile function in open-file.ts was requiring a workspace root to be present,
which prevented opening global files (like MCP settings) when no workspace was open.
Modified the function to handle absolute paths without this requirement.
Previously, trying to open MCP settings in a new window without a workspace would
error with "Could not open file: No workspace root found". Now the function
properly handles both workspace-relative and absolute paths, allowing global
settings files to be accessed in any context.
Changes:
- Removed workspace root requirement in openFile
- Added fallback for relative paths when no workspace is present
* fix: update openFile function to use provided path without modification
---------
Co-authored-by: Roo Code <support@roo.vet>
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
commit 0f64849542
Author: Daniel <57051444+daniel-lxs@users.noreply.github.com>
Date: Wed Apr 23 00:09:14 2025 -0500
feat: allow variable interpolation into the custom system prompt (#2863)
* feat: allow variable interpolation into the custom system prompt
* fix: allow the test to pass on windows by using the path module
commit c6f91a3b2f
Author: System233 <cdkeylzh@outlook.com>
Date: Wed Apr 23 03:40:33 2025 +0800
Fix error message not showing after canceling API request (#2845)
commit 1a376c262e
Author: Dicha Zelianivan Arkana <51877647+elianiva@users.noreply.github.com>
Date: Wed Apr 23 02:39:45 2025 +0700
[DRAFT] feat(menu): use material icons for files and folders (#2739)
feat(menu): use material icons for files and folders
commit 4f5b04b6c9
Merge: 2c6ef8a1 62d55893
Author: JB Brown <jb.brown@smartsheet.com>
Date: Tue Apr 22 12:30:06 2025 -0700
Merge pull request #4 from Smartsheet-JB-Brown/hobbsessr/fix-marketplace-redraw-issue
Hobbsessr/fix marketplace redraw issue
commit 62d5589397
Author: HobbesSR <20545418+HobbesSR@users.noreply.github.com>
Date: Tue Apr 22 12:16:01 2025 -0500
Fix marketplace tab switching and redraw issue that occurred every 30 seconds
commit 0d561f8a27
Author: System233 <cdkeylzh@outlook.com>
Date: Tue Apr 22 22:12:29 2025 +0800
Fix redundant 'TASK RESUMPTION' prompts (#2842)
commit 8ac911244c
Author: System233 <cdkeylzh@outlook.com>
Date: Tue Apr 22 22:10:34 2025 +0800
Fix user feedback not being added to conversation history in API error state (#2844)
Fix user feedback not being added to conversation history in the API error state
commit f1c79759a0
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Tue Apr 22 00:15:46 2025 -0400
Add line wrapping to MCP arguments (#2831)
commit 3e2d20f37c
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Mon Apr 21 22:57:58 2025 -0400
Search and replace fixes (#2830)
* Allow replacing with an empty string
* Visual cleanup
commit 8b5d48013e
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Mon Apr 21 22:39:39 2025 -0400
Fix MCP hub lookup during view transition (#2829)
commit b6ea9d1465
Author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Date: Mon Apr 21 21:22:54 2025 -0400
Update contributors list (#2803)
docs: update contributors list [skip ci]
Co-authored-by: mrubens <mrubens@users.noreply.github.com>
commit ff5430a95c
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Mon Apr 21 21:18:25 2025 -0400
Try adding better errors for write_to_file truncated output (#2821)
commit f06567d579
Author: Sam Hoang Van <samhv.ict@gmail.com>
Date: Tue Apr 22 03:49:27 2025 +0700
Feat/improve insert block content (#2510)
* refactor: enhance insertGroups and insertContentTool for better handling of insertion operations
* refactor: simplify insert_content tool
- Remove operations-based implementation in favor of single line insertion
- Update parameters from operations to line and content
- Simplify insertion logic and error handling
- Update tool description and documentation
- Remove XML parsing for operations
- Clean up code and improve error messages
* refactor: remove insert_content experiment and related tests
* Remove the append_to_file tool
* Improvements to chat row and instructions
---------
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
commit 80b298492c
Author: Sam Hoang Van <samhv.ict@gmail.com>
Date: Tue Apr 22 02:25:03 2025 +0700
improve search and replace tool (#2764)
* Refactor search and replace tool
* feat(search-replace): enhance search/replace tool UI and messaging
Refactor search/replace tool message structure for better consistency
Add dedicated UI component for displaying search/replace operations
Add i18n support for search/replace operations in all supported languages
Improve partial tool handling in searchAndReplaceTool
* Remove search_and_replace experiment and related references
commit 61e23cccb6
Author: Chris Estreich <cestreich@gmail.com>
Date: Mon Apr 21 11:04:35 2025 -0700
Record tool use errors encountered during eval runs (#2816)
commit a244a9dc21
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Mon Apr 21 13:54:02 2025 -0400
Fix a bad search/replace when moving tools into their own files (#2815)
commit b955dbc1fc
Author: Daniel Trugman <dtrugman@gmail.com>
Date: Mon Apr 21 16:56:02 2025 +0100
Requesty models behind api key (#2813)
* Don't fetch Requesty models on startup, only when opening settings
* Provide api key when fetching models
commit f1c3edeb75
Author: Felix NyxJae <52313587+NyxJae@users.noreply.github.com>
Date: Mon Apr 21 21:27:24 2025 +0800
Fix: Improve drag-and-drop and SSH path handling (#2808)
* Fix: Correct path handling for dragged files on Windows
* Fix: Improve drag-and-drop and SSH path handling
commit 29137fbfec
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Mon Apr 21 00:24:52 2025 -0400
Revert changes on omitted line count in write_to_file (#2807)
commit 2c6ef8a184
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Sun Apr 20 20:56:53 2025 -0700
allow relative path from pacakge items to outside of packages directory
commit 19df13c760
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Sun Apr 20 23:28:19 2025 -0400
Add a warning display when a system prompt override is active (#2804)
commit d2e5019f88
Author: Dicha Zelianivan Arkana <51877647+elianiva@users.noreply.github.com>
Date: Mon Apr 21 09:04:23 2025 +0700
fix(icons): use geometricPrecision to avoid blurriness (#2756)
commit 47230d5a5c
Author: Dicha Zelianivan Arkana <51877647+elianiva@users.noreply.github.com>
Date: Mon Apr 21 08:46:01 2025 +0700
fix(action): handle edge case for add to context action (#2780)
commit 612b948193
Author: Sacha Sayan <sacha.sayan@gmail.com>
Date: Sun Apr 20 21:38:08 2025 -0400
Refactor: Use path aliases in webview source files. (#2801)
* Refactor: Use path aliases in webview source files.
* Add module resolution to root jest config.
* Add tests.
Broken import.
Another broken import.
Broken test.
commit cf54c7d820
Author: Sacha Sayan <sacha.sayan@gmail.com>
Date: Sun Apr 20 21:19:42 2025 -0400
Quickfix: Change cloud-download icon to more appropriate desktop-download icon. (#2802)
commit eeb73c3c06
Author: Nico Bihan <nicolas.bihan@wellsky.com>
Date: Sat Apr 19 23:30:20 2025 -0500
Adds Gemini 2.5 Flash "thinking" model to Vertex AI Provider (#2794)
commit ed102d1850
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Sat Apr 19 15:43:27 2025 -0400
Remove the strict line bounds check from the diff (#2790)
commit 2205606118
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Sat Apr 19 14:26:35 2025 -0400
Remove globby as it's no longer used (#2788)
commit 5d0aa20f97
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Sat Apr 19 14:13:45 2025 -0400
Switch list files from globby to ripgrep (#2689)
* Switch list files from globby to ripgrep
* PR feedback
* PR fix
commit c5f48a8bc6
Author: R00-B0T <110429663+R00-B0T@users.noreply.github.com>
Date: Fri Apr 18 21:14:30 2025 -0700
Changeset version bump (#2777)
* changeset version bump
* Update CHANGELOG.md
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Chris Estreich <cestreich@gmail.com>
commit 8c18727f9a
Author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Date: Fri Apr 18 21:13:43 2025 -0700
Update contributors list (#2779)
docs: update contributors list [skip ci]
Co-authored-by: cte <cte@users.noreply.github.com>
commit e74b69dfbc
Author: Chris Estreich <cestreich@gmail.com>
Date: Fri Apr 18 21:01:21 2025 -0700
v3.13 announcement (#2778)
* v3.13 announcement
* Update README.md
* Update lastShownAnnouncementId
* Tweak copy
commit c10600e354
Author: Chris Estreich <cestreich@gmail.com>
Date: Fri Apr 18 19:25:42 2025 -0700
Pass baseURL to Gemini API if googleGeminiBaseUrl is set (#2776)
commit b8b90737fa
Author: R00-B0T <110429663+R00-B0T@users.noreply.github.com>
Date: Fri Apr 18 17:39:24 2025 -0700
Changeset version bump (#2768)
* changeset version bump
* Update CHANGELOG.md
* Update CHANGELOG.md
* Update CHANGELOG.md
* Update CHANGELOG.md
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Chris Estreich <cestreich@gmail.com>
commit 5eaf8ba876
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Fri Apr 18 16:48:28 2025 -0700
fix regex in response to security scan concern in CI build
commit 130493e43d
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Fri Apr 18 15:25:35 2025 -0700
fix renaming bug for locales
commit 2a5d472f0d
Author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Date: Fri Apr 18 15:14:38 2025 -0700
Update contributors list (#2770)
docs: update contributors list [skip ci]
Co-authored-by: cte <cte@users.noreply.github.com>
commit d5fe876c9b
Author: Chris Estreich <cestreich@gmail.com>
Date: Fri Apr 18 14:56:01 2025 -0700
v3.13.1 (#2774)
commit 1a05a4190f
Merge: a122dc74 96ff9fc3
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Fri Apr 18 14:52:12 2025 -0700
Merge branch 'main' into jbbrown/marketplace
commit a122dc7465
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Fri Apr 18 11:08:21 2025 -0700
fix failing tests from state management changes
commit 96ff9fc380
Author: Chris Estreich <cestreich@gmail.com>
Date: Fri Apr 18 14:43:27 2025 -0700
Fix pricing for Gemini 2.5 Flash (Thinking) (#2773)
* Fix pricing for Gemini 2.5 Flash (Thinking)
* Looks like it's actually $3.50
* We aren't honoring custom thinking token budgets on Vertex yet
commit f6e4e3504f
Author: Chris Estreich <cestreich@gmail.com>
Date: Fri Apr 18 14:43:04 2025 -0700
Move executeCommand out of Cline and add telemetry for shell integration errors (#2771)
commit 0bb5ec18c5
Author: Sacha Sayan <sacha.sayan@gmail.com>
Date: Fri Apr 18 16:43:03 2025 -0400
UI: Auto-approve toggle styling tweak. (#2769)
commit 5abea50cf1
Author: Chris Estreich <cestreich@gmail.com>
Date: Fri Apr 18 12:26:17 2025 -0700
Support Gemini 2.5 Flash thinking (#2752)
commit 968e19047b
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Fri Apr 18 10:23:02 2025 -0700
rebrand to Marketplace
commit 6772306ade
Author: Felix NyxJae <52313587+NyxJae@users.noreply.github.com>
Date: Fri Apr 18 22:29:32 2025 +0800
Fix: Correct path handling for dragged files on Windows (#2753)
commit b3065d2ab3
Author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Date: Fri Apr 18 03:09:14 2025 -0400
Update contributors list (#2715)
docs: update contributors list [skip ci]
Co-authored-by: mrubens <mrubens@users.noreply.github.com>
commit 31656d9b16
Author: R00-B0T <110429663+R00-B0T@users.noreply.github.com>
Date: Thu Apr 17 23:47:41 2025 -0700
Changeset version bump (#2716)
* changeset version bump
* Update CHANGELOG.md
* Update package.json
* Update package-lock.json
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
commit c329b4509f
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Fri Apr 18 02:29:13 2025 -0400
v3.12.4 (#2745)
commit 06882f5687
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Fri Apr 18 02:23:55 2025 -0400
Don't break if an end_line is passed into a diff (#2743)
commit 87af3b3424
Author: Chris Estreich <cestreich@gmail.com>
Date: Thu Apr 17 22:21:14 2025 -0700
Record tool usages in the `Cline` object, and persist them in the db for evals (#2729)
commit 887d2ac330
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Thu Apr 17 21:52:31 2025 -0700
fix missing translations
commit 9e2476c667
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Thu Apr 17 21:47:50 2025 -0700
attempt to fix failing test on windows in ci build
commit c0f615bca7
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Thu Apr 17 21:40:26 2025 -0700
more memory pressure work
commit b5a77e34a4
Author: Nico Bihan <nicolas.bihan@wellsky.com>
Date: Thu Apr 17 23:35:06 2025 -0500
Fixes maximum token limit for Gemini provider 2.5 pro exp (#2737)
Corrects the maximum token limit for the "gemini-2.5-pro-exp-03-25" model, ensuring accurate configuration.
commit fffebf1a2e
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Fri Apr 18 00:28:55 2025 -0400
Remove experiment for append block (#2738)
* Remove experiment for append block
* Fix bugs in experiment lookups
commit c84343dbbe
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Thu Apr 17 21:02:15 2025 -0700
reduce memory use during MetaDataScan
commit b05c3100bd
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Thu Apr 17 23:25:35 2025 -0400
Fix context window bar color (#2733)
* Fix context window bar color
* Make task header cost badge match the other ones
* Fix test
commit 86636526bd
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Thu Apr 17 23:25:23 2025 -0400
Update the style of the suggestions (#2734)
* Update the style of the suggestions
* Cleanup
commit 422aed61b7
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Thu Apr 17 20:21:16 2025 -0700
memory optimization and revert test command in package.json to main
commit bea36c8977
Author: Nico Bihan <nicolas.bihan@wellsky.com>
Date: Thu Apr 17 22:19:40 2025 -0500
Gemini 2.5 Flash Preview fix Max Tokens Count (#2735)
Gemini 2.5 Flash Preview fix Max Tokens
commit 93b96c3253
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Thu Apr 17 19:53:24 2025 -0700
attempt at memory cleanup
commit 3cc3dab9e5
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Thu Apr 17 19:28:50 2025 -0700
change metadatascanner to be more memory efficient
commit b7f45f7870
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Thu Apr 17 19:23:29 2025 -0700
reduce memory usage - avoid deep copy
commit a2d7f19418
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Thu Apr 17 17:30:53 2025 -0700
round 5
commit 1cb77542e1
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Thu Apr 17 17:25:28 2025 -0700
round 4
commit 8a9240000c
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Thu Apr 17 17:20:14 2025 -0700
round 3 of locale CI fixes
commit 724a2f2350
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Thu Apr 17 17:13:02 2025 -0700
another round of locale bugs in the CI build but not local
commit 38d96fa4fb
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Thu Apr 17 17:06:07 2025 -0700
try to fix locale bugs that happen in CI build but not locally
commit 78edc21f2e
Merge: 1aea6b4d 3c937c38
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Thu Apr 17 16:53:19 2025 -0700
merge main
commit 1aea6b4d9b
Merge: 4f8799f1 92c55d54
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Thu Apr 17 15:54:34 2025 -0700
Merge hobbessr/metadatascanner-test-path-recursion-fix
commit 92c55d542e
Author: HobbesSR <20545418+HobbesSR@users.noreply.github.com>
Date: Thu Apr 17 17:38:07 2025 -0500
Fix the MetaDataScanner.test.ts infinite recursion in its mock setup
commit 3c937c3827
Author: Chris Estreich <cestreich@gmail.com>
Date: Thu Apr 17 15:28:11 2025 -0700
Task header theme fixes (#2721)
commit b077267b4a
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Thu Apr 17 15:25:11 2025 -0700
fixes image support in bedrock. regression from prompt cache implementation (#2723)
fixes image support in bedrock. regression created during prompt caching implementation
commit d86d601104
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Thu Apr 17 16:39:00 2025 -0400
Add gemini 2.5 flash preview (#2720)
commit 4f8799f169
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Thu Apr 17 13:32:08 2025 -0700
fix localization bugs
commit 471caff000
Author: Chris Estreich <cestreich@gmail.com>
Date: Thu Apr 17 13:31:48 2025 -0700
Clean up types related to tools (#2719)
commit 026091e432
Author: Hannes Rudolph <hrudolph@gmail.com>
Date: Thu Apr 17 14:18:50 2025 -0600
Fix filename format in downloadTask function for markdown export (#2717)
* Fix filename format in downloadTask function for markdown export
* Update src/integrations/misc/export-markdown.ts
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
---------
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
commit 1853ebaa9c
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Thu Apr 17 12:59:13 2025 -0700
add sourceUrl to support packages hosted external to the package manager repo
commit 1e0e01b9f3
Author: Sam Hoang Van <samhv.ict@gmail.com>
Date: Fri Apr 18 02:58:22 2025 +0700
feat: add append_to_file tool for appending content to files (#2712)
- Implemented the append_to_file tool to allow users to append content to existing files or create new ones if they do not exist.
- Updated the rules and instructions to include the new tool.
- Added tests for the append_to_file functionality, covering various scenarios including error handling and content preprocessing.
- Enhanced the experiment schema to include the new append_to_file experiment ID.
- Updated relevant interfaces and types to accommodate the new tool.
- Modified the UI to display the append_to_file tool in the appropriate sections.
commit d3c37eaec8
Author: Chris Estreich <cestreich@gmail.com>
Date: Thu Apr 17 10:23:19 2025 -0700
Add missing translation (#2714)
* Add missing translation
* More translation fixes
commit 159e4005e5
Author: Sacha Sayan <sacha.sayan@gmail.com>
Date: Thu Apr 17 12:54:55 2025 -0400
UI Glam Sesh: Makeover for TaskHeader, ChatView, HistoryPreview... (#2701)
* - UI Glam Session -> Makeover for TaskHeader, ChatView, HistoryPreview, WelcomeView
- In particular, display a "no tasks in workspace" message when no tasks are found.
- Clean up Inferface Settings (not needed now) on Settings View
- Copy updates throughout these areas.
* Apply suggestions from code review
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
* Missing translation string.
* Fix test
* Fix tests
* Improve translations / fix missing strings.
* Support xAI for evals (#2703)
* Make sure the slash commands only fire if they're the first character (#2702)
* Update contributors list (#2675)
docs: update contributors list [skip ci]
Co-authored-by: mrubens <mrubens@users.noreply.github.com>
* v3.12.3 (#2710)
* Changeset version bump (#2711)
* changeset version bump
* Updating CHANGELOG.md format
* Update CHANGELOG.md
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: R00-B0T <github-actions@github.com>
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
* Update translations
---------
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
Co-authored-by: cte <cestreich@gmail.com>
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: R00-B0T <110429663+R00-B0T@users.noreply.github.com>
Co-authored-by: R00-B0T <github-actions@github.com>
commit 0736379ad0
Author: R00-B0T <110429663+R00-B0T@users.noreply.github.com>
Date: Thu Apr 17 07:45:43 2025 -0700
Changeset version bump (#2711)
* changeset version bump
* Updating CHANGELOG.md format
* Update CHANGELOG.md
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: R00-B0T <github-actions@github.com>
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
commit 94842f21af
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Thu Apr 17 10:34:32 2025 -0400
v3.12.3 (#2710)
commit d3ba74c568
Author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Date: Thu Apr 17 10:26:50 2025 -0400
Update contributors list (#2675)
docs: update contributors list [skip ci]
Co-authored-by: mrubens <mrubens@users.noreply.github.com>
commit 511ebb7a98
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Thu Apr 17 07:55:53 2025 -0400
Make sure the slash commands only fire if they're the first character (#2702)
commit 0374436fac
Author: Chris Estreich <cestreich@gmail.com>
Date: Wed Apr 16 23:08:58 2025 -0700
Support xAI for evals (#2703)
commit 876742a887
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Wed Apr 16 21:41:30 2025 -0700
try another modification to the test script for windows out of memory errors in ci build
commit 0c817af055
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Wed Apr 16 21:20:13 2025 -0700
increase max heap size to try and get windows passing in the CI build
commit c27aa9c370
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Wed Apr 16 21:11:51 2025 -0700
heap cleanup
commit e5556dd115
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Wed Apr 16 20:46:26 2025 -0700
attempt to incrase node heap size
commit b7033133ad
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Wed Apr 16 20:36:46 2025 -0700
try a path change to see if it helps the CI build pass in the GitHub environment
commit 981c7a1270
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Wed Apr 16 20:27:20 2025 -0700
rework git test to see if it helps in the CI build
commit 68b0a1843d
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Wed Apr 16 20:23:12 2025 -0700
fix knip and missing translation errors
commit 30c44ff14c
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Wed Apr 16 23:04:16 2025 -0400
Support dragging and dropping tabs into chat text area (#2698)
commit 6e299c73f5
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Wed Apr 16 19:28:39 2025 -0700
update documentation
commit 9a24ed46a2
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Wed Apr 16 18:02:38 2025 -0700
allow custom git domains to support internal dns at companies
commit c2d840cc47
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Wed Apr 16 17:08:43 2025 -0700
remove console log statements used to debug tests
commit 9c28626f24
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Wed Apr 16 16:31:24 2025 -0700
refactor: remove package manager state files
commit a4a4532922
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Wed Apr 16 16:06:23 2025 -0700
fix typescript errors preventing succesful build
commit ba5af60109
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Wed Apr 16 18:05:04 2025 -0400
Fix diff escaping issues (#2694)
* Fix diff escaping issues
* Potential fix for code scanning alert no. 75: Double escaping or unescaping
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
---------
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
commit f04487cd3e
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Wed Apr 16 14:35:39 2025 -0700
All but 1 test passing
commit a575d25252
Merge: 0caf685b 4bf746d6
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Wed Apr 16 12:43:51 2025 -0700
Merge branch 'main' into jbbrown/package-manager
* main: (161 commits)
Changeset version bump (#2688)
v3.12.2 (#2693)
Add support for different reasoning effort (#2692)
Add OpenAI o3 & 4o-mini (#2691)
Add consecutive mistake count to diff error telemetry (#2687)
refactor(context-menu): handle filename display better (#2684)
Changeset version bump (#2683)
Fix select dropdown styling (#2682)
Changeset version bump (#2676)
v3.12.0 (#2674)
Fix configuration titles (#2672)
feat: Add 'roo.acceptInput' command (#2598)
Add xAI provider (#2667)
Await checkpoint saves (except the initial) (#2665)
Safe JSON parse in ChatRow (#2666)
feat: Cost Display in Task Header - Suppress Zero Cost Values and Ensure Visibility for Gemini, OpenAI, LM Studio, and Ollama (#2662)
test: limit Jest worker count to 40% per suite (#2658)
DRY up the auto-approve toggles (#2664)
Expose reasoning effort option for reasoning models on OpenRouter (#2483)
Better string normalization for diffs (#2659)
...
commit 0caf685b4f
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Wed Apr 16 12:33:20 2025 -0700
rolled back unintended changes not related or needed to support package manager. first phases of build pass, but fail at type checking on things that don't seem related to what I've done
commit 4bf746d635
Author: R00-B0T <110429663+R00-B0T@users.noreply.github.com>
Date: Wed Apr 16 11:52:22 2025 -0700
Changeset version bump (#2688)
* changeset version bump
* Update CHANGELOG.md
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
commit 454df52462
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Wed Apr 16 14:47:35 2025 -0400
v3.12.2 (#2693)
commit 43668e0429
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Wed Apr 16 14:16:51 2025 -0400
Add support for different reasoning effort (#2692)
commit 250ea6867a
Author: Peter Dave Hello <hsu@peterdavehello.org>
Date: Thu Apr 17 02:10:10 2025 +0800
Add OpenAI o3 & 4o-mini (#2691)
Reference:
- https://platform.openai.com/docs/models/o3
- https://platform.openai.com/docs/models/o4-mini
- https://openai.com/index/introducing-o3-and-o4-mini/
commit 9d761e23e2
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Wed Apr 16 12:02:49 2025 -0400
Add consecutive mistake count to diff error telemetry (#2687)
commit 1d029ed0cb
Author: Dicha Zelianivan Arkana <51877647+elianiva@users.noreply.github.com>
Date: Wed Apr 16 20:46:06 2025 +0700
refactor(context-menu): handle filename display better (#2684)
* refactor(context-menu): handle filename display better
* refactor(context-menu): reduce string computation
commit c980662728
Author: R00-B0T <110429663+R00-B0T@users.noreply.github.com>
Date: Wed Apr 16 03:44:17 2025 -0700
Changeset version bump (#2683)
* changeset version bump
* Updating CHANGELOG.md format
* Update CHANGELOG.md
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: R00-B0T <github-actions@github.com>
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
commit 2d360dc59e
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Wed Apr 16 06:40:43 2025 -0400
Fix select dropdown styling (#2682)
commit 923e391bb8
Author: R00-B0T <110429663+R00-B0T@users.noreply.github.com>
Date: Tue Apr 15 22:22:03 2025 -0700
Changeset version bump (#2676)
* changeset version bump
* Updating CHANGELOG.md format
* Update CHANGELOG.md
* Update package.json
* Update package-lock.json
* Update CHANGELOG.md
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: R00-B0T <github-actions@github.com>
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
commit e2d649dc50
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Wed Apr 16 01:16:06 2025 -0400
v3.12.0 (#2674)
commit 82df05a297
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Tue Apr 15 23:09:18 2025 -0400
Fix configuration titles (#2672)
commit 1cca4f47c7
Author: Aleksandr Kirillov <32141102+axkirillov@users.noreply.github.com>
Date: Wed Apr 16 05:01:45 2025 +0200
feat: Add 'roo.acceptInput' command (#2598)
* feat: Add 'roo.acceptInput' command
* Update package.nls.json
* Update translations
---------
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
commit 37f7d83792
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Tue Apr 15 22:16:52 2025 -0400
Add xAI provider (#2667)
* Add xAI provider
* Add model reasoning effort
* DRY this up
* Handle undefined delta
* Cleanup getModel to fix test
* Add missing translations
* Small type cleanup
* Support temperature
---------
Co-authored-by: cte <cestreich@gmail.com>
commit 3b19d7a455
Author: Chris Estreich <cestreich@gmail.com>
Date: Tue Apr 15 16:57:12 2025 -0700
Await checkpoint saves (except the initial) (#2665)
commit 75a6bc100e
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Tue Apr 15 19:49:25 2025 -0400
Safe JSON parse in ChatRow (#2666)
commit 1e77f62063
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Tue Apr 15 15:39:36 2025 -0700
remove improvement proposals because I already did them
commit 7fab2f7738
Author: Dominik Oswald <6849456+d-oit@users.noreply.github.com>
Date: Tue Apr 15 23:57:42 2025 +0200
feat: Cost Display in Task Header - Suppress Zero Cost Values and Ensure Visibility for Gemini, OpenAI, LM Studio, and Ollama (#2662)
test: Add unit tests for TaskHeader component cost display logic
commit 8839d72243
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Tue Apr 15 14:40:00 2025 -0700
stragllers
commit da2ab6e388
Author: KJ7LNW <93454819+KJ7LNW@users.noreply.github.com>
Date: Tue Apr 15 14:37:27 2025 -0700
test: limit Jest worker count to 40% per suite (#2658)
- Each test suite (extension/webview) limited to 40% CPU usage
- Total CPU utilization capped at 80% (40% x 2 suites)
- Reserves 20% CPU for system and user tasks
- Prevents memory thrashing and system slowdown
- Reduces risk of OOM kills on memory-constrained systems
- Maintains smooth UI responsiveness during test runs
- Improves test reliability while keeping parallel execution
Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
Co-authored-by: Eric Wheeler <roo-code@z.ewheeler.org>
commit 1bbfd2e8e6
Author: Chris Estreich <cestreich@gmail.com>
Date: Tue Apr 15 14:36:13 2025 -0700
DRY up the auto-approve toggles (#2664)
* DRY up the auto-approve toggles
* Better subtask icon per @joemanley201
commit e7a57ea774
Author: Chris Estreich <cestreich@gmail.com>
Date: Tue Apr 15 13:51:52 2025 -0700
Expose reasoning effort option for reasoning models on OpenRouter (#2483)
* Specify reasoning effort for OpenRouter reasoning models
* Add ReasoningEffort type
* Fix ReasoningEffort props
* Remove copypasta
* Set reasoning effort for Grok 3 Mini
* Use translations
* Add translations
* Remove this check
commit 448fb3dd20
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Tue Apr 15 13:50:53 2025 -0700
update implementation documentation
commit b669edd82b
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Tue Apr 15 13:38:38 2025 -0700
fix author and authorUrl display
commit 51bcade4c5
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Tue Apr 15 16:20:20 2025 -0400
Better string normalization for diffs (#2659)
commit f5b97baac4
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Tue Apr 15 13:15:05 2025 -0700
translations
commit 8eec97af25
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Tue Apr 15 13:09:04 2025 -0700
component details matched tags showing correctly
commit c0672358c7
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Tue Apr 15 12:47:00 2025 -0700
Remove problematic debounce logic
commit 648c6e7d28
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Tue Apr 15 14:59:27 2025 -0400
Move diff editing config to provider settings (#2655)
* Move diff editing config to provider settings
* Fix tests
commit a3b2ebc026
Author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Date: Tue Apr 15 14:41:12 2025 -0400
Update contributors list (#2595)
docs: update contributors list [skip ci]
Co-authored-by: cte <cte@users.noreply.github.com>
commit a4d2de4534
Author: Chris Estreich <cestreich@gmail.com>
Date: Tue Apr 15 11:29:38 2025 -0700
Add pass / fail events for evals (#2656)
commit 74a6de7b6f
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Tue Apr 15 10:14:47 2025 -0700
remove un-needed refresh button
commit 4e645b457b
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Tue Apr 15 10:07:50 2025 -0700
sorting controls work
commit d966884b1d
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Tue Apr 15 10:01:57 2025 -0700
card item buttons working again
commit 5f19ea4a06
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Tue Apr 15 10:49:01 2025 -0400
Add telemetry for prompt enhancement (#2651)
commit 31fd6e1470
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Tue Apr 15 10:37:48 2025 -0400
Capture telemetry for usage of code actions (#2650)
commit f551fe2476
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Tue Apr 15 07:26:27 2025 -0700
All display of items seems to work properly
commit afbf174e19
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Tue Apr 15 10:17:17 2025 -0400
Add telemetry for consecutive mistake error (#2649)
commit 84c574c947
Author: Sam Hoang Van <samhv.ict@gmail.com>
Date: Tue Apr 15 21:14:39 2025 +0700
Fuzzy search bar select dropdown (#2635)
* Revert "Revert "feat: implement fuzzy search and dropdown grouping in SelectDropdown component" (#2627)"
This reverts commit f1ad8ab751.
* Fix double scroll bar on provider select dropdown
* Update webview-ui/src/components/ui/select-dropdown.tsx
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
---------
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
commit ab0e7cd81e
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Tue Apr 15 00:00:32 2025 -0700
source change results in reload
commit 309c889483
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Mon Apr 14 23:47:52 2025 -0700
deleting a source causes browse refresh
commit 15d310469c
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Mon Apr 14 23:23:39 2025 -0700
initialize first load with data on browse
commit 9a5023504c
Author: R00-B0T <110429663+R00-B0T@users.noreply.github.com>
Date: Mon Apr 14 23:15:02 2025 -0700
Changeset version bump (#2629)
* changeset version bump
* Update CHANGELOG.md
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
commit 6e567845cb
Author: Chris Estreich <cestreich@gmail.com>
Date: Mon Apr 14 23:11:13 2025 -0700
Remove auto-approve button useMemo (#2630)
commit 249d53b30d
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Tue Apr 15 02:06:58 2025 -0400
v3.11.17 (#2628)
commit 75dce909f0
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Mon Apr 14 23:00:21 2025 -0700
Add default source on first load
commit f1ad8ab751
Author: Chris Estreich <cestreich@gmail.com>
Date: Mon Apr 14 22:53:52 2025 -0700
Revert "feat: implement fuzzy search and dropdown grouping in SelectDropdown component" (#2627)
Revert "feat: implement fuzzy search and dropdown grouping in SelectDropdown …"
This reverts commit 89107b82a3.
commit ad91533205
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Mon Apr 14 22:47:56 2025 -0700
re add default source when all sources are deleted
commit 1cdedf3048
Author: Chris Estreich <cestreich@gmail.com>
Date: Mon Apr 14 22:47:27 2025 -0700
Update translations (#2623)
* Update translations
* Clean up the buttons
commit c92be6aadb
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Mon Apr 14 22:42:11 2025 -0700
more unit test work
commit 00609aa2f3
Author: KJ7LNW <93454819+KJ7LNW@users.noreply.github.com>
Date: Mon Apr 14 22:34:45 2025 -0700
fix: race in short-running command output capture (#2624)
Previously, handlers for terminal output were registered after starting the process,
which could cause output to be missed for fast-executing commands that complete
before handlers are set up.
- Adds CommandCallbacks interface to register handlers upfront
- Moves handler registration before process start to ensure no output is missed
- Provides process instance in callbacks for direct control
- Adds debug logging to help diagnose timing issues
Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
Co-authored-by: Eric Wheeler <roo-code@z.ewheeler.org>
commit 4c88e930e9
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Mon Apr 14 22:24:41 2025 -0700
all package manager related tests passing together
commit c05d760747
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Mon Apr 14 21:59:39 2025 -0700
PackageManagerView tests all passing
commit beb151e1b0
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Mon Apr 14 21:59:22 2025 -0700
PackageManagerView tests all passing
commit 4f5796c152
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Tue Apr 15 00:27:34 2025 -0400
Add telemetry for diff errors (#2619)
commit 111ac9ca47
Author: Sacha Sayan <sacha.sayan@gmail.com>
Date: Tue Apr 15 00:20:54 2025 -0400
UI Fix: Approve Tool Use Grid Toggles. (#2487)
Improv auto approve layout + refactor.
Button grid layout. Consolidates shortNames and labels, includes translations.
commit 5b48a2d708
Author: Chris Estreich <cestreich@gmail.com>
Date: Mon Apr 14 21:13:08 2025 -0700
More small evals tweaks (#2620)
commit 89107b82a3
Author: Sam Hoang Van <samhv.ict@gmail.com>
Date: Tue Apr 15 11:05:42 2025 +0700
feat: implement fuzzy search and dropdown grouping in SelectDropdown component (#2431)
* feat: implement fuzzy search and dropdown grouping in SelectDropdown component
* refactor: optimize SelectDropdown component with memoization and improved performance
* Remove focus output and translate placeholder
---------
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
commit a64cab92dc
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Mon Apr 14 22:55:06 2025 -0400
Fix openai cache tracking and cost estimates (#2616)
* fix(api): update cacheReadsPrice for OpenAI GPT-4.1 models (#2887)
Set correct cacheReadsPrice (cached input price) for gpt-4.1, gpt-4.1 mini, and gpt-4.1 nano based on official OpenAI pricing. No changes to cacheWritesPrice as per current OpenAI documentation. This ensures prompt caching costs are accurately reflected for these models in cost calculations.
* Update more OpenAI cache prices
* Track cache tokens and cost correctly for OpenAI
* Update tests
---------
Co-authored-by: monotykamary <tom81094@gmail.com>
commit 1eb29be33d
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Mon Apr 14 20:39:27 2025 -0400
Remove the end_line from the multi_diff instructions and logic (#2615)
commit 72d9be2a6c
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Mon Apr 14 17:13:26 2025 -0700
packagemanagermanager tests passing
commit e1c590442c
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Mon Apr 14 17:10:09 2025 -0700
PackageManagerSourceValidation, GitFetcher, and MetadataScanner tests all pass
commit 30c3a65b7b
Author: nobuo kawasaki <nobu007@users.noreply.github.com>
Date: Tue Apr 15 08:25:09 2025 +0900
Fix eslint error about --ext option by remove it (#2543)
commit 1a123ed307
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Mon Apr 14 15:20:35 2025 -0700
documentation update
commit 39332513ae
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Mon Apr 14 15:00:14 2025 -0700
documentation updates
commit 6f8f8c6a1d
Author: Chris Estreich <cestreich@gmail.com>
Date: Mon Apr 14 14:51:53 2025 -0700
Parse providers individually (#2611)
* Parse providers individually
* Remove .strict()
* Clean up tests
commit b196489b97
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Mon Apr 14 17:09:38 2025 -0400
Fix overdependence on start/end lines in diff strategy (#2567)
* Add test for issue #2556
* Fix overdependence on start/end lines in diff strategy
commit af6c27bdff
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Mon Apr 14 13:33:58 2025 -0700
locale fixes for metadata
commit 71d30b835b
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Mon Apr 14 12:37:27 2025 -0700
documentation start
commit 45fb958505
Author: R00-B0T <110429663+R00-B0T@users.noreply.github.com>
Date: Mon Apr 14 10:32:18 2025 -0700
Changeset version bump (#2606)
* changeset version bump
* Update CHANGELOG.md
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
commit ab7ca17f29
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Mon Apr 14 13:27:33 2025 -0400
Fix test (#2607)
commit 929503a4c8
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Mon Apr 14 13:21:47 2025 -0400
Add GPT 4.1 (#2605)
commit 2ca7a85c20
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Mon Apr 14 10:13:48 2025 -0700
consolidate packagemanager test files
commit dee7510ed7
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Mon Apr 14 09:18:48 2025 -0700
searching finds items in components of a pcakage and shows which ones match
commit 8cf3c532cf
Author: Hannes Rudolph <hrudolph@gmail.com>
Date: Mon Apr 14 09:58:52 2025 -0600
Update log messages for Cline instances to Roo Code instances in regi… (#2604)
Update log messages for Cline instances to Roo Code instances in registerCommands.ts and corresponding test adjustments
commit d73789cfe4
Author: Chris Estreich <cestreich@gmail.com>
Date: Mon Apr 14 08:15:20 2025 -0700
Update default settings for evals (#2601)
commit 81304a22ea
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Mon Apr 14 06:54:17 2025 -0700
package details visible in expandable section
commit 4fe0b3c419
Author: feifei <46489071+feifei325@users.noreply.github.com>
Date: Mon Apr 14 19:20:40 2025 +0800
feat: Add modelId support when exporting tasks (#2142)
Added modelId to the task export process.
Signed-off-by: feifei <liuyansheng1121@gmail.com>
commit 965a7ae90c
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Sun Apr 13 21:01:47 2025 -0700
refactor(search): simplify to basic string contains match while preserving subcomponent handling
- Replace complex word boundary/regex matching with simple string.includes()
- Maintain all subcomponents in search results
- Preserve proper matchInfo for packages and subcomponents
commit df5be205f5
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Sun Apr 13 20:35:42 2025 -0700
test: add comprehensive substring matching tests for package manager search
- Add test cases for case-insensitive matching
- Add test cases for partial string matching
- Add test cases using real data from package-manager-template
- Verify search behavior with actual component data
commit 3ffe61f548
Author: R00-B0T <110429663+R00-B0T@users.noreply.github.com>
Date: Sun Apr 13 20:33:12 2025 -0700
Changeset version bump (#2587)
* changeset version bump
* Updating CHANGELOG.md format
* Update CHANGELOG.md
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: R00-B0T <github-actions@github.com>
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
commit 180f904528
Author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Date: Sun Apr 13 23:10:53 2025 -0400
Update contributors list (#2583)
docs: update contributors list [skip ci]
Co-authored-by: mrubens <mrubens@users.noreply.github.com>
commit 73a6ab8acd
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Sun Apr 13 22:57:36 2025 -0400
v3.11.15 (#2586)
commit db85df86d9
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Sun Apr 13 22:53:15 2025 -0400
Update NLS translations (#2584)
commit eab61289a2
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Sun Apr 13 22:27:28 2025 -0400
Add workspace filter to historypreview as well (#2582)
* Add workspace filter to historypreview as well
* PR feedback
commit dff42d28c0
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Sun Apr 13 12:24:48 2025 -0700
feat(package-manager): enhance subcomponent metadata scanning
- Add recursive scanning for nested components
- Fix path handling for nested directories
- Improve test coverage for package subcomponents
- Fix timestamp handling in git-based dates
commit 1c03234d64
Author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Date: Sun Apr 13 13:52:38 2025 -0400
Update contributors list (#2519)
docs: update contributors list [skip ci]
Co-authored-by: mrubens <mrubens@users.noreply.github.com>
commit ff92a61281
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Sun Apr 13 13:51:14 2025 -0400
Revert "Add o1-pro to api.ts" (#2574)
Revert "Add o1-pro to api.ts (#2433)"
This reverts commit 16d8f14371.
commit 494af3090a
Author: Sam Hoang Van <samhv.ict@gmail.com>
Date: Mon Apr 14 00:44:03 2025 +0700
fix: normalize file paths to POSIX format in search results (#2569)
commit caf1ae35e1
Author: pokutuna <mail@pokutuna.com>
Date: Mon Apr 14 02:42:59 2025 +0900
fix: Restore focus ring for VSCodeButton component (#2572)
commit 4ecec98384
Author: Chris Estreich <cestreich@gmail.com>
Date: Sun Apr 13 10:30:06 2025 -0700
Support all providers in evals settings (#2573)
commit 352a6b9c75
Author: Chris Estreich <cestreich@gmail.com>
Date: Sun Apr 13 09:44:45 2025 -0700
More unit test kill -9 fixes (#2570)
* More unit test kill -9 fixes
* Fix linter warning
commit 8bb3839b4d
Author: Zhang Tony <157202938+zhangtony239@users.noreply.github.com>
Date: Sun Apr 13 22:09:46 2025 +0800
fix: background color for new profile dialog (#2560)
commit adadc3add2
Author: Sam Hoang Van <samhv.ict@gmail.com>
Date: Sun Apr 13 15:00:23 2025 +0700
fix build vsix package (#2554)
commit ef9b339027
Author: Chris Estreich <cestreich@gmail.com>
Date: Sun Apr 13 00:57:33 2025 -0700
Evals improvements (#2555)
* Evals improvements
* Remove debugging
commit 4891fb04b7
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Sat Apr 12 23:12:40 2025 -0700
feat: implement git-based lastUpdated dates
commit 3e8d35b490
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Sat Apr 12 23:11:03 2025 -0700
fix: package manager refresh state handling and item display
commit d3c65ceac6
Author: Zhang Tony <157202938+zhangtony239@users.noreply.github.com>
Date: Sun Apr 13 13:16:01 2025 +0800
feature: Closable welcome message (#2541)
* draft: try to add a setting button
* Add showGreeting setting and related changes
* i18n: showGreeting
* fix chinese i18n 'dot'
commit 6f1befb235
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Sat Apr 12 21:56:52 2025 -0700
feat: improve package scanning
- Stop scanning at package boundaries
- Remove items array handling (only for external refs)
- Add comprehensive test coverage
commit 172f8d747c
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Sat Apr 12 21:36:25 2025 -0700
feat: enhance iterate routine with progressive git operations and post-validation workflow
commit 9666a56a0e
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Sat Apr 12 21:28:59 2025 -0700
feat: enhance iterate routine with progressive git operations
commit 14de4899b8
Author: Sam Hoang Van <samhv.ict@gmail.com>
Date: Sun Apr 13 11:15:08 2025 +0700
Add localization support for Roo Code extension in multiple languages (#2523)
* Add localization support for Roo Code extension in multiple languages
- Created new localization files for Catalan, German, Spanish, French, Hindi, Italian, Japanese, Korean, Polish, Portuguese (Brazil), Turkish, Vietnamese, Chinese (Simplified), and Chinese (Traditional).
- Updated extension activation and deactivation messages to use localized strings.
- Enhanced user experience by providing translated command titles and descriptions for various functionalities within the extension.
* Revert changes to extension.ts
* Remove l10n
---------
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
commit e94e58ff1a
Author: KJ7LNW <93454819+KJ7LNW@users.noreply.github.com>
Date: Sat Apr 12 20:54:43 2025 -0700
docs: document process for adding new settings (#2552)
Add documentation explaining how to implement new settings in Roo Code,
using the command risk level feature as a practical example.
Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
Co-authored-by: Eric Wheeler <roo-code@z.ewheeler.org>
commit 08110ae35b
Author: Sam Hoang Van <samhv.ict@gmail.com>
Date: Sun Apr 13 10:20:14 2025 +0700
Filter & Search Workspace Task History (#2526)
* Enhancement: Add 'Show all workspaces' feature to task history and update translations
* Enhancement: Add Checkbox component and integrate it into HistoryPreview and HistoryView
* Simplify the UX
---------
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
commit 628d232f9f
Author: vagadiya <32499123+vagadiya@users.noreply.github.com>
Date: Sun Apr 13 03:23:48 2025 +0100
Fix AWS token expiry issue when cached token expires when using AWS Profile for Bedrock (#2469) (#2530)
Fix AWS token expiry issue when cached token expires and using AWS Profile (#2469)
commit 37cfd75ebf
Author: mecab <mecab@misosi.ru>
Date: Sun Apr 13 03:20:03 2025 +0100
Add Anthropic option to pass API Token as Authorization header instead of X-Api-Key for the custom base URL (#2531)
Add Anthropic option to use authToken over apiKey
commit 7a62bc504c
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Sat Apr 12 17:44:31 2025 -0700
refactor: remove package manager e2e tests in favor of enhanced unit tests
- Removed e2e/src/suite/package-manager.test.ts
- Reverted src/__mocks__/vscode.js to simpler version
- Added enhanced unit tests with better coverage:
* Cache directory handling
* Localization testing
* External items validation
* Security checks
* Error handling
Task ID: e2e_analysis_20250412
commit 61c9480b5b
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Sat Apr 12 17:34:00 2025 -0700
refactor: rename prepare-for-commit to iterate
- Renamed prepare_logs to iterations
- Updated task manager to use new terminology
- Simplified CLI interface
- Added better TypeScript types
- Improved error handling
Task ID: 4d3f7a2e-8c1b-4f9d-b5e2-9c1d8f3a6b4d
commit 589b159687
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Sat Apr 12 17:30:11 2025 -0700
refactor: remove unused YamlParser implementation
- Removed src/services/package-manager/YamlParser.ts
- Verified no test files or imports existed
- All tests passing (1263 pass, 0 fail, 23 pending)
Task ID: 4d3f7a2e-8c1b-4f9d-b5e2-9c1d8f3a6b4d
commit 4417886324
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Sat Apr 12 16:43:32 2025 -0700
checkpoint: pre-package-manager-state-fix
commit 294b52ef6a
Author: vagadiya <32499123+vagadiya@users.noreply.github.com>
Date: Sat Apr 12 18:43:35 2025 +0100
Fix to Bedrock ARN validation (#2538)
Fixes Bedrock ARN validation
Updates the Bedrock ARN regex to allow alphanumeric characters, dots, hyphens, and colons in the resource ID.
This prevents validation errors when using ARNs containing those characters.
commit 973be7640e
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Sat Apr 12 10:22:33 2025 -0700
working mvp
commit 6b9b2aa4de
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Sat Apr 12 09:55:08 2025 -0700
no type script or linting errors
commit e9980bcfa9
Author: Sam Hoang Van <samhv.ict@gmail.com>
Date: Sat Apr 12 19:28:41 2025 +0700
Fix duplicate mention suggestion (#2528)
improve deduplication logic in getContextMenuOptions to handle context menu item keys more accurately
commit 2c8304ea31
Author: Chris Estreich <cestreich@gmail.com>
Date: Sat Apr 12 01:06:03 2025 -0700
Fix node version string when running asdf install nodejs (#2524)
commit 91178628aa
Author: Chris Estreich <cestreich@gmail.com>
Date: Sat Apr 12 00:07:30 2025 -0700
Evals enhancements: delete runs, show all run instead of just completed runs (#2520)
commit c25163aa20
Author: Bogdan Dolin <d.bogdan@inbox.ru>
Date: Sat Apr 12 13:09:18 2025 +0700
Fix: Remove 'v' prefix from Node.js version in .tool-versions file (#2515)
Fix formatting of Node.js version in .tool-versions
commit df01e7948b
Author: Chris Estreich <cestreich@gmail.com>
Date: Fri Apr 11 22:45:14 2025 -0700
Add reasoningEffort to provider settings schema (#2518)
commit 6ab9aa9f15
Author: Chris Estreich <cestreich@gmail.com>
Date: Fri Apr 11 22:21:15 2025 -0700
Control evals concurrency in web app (#2265)
commit 1f6da88d24
Author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Date: Sat Apr 12 00:34:38 2025 -0400
Update contributors list (#2516)
docs: update contributors list [skip ci]
Co-authored-by: mrubens <mrubens@users.noreply.github.com>
commit e10c25e0a4
Author: R00-B0T <110429663+R00-B0T@users.noreply.github.com>
Date: Fri Apr 11 21:25:55 2025 -0700
Changeset version bump (#2517)
* changeset version bump
* Updating CHANGELOG.md format
* Update CHANGELOG.md
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: R00-B0T <github-actions@github.com>
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
commit 624691abb0
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Sat Apr 12 00:14:39 2025 -0400
Respect the setting to always read the full file (#2514)
commit e453690e7f
Author: Taisuke Oe <oeuia.t@gmail.com>
Date: Sat Apr 12 12:32:57 2025 +0900
Fix bug not to respect symbolic linked rules, if target is a directory or another symbolic link (#2513)
* read symbolic linked dir and files recursively
* add symlinked dir and nested symlink test case for custom-instructions
* enhance comments
* add changeset
commit 2eba534dd6
Author: Chris Estreich <cestreich@gmail.com>
Date: Fri Apr 11 14:54:03 2025 -0700
Evals fixes (#2505)
* Allow Turso URLs, add support for API providers beyond OpenRouter
* Make the git branch name unique
commit 15b91ab03c
Author: R00-B0T <110429663+R00-B0T@users.noreply.github.com>
Date: Fri Apr 11 14:47:29 2025 -0700
Changeset version bump (#2504)
* changeset version bump
* Updating CHANGELOG.md format
* Update CHANGELOG.md
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: R00-B0T <github-actions@github.com>
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
commit 4048d36ab4
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Fri Apr 11 17:36:00 2025 -0400
v3.11.13 (#2503)
commit aaf0567e8d
Author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Date: Fri Apr 11 17:35:40 2025 -0400
Update contributors list (#2501)
docs: update contributors list [skip ci]
Co-authored-by: mrubens <mrubens@users.noreply.github.com>
commit ba307f8e1f
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Fri Apr 11 17:32:29 2025 -0400
Revert "♻️ refactor(webview): move webview HTML generation to WebviewHTMLManager" (#2502)
Revert "♻️ refactor(webview): move webview HTML generation to WebviewHTMLMana…"
This reverts commit e70954f3de.
commit 9c3c93567b
Author: KJ7LNW <93454819+KJ7LNW@users.noreply.github.com>
Date: Fri Apr 11 14:10:54 2025 -0700
Merge pull request #2427 from KJ7LNW/fix-vscode-lm-content-preservation
fix: preserve content integrity in VS Code LM provider
commit 9dead72ace
Merge: e70954f3 ae0ab566
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Fri Apr 11 17:04:42 2025 -0400
Merge pull request #2456 from KJ7LNW/fix-misc-terminal-issues
Terminal improvements: command delay, PowerShell counter, and ZSH EOL mark
commit ae0ab56654
Author: Eric Wheeler <roo-code@z.ewheeler.org>
Date: Fri Apr 11 13:23:57 2025 -0700
intl: enhance shell integration troubleshooting translations
Add new i18n strings for shell integration steps and expand troubleshooting text across all supported languages
Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
commit 4ef62c6a13
Author: Eric Wheeler <roo-code@z.ewheeler.org>
Date: Thu Apr 10 21:18:02 2025 -0700
feat: add ZDOTDIR handling for zsh shell integration
Creates a temporary ZDOTDIR to handle zsh shell integration properly while preserving user's zsh configuration. This ensures VSCode shell integration works correctly with zsh without modifying the user's existing setup.
- Add terminalZdotdir setting (disabled by default)
- Create temporary directory with proper security (sticky bit)
- Add automatic cleanup on terminal close
- Add translations for all supported languages
User confirmed fixes:
Fixes: #2205
Fixes: #2129
Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
commit e70954f3de
Author: Bhavesh Ramburn <nitrogen@gmail.com>
Date: Fri Apr 11 20:40:07 2025 +0100
♻️ refactor(webview): move webview HTML generation to WebviewHTMLManager (#2494)
- 【What】Move the logic for generating webview HTML content from ClineProvider to a new WebviewHTMLManager class.
- 【Why】This improves code organization and maintainability by separating concerns related to webview HTML generation.
- 【Why】This allows for easier testing and modification of the HTML generation logic without affecting the ClineProvider class.
commit 405e599206
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Fri Apr 11 15:38:59 2025 -0400
Exclude demo gif from extension build (#2481)
commit aa066935ce
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Fri Apr 11 06:34:50 2025 -0700
fix linting errors
commit d323015b25
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Fri Apr 11 06:22:50 2025 -0700
UI Control work
commit b4c67f133b
Author: Eric Wheeler <roo-code@z.ewheeler.org>
Date: Thu Apr 10 17:38:56 2025 -0700
feat: add terminal settings for Oh My Zsh and Powerlevel10k shell integration
Added two new terminal settings:
- terminalZshOhMy: Sets ITERM_SHELL_INTEGRATION_INSTALLED=Yes for Oh My Zsh
- terminalZshP10k: Sets POWERLEVEL9K_TERM_SHELL_INTEGRATION=true for Powerlevel10k
Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
commit b020e46076
Author: Eric Wheeler <roo-code@z.ewheeler.org>
Date: Wed Apr 9 22:12:14 2025 -0700
fix: clear ZSH EOL mark to prevent command output interpretation issues
Added a new configuration option 'terminalZshClearEolMark' (default: true) that
sets PROMPT_EOL_MARK='' in the terminal environment. This prevents issues with
command output interpretation when the output ends with special characters like '%'.
Added translations for all supported languages.
Fixes: #2194
Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
commit 211e31b8f6
Author: Eric Wheeler <roo-code@z.ewheeler.org>
Date: Wed Apr 9 21:50:33 2025 -0700
feat: add terminalPowershellCounter configuration option
Add a new configuration option that allows users to toggle the PowerShell counter workaround. This workaround adds a counter to PowerShell commands to ensure proper command execution and output capture.
The setting is disabled by default, allowing users to enable it only when needed.
Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
commit 4d1cfe8141
Author: Eric Wheeler <roo-code@z.ewheeler.org>
Date: Wed Apr 9 20:48:49 2025 -0700
feat: add terminal.commandDelay setting
Add a new configurable setting to control command execution delays in terminals.
When set to a non-zero value, this adds a sleep delay after command execution
via PROMPT_COMMAND in bash/zsh and start-sleep in PowerShell.
The default value is 0, which disables the delay completely. This setting
replaces the previous hardcoded delay of 50ms that was added as a workaround
for VSCode bug #237208.
Fixes: #2017
Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
commit 902d6d5017
Author: Eric Wheeler <roo-code@z.ewheeler.org>
Date: Wed Apr 9 20:05:23 2025 -0700
fix: prevent UI hang when shell integration is unavailable
When shell integration is unavailable, the UI would hang because the process
was never properly released. This change fixes the issue by:
- Emitting a 'completed' event with a descriptive message
- Marking the terminal as not busy
- Clearing the active stream
- Allowing the process to continue
Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
commit 0b0c438284
Author: Eric Wheeler <roo-code@z.ewheeler.org>
Date: Wed Apr 9 17:25:12 2025 -0700
fix: standardize terminal integration timeout values
Replace hardcoded 3000ms timeout with configurable Terminal.shellIntegrationTimeout
in TerminalProcess.ts. This ensures consistent timeout behavior across all
terminal integration features and allows users to control both timeouts through
a single setting.
The error messages are also updated to display the dynamic timeout value,
providing clearer feedback when shell integration issues occur.
Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
commit b8bf634fd2
Author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Date: Thu Apr 10 23:53:03 2025 -0400
Update contributors list (#2466)
docs: update contributors list [skip ci]
Co-authored-by: mrubens <mrubens@users.noreply.github.com>
commit 2ba7200f37
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Thu Apr 10 23:44:38 2025 -0400
Fix discard changes in settings (#2485)
commit 3cc81c73cf
Author: KJ7LNW <93454819+KJ7LNW@users.noreply.github.com>
Date: Thu Apr 10 19:34:18 2025 -0700
docs: update settings.md with comprehensive steps (#2451)
* docs: update settings.md with comprehensive steps
Update the settings documentation to include all necessary steps for adding
a new configuration item, including schema definitions, type definitions,
and critical steps for persistence and UI display.
This ensures the documentation accurately reflects the complete process
required when adding new settings to the application.
Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
* docs: add style considerations for checkbox settings
Add documentation about styling checkbox settings in the UI, including:
- Using VSCodeCheckbox component
- Proper wrapping and spacing
- Consistent styling for labels and descriptions
- Example implementation based on terminalPowershellCounter
Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
* docs: add comprehensive guide for adding new configuration items
Add a new section to settings.md that provides a complete checklist for adding new configuration items to the system. This guide covers all aspects from UI to persistence to functionality, based on implementation experience.
Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
* docs: update settings documentation with persistence guidelines
Add comprehensive guidance for ensuring settings persist across reload
Include debugging steps for troubleshooting persistence issues
Replace 'Avoiding Duplicates' section with more detailed information
Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
---------
Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
Co-authored-by: Eric Wheeler <roo-code@z.ewheeler.org>
commit e41d6a42ac
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Thu Apr 10 22:32:11 2025 -0400
Better display of diff errors (#2478)
commit c0ba1f5080
Author: Hannes Rudolph <hrudolph@gmail.com>
Date: Thu Apr 10 19:55:52 2025 -0600
Update README.md to reflect new branding and add demo GIF; (#2479)
* Update README.md to reflect new branding and add demo GIF; optimize demo GIF size
* Update README.md to reflect branding change from "Roo Cline" to "Roo Code" and remove duplicate title entry.
commit fd48ffcfe9
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Thu Apr 10 14:42:11 2025 -0700
add sourceURL option to yaml
commit 77daf8559b
Author: Steven T. Cramer <Steven.Cramer@TimeWarp.Enterprises>
Date: Fri Apr 11 03:51:27 2025 +0700
Move "Previously Roo Cline" to description from title (#2476)
* Move "Previously Roo Cline" to description from title
* Add a period.
commit a031b74bf5
Author: ronyblum <ronyblum@gmail.com>
Date: Thu Apr 10 12:47:34 2025 -0700
Modification of AWS Bedrock to Amazon Bedrock (#2473)
* Modification of AWS Bedrock to Amazon Bedrock
* Duplicated comment removal
commit 655899673b
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Thu Apr 10 12:26:12 2025 -0700
walking skeleton
commit cd5b894578
Author: Smartsheet-JB-Brown <jb.brown@smartsheet.com>
Date: Thu Apr 10 12:25:12 2025 -0700
walking skeleton
commit 255a158e75
Author: Zhang Tony <157202938+zhangtony239@users.noreply.github.com>
Date: Thu Apr 10 23:12:49 2025 +0800
Bug Fixed: Chinese i18n css error (#2470)
* bug fixed: chinese i18n css error
* Update webview-ui/src/components/settings/ApiOptions.tsx
---------
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
commit 5352beb95c
Author: Sam Hoang Van <samhv.ict@gmail.com>
Date: Thu Apr 10 21:56:52 2025 +0700
feat: Add file context tracking system (#2440)
* feat: Add file context tracking system
This commit adds a comprehensive file context tracking system that monitors file operations (reads, edits) by both Roo and users. The system helps prevent stale context issues and improves checkpoint management.
Key features:
- Track files accessed via tools, mentions, or edits
- Monitor file changes outside of Roo using file watchers
- Store file operation metadata with timestamps
- Trigger checkpoints automatically when files are modified
- Prevent false positives by distinguishing between Roo and user edits
The implementation includes:
- New FileContextTracker class to manage file operations
- Type definitions for file metadata tracking
- Integration with all file-related tools
- File mention tracking in the mentions system
- Improved checkpoint triggering based on file modifications
* Update src/core/context-tracking/FileContextTracker.ts
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
* Update src/core/context-tracking/FileContextTracker.ts
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
* test: Add mocks for getFileContextTracker in Cline tests
---------
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
commit 0ddfa4d0bf
Author: Wojciech Kordalski <wkordalski@pm.me>
Date: Thu Apr 10 15:51:27 2025 +0200
`.direnv` directory should not be packaged (#2464)
If somebody uses direnv tool, the `.direnv` directory is created,
that contains some data and especially some symlinks.
Symlinks makes `vsce` to fail zipping the built VSIX.
Generally `.direnv` should not be added to the resulting VSIX,
therefore I add it to .vscodeignore.
commit 82bf3dc42e
Author: R00-B0T <110429663+R00-B0T@users.noreply.github.com>
Date: Wed Apr 9 22:11:59 2025 -0700
Changeset version bump (#2455)
* changeset version bump
* Updating CHANGELOG.md format
* Update CHANGELOG.md
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: R00-B0T <github-actions@github.com>
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
commit fba24ac13d
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Thu Apr 10 01:05:38 2025 -0400
v3.11.12 (#2454)
commit 8da3538744
Author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Date: Thu Apr 10 01:05:18 2025 -0400
Update contributors list (#2434)
docs: update contributors list [skip ci]
Co-authored-by: mrubens <mrubens@users.noreply.github.com>
commit 1445bb0a3c
Author: amittell <mittell@me.com>
Date: Thu Apr 10 00:35:49 2025 -0400
Make Grok3 streaming work with OpenAI Compatible (#2449)
commit c18e25f4bc
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Thu Apr 10 00:29:50 2025 -0400
Fall back on aggressive line number stripping in diffs (#2453)
* Add option for aggressive line number stripping
* Fall back on aggressive line number stripping in diffs
commit 4e716263d0
Author: Chris Estreich <cestreich@gmail.com>
Date: Wed Apr 9 21:18:07 2025 -0700
Add a script to copy eval run results to Turso (#2452)
commit 5fa555e60d
Author: Chris Estreich <cestreich@gmail.com>
Date: Wed Apr 9 13:12:55 2025 -0700
Fix gh fork command (#2442)
commit 6d9ebe3fcf
Author: Chris Estreich <cestreich@gmail.com>
Date: Wed Apr 9 13:04:36 2025 -0700
More sane evals default concurrency + staggered startup (#2441)
commit 5c3237a6b0
Author: R00-B0T <110429663+R00-B0T@users.noreply.github.com>
Date: Wed Apr 9 08:41:00 2025 -0700
Changeset version bump (#2436)
* changeset version bump
* Update CHANGELOG.md
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
commit fb0dd75fe0
Author: Chris Estreich <cestreich@gmail.com>
Date: Wed Apr 9 08:20:17 2025 -0700
API fixes (#2438)
commit eda53815ab
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Wed Apr 9 09:31:58 2025 -0400
v3.11.11 (#2435)
commit 7e4000b4e0
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Wed Apr 9 09:20:13 2025 -0400
Add custom instructions for de (#2383)
commit f6467ca371
Author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Date: Wed Apr 9 09:06:42 2025 -0400
Update contributors list (#2411)
docs: update contributors list [skip ci]
Co-authored-by: mrubens <mrubens@users.noreply.github.com>
commit 16d8f14371
Author: arthur <51604173+arthurauffray@users.noreply.github.com>
Date: Thu Apr 10 01:04:55 2025 +1200
Add o1-pro to api.ts (#2433)
Add the o1-pro model to the openai section. Sourced model info from: https://platform.openai.com/docs/models/o1-pro
commit 75ba1db3ca
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Wed Apr 9 01:45:45 2025 -0400
Improve subtasks UI (#2426)
commit 270fd88cc8
Author: KJ7LNW <93454819+KJ7LNW@users.noreply.github.com>
Date: Tue Apr 8 20:04:44 2025 -0700
refactor: improve readFileTool XML output format (#2340)
* fix: addLineNumbers handling of empty content
Empty files should not have line numbers, but non-empty files with empty content at a specific line offset should.
- If content is empty, return empty string for empty files
- If content is empty but startLine > 1, return line number for empty content at that offset
This ensures that the model does not think the file contains a single empty line.
Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
* refactor: improve readFileTool XML output format
- Remove unnecessary XML indentation that could confuse the model
- Separate file content from notices and errors using dedicated tags
- Add line range information to content tags
- Handle empty files properly with self-closing tags
- Add comprehensive test coverage
Fixes #2278
Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
* fix: always show line numbers in read_file XML output
- Always display line numbers in non-range reads
- Improve XML formatting with consistent newlines for better readability
Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
* test: update tests to match new XML format with line numbers
- Update test expectations to match the new XML format with newlines
- Update tests to expect line numbers attribute in content tags
- Modify test assertions to check for the correct line range values
Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
* fix: consistent blank line handling in addLineNumbers
- Add newline to all output
- Handle trailing newlines and empty lines consistently
- Add test cases for blank lines:
- Multiple blank lines within content
- Multiple trailing blank lines
- Only blank lines with offset
- Trailing newlines
Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
* test: use actual addLineNumbers in read-file-xml tests
- Modified extract-text mock to preserve actual addLineNumbers implementation
- Removed mock implementation of addLineNumbers
- Updated test data to account for trailing newline
- Removed unnecessary mock verification
Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
* test: ensure actual addLineNumbers function is called in tests
- Replace direct mocking of addLineNumbers with spy on actual implementation
- Add verification to ensure the real function is called when appropriate
- Add skipAddLineNumbersCheck option for cases where function should not be called
- Update test cases to use appropriate verification options
- Fix numberedFileContent to include trailing newline for consistency
Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
* fix: modify readLines to process data directly instead of line by line
- Direct data processing provides more accurate results by preserving exact content with carriage returns
- Improved performance through minimal buffering and efficient string operations
- Use string indexes to find newlines while maintaining their original format
- Handle all edge cases correctly with preserved line endings
- Add tests for various edge cases including empty files, single lines, and different line endings
Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
* test: remove unused mockInputContent variable
Remove unused variable declaration to appease ellipsis-dev linter requirements.
Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
---------
Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
Co-authored-by: Eric Wheeler <roo-code@z.ewheeler.org>
commit 2779e8f703
Author: KJ7LNW <93454819+KJ7LNW@users.noreply.github.com>
Date: Tue Apr 8 19:58:33 2025 -0700
fix: clarify difference between workspace directory and terminal working directory (#2418)
* fix: clarify difference between workspace directory and terminal working directory
This commit addresses confusion between the VS Code workspace directory and
terminal working directory. Roo was not properly distinguishing between these
concepts, leading to issues when terminal commands changed directories.
- Renamed 'Current Working Directory' to 'Current Workspace Directory' throughout
- Added clearer notice when a command changes the working directory in a terminal
- Added explanation about the difference between workspace and working directories
- Updated all tool descriptions to reference 'workspace directory'
References: https://www.reddit.com/r/RooCode/s/6L19EvsFbF
Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
* test: update directory terminology in test files
Update terminology from 'working directory' to 'workspace directory' in tests
to reflect VSCode's concept of workspace vs working directory.
Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
---------
Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
Co-authored-by: Eric Wheeler <roo-code@z.ewheeler.org>
commit 63c8f92393
Merge: c2fef2d0 c2dda05b
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Tue Apr 8 22:57:07 2025 -0400
Merge pull request #2420 from KJ7LNW/tree-sitter-eval-definitions
feat: enhance tree-sitter parsers for multiple languages
commit c2fef2d01f
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Tue Apr 8 22:51:58 2025 -0400
Follow symlinks for rules files (#2421)
commit c2dda05b2c
Author: Eric Wheeler <roo-code@z.ewheeler.org>
Date: Tue Apr 8 19:40:19 2025 -0700
feat: enhance Python tree-sitter parser with advanced language structures
This commit significantly enhances the Python tree-sitter parser to support a comprehensive range of Python language constructs, enabling more accurate and detailed code analysis.
Key improvements:
- Added support for method definitions (instance, class, and static methods)
- Added support for decorators on functions and classes
- Added support for module-level variables and constants
- Added support for async functions and methods
- Added support for property getters/setters
- Added support for type annotations in various contexts
- Added support for dataclasses
- Added support for nested functions and classes
- Added support for generator functions
- Added support for list/dict/set comprehensions
- Added support for lambda functions
- Added support for abstract base classes and methods
The parser now handles Python's rich feature set more comprehensively, including special Python patterns like decorators, type annotations, and various comprehension types. This enables better code navigation, understanding, and analysis for Python codebases.
Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
commit 56d7cf6199
Author: Eric Wheeler <roo-code@z.ewheeler.org>
Date: Tue Apr 8 19:38:09 2025 -0700
feat: enhance Java tree-sitter parser with advanced language structures
This enhancement significantly expands the Java parser's capabilities to recognize and parse a wide range of Java language constructs:
- Added support for enum declarations and enum constants
- Added support for annotation type declarations and elements
- Added support for field declarations
- Added support for constructor declarations
- Added support for lambda expressions
- Added support for inner and anonymous classes
- Added support for type parameters (generics)
- Added support for package and import declarations
These improvements enable more comprehensive code analysis for Java projects, providing better definition extraction and navigation capabilities.
Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
commit 599c1849d0
Author: Eric Wheeler <roo-code@z.ewheeler.org>
Date: Tue Apr 8 19:33:28 2025 -0700
feat: enhance Go tree-sitter parser with advanced language structures
This enhancement significantly expands the Go parser's capabilities to recognize and extract a comprehensive set of language constructs:
- Added support for struct and interface definitions with proper type identification
- Implemented parsing for constant declarations (both single and in blocks)
- Added support for variable declarations (both single and in blocks)
- Added recognition of type aliases with proper distinction from regular types
- Implemented special handling for init functions
- Added support for anonymous functions, including nested function literals
- Improved documentation and organization of query patterns
These enhancements enable more accurate code navigation, better symbol extraction, and improved code intelligence for Go codebases.
Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
commit 749f793c08
Author: Eric Wheeler <roo-code@z.ewheeler.org>
Date: Tue Apr 8 19:27:00 2025 -0700
feat: enhance C++ tree-sitter parser with advanced language structures
This enhancement significantly expands the C++ parser's capabilities to recognize and extract a wide range of modern C++ language constructs, improving code navigation and analysis.
New supported language constructs include:
- Union declarations and their members
- Destructors and their implementations
- Operator overloading (including stream operators)
- Free-standing and namespace-scoped functions
- Enum declarations (both traditional and scoped enum class)
- Lambda expressions and their captures
- Attributes and annotations
- Method overrides with virtual/override specifiers
- Exception specifications (noexcept)
- Default parameters in function declarations
- Variadic templates and parameter packs
- Structured bindings (C++17)
- Inline namespaces and nested namespace declarations
- Template specializations and instantiations
- Constructor implementations
This enhancement provides more comprehensive code structure analysis for C++ codebases, particularly those using modern C++ features from C++11, C++14, and C++17 standards.
Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
commit 63358e794d
Author: Eric Wheeler <roo-code@z.ewheeler.org>
Date: Tue Apr 8 19:22:10 2025 -0700
feat: enhance TypeScript/TSX tree-sitter parser
- Enhanced the Tree-Sitter parser for JavaScript/TypeScript with support for advanced language constructs
- Modified the parser to exclude comments from the output
- Consolidated sample code in tests for better maintainability
Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
commit b01615f122
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Tue Apr 8 22:02:27 2025 -0400
Add the option to use a custom Host header for openai-compatible (#2399)
commit 9f724bd360
Author: Atlas Gong <68199735+atlasgong@users.noreply.github.com>
Date: Tue Apr 8 15:05:04 2025 -0400
fix: z-index of highlight layer should be under mode/profile dropdowns (#2417)
commit 6135d351df
Author: R00-B0T <110429663+R00-B0T@users.noreply.github.com>
Date: Tue Apr 8 08:51:58 2025 -0700
Changeset version bump (#2412)
* changeset version bump
* Update CHANGELOG.md
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
commit 75d17a5233
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Tue Apr 8 11:46:32 2025 -0400
v3.11.10 (#2413)
commit a4530eea5b
Author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Date: Tue Apr 8 11:40:53 2025 -0400
Update contributors list (#2402)
docs: update contributors list [skip ci]
Co-authored-by: cte <cte@users.noreply.github.com>
commit 08b586334b
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Tue Apr 8 11:40:23 2025 -0400
Remove extra colon from rules content (#2409)
commit 9ab4a88176
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Tue Apr 8 11:40:10 2025 -0400
Fix typo in diff prompt (#2410)
commit e1f6eb625b
Author: Sam Hoang Van <samhv.ict@gmail.com>
Date: Tue Apr 8 22:29:03 2025 +0700
feat: Add CommandOutputViewer component and integrate it into ChatRow (#2326)
* feat: Add CommandOutputViewer component and integrate it into ChatRow
* Remove unnecessary memo wrapper from CommandOutputViewer component
commit 58149a083e
Author: Ross McFarland <rwmcfa1@gmail.com>
Date: Tue Apr 8 08:21:54 2025 -0700
Clean up global `rateLimitSeconds` & fully shift to provider version (#2408)
* Directly use provider rateLimitSeconds and remove uneeded default
* remove a bunch of unused rateLimitSeconds references
* rateLimitSettings field def in GlobalSettingsRecord isn't needed for migration
commit dedd655c9e
Author: Taisuke Oe <oeuia.t@gmail.com>
Date: Wed Apr 9 00:17:21 2025 +0900
Fix a bug not to read rule files properly, under nested `.roo/rules` directories (#2405)
* fix .roo/rules/subdir/file path calculation
* add changeset
commit 4c81f7e167
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Tue Apr 8 09:39:40 2025 -0400
Update CHANGELOG.md (#2406)
commit 72a9e0bd39
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Tue Apr 8 01:20:45 2025 -0400
Fix cache usage tracking for openai-compatible (#2401)
commit 903f3b64a1
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Mon Apr 7 23:06:54 2025 -0400
Add custom instructions for zh-CN (#2381)
* Add custom instructions for zh-CN
* Updates from System233
commit 06e0fc6ee4
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Mon Apr 7 18:33:25 2025 -0400
Update CHANGELOG.md (#2396)
commit fbb7887f8d
Author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Date: Mon Apr 7 17:38:38 2025 -0400
Update contributors list (#2338)
docs: update contributors list [skip ci]
Co-authored-by: mrubens <mrubens@users.noreply.github.com>
commit 70a05d3302
Author: R00-B0T <110429663+R00-B0T@users.noreply.github.com>
Date: Mon Apr 7 14:35:49 2025 -0700
Changeset version bump (#2394)
* changeset version bump
* Updating CHANGELOG.md format
* Apply suggestions from code review
* Update CHANGELOG.md
* Update CHANGELOG.md
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: R00-B0T <github-actions@github.com>
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
commit 1ad8e9c7d3
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Mon Apr 7 17:18:14 2025 -0400
v3.11.9 (#2393)
commit 260fc30043
Author: Ross McFarland <rwmcfa1@gmail.com>
Date: Mon Apr 7 13:24:38 2025 -0700
feat(settings): Rate-limit setting updated to be per-profile (#2376)
* Rate-limit setting updated to be per-profile
* Correct rateLimitSeconds translations
* Add missing d to rateLimitSecondsMigrate
* Fix fr rate-limit translation
commit af9e471c92
Author: KJ7LNW <93454819+KJ7LNW@users.noreply.github.com>
Date: Mon Apr 7 12:12:41 2025 -0700
dev: dynamic Vite port detection for webview development (#2339)
Implements a solution for the Vite port collision issue that allows easier development of Roo across multiple instances of VSCode in different Roo repository directories.
This may also fix crashes and strange behavior between multiple running instances that would otherwise create port conflicts. This is solved using the following automated process:
- When Vite automatically selects an alternative port, a custom plugin automatically writes the port to a '.vite-port' file in the repository root
- ClineProvider automatically reads the port from this file, falling back gracefully to port 5173 if the file doesn't exist
- No user intervention is necessary as the entire process is handled automatically
- Added detailed logging for debugging
- Added .vite-port to .gitignore
The extension now connects to the correct Vite development server port automatically, even when the default port (5173) is already in use.
Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
Co-authored-by: Eric Wheeler <roo-code@z.ewheeler.org>
commit d0661fb0c7
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Mon Apr 7 14:57:02 2025 -0400
Don't automatically convert types when parsing XML (#2389)
* Don't automatically convert types when parsing XML
* PR feedback
commit d5aee1e1d8
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Mon Apr 7 14:24:23 2025 -0400
Add custom instructions for zh-TW (#2382)
* Add custom instructions for zh-TW
* Move custom instructions to a rules file for easier reading
* PR feedback
commit 320ef77d79
Author: Nico Bihan <nicolas.bihan@wellsky.com>
Date: Mon Apr 7 11:15:09 2025 -0500
Added to Vertex AI Provider gemini 2.5 Pro Preview (#2384)
* Added Gemini 2.5 Pro model to Vertex AI Provider
* Adds Gemini 2.5 Pro preview model
Adds configuration for the new Gemini 2.5 Pro preview model,
including its token limits, context window size, image support, and
pricing information.
commit 009faf349e
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Mon Apr 7 12:14:04 2025 -0400
Move .roorules to .roo/rules/ (#2385)
commit 580672ffc8
Author: Yu SERIZAWA <upamune@gmail.com>
Date: Tue Apr 8 00:46:30 2025 +0900
feat: enhance rule file loading with .roo/rules directory support (#2354)
* feat: enhance rule file loading with .roo/rules directory support
- Introduced functions to safely read files and check for directory existence.
- Added capability to read all text files from a specified directory in alphabetical order.
- Updated `loadRuleFiles` to prioritize loading rules from a `.roo/rules/` directory, falling back to existing rule files if necessary.
- Enhanced `addCustomInstructions` to support loading mode-specific rules from a `.roo/rules-{mode}/` directory, improving flexibility in rule management.
This change improves the organization and retrieval of rule files, allowing for better modularity and maintainability.
* Updated strings and translations
* Add tests
* Revert changes to system prompt translations
* Fix path resolution
* Make instruction structure clearer
---------
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
commit 0e4be83c35
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Mon Apr 7 11:41:08 2025 -0400
Fixes to resumeTask and isTaskInHistory (#2380)
commit cf74568f66
Author: Franciszek Piszcz <franekp@users.noreply.github.com>
Date: Mon Apr 7 17:19:47 2025 +0200
feat(RooCodeAPI): implement resumeTask and isTaskInHistory (#1672)
commit f5a4b425da
Author: Marco Quinten <splittydev@gmail.com>
Date: Mon Apr 7 20:35:14 2025 +0700
feat(browserTool): Implement hover action (#2368)
* Implement hover action for the browser action tool
* Update snapshots
commit fff8fdd3f3
Author: Marco Quinten <splittydev@gmail.com>
Date: Mon Apr 7 20:34:32 2025 +0700
feat(browserTool): Implement resize action (#2370)
* Implement resize action for browser action tool
* Update snapshots
commit 88cac3c9d8
Author: Aleksandr Kirillov <32141102+axkirillov@users.noreply.github.com>
Date: Mon Apr 7 13:54:49 2025 +0200
feat: add command to focus Roo Code input field (#2369)
* feat: add command to focus Roo Code input field
* fixup! feat: add command to focus Roo Code input field
* fixup! feat: add command to focus Roo Code input field
commit 84c703e050
Author: Kyle Tse <shtse8@gmail.com>
Date: Mon Apr 7 03:17:49 2025 +0100
fix: Prevent unnecessary autoscroll when buttons appear (#1280) (#2334)
* fix: Prevent unnecessary autoscroll when buttons appear (#1280)
* Remove commented out code
---------
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
commit 0317374acb
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Sun Apr 6 16:04:34 2025 -0400
Move .clinerules to .roorules (#2357)
commit 393688c584
Author: Matt Rubens <mrubens@users.noreply.github.com>
Date: Sun Apr 6 14:31:14 2025 -0400
Add deep links to settings sections (#2355)
commit 57d97319dc
Author: Greg Taylor <gtaylor@users.noreply.github.com>
Date: Sun Apr 6 08:19:30 2025 -0700
fix: persist settings on api.setConfiguration (#2341)
Values weren't being saved to the settings store, preventing
switching to newly created profiles.
Co-authored-by: Greg Taylor <gregtaylor@netflix.com>
This commit is contained in:
parent
43fa77ca94
commit
5c49366c49
104 changed files with 16719 additions and 17 deletions
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -83,4 +83,4 @@ body:
|
|||
attributes:
|
||||
label: 📄 Relevant Logs or Errors
|
||||
description: Paste API logs, terminal output, or errors here. Use triple backticks (```) for code formatting.
|
||||
render: shell
|
||||
render: shell
|
||||
|
|
|
|||
50
cline_docs/marketplace/README.md
Normal file
50
cline_docs/marketplace/README.md
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# Marketplace Documentation
|
||||
|
||||
This directory contains comprehensive documentation for the Roo Code Marketplace, including both user guides and implementation details.
|
||||
|
||||
## Documentation Structure
|
||||
|
||||
### User Guide
|
||||
|
||||
The user guide provides end-user documentation for using the Marketplace:
|
||||
|
||||
1. [Introduction to Marketplace](./user-guide/01-introduction.md) - Overview and purpose of the Marketplace
|
||||
2. [Browsing Items](./user-guide/02-browsing-items.md) - Understanding the interface and navigating items
|
||||
3. [Searching and Filtering](./user-guide/03-searching-and-filtering.md) - Using search and filters to find items
|
||||
4. [Working with Package Details](./user-guide/04-working-with-details.md) - Exploring package details and items
|
||||
5. [Adding Packages](./user-guide/05-adding-packages.md) - Creating and contributing your own items
|
||||
6. [Adding Custom Sources](./user-guide/06-adding-custom-sources.md) - Setting up and managing custom sources
|
||||
|
||||
### Implementation Documentation
|
||||
|
||||
The implementation documentation provides technical details for developers:
|
||||
|
||||
1. [Architecture](./implementation/01-architecture.md) - High-level architecture of the Marketplace
|
||||
2. [Core Components](./implementation/02-core-components.md) - Key components and their responsibilities
|
||||
3. [Data Structures](./implementation/03-data-structures.md) - Data models and structures used in the Marketplace
|
||||
4. [Search and Filter](./implementation/04-search-and-filter.md) - Implementation of search and filtering functionality
|
||||
|
||||
## Key Features
|
||||
|
||||
The Marketplace provides the following key features:
|
||||
|
||||
- **Component Discovery**: Browse and search for items
|
||||
- **Item Management**: Add/update/remove items to your environment
|
||||
- **Custom Sources**: Add your own repositories of team or private Marketplaces
|
||||
- **Localization Support**: View items in your preferred language
|
||||
- **Filtering**: Filter items by type, search term, and tags
|
||||
|
||||
## Default Marketplace Repository
|
||||
|
||||
The default Marketplace repository is located at:
|
||||
[https://github.com/RooVetGit/Roo-Code-Marketplace](https://github.com/RooVetGit/Roo-Code-Marketplace)
|
||||
|
||||
## Contributing
|
||||
|
||||
To contribute to the Marketplace documentation:
|
||||
|
||||
1. Make your changes to the relevant markdown files
|
||||
2. Ensure that your changes are accurate and consistent with the actual implementation
|
||||
3. Submit a pull request with your changes
|
||||
|
||||
For code changes to the Marketplace itself, please refer to the main [CONTRIBUTING.md](../../CONTRIBUTING.md) file.
|
||||
373
cline_docs/marketplace/implementation/01-architecture.md
Normal file
373
cline_docs/marketplace/implementation/01-architecture.md
Normal file
|
|
@ -0,0 +1,373 @@
|
|||
# Marketplace Architecture
|
||||
|
||||
This document provides a comprehensive overview of the Marketplace's architecture, including its components, interactions, and data flow.
|
||||
|
||||
## System Overview
|
||||
|
||||
The Marketplace is built on a modular architecture that separates concerns between data management, UI rendering, and user interactions. The system consists of several key components that work together to provide a seamless experience for discovering, browsing, and managing items.
|
||||
|
||||
### High-Level Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
User[User] -->|Interacts with| UI[Marketplace UI]
|
||||
UI -->|Sends messages| MH[Message Handler]
|
||||
MH -->|Processes requests| PM[MarketplaceManager]
|
||||
PM -->|Validates sources| PSV[MarketplaceSourceValidation]
|
||||
PM -->|Fetches repos| GF[GitFetcher]
|
||||
GF -->|Scans metadata| MS[MetadataScanner]
|
||||
MS -->|Reads| FS[File System / Git Repositories]
|
||||
PM -->|Returns filtered data| MH
|
||||
MH -->|Updates state| UI
|
||||
UI -->|Displays| User
|
||||
```
|
||||
|
||||
The architecture follows a message-based pattern where:
|
||||
|
||||
1. The UI sends messages to the backend through a message handler
|
||||
2. The backend processes these messages and returns results
|
||||
3. The UI updates based on the returned data
|
||||
4. Components are loosely coupled through message passing
|
||||
|
||||
## Component Interactions
|
||||
|
||||
The Marketplace components interact through a well-defined message flow:
|
||||
|
||||
### Core Interaction Patterns
|
||||
|
||||
1. **Data Loading**:
|
||||
|
||||
- GitFetcher handles repository cloning and updates
|
||||
- MetadataScanner loads item data from repositories
|
||||
- MarketplaceManager manages caching and concurrency
|
||||
- UI requests data through the message handler
|
||||
|
||||
2. **Filtering and Search**:
|
||||
|
||||
- UI sends filter/search criteria to the backend
|
||||
- MarketplaceManager applies filters with match info
|
||||
- Filtered results are returned to the UI
|
||||
- State manager handles view-level filtering
|
||||
|
||||
3. **Source Management**:
|
||||
- UI sends source management commands
|
||||
- MarketplaceManager coordinates with GitFetcher
|
||||
- Cache is managed with timeout protection
|
||||
- Sources are processed with concurrency control
|
||||
|
||||
## Data Flow Diagram
|
||||
|
||||
The following diagram illustrates the data flow through the Marketplace system:
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph Sources
|
||||
GR[Git Repositories]
|
||||
FS[File System]
|
||||
end
|
||||
|
||||
subgraph Backend
|
||||
GF[GitFetcher]
|
||||
MS[MetadataScanner]
|
||||
PM[MarketplaceManager]
|
||||
MH[Message Handler]
|
||||
end
|
||||
|
||||
subgraph Frontend
|
||||
UI[UI Components]
|
||||
State[State Management]
|
||||
end
|
||||
|
||||
GR -->|Clone/Pull| GF
|
||||
FS -->|Cache| GF
|
||||
GF -->|Metadata| MS
|
||||
MS -->|Parsed Data| PM
|
||||
PM -->|Cached Items| PM
|
||||
UI -->|User Actions| MH
|
||||
MH -->|Messages| PM
|
||||
PM -->|Filtered Data| MH
|
||||
MH -->|Updates| State
|
||||
State -->|Renders| UI
|
||||
```
|
||||
|
||||
## Sequence Diagrams
|
||||
|
||||
### Item Loading Sequence
|
||||
|
||||
The following sequence diagram shows how items are loaded from sources:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant UI as UI Components
|
||||
participant MH as Message Handler
|
||||
participant PM as MarketplaceManager
|
||||
participant GF as GitFetcher
|
||||
participant MS as MetadataScanner
|
||||
participant FS as File System/Git
|
||||
|
||||
User->>UI: Open Marketplace
|
||||
UI->>MH: Send init message
|
||||
MH->>PM: Initialize
|
||||
PM->>GF: Request repository data
|
||||
GF->>FS: Clone/pull repository
|
||||
GF->>MS: Request metadata scan
|
||||
MS->>FS: Read repository data
|
||||
FS-->>MS: Return raw data
|
||||
MS-->>GF: Return parsed metadata
|
||||
GF-->>PM: Return repository data
|
||||
PM-->>MH: Return initial items
|
||||
MH-->>UI: Update with items
|
||||
UI-->>User: Display items
|
||||
```
|
||||
|
||||
### Search and Filter Sequence
|
||||
|
||||
This sequence diagram illustrates the search and filter process:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant UI as UI Components
|
||||
participant State as State Manager
|
||||
participant MH as Message Handler
|
||||
participant PM as MarketplaceManager
|
||||
|
||||
User->>UI: Enter search term
|
||||
UI->>State: Update filters
|
||||
State->>MH: Send search message
|
||||
MH->>PM: Apply search filter
|
||||
PM->>PM: Filter items with match info
|
||||
PM-->>MH: Return filtered items
|
||||
MH-->>State: Update with filtered items
|
||||
State-->>UI: Update view
|
||||
UI-->>User: Display filtered results
|
||||
|
||||
User->>UI: Select type filter
|
||||
UI->>State: Update type filter
|
||||
State->>MH: Send type filter message
|
||||
MH->>PM: Apply type filter
|
||||
PM->>PM: Filter by type with match info
|
||||
PM-->>MH: Return type-filtered items
|
||||
MH-->>State: Update filtered items
|
||||
State-->>UI: Update view
|
||||
UI-->>User: Display type-filtered results
|
||||
```
|
||||
|
||||
## Class Diagrams
|
||||
|
||||
### Core Classes
|
||||
|
||||
The following class diagram shows the main classes in the Marketplace system:
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class MarketplaceManager {
|
||||
-currentItems: MarketplaceItem[]
|
||||
-cache: Map
|
||||
-gitFetcher: GitFetcher
|
||||
-activeSourceOperations: Set
|
||||
+getMarketplaceItems(): MarketplaceItem[]
|
||||
+filterItems(filters): MarketplaceItem[]
|
||||
+sortItems(sortBy, order): MarketplaceItem[]
|
||||
+refreshRepository(url): void
|
||||
-queueOperation(operation): void
|
||||
-validateSources(sources): ValidationError[]
|
||||
}
|
||||
|
||||
class MarketplaceSourceValidation {
|
||||
+validateSourceUrl(url): ValidationError[]
|
||||
+validateSourceName(name): ValidationError[]
|
||||
+validateSourceDuplicates(sources): ValidationError[]
|
||||
+validateSource(source): ValidationError[]
|
||||
+validateSources(sources): ValidationError[]
|
||||
-isValidGitRepositoryUrl(url): boolean
|
||||
}
|
||||
|
||||
class GitFetcher {
|
||||
-cacheDir: string
|
||||
-metadataScanner: MetadataScanner
|
||||
+fetchRepository(url): MarketplaceRepository
|
||||
-cloneOrPullRepository(url): void
|
||||
-validateRegistryStructure(dir): void
|
||||
-parseRepositoryMetadata(dir): RepositoryMetadata
|
||||
}
|
||||
|
||||
class MetadataScanner {
|
||||
-git: SimpleGit
|
||||
+scanDirectory(path): MarketplaceItem[]
|
||||
+parseMetadata(file): ComponentMetadata
|
||||
-buildComponentHierarchy(items): MarketplaceItem[]
|
||||
}
|
||||
|
||||
class MarketplaceViewStateManager {
|
||||
-state: ViewState
|
||||
-stateChangeHandlers: Set
|
||||
-fetchTimeoutId: NodeJS.Timeout
|
||||
-sourcesModified: boolean
|
||||
+initialize(): void
|
||||
+onStateChange(handler): () => void
|
||||
+cleanup(): void
|
||||
+getState(): ViewState
|
||||
+transition(transition): Promise<void>
|
||||
-notifyStateChange(): void
|
||||
-clearFetchTimeout(): void
|
||||
-isFilterActive(): boolean
|
||||
-filterItems(items): MarketplaceItem[]
|
||||
-sortItems(items): MarketplaceItem[]
|
||||
+handleMessage(message): Promise<void>
|
||||
}
|
||||
|
||||
MarketplaceManager --> GitFetcher: uses
|
||||
MarketplaceManager --> MarketplaceSourceValidation: uses
|
||||
GitFetcher --> MetadataScanner: uses
|
||||
MarketplaceManager --> MarketplaceViewStateManager: updates
|
||||
```
|
||||
|
||||
## Component Responsibilities
|
||||
|
||||
### Backend Components
|
||||
|
||||
1. **GitFetcher**
|
||||
|
||||
- Handles Git repository operations
|
||||
- Manages repository caching
|
||||
- Validates repository structure
|
||||
- Coordinates with MetadataScanner
|
||||
|
||||
2. **MetadataScanner**
|
||||
|
||||
- Scans directories and repositories
|
||||
- Parses YAML metadata files
|
||||
- Builds component hierarchies
|
||||
- Handles file system operations
|
||||
|
||||
3. **MarketplaceManager**
|
||||
|
||||
- Manages concurrent operations
|
||||
- Handles caching with timeout protection
|
||||
- Coordinates repository operations
|
||||
- Provides filtering and sorting
|
||||
|
||||
4. **marketplaceMessageHandler**
|
||||
- Routes messages between UI and backend
|
||||
- Processes commands from the UI
|
||||
- Returns data and status updates
|
||||
- Handles error conditions
|
||||
|
||||
### Frontend Components
|
||||
|
||||
1. **MarketplaceViewStateManager**
|
||||
|
||||
- Manages frontend state and backend synchronization
|
||||
- Handles state transitions and message processing
|
||||
- Manages filtering, sorting, and view preferences
|
||||
- Coordinates with backend state
|
||||
- Handles timeout protection for operations
|
||||
- Manages source modification tracking
|
||||
- Provides state change subscriptions
|
||||
|
||||
2. **MarketplaceSourceValidation**
|
||||
|
||||
- Validates Git repository URLs for any domain
|
||||
- Validates source names and configurations
|
||||
- Detects duplicate sources (case-insensitive)
|
||||
- Provides structured validation errors
|
||||
- Supports multiple Git protocols (HTTPS, SSH, Git)
|
||||
|
||||
3. **MarketplaceItemCard**
|
||||
|
||||
- Displays item information
|
||||
- Handles tag interactions
|
||||
- Manages expandable sections
|
||||
- Shows match highlights
|
||||
- Handle item actions.
|
||||
|
||||
4. **ExpandableSection**
|
||||
|
||||
- Provides collapsible sections
|
||||
- Manages expand/collapse state
|
||||
- Handles animations
|
||||
- Shows section metadata
|
||||
|
||||
5. **TypeGroup**
|
||||
- Groups items by type
|
||||
- Formats item lists
|
||||
- Highlights search matches
|
||||
- Maintains consistent styling
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
The Marketplace architecture addresses several performance challenges:
|
||||
|
||||
1. **Concurrency Control**:
|
||||
|
||||
- Source operations are locked to prevent conflicts
|
||||
- Operations are queued during metadata scanning
|
||||
- Cache timeouts prevent hanging operations
|
||||
- Repository operations are atomic
|
||||
|
||||
2. **Efficient Caching**:
|
||||
|
||||
- Repository data is cached with expiry
|
||||
- Cache is cleaned up automatically
|
||||
- Forced refresh available when needed
|
||||
- Cache directories managed efficiently
|
||||
|
||||
3. **Smart Filtering**:
|
||||
- Match info tracks filter matches
|
||||
- Filtering happens at multiple levels
|
||||
- View state optimizes re-renders
|
||||
- Search is case-insensitive and normalized
|
||||
|
||||
## Error Handling
|
||||
|
||||
The architecture includes robust error handling:
|
||||
|
||||
1. **Repository Operations**:
|
||||
|
||||
- Git lock files are cleaned up
|
||||
- Failed clones are retried
|
||||
- Corrupt repositories are re-cloned
|
||||
- Network timeouts are handled
|
||||
|
||||
2. **Data Processing**:
|
||||
|
||||
- Invalid metadata is gracefully handled
|
||||
- Missing files are reported clearly
|
||||
- Parse errors preserve partial data
|
||||
- Type validation ensures consistency
|
||||
|
||||
3. **State Management**:
|
||||
- Invalid filters are normalized
|
||||
- Sort operations handle missing data
|
||||
- View updates are atomic
|
||||
- Error states are preserved
|
||||
|
||||
## Extensibility Points
|
||||
|
||||
The Marketplace architecture is designed for extensibility:
|
||||
|
||||
1. **Repository Sources**:
|
||||
|
||||
- Support for multiple Git providers
|
||||
- Custom repository validation
|
||||
- Flexible metadata formats
|
||||
- Localization support
|
||||
|
||||
2. **Filtering System**:
|
||||
|
||||
- Custom filter types
|
||||
- Extensible match info
|
||||
- Flexible sort options
|
||||
- View state customization
|
||||
|
||||
3. **UI Components**:
|
||||
- Custom item renderers
|
||||
- Flexible layout system
|
||||
- Theme integration
|
||||
- Accessibility support
|
||||
|
||||
---
|
||||
|
||||
**Previous**: [Adding Custom Item Sources](../user-guide/06-adding-custom-sources.md) | **Next**: [Core Components](./02-core-components.md)
|
||||
244
cline_docs/marketplace/implementation/02-core-components.md
Normal file
244
cline_docs/marketplace/implementation/02-core-components.md
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
# Core Components
|
||||
|
||||
This document provides detailed information about the core components of the Marketplace system, their responsibilities, implementation details, and interactions.
|
||||
|
||||
## GitFetcher
|
||||
|
||||
The GitFetcher is responsible for managing Git repository operations, including cloning, pulling, and caching repository data.
|
||||
|
||||
### Responsibilities
|
||||
|
||||
- Cloning and updating Git repositories
|
||||
- Managing repository cache
|
||||
- Validating repository structure
|
||||
- Coordinating with MetadataScanner
|
||||
- Handling repository timeouts and errors
|
||||
|
||||
### Implementation Details
|
||||
|
||||
[/src/services/marketplace/GitFetcher.ts](/src/services/marketplace/GitFetcher.ts)
|
||||
|
||||
### Key Algorithms
|
||||
|
||||
#### Repository Management
|
||||
|
||||
The repository management process includes:
|
||||
|
||||
1. **Cache Management**:
|
||||
|
||||
- Check if repository exists in cache
|
||||
- Validate cache freshness
|
||||
- Clean up stale cache entries
|
||||
- Handle cache directory creation
|
||||
|
||||
2. **Repository Operations**:
|
||||
|
||||
- Clone new repositories
|
||||
- Pull updates for existing repos
|
||||
- Handle git lock files
|
||||
- Clean up failed operations
|
||||
|
||||
3. **Error Recovery**:
|
||||
- Handle network timeouts
|
||||
- Recover from corrupt repositories
|
||||
- Clean up partial clones
|
||||
- Retry failed operations
|
||||
|
||||
## MetadataScanner
|
||||
|
||||
The MetadataScanner is responsible for reading and parsing item metadata from repositories.
|
||||
|
||||
### Responsibilities
|
||||
|
||||
- Scanning directories for item metadata files
|
||||
- Parsing YAML metadata into structured objects
|
||||
- Building component hierarchies
|
||||
- Supporting localized metadata
|
||||
- Validating metadata structure
|
||||
|
||||
### Implementation Details
|
||||
|
||||
[/src/services/marketplace/MetadataScanner.ts](/src/services/marketplace/MetadataScanner.ts)
|
||||
|
||||
## MarketplaceManager
|
||||
|
||||
The MarketplaceManager is the central component that manages marketplace data, caching, and operations.
|
||||
|
||||
### Responsibilities
|
||||
|
||||
- Managing concurrent operations
|
||||
- Handling repository caching
|
||||
- Coordinating with GitFetcher
|
||||
- Applying filters and sorting
|
||||
- Managing registry sources
|
||||
|
||||
### Implementation Details
|
||||
|
||||
[/src/services/marketplace/MarketplaceManager.ts](/src/services/marketplace/MarketplaceManager.ts)
|
||||
|
||||
### Key Algorithms
|
||||
|
||||
#### Concurrency Control
|
||||
|
||||
The manager implements sophisticated concurrency control:
|
||||
|
||||
1. **Operation Queueing**:
|
||||
|
||||
- Queue operations during active scans
|
||||
- Process operations sequentially
|
||||
- Handle operation dependencies
|
||||
- Maintain operation order
|
||||
|
||||
2. **Source Locking**:
|
||||
|
||||
- Lock sources during operations
|
||||
- Prevent concurrent source access
|
||||
- Handle lock timeouts
|
||||
- Clean up stale locks
|
||||
|
||||
3. **Cache Management**:
|
||||
- Implement cache expiration
|
||||
- Handle cache invalidation
|
||||
- Clean up unused cache
|
||||
- Optimize cache storage
|
||||
|
||||
#### Advanced Filtering
|
||||
|
||||
The filtering system provides rich functionality:
|
||||
|
||||
1. **Multi-level Filtering**:
|
||||
|
||||
- Filter parent items
|
||||
- Filter subcomponents
|
||||
- Handle item-specific logic
|
||||
- Track match information
|
||||
|
||||
2. **Match Information**:
|
||||
- Track match reasons
|
||||
- Handle partial matches
|
||||
- Support highlighting
|
||||
- Maintain match context
|
||||
|
||||
## MarketplaceValidation
|
||||
|
||||
The MarketplaceValidation component handles validation of marketplace sources and their configurations.
|
||||
|
||||
### Responsibilities
|
||||
|
||||
- Validating Git repository URLs for any domain
|
||||
- Validating source names and configurations
|
||||
- Detecting duplicate sources
|
||||
- Providing structured validation errors
|
||||
- Supporting multiple Git protocols
|
||||
|
||||
### Implementation Details
|
||||
|
||||
[/src/shared/MarketplaceValidation.ts](/src/shared/MarketplaceValidation.ts)
|
||||
|
||||
### Key Algorithms
|
||||
|
||||
#### URL Validation
|
||||
|
||||
The URL validation system supports:
|
||||
|
||||
1. **Protocol Validation**:
|
||||
|
||||
- HTTPS URLs
|
||||
- SSH URLs
|
||||
- Git protocol URLs
|
||||
- Custom domains and ports
|
||||
|
||||
2. **Domain Validation**:
|
||||
|
||||
- Any valid domain name
|
||||
- IP addresses
|
||||
- Localhost for testing
|
||||
- Internal company domains
|
||||
|
||||
3. **Path Validation**:
|
||||
- Username/organization
|
||||
- Repository name
|
||||
- Optional .git suffix
|
||||
- Subpath support
|
||||
|
||||
## MarketplaceViewStateManager
|
||||
|
||||
The MarketplaceViewStateManager manages frontend state and synchronization with the backend.
|
||||
|
||||
### Responsibilities
|
||||
|
||||
- Managing frontend state transitions
|
||||
- Handling message processing
|
||||
- Managing timeouts and retries
|
||||
- Coordinating with backend state
|
||||
- Providing state change subscriptions
|
||||
- Managing source modification tracking
|
||||
- Handling filtering and sorting
|
||||
|
||||
### Implementation Details
|
||||
|
||||
[/webview-ui/src/components/marketplace/MarketplaceViewStateManager.ts](/webview-ui/src/components/marketplace/MarketplaceViewStateManager.ts)
|
||||
|
||||
## Component Integration
|
||||
|
||||
The components work together through well-defined interfaces:
|
||||
|
||||
### Data Flow
|
||||
|
||||
1. **Repository Operations**:
|
||||
|
||||
- MarketplaceManager validates sources with MarketplaceValidation
|
||||
- MarketplaceManager coordinates with GitFetcher
|
||||
- GitFetcher manages repository state
|
||||
- MetadataScanner processes repository content
|
||||
- Results flow back to MarketplaceManager
|
||||
|
||||
2. **State Management**:
|
||||
|
||||
- MarketplaceManager maintains backend state
|
||||
- ViewStateManager handles UI state transitions
|
||||
- ViewStateManager processes messages
|
||||
- State changes notify subscribers
|
||||
- Components react to state changes
|
||||
- Timeout protection ensures responsiveness
|
||||
|
||||
3. **User Interactions**:
|
||||
- UI events trigger state updates
|
||||
- ViewStateManager processes changes
|
||||
- Changes propagate to backend
|
||||
- Results update UI state
|
||||
|
||||
## Performance Optimizations
|
||||
|
||||
The system includes several optimizations:
|
||||
|
||||
1. **Concurrent Operations**:
|
||||
|
||||
- Operation queueing
|
||||
- Source locking
|
||||
- Parallel processing where safe
|
||||
- Resource management
|
||||
|
||||
2. **Efficient Caching**:
|
||||
|
||||
- Multi-level cache
|
||||
- Cache invalidation
|
||||
- Lazy loading
|
||||
- Cache cleanup
|
||||
|
||||
3. **Smart Filtering**:
|
||||
|
||||
- Optimized algorithms
|
||||
- Match tracking
|
||||
- Incremental updates
|
||||
- Result caching
|
||||
|
||||
4. **State Management**:
|
||||
- Minimal updates
|
||||
- State normalization
|
||||
- Change batching
|
||||
- Update optimization
|
||||
|
||||
---
|
||||
|
||||
**Previous**: [Marketplace Architecture](./01-architecture.md) | **Next**: [Data Structures](./03-data-structures.md)
|
||||
218
cline_docs/marketplace/implementation/03-data-structures.md
Normal file
218
cline_docs/marketplace/implementation/03-data-structures.md
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
# Data Structures
|
||||
|
||||
This document details the key data structures used in the Marketplace, including their definitions, relationships, and usage patterns.
|
||||
|
||||
## Item Types
|
||||
|
||||
The Marketplace uses a type system to categorize different kinds of items:
|
||||
|
||||
### MarketplaceItemType Enumeration
|
||||
|
||||
[/src/services/marketplace/types.ts](/src/services/marketplace/types.ts)
|
||||
|
||||
These types represent the different kinds of components that can be managed by the Marketplace:
|
||||
|
||||
1. **mode**: AI assistant personalities with specialized capabilities
|
||||
2. **prompt**: Pre-configured instructions for specific tasks
|
||||
3. **mcp**: Model Context Protocol servers that provide additional functionality
|
||||
4. **package**: Collections of items (multiple modes, mcps,..., like `roo-commander`)
|
||||
|
||||
## Core Data Structures
|
||||
|
||||
### MarketplaceRepository
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Represents a repository with its metadata and items
|
||||
*/
|
||||
export interface MarketplaceRepository {
|
||||
metadata: RepositoryMetadata
|
||||
items: MarketplaceItem[]
|
||||
url: string
|
||||
defaultBranch: string
|
||||
error?: string
|
||||
}
|
||||
```
|
||||
|
||||
This interface represents a complete repository:
|
||||
|
||||
- **metadata**: The repository metadata
|
||||
- **items**: Array of items in the repository
|
||||
- **url**: The URL to the repository
|
||||
- **defaultBranch**: The default Git branch (e.g., "main")
|
||||
- **error**: Optional error message if there was a problem
|
||||
|
||||
### MarketplaceItem
|
||||
|
||||
[/src/services/marketplace/types.ts](/src/services/marketplace/types.ts)
|
||||
|
||||
Key changes:
|
||||
|
||||
- Added **defaultBranch** field for Git branch tracking
|
||||
- Enhanced **matchInfo** structure for better filtering
|
||||
- Improved subcomponent handling
|
||||
|
||||
### MatchInfo
|
||||
|
||||
[/src/services/marketplace/types.ts](/src/services/marketplace/types.ts)
|
||||
|
||||
Enhanced match tracking:
|
||||
|
||||
- Added **typeMatch** for component type filtering
|
||||
- More detailed match reasons
|
||||
- Support for subcomponent matching
|
||||
|
||||
## State Management Structures
|
||||
|
||||
### ValidationError
|
||||
|
||||
[/src/shared/MarketplaceValidation.ts](/src/shared/MarketplaceValidation.ts)
|
||||
|
||||
Used for structured validation errors:
|
||||
|
||||
- **field**: The field that failed validation (e.g., "url", "name")
|
||||
- **message**: Human-readable error message
|
||||
|
||||
### ViewState
|
||||
|
||||
[/webview-ui/src/components/marketplace/MarketplaceViewStateManager.ts](/webview-ui/src/components/marketplace/MarketplaceViewStateManager.ts)
|
||||
|
||||
Manages UI state:
|
||||
|
||||
- **allItems**: All available items
|
||||
- **displayItems**: Currently filtered/displayed items
|
||||
- **isFetching**: Loading state indicator
|
||||
- **activeTab**: Current view tab
|
||||
- **refreshingUrls**: Sources being refreshed
|
||||
- **sources**: Marketplace sources
|
||||
- **filters**: Active filters
|
||||
- **sortConfig**: Sort configuration
|
||||
|
||||
### ViewStateTransition
|
||||
|
||||
[/webview-ui/src/components/marketplace/MarketplaceViewStateManager.ts](/webview-ui/src/components/marketplace/MarketplaceViewStateManager.ts)
|
||||
|
||||
Defines state transitions:
|
||||
|
||||
- Operation types
|
||||
- Optional payloads
|
||||
- Type-safe transitions
|
||||
|
||||
### Filters
|
||||
|
||||
[/webview-ui/src/components/marketplace/MarketplaceViewStateManager.ts](/webview-ui/src/components/marketplace/MarketplaceViewStateManager.ts)
|
||||
|
||||
Enhanced filtering:
|
||||
|
||||
- Component type filtering
|
||||
- Text search
|
||||
- Tag-based filtering
|
||||
|
||||
## Metadata Interfaces
|
||||
|
||||
### BaseMetadata
|
||||
|
||||
[/src/services/marketplace/types.ts](/src/services/marketplace/types.ts)
|
||||
|
||||
Common metadata properties:
|
||||
|
||||
- **name**: Display name
|
||||
- **description**: Detailed explanation
|
||||
- **version**: Semantic version
|
||||
- **tags**: Optional keywords
|
||||
|
||||
### ComponentMetadata
|
||||
|
||||
[/src/services/marketplace/types.ts](/src/services/marketplace/types.ts)
|
||||
|
||||
Added:
|
||||
|
||||
- **type** field for item component type
|
||||
|
||||
### PackageMetadata
|
||||
|
||||
[/src/services/marketplace/types.ts](/src/services/marketplace/types.ts)
|
||||
|
||||
Enhanced with:
|
||||
|
||||
- Subcomponent tracking
|
||||
|
||||
## Source Management
|
||||
|
||||
### MarketplaceSource
|
||||
|
||||
[/src/services/marketplace/types.ts](/src/services/marketplace/types.ts)
|
||||
|
||||
## Message Structures
|
||||
|
||||
> TBA
|
||||
|
||||
## Data Validation
|
||||
|
||||
### Metadata Validation
|
||||
|
||||
[/src/services/marketplace/schemas.ts](/src/services/marketplace/schemas.ts)
|
||||
|
||||
### URL Validation
|
||||
|
||||
[/src/shared/MarketplaceValidation.ts](/src/shared/MarketplaceValidation.ts)
|
||||
|
||||
Supports:
|
||||
|
||||
- Any valid domain name
|
||||
- Multiple Git protocols
|
||||
- Optional .git suffix
|
||||
- Subpath components
|
||||
|
||||
## Data Flow
|
||||
|
||||
The Marketplace transforms data through several stages:
|
||||
|
||||
1. **Repository Level**:
|
||||
|
||||
- Clone/pull Git repositories
|
||||
- Parse metadata files
|
||||
- Build component hierarchy
|
||||
|
||||
2. **Cache Level**:
|
||||
|
||||
- Store repository data
|
||||
- Track timestamps
|
||||
- Handle expiration
|
||||
|
||||
3. **View Level**:
|
||||
- Apply filters
|
||||
- Sort items
|
||||
- Track matches
|
||||
- Manage UI state
|
||||
|
||||
## Data Relationships
|
||||
|
||||
### Component Hierarchy
|
||||
|
||||
```
|
||||
Repository
|
||||
├── Metadata
|
||||
└── Items
|
||||
├── Package
|
||||
│ ├── Mode
|
||||
│ ├── MCP
|
||||
│ └── Prompt
|
||||
└── Standalone Components (Modes, MCP, Prompts)
|
||||
```
|
||||
|
||||
### State Flow
|
||||
|
||||
```
|
||||
Git Repository → Cache → Marketplace → ViewState → UI
|
||||
```
|
||||
|
||||
### Filter Chain
|
||||
|
||||
```
|
||||
Raw Items → Type Filter → Search Filter → Tag Filter → Sorted Results
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Previous**: [Core Components](./02-core-components.md) | **Next**: [Search and Filter Implementation](./04-search-and-filter.md)
|
||||
105
cline_docs/marketplace/implementation/04-search-and-filter.md
Normal file
105
cline_docs/marketplace/implementation/04-search-and-filter.md
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
# Search and Filter Implementation
|
||||
|
||||
This document details the implementation of search and filtering functionality in the Marketplace, including algorithms, optimization techniques, and performance considerations.
|
||||
|
||||
## Core Filter System
|
||||
|
||||
The Marketplace implements a comprehensive filtering system that handles multiple filter types, concurrent operations, and detailed match tracking.
|
||||
|
||||
### Filter Implementation
|
||||
|
||||
[/src/services/marketplace/MarketplaceManager.ts](/src/services/marketplace/MarketplaceManager.ts)
|
||||
|
||||
## Sort System
|
||||
|
||||
The Marketplace implements flexible sorting with subcomponent support:
|
||||
|
||||
[/src/services/marketplace/MarketplaceManager.ts](/src/services/marketplace/MarketplaceManager.ts)
|
||||
|
||||
## State Management Integration
|
||||
|
||||
The filtering system integrates with the state management through state transitions:
|
||||
|
||||
[/webview-ui/src/components/marketplace/MarketplaceViewStateManager.ts](/webview-ui/src/components/marketplace/MarketplaceViewStateManager.ts)
|
||||
|
||||
## Performance Optimizations
|
||||
|
||||
### Concurrent Operation Handling
|
||||
|
||||
[/src/services/marketplace/MarketplaceManager.ts](/src/services/marketplace/MarketplaceManager.ts)
|
||||
|
||||
### Filter Optimizations
|
||||
|
||||
1. **Early Termination**:
|
||||
|
||||
- Returns as soon as any field matches
|
||||
- Avoids unnecessary checks
|
||||
- Handles empty filters efficiently
|
||||
|
||||
2. **Efficient String Operations**:
|
||||
|
||||
- Normalizes text once
|
||||
- Uses native string methods
|
||||
- Avoids regex for simple matches
|
||||
|
||||
3. **State Management**:
|
||||
- State transitions for predictable updates
|
||||
- Subscriber pattern for state changes
|
||||
- Separation of all items and display items
|
||||
- Backend-driven filtering
|
||||
- Optimistic UI updates
|
||||
- Efficient state synchronization
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
```typescript
|
||||
describe("Filter System", () => {
|
||||
describe("Match Tracking", () => {
|
||||
it("should track type matches", () => {
|
||||
const result = filterItems([testItem], { type: "mode" })
|
||||
expect(result[0].matchInfo.matchReason.typeMatch).toBe(true)
|
||||
})
|
||||
|
||||
it("should track subcomponent matches", () => {
|
||||
const result = filterItems([testPack], { search: "test" })
|
||||
const subItem = result[0].items![0]
|
||||
expect(subItem.matchInfo.matched).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Sort System", () => {
|
||||
it("should sort subcomponents", () => {
|
||||
const result = sortItems([testPack], "name", "asc", true)
|
||||
expect(result[0].items).toBeSorted((a, b) => a.metadata.name.localeCompare(b.metadata.name))
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The system includes robust error handling:
|
||||
|
||||
1. **Filter Errors**:
|
||||
|
||||
- Invalid filter types
|
||||
- Malformed search terms
|
||||
- Missing metadata
|
||||
|
||||
2. **Sort Errors**:
|
||||
|
||||
- Invalid sort fields
|
||||
- Missing sort values
|
||||
- Type mismatches
|
||||
|
||||
3. **State Errors**:
|
||||
- Invalid state transitions
|
||||
- Message handling errors
|
||||
- State synchronization issues
|
||||
- Timeout handling
|
||||
- Source modification tracking
|
||||
- Filter validation errors
|
||||
|
||||
---
|
||||
|
||||
**Previous**: [Data Structures](./03-data-structures.md) | **Next**: [UI Component Design](./05-ui-components.md)
|
||||
398
cline_docs/marketplace/implementation/05-ui-components.md
Normal file
398
cline_docs/marketplace/implementation/05-ui-components.md
Normal file
|
|
@ -0,0 +1,398 @@
|
|||
# UI Component Design
|
||||
|
||||
This document details the design and implementation of the Marketplace's UI components, including their structure, styling, interactions, and accessibility features.
|
||||
|
||||
## MarketplaceView
|
||||
|
||||
The MarketplaceView is the main container component that manages the overall marketplace interface.
|
||||
|
||||
### Component Structure
|
||||
|
||||
[/webview-ui/src/components/marketplace/MarketplaceView.tsx](/webview-ui/src/components/marketplace/MarketplaceView.tsx)
|
||||
|
||||
### State Management Integration
|
||||
|
||||
The component uses the MarketplaceViewStateManager through the useStateManager hook:
|
||||
|
||||
```tsx
|
||||
const [state, manager] = useStateManager()
|
||||
```
|
||||
|
||||
Key features:
|
||||
|
||||
- Manages tab state (browse/sources)
|
||||
- Handles source configuration
|
||||
- Coordinates filtering and sorting
|
||||
- Manages loading states
|
||||
- Handles source validation
|
||||
|
||||
## MarketplaceItemCard
|
||||
|
||||
The MarketplaceItemCard is the primary component for displaying item information in the UI.
|
||||
|
||||
### Component Structure
|
||||
|
||||
[/webview-ui/src/components/marketplace/components/MarketplaceItemCard.tsx](/webview-ui/src/components/marketplace/components/MarketplaceItemCard.tsx)
|
||||
|
||||
### Design Considerations
|
||||
|
||||
1. **Visual Hierarchy**:
|
||||
|
||||
- Clear distinction between header, content, and footer
|
||||
- Type badge stands out with color coding
|
||||
- Important information is emphasized with typography
|
||||
|
||||
2. **Interactive Elements**:
|
||||
|
||||
- Tags are clickable for filtering
|
||||
- External link button for source access
|
||||
- Expandable details section for subcomponents
|
||||
|
||||
3. **Information Density**:
|
||||
|
||||
- Balanced display of essential information
|
||||
- Optional elements only shown when available
|
||||
- Expandable section for additional details
|
||||
|
||||
4. **VSCode Integration**:
|
||||
- Uses VSCode theme variables for colors
|
||||
- Matches VSCode UI patterns
|
||||
- Integrates with VSCode messaging system
|
||||
|
||||
## ExpandableSection
|
||||
|
||||
The ExpandableSection component provides a collapsible container for content that doesn't need to be visible at all times.
|
||||
|
||||
### Component Structure
|
||||
|
||||
[/webview-ui/src/components/marketplace/components/ExpandableSection.tsx](/webview-ui/src/components/marketplace/components/ExpandableSection.tsx)
|
||||
|
||||
### Design Considerations
|
||||
|
||||
1. **Animation**:
|
||||
|
||||
- Smooth height transition for expand/collapse
|
||||
- Opacity change for better visual feedback
|
||||
- Chevron icon rotation for state indication
|
||||
|
||||
2. **Accessibility**:
|
||||
|
||||
- Proper ARIA attributes for screen readers
|
||||
- Keyboard navigation support
|
||||
- Clear visual indication of interactive state
|
||||
|
||||
3. **Flexibility**:
|
||||
|
||||
- Accepts any content as children
|
||||
- Optional badge for additional information
|
||||
- Customizable through className prop
|
||||
|
||||
4. **State Management**:
|
||||
- Internal state for expanded/collapsed
|
||||
- Can be controlled through defaultExpanded prop
|
||||
- Preserves state during component lifecycle
|
||||
|
||||
## TypeGroup
|
||||
|
||||
The TypeGroup component displays a collection of items of the same type, with special handling for search matches.
|
||||
|
||||
### Component Structure
|
||||
|
||||
[/webview-ui/src/components/marketplace/components/TypeGroup.tsx](/webview-ui/src/components/marketplace/components/TypeGroup.tsx)
|
||||
|
||||
### Design Considerations
|
||||
|
||||
1. **List Presentation**:
|
||||
|
||||
- Ordered list with automatic numbering
|
||||
- Clear type heading for context
|
||||
- Consistent spacing for readability
|
||||
|
||||
2. **Search Match Highlighting**:
|
||||
|
||||
- Visual distinction for matching items
|
||||
- "match" badge for quick identification
|
||||
- Color change for matched text
|
||||
|
||||
3. **Information Display**:
|
||||
|
||||
- Name and description clearly separated
|
||||
- Tooltip shows path information on hover
|
||||
- Truncation for very long descriptions
|
||||
|
||||
4. **Empty State Handling**:
|
||||
- Returns null when no items are present
|
||||
- Avoids rendering empty containers
|
||||
- Prevents unnecessary UI elements
|
||||
|
||||
## Source Configuration Components
|
||||
|
||||
The Marketplace includes components for managing item sources.
|
||||
|
||||
[/webview-ui/src/components/marketplace/MarketplaceView.tsx](/webview-ui/src/components/marketplace/MarketplaceView.tsx)
|
||||
|
||||
## Filter Components
|
||||
|
||||
The Marketplace includes components for filtering and searching.
|
||||
|
||||
[/webview-ui/src/components/marketplace/MarketplaceView.tsx](/webview-ui/src/components/marketplace/MarketplaceView.tsx)
|
||||
|
||||
### TypeFilterGroup
|
||||
|
||||
[/webview-ui/src/components/marketplace/MarketplaceView.tsx](/webview-ui/src/components/marketplace/MarketplaceView.tsx)
|
||||
|
||||
### TagFilterGroup
|
||||
|
||||
[/webview-ui/src/components/marketplace/MarketplaceView.tsx](/webview-ui/src/components/marketplace/MarketplaceView.tsx)
|
||||
|
||||
## Styling Approach
|
||||
|
||||
The Marketplace UI uses a combination of Tailwind CSS and VSCode theme variables for styling.
|
||||
|
||||
## Responsive Design
|
||||
|
||||
The Marketplace UI is designed to work across different viewport sizes:
|
||||
|
||||
## Accessibility Features
|
||||
|
||||
The Marketplace UI includes several accessibility features:
|
||||
|
||||
### Keyboard Navigation
|
||||
|
||||
```tsx
|
||||
// Example of keyboard navigation support
|
||||
<button
|
||||
className="filter-button"
|
||||
onClick={handleClick}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault()
|
||||
handleClick()
|
||||
}
|
||||
}}
|
||||
tabIndex={0}
|
||||
role="checkbox"
|
||||
aria-checked={isSelected}>
|
||||
{label}
|
||||
</button>
|
||||
```
|
||||
|
||||
### Screen Reader Support
|
||||
|
||||
```tsx
|
||||
// Example of screen reader support
|
||||
<div role="region" aria-label="Item details" aria-expanded={isExpanded}>
|
||||
<button aria-controls="details-content" aria-expanded={isExpanded} onClick={toggleExpanded}>
|
||||
{isExpanded ? "Hide details" : "Show details"}
|
||||
</button>
|
||||
<div id="details-content" hidden={!isExpanded}>
|
||||
{/* Details content */}
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Focus Management
|
||||
|
||||
```tsx
|
||||
// Example of focus management
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && buttonRef.current) {
|
||||
buttonRef.current.focus()
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
return (
|
||||
<button ref={buttonRef} className="focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
{label}
|
||||
</button>
|
||||
)
|
||||
```
|
||||
|
||||
### Color Contrast
|
||||
|
||||
The UI ensures sufficient color contrast for all text:
|
||||
|
||||
- Text uses VSCode theme variables that maintain proper contrast
|
||||
- Interactive elements have clear focus states
|
||||
- Color is not the only means of conveying information
|
||||
|
||||
## Animation and Transitions
|
||||
|
||||
The Marketplace UI uses subtle animations to enhance the user experience:
|
||||
|
||||
### Expand/Collapse Animation
|
||||
|
||||
```tsx
|
||||
// Example of expand/collapse animation
|
||||
<div
|
||||
className={cn(
|
||||
"overflow-hidden transition-[max-height,opacity] duration-200 ease-in-out",
|
||||
isExpanded ? "max-h-[500px] opacity-100" : "max-h-0 opacity-0",
|
||||
)}>
|
||||
{children}
|
||||
</div>
|
||||
```
|
||||
|
||||
### Hover Effects
|
||||
|
||||
```tsx
|
||||
// Example of hover effects
|
||||
<button className="px-2 py-1 rounded-md transition-colors duration-150 hover:bg-vscode-button-hoverBackground">
|
||||
{label}
|
||||
</button>
|
||||
```
|
||||
|
||||
### Loading States
|
||||
|
||||
```tsx
|
||||
// Example of loading state animation
|
||||
<div className="loading-indicator">
|
||||
<div className="spinner animate-spin h-5 w-5 border-2 border-t-transparent rounded-full"></div>
|
||||
<span>Loading items...</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Error Handling in UI
|
||||
|
||||
The Marketplace UI includes graceful error handling:
|
||||
|
||||
### Error States
|
||||
|
||||
```tsx
|
||||
// Example of error state display
|
||||
const ErrorDisplay: React.FC<{ error: string; retry: () => void }> = ({ error, retry }) => {
|
||||
return (
|
||||
<div className="error-container p-4 border border-red-500 rounded-md bg-red-50 text-red-700">
|
||||
<div className="flex items-center">
|
||||
<span className="codicon codicon-error mr-2"></span>
|
||||
<h3 className="font-medium">Error loading items</h3>
|
||||
</div>
|
||||
<p className="mt-2 mb-4">{error}</p>
|
||||
<button className="px-3 py-1 bg-red-600 text-white rounded-md hover:bg-red-700" onClick={retry}>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Empty States
|
||||
|
||||
```tsx
|
||||
// Example of empty state display
|
||||
const EmptyState: React.FC<{ message: string }> = ({ message }) => {
|
||||
return (
|
||||
<div className="empty-state p-8 text-center text-vscode-descriptionForeground">
|
||||
<div className="codicon codicon-info text-4xl mb-2"></div>
|
||||
<p>{message}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Component Testing
|
||||
|
||||
The Marketplace UI components include comprehensive tests:
|
||||
|
||||
### Unit Tests
|
||||
|
||||
```typescript
|
||||
// Example of component unit test
|
||||
describe("MarketplaceItemCard", () => {
|
||||
const mockItem: MarketplaceItem = {
|
||||
name: "Test Package",
|
||||
description: "A test package",
|
||||
type: "package",
|
||||
url: "https://example.com",
|
||||
repoUrl: "https://github.com/example/repo",
|
||||
tags: ["test", "example"],
|
||||
version: "1.0.0",
|
||||
lastUpdated: "2025-04-01"
|
||||
};
|
||||
|
||||
const mockFilters = { type: "", search: "", tags: [] };
|
||||
const mockSetFilters = jest.fn();
|
||||
const mockSetActiveTab = jest.fn();
|
||||
|
||||
it("renders correctly", () => {
|
||||
render(
|
||||
<MarketplaceItemCard
|
||||
item={mockItem}
|
||||
filters={mockFilters}
|
||||
setFilters={mockSetFilters}
|
||||
activeTab="browse"
|
||||
setActiveTab={mockSetActiveTab}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("Test Package")).toBeInTheDocument();
|
||||
expect(screen.getByText("A test package")).toBeInTheDocument();
|
||||
expect(screen.getByText("Package")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("handles tag clicks", () => {
|
||||
render(
|
||||
<MarketplaceItemCard
|
||||
item={mockItem}
|
||||
filters={mockFilters}
|
||||
setFilters={mockSetFilters}
|
||||
activeTab="browse"
|
||||
setActiveTab={mockSetActiveTab}
|
||||
/>
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText("test"));
|
||||
|
||||
expect(mockSetFilters).toHaveBeenCalledWith({
|
||||
type: "",
|
||||
search: "",
|
||||
tags: ["test"]
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Snapshot Tests
|
||||
|
||||
```typescript
|
||||
// Example of snapshot test
|
||||
it("matches snapshot", () => {
|
||||
const { container } = render(
|
||||
<MarketplaceItemCard
|
||||
item={mockItem}
|
||||
filters={mockFilters}
|
||||
setFilters={mockSetFilters}
|
||||
activeTab="browse"
|
||||
setActiveTab={mockSetActiveTab}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
```
|
||||
|
||||
### Accessibility Tests
|
||||
|
||||
```typescript
|
||||
// Example of accessibility test
|
||||
it("meets accessibility requirements", async () => {
|
||||
const { container } = render(
|
||||
<MarketplaceItemCard
|
||||
item={mockItem}
|
||||
filters={mockFilters}
|
||||
setFilters={mockSetFilters}
|
||||
activeTab="browse"
|
||||
setActiveTab={mockSetActiveTab}
|
||||
/>
|
||||
);
|
||||
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Previous**: [Search and Filter Implementation](./04-search-and-filter.md) | **Next**: [Testing Strategy](./06-testing-strategy.md)
|
||||
1170
cline_docs/marketplace/implementation/06-testing-strategy.md
Normal file
1170
cline_docs/marketplace/implementation/06-testing-strategy.md
Normal file
File diff suppressed because it is too large
Load diff
926
cline_docs/marketplace/implementation/07-extending.md
Normal file
926
cline_docs/marketplace/implementation/07-extending.md
Normal file
|
|
@ -0,0 +1,926 @@
|
|||
# Extending the Marketplace
|
||||
|
||||
This document provides guidance on extending the Marketplace with new features, component types, and customizations.
|
||||
|
||||
## Adding New Component Types
|
||||
|
||||
The Marketplace is designed to be extensible, allowing for the addition of new component types beyond the default ones (mode, mcp, prompt, package).
|
||||
|
||||
### Extending the MarketplaceItemType
|
||||
|
||||
To add a new component type:
|
||||
|
||||
1. **Update the MarketplaceItemType Type**:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Supported component types
|
||||
*/
|
||||
export type MarketplaceItemType = "mode" | "prompt" | "package" | "mcp" | "your-new-type"
|
||||
```
|
||||
|
||||
2. **Update Type Label Functions**:
|
||||
|
||||
```typescript
|
||||
const getTypeLabel = (type: string) => {
|
||||
switch (type) {
|
||||
case "mode":
|
||||
return "Mode"
|
||||
case "mcp":
|
||||
return "MCP Server"
|
||||
case "prompt":
|
||||
return "Prompt"
|
||||
case "package":
|
||||
return "Package"
|
||||
case "your-new-type":
|
||||
return "Your New Type"
|
||||
default:
|
||||
return "Other"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Update Type Color Functions**:
|
||||
|
||||
```typescript
|
||||
const getTypeColor = (type: string) => {
|
||||
switch (type) {
|
||||
case "mode":
|
||||
return "bg-blue-600"
|
||||
case "mcp":
|
||||
return "bg-green-600"
|
||||
case "prompt":
|
||||
return "bg-purple-600"
|
||||
case "package":
|
||||
return "bg-orange-600"
|
||||
case "your-new-type":
|
||||
return "bg-yellow-600" // Choose a distinctive color
|
||||
default:
|
||||
return "bg-gray-600"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. **Update Type Group Labels**:
|
||||
|
||||
```typescript
|
||||
const getTypeGroupLabel = (type: string) => {
|
||||
switch (type) {
|
||||
case "mode":
|
||||
return "Modes"
|
||||
case "mcp":
|
||||
return "MCP Servers"
|
||||
case "prompt":
|
||||
return "Prompts"
|
||||
case "package":
|
||||
return "Packages"
|
||||
case "your-new-type":
|
||||
return "Your New Types"
|
||||
default:
|
||||
return `${type.charAt(0).toUpperCase()}${type.slice(1)}s`
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Directory Structure for New Types
|
||||
|
||||
When adding a new component type, follow this directory structure in your source repository:
|
||||
|
||||
```
|
||||
repository-root/
|
||||
├── metadata.en.yml
|
||||
├── your-new-type/ # Directory for your new component type
|
||||
│ ├── component-1/
|
||||
│ │ └── metadata.en.yml
|
||||
│ └── component-2/
|
||||
│ └── metadata.en.yml
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Metadata for New Types
|
||||
|
||||
The metadata for your new component type should follow the standard format:
|
||||
|
||||
```yaml
|
||||
name: "Your Component Name"
|
||||
description: "Description of your component"
|
||||
version: "1.0.0"
|
||||
type: "your-new-type"
|
||||
tags:
|
||||
- relevant-tag-1
|
||||
- relevant-tag-2
|
||||
```
|
||||
|
||||
### UI Considerations for New Types
|
||||
|
||||
When adding a new component type, consider these UI aspects:
|
||||
|
||||
1. **Type Filtering**:
|
||||
|
||||
- Add your new type to the type filter options
|
||||
- Ensure proper labeling and styling
|
||||
|
||||
2. **Type-Specific Rendering**:
|
||||
|
||||
- Consider if your type needs special rendering in the UI
|
||||
- Add any type-specific UI components or styles
|
||||
|
||||
3. **Type Icons**:
|
||||
- Choose an appropriate icon for your type
|
||||
- Add it to the icon mapping
|
||||
|
||||
```typescript
|
||||
const getTypeIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case "mode":
|
||||
return "codicon-person"
|
||||
case "mcp":
|
||||
return "codicon-server"
|
||||
case "prompt":
|
||||
return "codicon-comment"
|
||||
case "package":
|
||||
return "codicon-package"
|
||||
case "your-new-type":
|
||||
return "codicon-your-icon" // Choose an appropriate icon
|
||||
default:
|
||||
return "codicon-symbol-misc"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Creating Custom Templates
|
||||
|
||||
You can create custom templates to provide a starting point for users creating new components.
|
||||
|
||||
### Template Structure
|
||||
|
||||
A custom template should follow this structure:
|
||||
|
||||
```
|
||||
custom-template/
|
||||
├── metadata.en.yml
|
||||
├── README.md
|
||||
└── [component-specific files]
|
||||
```
|
||||
|
||||
### Template Metadata
|
||||
|
||||
The template metadata should include:
|
||||
|
||||
```yaml
|
||||
name: "Your Template Name"
|
||||
description: "Description of your template"
|
||||
version: "1.0.0"
|
||||
type: "your-component-type"
|
||||
template: true
|
||||
templateFor: "your-component-type"
|
||||
```
|
||||
|
||||
### Template Registration
|
||||
|
||||
Register your template with the Marketplace:
|
||||
|
||||
```typescript
|
||||
// In your extension code
|
||||
const registerTemplates = (context: vscode.ExtensionContext) => {
|
||||
const templatePath = path.join(context.extensionPath, "templates", "your-template")
|
||||
marketplace.registerTemplate(templatePath)
|
||||
}
|
||||
```
|
||||
|
||||
### Template Usage
|
||||
|
||||
Users can create new components from your template:
|
||||
|
||||
```typescript
|
||||
// In the UI
|
||||
const createFromTemplate = (templateName: string) => {
|
||||
vscode.postMessage({
|
||||
type: "createFromTemplate",
|
||||
templateName,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Implementing New Features
|
||||
|
||||
The Marketplace is designed to be extended with new features. Here's how to implement common types of features:
|
||||
|
||||
### Adding a New Filter Type
|
||||
|
||||
To add a new filter type (beyond type, search, and tags):
|
||||
|
||||
1. **Update the Filters Interface**:
|
||||
|
||||
```typescript
|
||||
interface Filters {
|
||||
type: string
|
||||
search: string
|
||||
tags: string[]
|
||||
yourNewFilter: string // Add your new filter
|
||||
}
|
||||
```
|
||||
|
||||
2. **Update the Filter Function**:
|
||||
|
||||
```typescript
|
||||
export function filterItems(
|
||||
items: MarketplaceItem[],
|
||||
filters: {
|
||||
type?: string
|
||||
search?: string
|
||||
tags?: string[]
|
||||
yourNewFilter?: string // Add your new filter
|
||||
},
|
||||
): MarketplaceItem[] {
|
||||
// Existing filter logic...
|
||||
|
||||
// Add your new filter logic
|
||||
if (filters.yourNewFilter) {
|
||||
result = result.filter((item) => {
|
||||
// Your filter implementation
|
||||
return yourFilterLogic(item, filters.yourNewFilter)
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
```
|
||||
|
||||
3. **Add UI Controls**:
|
||||
|
||||
```tsx
|
||||
const YourNewFilterControl: React.FC<{
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
}> = ({ value, onChange }) => {
|
||||
return (
|
||||
<div className="filter-group">
|
||||
<h3 className="filter-heading">Your New Filter</h3>
|
||||
{/* Your filter UI controls */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
4. **Integrate with the Main UI**:
|
||||
|
||||
```tsx
|
||||
<FilterPanel>
|
||||
<TypeFilterGroup selectedType={filters.type} onChange={handleTypeChange} availableTypes={availableTypes} />
|
||||
<SearchInput value={filters.search} onChange={handleSearchChange} />
|
||||
<TagFilterGroup selectedTags={filters.tags} onChange={handleTagsChange} availableTags={availableTags} />
|
||||
<YourNewFilterControl value={filters.yourNewFilter} onChange={handleYourNewFilterChange} />
|
||||
</FilterPanel>
|
||||
```
|
||||
|
||||
### Adding a New View Mode
|
||||
|
||||
To add a new view mode (beyond the card view):
|
||||
|
||||
1. **Add a View Mode State**:
|
||||
|
||||
```typescript
|
||||
type ViewMode = "card" | "list" | "yourNewView"
|
||||
|
||||
const [viewMode, setViewMode] = useState<ViewMode>("card")
|
||||
```
|
||||
|
||||
2. **Create the View Component**:
|
||||
|
||||
```tsx
|
||||
const YourNewView: React.FC<{
|
||||
items: MarketplaceItem[]
|
||||
filters: Filters
|
||||
setFilters: (filters: Filters) => void
|
||||
}> = ({ items, filters, setFilters }) => {
|
||||
return <div className="your-new-view">{/* Your view implementation */}</div>
|
||||
}
|
||||
```
|
||||
|
||||
3. **Add View Switching Controls**:
|
||||
|
||||
```tsx
|
||||
const ViewModeSelector: React.FC<{
|
||||
viewMode: ViewMode
|
||||
setViewMode: (mode: ViewMode) => void
|
||||
}> = ({ viewMode, setViewMode }) => {
|
||||
return (
|
||||
<div className="view-mode-selector">
|
||||
<button
|
||||
className={`view-mode-button ${viewMode === "card" ? "active" : ""}`}
|
||||
onClick={() => setViewMode("card")}
|
||||
aria-pressed={viewMode === "card"}
|
||||
title="Card View">
|
||||
<span className="codicon codicon-preview"></span>
|
||||
</button>
|
||||
<button
|
||||
className={`view-mode-button ${viewMode === "list" ? "active" : ""}`}
|
||||
onClick={() => setViewMode("list")}
|
||||
aria-pressed={viewMode === "list"}
|
||||
title="List View">
|
||||
<span className="codicon codicon-list-flat"></span>
|
||||
</button>
|
||||
<button
|
||||
className={`view-mode-button ${viewMode === "yourNewView" ? "active" : ""}`}
|
||||
onClick={() => setViewMode("yourNewView")}
|
||||
aria-pressed={viewMode === "yourNewView"}
|
||||
title="Your New View">
|
||||
<span className="codicon codicon-your-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
4. **Integrate with the Main UI**:
|
||||
|
||||
```tsx
|
||||
<div className="marketplace-container">
|
||||
<div className="toolbar">
|
||||
<ViewModeSelector viewMode={viewMode} setViewMode={setViewMode} />
|
||||
{/* Other toolbar items */}
|
||||
</div>
|
||||
|
||||
<div className="content">
|
||||
{viewMode === "card" && <CardView items={items} filters={filters} setFilters={setFilters} />}
|
||||
{viewMode === "list" && <ListView items={items} filters={filters} setFilters={setFilters} />}
|
||||
{viewMode === "yourNewView" && <YourNewView items={items} filters={filters} setFilters={setFilters} />}
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Adding Custom Actions
|
||||
|
||||
To add custom actions for package items:
|
||||
|
||||
1. **Create an Action Handler**:
|
||||
|
||||
```typescript
|
||||
const handleCustomAction = (item: MarketplaceItem) => {
|
||||
vscode.postMessage({
|
||||
type: "customAction",
|
||||
item: item.name,
|
||||
itemType: item.type,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
2. **Add Action Button to the UI**:
|
||||
|
||||
```tsx
|
||||
<Button onClick={() => handleCustomAction(item)} className="custom-action-button">
|
||||
<span className="codicon codicon-your-icon mr-2"></span>
|
||||
Your Custom Action
|
||||
</Button>
|
||||
```
|
||||
|
||||
3. **Handle the Action in the Message Handler**:
|
||||
|
||||
```typescript
|
||||
case "customAction":
|
||||
// Handle the custom action
|
||||
const { item, itemType } = message;
|
||||
// Your custom action implementation
|
||||
return {
|
||||
type: "customActionResult",
|
||||
success: true,
|
||||
data: { /* result data */ }
|
||||
};
|
||||
```
|
||||
|
||||
## Customizing the UI
|
||||
|
||||
The Marketplace UI can be customized in several ways:
|
||||
|
||||
### Custom Styling
|
||||
|
||||
To customize the styling:
|
||||
|
||||
1. **Add Custom CSS Variables**:
|
||||
|
||||
```css
|
||||
/* In your CSS file */
|
||||
:root {
|
||||
--package-card-bg: var(--vscode-panel-background);
|
||||
--package-card-border: var(--vscode-panel-border);
|
||||
--package-card-hover: var(--vscode-list-hoverBackground);
|
||||
--your-custom-variable: #your-color;
|
||||
}
|
||||
```
|
||||
|
||||
2. **Use Custom Classes**:
|
||||
|
||||
```tsx
|
||||
<div className="your-custom-component">
|
||||
<div className="your-custom-header">{/* Your custom UI */}</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
3. **Add Custom Themes**:
|
||||
|
||||
```typescript
|
||||
type Theme = "default" | "compact" | "detailed" | "yourCustomTheme"
|
||||
|
||||
const [theme, setTheme] = useState<Theme>("default")
|
||||
|
||||
// Theme-specific styles
|
||||
const getThemeClasses = (theme: Theme) => {
|
||||
switch (theme) {
|
||||
case "compact":
|
||||
return "compact-theme"
|
||||
case "detailed":
|
||||
return "detailed-theme"
|
||||
case "yourCustomTheme":
|
||||
return "your-custom-theme"
|
||||
default:
|
||||
return "default-theme"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Components
|
||||
|
||||
To replace or extend existing components:
|
||||
|
||||
1. **Create a Custom Component**:
|
||||
|
||||
```tsx
|
||||
const CustomPackageCard: React.FC<MarketplaceItemCardProps> = (props) => {
|
||||
// Your custom implementation
|
||||
return (
|
||||
<div className="custom-package-card">
|
||||
{/* Your custom UI */}
|
||||
<h3>{props.item.name}</h3>
|
||||
{/* Additional custom elements */}
|
||||
<div className="custom-footer">{/* Custom footer content */}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
2. **Use Component Injection**:
|
||||
|
||||
```tsx
|
||||
interface ComponentOverrides {
|
||||
PackageCard?: React.MarketplaceItemType<MarketplaceItemCardProps>
|
||||
ExpandableSection?: React.MarketplaceItemType<ExpandableSectionProps>
|
||||
TypeGroup?: React.MarketplaceItemType<TypeGroupProps>
|
||||
}
|
||||
|
||||
const MarketplaceView: React.FC<{
|
||||
initialItems: MarketplaceItem[]
|
||||
componentOverrides?: ComponentOverrides
|
||||
}> = ({ initialItems, componentOverrides = {} }) => {
|
||||
// Component selection logic
|
||||
const PackageCard = componentOverrides.PackageCard || MarketplaceItemCard
|
||||
|
||||
return (
|
||||
<div className="marketplace">
|
||||
{items.map((item) => (
|
||||
<PackageCard
|
||||
key={item.name}
|
||||
item={item}
|
||||
filters={filters}
|
||||
setFilters={setFilters}
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Layouts
|
||||
|
||||
To implement custom layouts:
|
||||
|
||||
1. **Create a Layout Component**:
|
||||
|
||||
```tsx
|
||||
const CustomLayout: React.FC<{
|
||||
sidebar: React.ReactNode
|
||||
content: React.ReactNode
|
||||
footer?: React.ReactNode
|
||||
}> = ({ sidebar, content, footer }) => {
|
||||
return (
|
||||
<div className="custom-layout">
|
||||
<div className="custom-sidebar">{sidebar}</div>
|
||||
<div className="custom-content">{content}</div>
|
||||
{footer && <div className="custom-footer">{footer}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
2. **Use the Layout in the Main UI**:
|
||||
|
||||
```tsx
|
||||
<CustomLayout
|
||||
sidebar={
|
||||
<FilterPanel
|
||||
filters={filters}
|
||||
setFilters={setFilters}
|
||||
availableTypes={availableTypes}
|
||||
availableTags={availableTags}
|
||||
/>
|
||||
}
|
||||
content={
|
||||
<div className="results-area">
|
||||
{filteredItems.map((item) => (
|
||||
<MarketplaceItemCard
|
||||
key={item.name}
|
||||
item={item}
|
||||
filters={filters}
|
||||
setFilters={setFilters}
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
footer={<div className="status-bar">{`Showing ${filteredItems.length} of ${items.length} packages`}</div>}
|
||||
/>
|
||||
```
|
||||
|
||||
## Extending Backend Functionality
|
||||
|
||||
The Marketplace backend can be extended with new functionality:
|
||||
|
||||
### Custom Source Providers
|
||||
|
||||
To add support for new source types:
|
||||
|
||||
1. **Create a Source Provider Interface**:
|
||||
|
||||
```typescript
|
||||
interface SourceProvider {
|
||||
type: string
|
||||
canHandle(url: string): boolean
|
||||
fetchItems(url: string): Promise<MarketplaceItem[]>
|
||||
}
|
||||
```
|
||||
|
||||
2. **Implement a Custom Provider**:
|
||||
|
||||
```typescript
|
||||
class CustomSourceProvider implements SourceProvider {
|
||||
type = "custom"
|
||||
|
||||
canHandle(url: string): boolean {
|
||||
return url.startsWith("custom://")
|
||||
}
|
||||
|
||||
async fetchItems(url: string): Promise<MarketplaceItem[]> {
|
||||
// Your custom implementation
|
||||
// Fetch items from your custom source
|
||||
return items
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Register the Provider**:
|
||||
|
||||
```typescript
|
||||
// In your extension code
|
||||
const registerSourceProviders = (marketplace: MarketplaceManager) => {
|
||||
marketplace.registerSourceProvider(new CustomSourceProvider())
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Metadata Processors
|
||||
|
||||
To add support for custom metadata formats:
|
||||
|
||||
1. **Create a Metadata Processor Interface**:
|
||||
|
||||
```typescript
|
||||
interface MetadataProcessor {
|
||||
canProcess(filePath: string): boolean
|
||||
process(filePath: string, content: string): Promise<any>
|
||||
}
|
||||
```
|
||||
|
||||
2. **Implement a Custom Processor**:
|
||||
|
||||
```typescript
|
||||
class CustomMetadataProcessor implements MetadataProcessor {
|
||||
canProcess(filePath: string): boolean {
|
||||
return filePath.endsWith(".custom")
|
||||
}
|
||||
|
||||
async process(filePath: string, content: string): Promise<any> {
|
||||
// Your custom processing logic
|
||||
return processedMetadata
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Register the Processor**:
|
||||
|
||||
```typescript
|
||||
// In your extension code
|
||||
const registerMetadataProcessors = (metadataScanner: MetadataScanner) => {
|
||||
metadataScanner.registerProcessor(new CustomMetadataProcessor())
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Message Handlers
|
||||
|
||||
To add support for custom messages:
|
||||
|
||||
1. **Extend the Message Handler**:
|
||||
|
||||
```typescript
|
||||
// In your extension code
|
||||
const extendMessageHandler = () => {
|
||||
const originalHandler = handleMarketplaceMessages
|
||||
|
||||
return async (message: any, marketplace: MarketplaceManager) => {
|
||||
// Handle custom messages
|
||||
if (message.type === "yourCustomMessage") {
|
||||
// Your custom message handling
|
||||
return {
|
||||
type: "yourCustomResponse",
|
||||
data: {
|
||||
/* response data */
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to the original handler
|
||||
return originalHandler(message, marketplace)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Register the Extended Handler**:
|
||||
|
||||
```typescript
|
||||
// In your extension code
|
||||
const customMessageHandler = extendMessageHandler()
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("marketplace.handleMessage", (message) => {
|
||||
return customMessageHandler(message, marketplace)
|
||||
}),
|
||||
)
|
||||
```
|
||||
|
||||
## Integration with Other Systems
|
||||
|
||||
The Marketplace can be integrated with other systems:
|
||||
|
||||
### Integration with External APIs
|
||||
|
||||
To integrate with external APIs:
|
||||
|
||||
1. **Create an API Client**:
|
||||
|
||||
```typescript
|
||||
class ExternalApiClient {
|
||||
private baseUrl: string
|
||||
|
||||
constructor(baseUrl: string) {
|
||||
this.baseUrl = baseUrl
|
||||
}
|
||||
|
||||
async fetchPackages(): Promise<MarketplaceItem[]> {
|
||||
const response = await fetch(`${this.baseUrl}/packages`)
|
||||
const data = await response.json()
|
||||
|
||||
// Transform API data to MarketplaceItem format
|
||||
return data.map((item) => ({
|
||||
name: item.name,
|
||||
description: item.description,
|
||||
type: item.type,
|
||||
url: item.url,
|
||||
repoUrl: item.repository_url,
|
||||
// Map other fields
|
||||
}))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Create a Source Provider for the API**:
|
||||
|
||||
```typescript
|
||||
class ApiSourceProvider implements SourceProvider {
|
||||
private apiClient: ExternalApiClient
|
||||
|
||||
constructor(apiUrl: string) {
|
||||
this.apiClient = new ExternalApiClient(apiUrl)
|
||||
}
|
||||
|
||||
type = "api"
|
||||
|
||||
canHandle(url: string): boolean {
|
||||
return url.startsWith("api://")
|
||||
}
|
||||
|
||||
async fetchItems(url: string): Promise<MarketplaceItem[]> {
|
||||
return this.apiClient.fetchPackages()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Register the API Provider**:
|
||||
|
||||
```typescript
|
||||
// In your extension code
|
||||
const registerApiProvider = (marketplace: MarketplaceManager) => {
|
||||
marketplace.registerSourceProvider(new ApiSourceProvider("https://your-api.example.com"))
|
||||
}
|
||||
```
|
||||
|
||||
### Integration with Authentication Systems
|
||||
|
||||
To integrate with authentication systems:
|
||||
|
||||
1. **Create an Authentication Provider**:
|
||||
|
||||
```typescript
|
||||
class AuthProvider {
|
||||
private token: string | null = null
|
||||
|
||||
async login(): Promise<boolean> {
|
||||
// Your authentication logic
|
||||
this.token = "your-auth-token"
|
||||
return true
|
||||
}
|
||||
|
||||
async getToken(): Promise<string | null> {
|
||||
if (!this.token) {
|
||||
await this.login()
|
||||
}
|
||||
return this.token
|
||||
}
|
||||
|
||||
isAuthenticated(): boolean {
|
||||
return !!this.token
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Use Authentication in API Requests**:
|
||||
|
||||
```typescript
|
||||
class AuthenticatedApiClient extends ExternalApiClient {
|
||||
private authProvider: AuthProvider
|
||||
|
||||
constructor(baseUrl: string, authProvider: AuthProvider) {
|
||||
super(baseUrl)
|
||||
this.authProvider = authProvider
|
||||
}
|
||||
|
||||
async fetchPackages(): Promise<MarketplaceItem[]> {
|
||||
const token = await this.authProvider.getToken()
|
||||
|
||||
if (!token) {
|
||||
throw new Error("Authentication required")
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/packages`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
|
||||
// Process response as before
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Integration with Local Development Tools
|
||||
|
||||
To integrate with local development tools:
|
||||
|
||||
1. **Create a Local Development Provider**:
|
||||
|
||||
```typescript
|
||||
class LocalDevProvider {
|
||||
private workspacePath: string
|
||||
|
||||
constructor(workspacePath: string) {
|
||||
this.workspacePath = workspacePath
|
||||
}
|
||||
|
||||
async createLocalPackage(template: string, name: string): Promise<string> {
|
||||
const targetPath = path.join(this.workspacePath, name)
|
||||
|
||||
// Create directory
|
||||
await fs.promises.mkdir(targetPath, { recursive: true })
|
||||
|
||||
// Copy template files
|
||||
// Your implementation
|
||||
|
||||
return targetPath
|
||||
}
|
||||
|
||||
async buildLocalPackage(packagePath: string): Promise<boolean> {
|
||||
// Your build implementation
|
||||
return true
|
||||
}
|
||||
|
||||
async testLocalPackage(packagePath: string): Promise<boolean> {
|
||||
// Your test implementation
|
||||
return true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Integrate with the Marketplace**:
|
||||
|
||||
```typescript
|
||||
// In your extension code
|
||||
const registerLocalDevTools = (context: vscode.ExtensionContext) => {
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders
|
||||
|
||||
if (!workspaceFolders) {
|
||||
return
|
||||
}
|
||||
|
||||
const workspacePath = workspaceFolders[0].uri.fsPath
|
||||
const localDevProvider = new LocalDevProvider(workspacePath)
|
||||
|
||||
// Register commands
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("marketplace.createLocal", async (template, name) => {
|
||||
return localDevProvider.createLocalPackage(template, name)
|
||||
}),
|
||||
|
||||
vscode.commands.registerCommand("marketplace.buildLocal", async (packagePath) => {
|
||||
return localDevProvider.buildLocalPackage(packagePath)
|
||||
}),
|
||||
|
||||
vscode.commands.registerCommand("marketplace.testLocal", async (packagePath) => {
|
||||
return localDevProvider.testLocalPackage(packagePath)
|
||||
}),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices for Extensions
|
||||
|
||||
When extending the Marketplace, follow these best practices:
|
||||
|
||||
### Maintainable Code
|
||||
|
||||
1. **Follow the Existing Patterns**:
|
||||
|
||||
- Use similar naming conventions
|
||||
- Follow the same code structure
|
||||
- Maintain consistent error handling
|
||||
|
||||
2. **Document Your Extensions**:
|
||||
|
||||
- Add JSDoc comments to functions and classes
|
||||
- Explain the purpose of your extensions
|
||||
- Document any configuration options
|
||||
|
||||
3. **Write Tests**:
|
||||
- Add unit tests for new functionality
|
||||
- Update integration tests as needed
|
||||
- Ensure test coverage remains high
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
1. **Lazy Loading**:
|
||||
|
||||
- Load data only when needed
|
||||
- Defer expensive operations
|
||||
- Use pagination for large datasets
|
||||
|
||||
2. **Efficient Data Processing**:
|
||||
|
||||
- Minimize data transformations
|
||||
- Use memoization for expensive calculations
|
||||
- Batch operations when possible
|
||||
|
||||
3. **UI Responsiveness**:
|
||||
- Keep the UI responsive during operations
|
||||
- Show loading indicators for async operations
|
||||
- Use debouncing for frequent events
|
||||
|
||||
### Compatibility
|
||||
|
||||
1. **VSCode API Compatibility**:
|
||||
|
||||
- Use stable VSCode API features
|
||||
- Handle API version differences
|
||||
- Test with multiple VSCode versions
|
||||
|
||||
2. **Cross-Platform Support**:
|
||||
|
||||
- Test on Windows, macOS, and Linux
|
||||
- Use path.join for file paths
|
||||
- Handle file system differences
|
||||
|
||||
3. **Theme Compatibility**:
|
||||
- Use VSCode theme variables
|
||||
- Test with light and dark themes
|
||||
- Support high contrast mode
|
||||
|
||||
---
|
||||
|
||||
**Previous**: [Testing Strategy](./06-testing-strategy.md)
|
||||
58
cline_docs/marketplace/user-guide/01-introduction.md
Normal file
58
cline_docs/marketplace/user-guide/01-introduction.md
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
# Introduction to Marketplace
|
||||
|
||||
## Overview and Purpose
|
||||
|
||||
The Marketplace is a powerful feature in Roo Code that allows you to discover, browse, and utilize various items to enhance your development experience. It serves as a centralized hub for accessing:
|
||||
|
||||
- **Modes**: Specialized AI assistants with different capabilities
|
||||
- **MCP Servers**: Model Context Protocol servers that provide additional functionality
|
||||
- **Prompts**: Pre-configured instructions for specific tasks
|
||||
- **Packages**: Collections of related components
|
||||
|
||||
The Marketplace simplifies the process of extending Roo Code's capabilities by providing a user-friendly interface to find, filter, and add new components to your environment.
|
||||
|
||||
## Key Features and Capabilities
|
||||
|
||||
### Component Discovery
|
||||
|
||||
- Browse a curated collection of components
|
||||
- View detailed information about each component
|
||||
- Explore subcomponents within packages
|
||||
|
||||
### Search and Filter
|
||||
|
||||
- Search by name and description
|
||||
- Filter by component type (mode, MCP server, etc.)
|
||||
- Use tags to find related components
|
||||
- Combine search and filters for precise results
|
||||
|
||||
### Component Details
|
||||
|
||||
- View comprehensive information about each component
|
||||
- See version information
|
||||
- Access source repositories directly
|
||||
- Explore subcomponents organized by type
|
||||
|
||||
### Item Management
|
||||
|
||||
- Add new components to your environment
|
||||
- Manage custom item sources
|
||||
- Create and contribute your own packages
|
||||
|
||||
## How to Access the Marketplace
|
||||
|
||||
The Marketplace can be accessed through the Roo Code extension in VS Code:
|
||||
|
||||
1. Open VS Code with the Roo Code extension installed
|
||||
2. Click on the Roo Code icon in the activity bar
|
||||
3. Select "Marketplace" from the available options
|
||||
|
||||
Alternatively, you can use the Command Palette:
|
||||
|
||||
1. Press `Ctrl+Shift+P` (Windows/Linux) or `Cmd+Shift+P` (Mac) to open the Command Palette
|
||||
2. Type "Roo Code: Open Marketplace"
|
||||
3. Press Enter to open the Marketplace
|
||||
|
||||
---
|
||||
|
||||
**Next**: [Browsing items](./02-browsing-items.md)
|
||||
148
cline_docs/marketplace/user-guide/02-browsing-items.md
Normal file
148
cline_docs/marketplace/user-guide/02-browsing-items.md
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
# Browsing
|
||||
|
||||
## Understanding the Marketplace Interface
|
||||
|
||||
The Marketplace interface is designed to provide a clean, intuitive experience for discovering and exploring available components. The main interface consists of several key areas:
|
||||
|
||||
### Main Sections
|
||||
|
||||
1. **Navigation Tabs**
|
||||
|
||||
- **Browse**: View all available marketplace items
|
||||
- **Sources**: Manage Marketplace sources
|
||||
|
||||
2. **Filter Panel**
|
||||
|
||||
- Type filters (Modes, MCP Servers, Packages, etc.)
|
||||
- Search box
|
||||
- Tag filters
|
||||
|
||||
3. **Results Area**
|
||||
- Marketplace items displaying component information
|
||||
- Sorting options
|
||||
|
||||
### Interface Layout
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ [Browse] [Sources] │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ FILTERS │
|
||||
│ Types: [] Mode [] MCP Server [] Package [] Prompt │
|
||||
│ Search: [ ] │
|
||||
│ Tags: [Tag cloud] │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ MARKETPLACE Items │
|
||||
│ ┌─────────────────────────────────────────────────┐ │
|
||||
│ │ Name [Type] │ │
|
||||
│ │ by Author │ │
|
||||
│ │ │ │
|
||||
│ │ Description text... │ │
|
||||
│ │ │ │
|
||||
│ │ [Tags] [Tags] [Tags] │ │
|
||||
│ │ │ │
|
||||
│ │ v1.0.0 Apr 12, 2025 [View] │ │
|
||||
│ └─────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────┐ │
|
||||
│ │ Another Item [Type] │ │
|
||||
│ │ ... │ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Marketplace Item and Information Displayed
|
||||
|
||||
Each item in the Marketplace is represented by a card that contains essential information about the component:
|
||||
|
||||
### Card Elements
|
||||
|
||||
1. **Header Section**
|
||||
|
||||
- **Name**: The name of the component
|
||||
- **Author**: The creator or maintainer of the component (if available)
|
||||
- **Type Badge**: Visual indicator of the component type (Mode, MCP Server, etc.)
|
||||
|
||||
2. **Description**
|
||||
|
||||
- A brief overview of the component's purpose and functionality
|
||||
|
||||
3. **Tags**
|
||||
|
||||
- Clickable tags that categorize the component
|
||||
- Can be used for filtering similar components
|
||||
|
||||
4. **Metadata**
|
||||
|
||||
- **Version**: The current version of the component (if available)
|
||||
- **Last Updated**: When the component was last modified (if available)
|
||||
|
||||
5. **Actions**
|
||||
|
||||
- **View**: Button to access the component's source repository or documentation
|
||||
|
||||
6. **Details Section** (expandable)
|
||||
- Shows subcomponents grouped by type
|
||||
- Displays additional information when expanded
|
||||
|
||||
### Example Item
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Data Platform Package [Package] │
|
||||
│ by Roo Team │
|
||||
│ │
|
||||
│ A comprehensive data processing and analysis │
|
||||
│ package with tools for ETL, visualization, and ML. │
|
||||
│ │
|
||||
│ [data] [analytics] [machine-learning] │
|
||||
│ │
|
||||
│ v2.1.0 Apr 10, 2025 [View] │
|
||||
│ │
|
||||
│ ▼ Component Details │
|
||||
│ MCP Servers: │
|
||||
│ 1. Data Validator - Validates data formats │
|
||||
│ 2. ML Predictor - Makes predictions on data │
|
||||
│ │
|
||||
│ Modes: │
|
||||
│ 1. Data Analyst - Helps with data analysis │
|
||||
│ 2. ETL Engineer - Assists with data pipelines │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Navigating Between Items
|
||||
|
||||
The Marketplace provides several ways to navigate through the available items:
|
||||
|
||||
### Navigation Methods
|
||||
|
||||
1. **Scrolling**
|
||||
|
||||
- Scroll through the list of item cards to browse all available components
|
||||
|
||||
2. **Filtering**
|
||||
|
||||
- Use the filter panel to narrow down the displayed items
|
||||
- Click on type filters to show only specific component types
|
||||
- Enter search terms to find items by name or description
|
||||
- Click on tags to filter by specific categories
|
||||
|
||||
3. **Sorting**
|
||||
|
||||
- Sort pacitemskages by name or last updated date
|
||||
- Toggle between ascending and descending order
|
||||
|
||||
4. **Tab Navigation**
|
||||
- Switch between "Browse" and "Sources" tabs to manage Marketplace sources
|
||||
|
||||
### Keyboard Navigation
|
||||
|
||||
For accessibility and efficiency, the Marketplace supports keyboard navigation:
|
||||
|
||||
- **Tab**: Move focus between interactive elements
|
||||
- **Space/Enter**: Activate buttons or toggle filters
|
||||
- **Arrow Keys**: Navigate between items
|
||||
- **Escape**: Close expanded details or clear filters
|
||||
|
||||
---
|
||||
|
||||
**Previous**: [Introduction to Marketplace](./01-introduction.md) | **Next**: [Searching and Filtering](./03-searching-and-filtering.md)
|
||||
140
cline_docs/marketplace/user-guide/03-searching-and-filtering.md
Normal file
140
cline_docs/marketplace/user-guide/03-searching-and-filtering.md
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
# Searching and Filtering
|
||||
|
||||
The Marketplace provides powerful search and filtering capabilities to help you quickly find the components you need. This guide explains how to effectively use these features to narrow down your search results.
|
||||
|
||||
## Using the Search Functionality
|
||||
|
||||
The search box allows you to find components by matching text in various fields:
|
||||
|
||||
### What Gets Searched
|
||||
|
||||
When you enter a search term, the Marketplace looks for matches in:
|
||||
|
||||
1. **Item Name**: The primary identifier of the item
|
||||
2. **Description**: The detailed explanation of the item's purpose
|
||||
3. **Subcomponent Names and Descriptions**: Text within nested items
|
||||
|
||||
### Search Features
|
||||
|
||||
- **Case Insensitive**: Searches ignore letter case for easier matching
|
||||
- **Whitespace Insensitive**: Extra spaces are normalized in the search
|
||||
- **Partial Matching**: Finds results that contain your search term anywhere in the text
|
||||
- **Instant Results**: Results update as you type
|
||||
- **Match Highlighting**: Matching subcomponents are highlighted and expanded automatically
|
||||
|
||||
### Search Implementation
|
||||
|
||||
The search uses a simple string contains match that is case and whitespace insensitive. This means:
|
||||
|
||||
- "Data" will match "data", "DATA", "Data", etc.
|
||||
- "machine learning" will match "Machine Learning", "machine-learning", etc.
|
||||
- Partial words will match: "valid" will match "validation", "validator", etc.
|
||||
|
||||
### Search Tips
|
||||
|
||||
- Use specific, distinctive terms to narrow results
|
||||
- Try different variations if you don't find what you're looking for
|
||||
- Search for technology names or specific functionality
|
||||
- Look for highlighted "match" indicators in expanded details sections
|
||||
|
||||
### Example Searches
|
||||
|
||||
| Search Term | Will Find |
|
||||
| ------------------ | --------------------------------------------------------------------------- |
|
||||
| "data" | Items with "data" in their name, description, or subcomponents |
|
||||
| "validator" | Items that include validation functionality or have validator subcomponents |
|
||||
| "machine learning" | Items related to machine learning technology |
|
||||
|
||||
## Filtering by Item Type
|
||||
|
||||
The type filter allows you to focus on specific categories of items:
|
||||
|
||||
### Available Type Filters
|
||||
|
||||
- **Mode**: AI assistant personalities with specialized capabilities
|
||||
- **MCP Server**: Model Context Protocol servers that provide additional functionality
|
||||
- **Package**: Collections of related items
|
||||
- **Prompt**: Pre-configured instructions for specific tasks
|
||||
|
||||
### Using Type Filters
|
||||
|
||||
1. Click on a type checkbox to show only items of that type
|
||||
2. Select multiple types to show items that match any of the selected types
|
||||
3. Clear all type filters to show all items again
|
||||
|
||||
When filtering by type, packages are handled specially:
|
||||
|
||||
- A package will be included if it matches the selected type
|
||||
- A package will also be included if it contains any subcomponents matching the selected type
|
||||
- When viewing a package that was included due to its subcomponents, the matching subcomponents will be highlighted
|
||||
|
||||
### Type Filter Behavior
|
||||
|
||||
- Type filters apply to both the primary item type and it's subcomponents
|
||||
- Packages are included if they contain subcomponents matching the selected type
|
||||
- The type is displayed as a badge on each item card
|
||||
- Type filtering can be combined with search terms and tag filters
|
||||
|
||||
## Using Tags for Filtering
|
||||
|
||||
Tags provide a way to filter items by category, technology, or purpose:
|
||||
|
||||
### Tag Functionality
|
||||
|
||||
- Tags appear as clickable buttons on item cards
|
||||
- Clicking a tag activates it as a filter
|
||||
- Active tag filters are highlighted
|
||||
- Items must have at least one of the selected tags to be displayed
|
||||
|
||||
### Finding and Using Tags
|
||||
|
||||
1. Browse through item cards to discover available tags
|
||||
2. Click on a tag to filter for items with that tag
|
||||
3. Click on additional tags to expand your filter (items with any of the selected tags will be shown)
|
||||
4. Click on an active tag to deactivate it
|
||||
|
||||
### Common Tags
|
||||
|
||||
- Technology areas: "data", "web", "security", "ai"
|
||||
- Programming languages: "python", "javascript", "typescript"
|
||||
- Functionality: "testing", "documentation", "analysis"
|
||||
- Domains: "finance", "healthcare", "education"
|
||||
|
||||
## Combining Search and Filters
|
||||
|
||||
For the most precise results, you can combine search terms, type filters, and tag filters:
|
||||
|
||||
### How Combined Filtering Works
|
||||
|
||||
1. **AND Logic Between Filter Types**: Items must match the search term AND the selected types AND have at least one of the selected tags
|
||||
2. **OR Logic Within Tag Filters**: Items must have at least one of the selected tags
|
||||
|
||||
### Combined Filter Examples
|
||||
|
||||
| Search Term | Type Filter | Tag Filter | Will Find |
|
||||
| --------------- | ----------- | ----------------------- | ---------------------------------------------------- |
|
||||
| "data" | MCP Server | "analytics" | MCP Servers related to data analytics |
|
||||
| "test" | Mode | "automation", "quality" | Test automation or quality-focused modes |
|
||||
| "visualization" | Package | "dashboard", "chart" | Packages for creating dashboards or charts |
|
||||
| "" | Mode | "" | All modes and packages containing mode subcomponents |
|
||||
|
||||
### Clearing Filters
|
||||
|
||||
To reset your search and start over:
|
||||
|
||||
1. Clear the search box
|
||||
2. Uncheck all type filters
|
||||
3. Deactivate all tag filters by clicking on them
|
||||
|
||||
### Filter Status Indicators
|
||||
|
||||
The Marketplace provides visual feedback about your current filters:
|
||||
|
||||
- Active type filters are checked
|
||||
- Active tag filters are highlighted
|
||||
- The search box shows your current search term
|
||||
- Result counts may be displayed to show how many items match your filters
|
||||
|
||||
---
|
||||
|
||||
**Previous**: [Browsing Items](./02-browsing-items.md) | **Next**: [Working with Package Details](./04-working-with-details.md)
|
||||
143
cline_docs/marketplace/user-guide/04-working-with-details.md
Normal file
143
cline_docs/marketplace/user-guide/04-working-with-details.md
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
# Working with Package Details
|
||||
|
||||
Marketplace items often contain multiple items organized in a hierarchical structure; these items are referred to as "Packages" and must have a type of `package`. The items organized within a package are referred to as "subitems" and have all the same metadata properties of regular items. This guide explains how to work with the details section of package cards to explore and understand the elements within each package.
|
||||
|
||||
## Expanding Package Details
|
||||
|
||||
Most packages in the Marketplace contain subcomponents that are hidden by default to keep the interface clean. You can expand these details to see what's inside each package:
|
||||
|
||||
### How to Expand Details
|
||||
|
||||
1. Look for the "Component Details" section at the bottom of a package card
|
||||
2. Click on the section header or the chevron icon (▶) to expand it
|
||||
3. The section will animate open, revealing the components inside the package
|
||||
4. Click again to collapse the section when you're done
|
||||
|
||||
### Automatic Expansion
|
||||
|
||||
The details section will expand automatically when:
|
||||
|
||||
- Your search term matches text in a subcomponent
|
||||
- This is the only condition for automatic expansion
|
||||
|
||||
### Details Section Badge
|
||||
|
||||
The details section may display a badge with additional information:
|
||||
|
||||
- **Match count**: When your search term matches subcomponents, a badge shows how many matches were found (e.g., "3 matches")
|
||||
- This helps you quickly identify which packages contain relevant subcomponents
|
||||
|
||||
## Understanding Component Types
|
||||
|
||||
Components within packages are grouped by their type to make them easier to find and understand:
|
||||
|
||||
### Common Component Types
|
||||
|
||||
1. **Modes**
|
||||
|
||||
- AI assistant personalities with specialized capabilities
|
||||
- Examples: Code Mode, Architect Mode, Debug Mode
|
||||
|
||||
2. **MCP Servers**
|
||||
|
||||
- Model Context Protocol servers that provide additional functionality
|
||||
- Examples: File Analyzer, Data Validator, Image Generator
|
||||
|
||||
3. **Prompts**
|
||||
|
||||
- Pre-configured instructions for specific tasks
|
||||
- Examples: Code Review, Documentation Generator, Test Case Creator
|
||||
|
||||
4. **Packages**
|
||||
- Nested collections of related components
|
||||
- Can contain any of the other component types
|
||||
|
||||
### Type Presentation
|
||||
|
||||
Each type section in the details view includes:
|
||||
|
||||
- A header with the type name (pluralized, e.g., "MCP Servers")
|
||||
- A numbered list of components of that type
|
||||
- Each component's name and description
|
||||
|
||||
## Viewing Subcomponents
|
||||
|
||||
The details section organizes subcomponents in a clear, structured format:
|
||||
|
||||
### Subcomponent List Format
|
||||
|
||||
```
|
||||
Component Details
|
||||
Type Name:
|
||||
1. Component Name - Description text goes here
|
||||
2. Another Component - Its description
|
||||
|
||||
Another Type:
|
||||
1. First Component - Description
|
||||
2. Second Component - Description
|
||||
```
|
||||
|
||||
### Subcomponent Information
|
||||
|
||||
Each subcomponent in the list displays:
|
||||
|
||||
1. **Number**: Sequential number within its type group
|
||||
2. **Name**: The name of the subcomponent
|
||||
3. **Description**: A brief explanation of the subcomponent's purpose (if available)
|
||||
4. **Match Indicator**: A "match" badge appears next to items that match your search term
|
||||
|
||||
### Navigating Subcomponents
|
||||
|
||||
- Scroll within the details section to see all subcomponents
|
||||
- Components are grouped by type, making it easier to find specific functionality
|
||||
- Long descriptions may be truncated with an ellipsis (...) to save space (limited to 100 characters)
|
||||
|
||||
## Matching Search Terms in Subcomponents
|
||||
|
||||
One of the most powerful features of the Marketplace is the ability to search within subcomponents:
|
||||
|
||||
### How Subcomponent Matching Works
|
||||
|
||||
1. Enter a search term in the search box
|
||||
2. The Marketplace searches through all subcomponent names and descriptions
|
||||
3. Packages with matching subcomponents remain visible in the results
|
||||
4. The details section automatically expands for packages with matches
|
||||
5. Matching subcomponents are highlighted and marked with a "match" badge
|
||||
|
||||
### Visual Indicators for Matches
|
||||
|
||||
When a subcomponent matches your search:
|
||||
|
||||
- The component name is highlighted in a different color
|
||||
- A "match" badge appears next to the component
|
||||
- The details section automatically expands
|
||||
- A badge on the details section header shows the number of matches
|
||||
|
||||
### Search Implementation
|
||||
|
||||
The search uses a simple string contains match that is case-insensitive:
|
||||
|
||||
- "validator" will match "Data Validator", "Validator Tool", etc.
|
||||
- "valid" will match "validation" or "validator"
|
||||
- validator will not match "validation"
|
||||
- The search will match any part of the name or description that contains the exact search term
|
||||
|
||||
### Example Scenario
|
||||
|
||||
If you search for "validator":
|
||||
|
||||
1. Packages containing components with "validator" in their name or description remain visible
|
||||
2. The details section expands automatically for packages with matching subcomponents
|
||||
3. Components like "Data Validator" or those with "validator" in their description are highlighted
|
||||
4. A badge might show "2 matches" if two subcomponents match your search term
|
||||
|
||||
### Benefits of Subcomponent Matching
|
||||
|
||||
- Find functionality buried deep within packages
|
||||
- Discover relationships between components
|
||||
- Identify packages that contain specific tools or capabilities
|
||||
- Locate similar components across different packages
|
||||
|
||||
---
|
||||
|
||||
**Previous**: [Searching and Filtering](./03-searching-and-filtering.md) | **Next**: [Adding Packages](./05-adding-packages.md)
|
||||
226
cline_docs/marketplace/user-guide/05-adding-packages.md
Normal file
226
cline_docs/marketplace/user-guide/05-adding-packages.md
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
## Item Structure, Metadata, and Features
|
||||
|
||||
### Overview
|
||||
|
||||
- Every component on the registry is an `item`.
|
||||
- An `item` can be of type: `mcp`, `mode`, `prompt`, `package`
|
||||
- Each item apart from `package` is a singular object, i.e: one mode, one mcp server.
|
||||
- A `package` contains multiple other `item`s
|
||||
- All internal sub-items of a `package` is contained in the binary on the `package` item metadata itself.
|
||||
- Each `item` requires specific metadata files and follows a consistent directory structure.
|
||||
|
||||
### Directory Structure
|
||||
|
||||
The `registry` structure could be the root or placed in a `registry` directory of any `git` repository, a sample structure for a registry is:
|
||||
|
||||
```
|
||||
registry/
|
||||
├── metadata.en.yml # Required metadata for the registry
|
||||
│
|
||||
├── modes/ # `mode` items
|
||||
│ └── a-mode-name/
|
||||
│ └── metadata.en.yml
|
||||
├── mcps/ # `mcp` items
|
||||
├── prompts/ # `prompt` items
|
||||
│
|
||||
└── packages/ # `package` items
|
||||
└── a-package-name/
|
||||
├── metadata.en.yml # Required metadata
|
||||
├── metadata.fr.yml # Optional localized metadata (French)
|
||||
├── modes/ # `a-package-name`'s internal `mode` items
|
||||
│ └── my-mode/
|
||||
│ └── metadata.en.yml
|
||||
├── mcps/ # `a-package-name`'s internal `mcp` items
|
||||
│ └── my-server/
|
||||
│ └── metadata.en.yml
|
||||
└── prompts/ # `a-package-name`'s internal `prompt` items
|
||||
└── my-prompt/
|
||||
└── metadata.en.yml
|
||||
```
|
||||
|
||||
### Metadata File Format
|
||||
|
||||
Metadata files use YAML format and must include specific fields:
|
||||
|
||||
#### `registry`:
|
||||
|
||||
```yaml
|
||||
name: "My Registry"
|
||||
description: "A concise description for your registry"
|
||||
version: "0.0.0"
|
||||
author: "your name" # optional
|
||||
authorUrl: "http://your.profile.url/" # optional
|
||||
```
|
||||
|
||||
#### `item`:
|
||||
|
||||
```yaml
|
||||
name: "My Package"
|
||||
description: "A concise description for your package"
|
||||
version: "0.0.0"
|
||||
type: "package" # One of: package, mode, mcp, prompt
|
||||
sourceUrl: "https://url.to/source-repository" # Optional
|
||||
binaryUrl: "https://url.to/binary.zip"
|
||||
binaryHash: "SHA256-of-binary"
|
||||
binarySource: "https://proof.of/source" # Optional, proof-of-source for the binary (tag/hash reference, build job, etc)
|
||||
tags:
|
||||
- tag1
|
||||
- tag2
|
||||
author: "your name" # optional
|
||||
authorUrl: "http://your.profile.url/" # optional
|
||||
```
|
||||
|
||||
### Localization Support
|
||||
|
||||
You can provide metadata in multiple languages by using locale-specific files:
|
||||
|
||||
**Important Notes on Localization:**
|
||||
|
||||
- Only files with the pattern `metadata.{locale}.yml` are supported
|
||||
- The Marketplace will display metadata in the user's locale if available
|
||||
- If the user's locale is not available, it will fall back to English
|
||||
- The English locale (`metadata.en.yml`) is required as a fallback
|
||||
- Files without a locale code (e.g., just `metadata.yml`) are not supported
|
||||
|
||||
### Configurable Support
|
||||
|
||||
Powered with [**`Roo Rocket`**](https://github.com/NamesMT/roo-rocket), the registry supports configurable items like:
|
||||
|
||||
- `mcp` with access token inputs.
|
||||
- `mode` / `prompt` with feature flags.
|
||||
- And further customizations that a creator can imagine.
|
||||
- E.g: a `package` could prompt you for the location of its context folder.
|
||||
|
||||
## Contributing Process
|
||||
|
||||
To contribute your package to the official repository, follow these steps:
|
||||
|
||||
### 1. Fork the Repository
|
||||
|
||||
1. Visit the official Roo Code Packages repository: [https://github.com/RooVetGit/Roo-Code-Marketplace](https://github.com/RooVetGit/Roo-Code-Marketplace)
|
||||
2. Click the "Fork" button in the top-right corner
|
||||
3. This creates your own copy of the repository where you can make changes
|
||||
|
||||
### 2. Clone Your Fork
|
||||
|
||||
Clone your forked repository to your local machine:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/YOUR-USERNAME/Roo-Code-Marketplace.git
|
||||
cd Roo-Code-Marketplace
|
||||
```
|
||||
|
||||
### 3. Create Your Item
|
||||
|
||||
1. Create a new directory for your item with an appropriate name
|
||||
2. Add the required metadata files (and subitem directories for `package`)
|
||||
3. Follow the structure and format described above
|
||||
4. Add `sourceUrl` that points to a repository or post with info/document for the item.
|
||||
|
||||
Example of creating a simple package:
|
||||
|
||||
```bash
|
||||
mkdir -p my-package/modes/my-mode
|
||||
touch my-package/metadata.en.yml
|
||||
touch my-package/README.md
|
||||
touch my-package/modes/my-mode/metadata.en.yml
|
||||
```
|
||||
|
||||
### 4. Test Your Package
|
||||
|
||||
Before submitting, test your package by adding your fork as a custom source in the Marketplace:
|
||||
|
||||
1. In VS Code, open the Marketplace
|
||||
2. Go to the "Settings" tab
|
||||
3. Click "Add Source"
|
||||
4. Enter your fork's URL (e.g., `https://github.com/YOUR-USERNAME/Roo-Code-Marketplace`)
|
||||
5. Click "Add"
|
||||
6. Verify that your package appears and functions correctly
|
||||
|
||||
### 5. Commit and Push Your Changes
|
||||
|
||||
Once you're satisfied with your package:
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "Add my-package with mode component"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
### 6. Create a Pull Request
|
||||
|
||||
1. Go to the original repository: [https://github.com/RooVetGit/Roo-Code-Marketplace](https://github.com/RooVetGit/Roo-Code-Marketplace)
|
||||
2. Click "Pull Requests" and then "New Pull Request"
|
||||
3. Click "Compare across forks"
|
||||
4. Select your fork as the head repository
|
||||
5. Click "Create Pull Request"
|
||||
6. Provide a clear title and description of your package
|
||||
7. Submit the pull request
|
||||
|
||||
### 7. Review Process
|
||||
|
||||
After submitting your pull request:
|
||||
|
||||
1. Maintainers will review your package
|
||||
2. They may request changes or improvements
|
||||
3. Once approved, your package will be merged into the main repository
|
||||
4. Your package will be available to all users of the Marketplace
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Clear Documentation**: Include detailed documentation in your README.md
|
||||
- **Descriptive Metadata**: Write clear, informative descriptions
|
||||
- **Appropriate Tags**: Use relevant tags to make your package discoverable
|
||||
- **Testing**: Thoroughly test your package before submitting
|
||||
- **Localization**: Consider providing metadata in multiple languages
|
||||
- **Semantic Versioning**: Follow semantic versioning for version numbers
|
||||
- **Consistent Naming**: Use clear, descriptive names for components
|
||||
|
||||
## Example package metadatas
|
||||
|
||||
### Data Science Toolkit
|
||||
|
||||
Here's an example of a data science package:
|
||||
|
||||
**data-science-toolkit/metadata.en.yml**:
|
||||
|
||||
```yaml
|
||||
name: "Data Science Toolkit"
|
||||
description: "A comprehensive collection of tools for data science workflows"
|
||||
version: "1.0.0"
|
||||
type: "package"
|
||||
tags:
|
||||
- data
|
||||
- science
|
||||
- analysis
|
||||
- visualization
|
||||
- machine learning
|
||||
```
|
||||
|
||||
**data-science-toolkit/modes/data-scientist-mode/metadata.en.yml**:
|
||||
|
||||
```yaml
|
||||
name: "Data Scientist Mode"
|
||||
description: "A specialized mode for data science tasks"
|
||||
version: "1.0.0"
|
||||
type: "mode"
|
||||
tags:
|
||||
- data
|
||||
- science
|
||||
- analysis
|
||||
```
|
||||
|
||||
**data-science-toolkit/prompts/data-cleaning/metadata.en.yml**:
|
||||
|
||||
```yaml
|
||||
name: "Data Cleaning Prompt"
|
||||
description: "A prompt for cleaning and preprocessing datasets"
|
||||
version: "1.0.0"
|
||||
type: "prompt"
|
||||
tags:
|
||||
- data
|
||||
- cleaning
|
||||
- preprocessing
|
||||
```
|
||||
|
||||
**Previous**: [Working with Package Details](./04-working-with-details.md) | **Next**: [Adding Custom Sources](./06-adding-custom-sources.md)
|
||||
152
cline_docs/marketplace/user-guide/06-adding-custom-sources.md
Normal file
152
cline_docs/marketplace/user-guide/06-adding-custom-sources.md
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
# Adding Custom Marketplace Sources
|
||||
|
||||
The Marketplace allows you to extend its functionality by adding custom sources. This guide explains how to set up and manage your own Marktplace repositories to access additional components beyond the default offerings.
|
||||
|
||||
## Setting up a Marketplace Source Repository
|
||||
|
||||
A Marketplace source repository is a Git repository that contains Marketplace items organized in a specific structure. You can create your own repository to host custom packages:
|
||||
|
||||
### Repository Requirements
|
||||
|
||||
1. **Proper Structure**: The repository must follow the required directory structure
|
||||
2. **Valid Metadata**: Each package must include properly formatted metadata files
|
||||
3. **Git Repository**: The source must be a Git repository accessible via HTTPS
|
||||
|
||||
### Building your registry repository
|
||||
|
||||
#### Start from a sample registry repository
|
||||
|
||||
Check the branches of the [**rm-samples**](https://github.com/NamesMT/rm-samples) repository here.
|
||||
|
||||
#### Creating a New Repository
|
||||
|
||||
1. Create a new repository on GitHub, GitLab, or another Git hosting service
|
||||
2. Initialize the repository with a README.md file
|
||||
3. Clone the repository to your local machine:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/your-username/your-registry-repo.git
|
||||
cd your-registry-repo
|
||||
```
|
||||
|
||||
4. Create the basic registry structure:
|
||||
|
||||
```bash
|
||||
mkdir -p packages modes mcps prompts
|
||||
touch metadata.en.yml
|
||||
```
|
||||
|
||||
5. Add repository metadata to `metadata.en.yml`:
|
||||
|
||||
```yaml
|
||||
name: "Your Repository Name"
|
||||
description: "A collection of custom packages for Roo Code"
|
||||
version: "1.0.0"
|
||||
```
|
||||
|
||||
6. Commit and push the initial structure:
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "Initialize package repository structure"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
## Adding Sources to Roo Code
|
||||
|
||||
Once you have a properly structured source repository, you can add it to your Roo Code Marketplace as a source:
|
||||
|
||||
### Default Package Source
|
||||
|
||||
Roo Code comes with a default package source:
|
||||
|
||||
- URL: `https://github.com/RooVetGit/Roo-Code-Marketplace`
|
||||
- This source is enabled by default, and anytime all sources have been deleted.
|
||||
|
||||
### Adding a New Source
|
||||
|
||||
1. Open VS Code with the Roo Code extension
|
||||
2. Navigate to the Marketplace
|
||||
3. Switch to the "Sources" tab
|
||||
4. Click the "Add Source" button
|
||||
5. Enter the repository URL:
|
||||
- Format: `https://github.com/username/repository.git`
|
||||
- Example: `https://github.com/your-username/your-registry-repo.git`
|
||||
6. Click "Add" to save the source
|
||||
|
||||
### Managing Sources
|
||||
|
||||
The "Sources" tab provides several options for managing your registry sources:
|
||||
|
||||
1. **Remove**: Delete a source from your configuration
|
||||
2. **Refresh**: Update the item list from a source - this is forced git clone/pull to override local caching of data
|
||||
|
||||
### Source Caching and Refreshing
|
||||
|
||||
Marketplace sources are cached to improve performance:
|
||||
|
||||
- **Cache Duration**: Sources are cached for 1 hour (3600000 ms)
|
||||
- **Force Refresh**: To force an immediate refresh of a source:
|
||||
1. Go to the "Sources" tab
|
||||
2. Click the "Refresh" button next to the source you want to update
|
||||
3. This will bypass the cache and fetch the latest data from the repository
|
||||
|
||||
### Troubleshooting Sources
|
||||
|
||||
If a source isn't loading properly:
|
||||
|
||||
1. Check that the repository URL is correct
|
||||
2. Ensure the repository follows the required structure
|
||||
3. Look for error messages in the Marketplace interface
|
||||
4. Try refreshing the sources list
|
||||
5. Disable and re-enable the source
|
||||
|
||||
## Creating Private Sources
|
||||
|
||||
For team or organization use, you might want to create private sources:
|
||||
|
||||
### Private Repository Setup
|
||||
|
||||
1. Create a private repository on your Git hosting service
|
||||
2. Follow the same structure requirements as public repositories
|
||||
3. Set up appropriate access controls for your team members
|
||||
|
||||
### Authentication Options
|
||||
|
||||
To access private repositories, you may need to:
|
||||
|
||||
1. Configure Git credentials on your system
|
||||
2. Use a personal access token with appropriate permissions
|
||||
3. Set up SSH keys for authentication
|
||||
|
||||
### Organization Best Practices
|
||||
|
||||
For teams and organizations:
|
||||
|
||||
1. Designate maintainers responsible for the source
|
||||
2. Establish quality standards for contributed items and packages
|
||||
3. Create a review process for new additions
|
||||
4. Document usage guidelines for team members
|
||||
5. Consider implementing versioning for your items and packages
|
||||
|
||||
## Using Multiple Sources
|
||||
|
||||
The Marketplace supports multiple sources simultaneously:
|
||||
|
||||
### Benefits of Multiple Sources
|
||||
|
||||
- Access components from different providers
|
||||
- Separate internal and external components
|
||||
- Test new work before contributing them to the main repository
|
||||
- Create specialized sources for different projects or teams
|
||||
|
||||
### Source Management Strategy
|
||||
|
||||
1. Keep the default source enabled for core components
|
||||
2. Add specialized sources for specific needs
|
||||
3. Create a personal source for testing and development
|
||||
4. Refresh sources after you've pushed changes to them to get the latest items
|
||||
|
||||
---
|
||||
|
||||
**Previous**: [Adding Packages](./05-adding-packages.md) | **Next**: [Marketplace Architecture](../implementation/01-architecture.md)
|
||||
|
|
@ -132,6 +132,11 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt
|
|||
|
||||
visibleProvider.postMessageToWebview({ type: "action", action: "historyButtonClicked" })
|
||||
},
|
||||
marketplaceButtonClicked: () => {
|
||||
const visibleProvider = getVisibleProviderOrLog(outputChannel)
|
||||
if (!visibleProvider) return
|
||||
visibleProvider.postMessageToWebview({ type: "action", action: "marketplaceButtonClicked" })
|
||||
},
|
||||
showHumanRelayDialog: (params: { requestId: string; promptText: string }) => {
|
||||
const panel = getPanel()
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { arePathsEqual, getWorkspacePath } from "../../utils/path"
|
|||
import { logger } from "../../utils/logging"
|
||||
import { GlobalFileNames } from "../../shared/globalFileNames"
|
||||
import * as yaml from "yaml"
|
||||
import { ensureSettingsDirectoryExists } from "../../utils/globalContext"
|
||||
|
||||
const ROOMODES_FILENAME = ".roomodes"
|
||||
|
||||
|
|
@ -115,7 +116,7 @@ export class CustomModesManager {
|
|||
}
|
||||
|
||||
public async getCustomModesFilePath(): Promise<string> {
|
||||
const settingsDir = await this.ensureSettingsDirectoryExists()
|
||||
const settingsDir = await ensureSettingsDirectoryExists(this.context)
|
||||
const filePath = path.join(settingsDir, GlobalFileNames.customModes)
|
||||
const fileExists = await fileExistsAtPath(filePath)
|
||||
|
||||
|
|
@ -360,12 +361,6 @@ export class CustomModesManager {
|
|||
}
|
||||
}
|
||||
|
||||
private async ensureSettingsDirectoryExists(): Promise<string> {
|
||||
const settingsDir = path.join(this.context.globalStorageUri.fsPath, "settings")
|
||||
await fs.mkdir(settingsDir, { recursive: true })
|
||||
return settingsDir
|
||||
}
|
||||
|
||||
public async resetCustomModes(): Promise<void> {
|
||||
try {
|
||||
const filePath = await this.getCustomModesFilePath()
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import fs from "fs/promises"
|
|||
import EventEmitter from "events"
|
||||
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { DEFAULT_MARKETPLACE_SOURCE } from "../../services/marketplace/constants"
|
||||
import delay from "delay"
|
||||
import axios from "axios"
|
||||
import pWaitFor from "p-wait-for"
|
||||
|
|
@ -38,6 +39,7 @@ import { getTheme } from "../../integrations/theme/getTheme"
|
|||
import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
|
||||
import { McpHub } from "../../services/mcp/McpHub"
|
||||
import { McpServerManager } from "../../services/mcp/McpServerManager"
|
||||
import { MarketplaceManager } from "../../services/marketplace"
|
||||
import { ShadowCheckpointService } from "../../services/checkpoints/ShadowCheckpointService"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import { setTtsEnabled, setTtsSpeed } from "../../utils/tts"
|
||||
|
|
@ -78,6 +80,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
|
|||
return this._workspaceTracker
|
||||
}
|
||||
protected mcpHub?: McpHub // Change from private to protected
|
||||
private marketplaceManager: MarketplaceManager
|
||||
|
||||
public isViewLaunched = false
|
||||
public settingsImportedAt?: number
|
||||
|
|
@ -117,6 +120,8 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
|
|||
.catch((error) => {
|
||||
this.log(`Failed to initialize MCP Hub: ${error}`)
|
||||
})
|
||||
|
||||
this.marketplaceManager = new MarketplaceManager(this.context)
|
||||
}
|
||||
|
||||
// Adds a new Cline instance to clineStack, marking the start of a new task.
|
||||
|
|
@ -221,6 +226,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
|
|||
this._workspaceTracker = undefined
|
||||
await this.mcpHub?.unregisterClient()
|
||||
this.mcpHub = undefined
|
||||
this.marketplaceManager?.cleanup()
|
||||
this.customModesManager?.dispose()
|
||||
this.log("Disposed all disposables")
|
||||
ClineProvider.activeInstances.delete(this)
|
||||
|
|
@ -701,7 +707,8 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
|
|||
* @param webview A reference to the extension webview
|
||||
*/
|
||||
private setWebviewMessageListener(webview: vscode.Webview) {
|
||||
const onReceiveMessage = async (message: WebviewMessage) => webviewMessageHandler(this, message)
|
||||
const onReceiveMessage = async (message: WebviewMessage) =>
|
||||
webviewMessageHandler(this, message, this.marketplaceManager)
|
||||
|
||||
webview.onDidReceiveMessage(onReceiveMessage, null, this.disposables)
|
||||
}
|
||||
|
|
@ -1227,6 +1234,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
|
|||
showRooIgnoredFiles,
|
||||
language,
|
||||
maxReadFileLine,
|
||||
marketplaceSources,
|
||||
terminalCompressProgressBar,
|
||||
historyPreviewCollapsed,
|
||||
} = await this.getState()
|
||||
|
|
@ -1236,12 +1244,18 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
|
|||
const allowedCommands = vscode.workspace.getConfiguration(Package.name).get<string[]>("allowedCommands") || []
|
||||
const cwd = this.cwd
|
||||
|
||||
const marketplaceItems = this.marketplaceManager.getCurrentItems() || []
|
||||
const marketplaceInstalledMetadata = this.marketplaceManager.IMM.fullMetadata
|
||||
|
||||
// Check if there's a system prompt override for the current mode
|
||||
const currentMode = mode ?? defaultModeSlug
|
||||
const hasSystemPromptOverride = await this.hasFileBasedSystemPromptOverride(currentMode)
|
||||
|
||||
return {
|
||||
version: this.context.extension?.packageJSON?.version ?? "",
|
||||
marketplaceItems,
|
||||
marketplaceSources: marketplaceSources ?? [],
|
||||
marketplaceInstalledMetadata,
|
||||
apiConfiguration,
|
||||
customInstructions,
|
||||
alwaysAllowReadOnly: alwaysAllowReadOnly ?? false,
|
||||
|
|
@ -1408,6 +1422,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
|
|||
telemetrySetting: stateValues.telemetrySetting || "unset",
|
||||
showRooIgnoredFiles: stateValues.showRooIgnoredFiles ?? true,
|
||||
maxReadFileLine: stateValues.maxReadFileLine ?? 500,
|
||||
marketplaceSources: stateValues.marketplaceSources ?? [DEFAULT_MARKETPLACE_SOURCE],
|
||||
historyPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false,
|
||||
}
|
||||
}
|
||||
|
|
@ -1503,6 +1518,14 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
|
|||
return this.mcpHub
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the marketplace manager instance
|
||||
* @param marketplaceManager The marketplace manager instance
|
||||
*/
|
||||
public setMarketplaceManager(marketplaceManager: MarketplaceManager) {
|
||||
this.marketplaceManager = marketplaceManager
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns properties to be included in every telemetry event
|
||||
* This method is called by the telemetry service to get context information
|
||||
|
|
|
|||
321
src/core/webview/marketplaceMessageHandler.ts
Normal file
321
src/core/webview/marketplaceMessageHandler.ts
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
import * as vscode from "vscode"
|
||||
import { ClineProvider } from "./ClineProvider"
|
||||
import { installMarketplaceItemWithParametersPayloadSchema, WebviewMessage } from "../../shared/WebviewMessage"
|
||||
import {
|
||||
MarketplaceManager,
|
||||
MarketplaceItemType,
|
||||
MarketplaceSource,
|
||||
validateSources,
|
||||
ValidationError,
|
||||
} from "../../services/marketplace"
|
||||
import { DEFAULT_MARKETPLACE_SOURCE } from "../../services/marketplace/constants"
|
||||
import { GlobalState } from "../../schemas"
|
||||
|
||||
/**
|
||||
* Handle marketplace-related messages from the webview
|
||||
*/
|
||||
export async function handleMarketplaceMessages(
|
||||
provider: ClineProvider,
|
||||
message: WebviewMessage,
|
||||
marketplaceManager: MarketplaceManager,
|
||||
): Promise<boolean> {
|
||||
// Utility function for updating global state
|
||||
const updateGlobalState = async <K extends keyof GlobalState>(key: K, value: GlobalState[K]) =>
|
||||
await provider.contextProxy.setValue(key, value)
|
||||
|
||||
switch (message.type) {
|
||||
case "openExternal": {
|
||||
if (message.url) {
|
||||
try {
|
||||
vscode.env.openExternal(vscode.Uri.parse(message.url))
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Marketplace: Failed to open URL: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
vscode.window.showErrorMessage(
|
||||
`Failed to open URL: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
console.error("Marketplace: openExternal called without a URL")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
case "marketplaceSources": {
|
||||
if (message.sources) {
|
||||
// Enforce maximum of 10 sources
|
||||
const MAX_SOURCES = 10
|
||||
let updatedSources: MarketplaceSource[]
|
||||
|
||||
if (message.sources.length > MAX_SOURCES) {
|
||||
// Truncate to maximum allowed and show warning
|
||||
updatedSources = message.sources.slice(0, MAX_SOURCES)
|
||||
vscode.window.showWarningMessage(
|
||||
`Maximum of ${MAX_SOURCES} marketplace sources allowed. Additional sources have been removed.`,
|
||||
)
|
||||
} else {
|
||||
updatedSources = message.sources
|
||||
}
|
||||
|
||||
// Validate sources using the validation utility
|
||||
const validationErrors = validateSources(updatedSources)
|
||||
|
||||
// Filter out invalid sources
|
||||
if (validationErrors.length > 0) {
|
||||
// Create a map of invalid indices
|
||||
const invalidIndices = new Set<number>()
|
||||
validationErrors.forEach((error: ValidationError) => {
|
||||
// Extract index from error message (Source #X: ...)
|
||||
const match = error.message.match(/Source #(\d+):/)
|
||||
if (match && match[1]) {
|
||||
const index = parseInt(match[1], 10) - 1 // Convert to 0-based index
|
||||
if (index >= 0 && index < updatedSources.length) {
|
||||
invalidIndices.add(index)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Filter out invalid sources
|
||||
updatedSources = updatedSources.filter((_, index) => !invalidIndices.has(index))
|
||||
|
||||
// Show validation errors
|
||||
const errorMessage = `Marketplace sources validation failed:\n${validationErrors.map((e: ValidationError) => e.message).join("\n")}`
|
||||
console.error(errorMessage)
|
||||
vscode.window.showErrorMessage(errorMessage)
|
||||
}
|
||||
|
||||
// Update the global state with the validated sources
|
||||
await updateGlobalState("marketplaceSources", updatedSources)
|
||||
|
||||
// Clean up cache directories for repositories that are no longer in the sources list
|
||||
try {
|
||||
await marketplaceManager.cleanupCacheDirectories(updatedSources)
|
||||
} catch (error) {
|
||||
console.error("Marketplace: Error during cache cleanup:", error)
|
||||
}
|
||||
|
||||
// Update the webview with the new state
|
||||
await provider.postStateToWebview()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
case "fetchMarketplaceItems": {
|
||||
// Prevent multiple simultaneous fetches
|
||||
if (marketplaceManager.isFetching) {
|
||||
await provider.postMessageToWebview({
|
||||
type: "state",
|
||||
text: "Fetch already in progress",
|
||||
})
|
||||
marketplaceManager.isFetching = false
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if we need to force refresh using type assertion
|
||||
// const forceRefresh = (message as any).forceRefresh === true
|
||||
try {
|
||||
marketplaceManager.isFetching = true
|
||||
|
||||
try {
|
||||
let sources = (provider.contextProxy.getValue("marketplaceSources") as MarketplaceSource[]) || []
|
||||
|
||||
if (!sources || sources.length === 0) {
|
||||
sources = [DEFAULT_MARKETPLACE_SOURCE]
|
||||
|
||||
// Save the default sources
|
||||
await provider.contextProxy.setValue("marketplaceSources", sources)
|
||||
}
|
||||
|
||||
const enabledSources = sources.filter((s) => s.enabled)
|
||||
|
||||
if (enabledSources.length === 0) {
|
||||
vscode.window.showInformationMessage(
|
||||
"No enabled sources configured. Add and enable sources to view items.",
|
||||
)
|
||||
await provider.postStateToWebview()
|
||||
return true
|
||||
}
|
||||
|
||||
const result = await marketplaceManager.getMarketplaceItems(enabledSources)
|
||||
|
||||
// If there are errors but also items, show warning
|
||||
if (result.errors && result.items.length > 0) {
|
||||
vscode.window.showWarningMessage(
|
||||
`Some marketplace sources failed to load:\n${result.errors.join("\n")}`,
|
||||
)
|
||||
}
|
||||
// If there are errors and no items, show error
|
||||
else if (result.errors && result.items.length === 0) {
|
||||
const errorMessage = `Failed to load marketplace sources:\n${result.errors.join("\n")}`
|
||||
vscode.window.showErrorMessage(errorMessage)
|
||||
await provider.postMessageToWebview({
|
||||
type: "state",
|
||||
text: errorMessage,
|
||||
})
|
||||
marketplaceManager.isFetching = false
|
||||
}
|
||||
|
||||
// The items are already stored in MarketplaceManager's currentItems
|
||||
// No need to store in global state
|
||||
|
||||
// Send state to webview
|
||||
await provider.postStateToWebview()
|
||||
|
||||
return true
|
||||
} catch (initError) {
|
||||
const errorMessage = `Marketplace initialization failed: ${initError instanceof Error ? initError.message : String(initError)}`
|
||||
console.error("Error in marketplace initialization:", initError)
|
||||
vscode.window.showErrorMessage(errorMessage)
|
||||
await provider.postMessageToWebview({
|
||||
type: "state",
|
||||
text: errorMessage,
|
||||
})
|
||||
// The state will already be updated with empty items by MarketplaceManager
|
||||
await provider.postStateToWebview()
|
||||
marketplaceManager.isFetching = false
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = `Failed to fetch marketplace items: ${error instanceof Error ? error.message : String(error)}`
|
||||
console.error("Failed to fetch marketplace items:", error)
|
||||
vscode.window.showErrorMessage(errorMessage)
|
||||
await provider.postMessageToWebview({
|
||||
type: "state",
|
||||
text: errorMessage,
|
||||
})
|
||||
marketplaceManager.isFetching = false
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
case "filterMarketplaceItems": {
|
||||
if (message.filters) {
|
||||
try {
|
||||
// Update filtered items and post state
|
||||
marketplaceManager.updateWithFilteredItems({
|
||||
type: message.filters.type as MarketplaceItemType | undefined,
|
||||
search: message.filters.search,
|
||||
tags: message.filters.tags,
|
||||
})
|
||||
await provider.postStateToWebview()
|
||||
} catch (error) {
|
||||
console.error("Marketplace: Error filtering items:", error)
|
||||
vscode.window.showErrorMessage("Failed to filter marketplace items")
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
case "refreshMarketplaceSource": {
|
||||
if (message.url) {
|
||||
try {
|
||||
// Get the current sources
|
||||
const sources = (provider.contextProxy.getValue("marketplaceSources") as MarketplaceSource[]) || []
|
||||
|
||||
// Find the source with the matching URL
|
||||
const source = sources.find((s) => s.url === message.url)
|
||||
|
||||
if (source) {
|
||||
try {
|
||||
// Refresh the repository with the source name
|
||||
const refreshResult = await marketplaceManager.refreshRepository(message.url, source.name)
|
||||
if (refreshResult.error) {
|
||||
vscode.window.showErrorMessage(
|
||||
`Failed to refresh source: ${source.name || message.url} - ${refreshResult.error}`,
|
||||
)
|
||||
} else {
|
||||
vscode.window.showInformationMessage(
|
||||
`Successfully refreshed marketplace source: ${source.name || message.url}`,
|
||||
)
|
||||
}
|
||||
await provider.postStateToWebview()
|
||||
} finally {
|
||||
// Always notify the webview that the refresh is complete, even if it failed
|
||||
await provider.postMessageToWebview({
|
||||
type: "repositoryRefreshComplete",
|
||||
url: message.url,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
console.error(`Marketplace: Source URL not found: ${message.url}`)
|
||||
vscode.window.showErrorMessage(`Source URL not found: ${message.url}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Marketplace: Failed to refresh source: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
vscode.window.showErrorMessage(
|
||||
`Failed to refresh source: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
case "installMarketplaceItem": {
|
||||
if (message.mpItem) {
|
||||
try {
|
||||
await marketplaceManager
|
||||
.installMarketplaceItem(message.mpItem, message.mpInstallOptions)
|
||||
.then(async (r) => r === "$COMMIT" && (await provider.postStateToWebview()))
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(
|
||||
`Failed to install item "${message.mpItem.name}":\n${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
console.error("Marketplace: installMarketplaceItem called without `mpItem`")
|
||||
}
|
||||
return true
|
||||
}
|
||||
case "installMarketplaceItemWithParameters":
|
||||
if (message.payload) {
|
||||
const result = installMarketplaceItemWithParametersPayloadSchema.safeParse(message.payload)
|
||||
|
||||
if (result.success) {
|
||||
const { item, parameters } = result.data
|
||||
|
||||
try {
|
||||
await marketplaceManager
|
||||
.installMarketplaceItem(item, { parameters })
|
||||
.then(async (r) => r === "$COMMIT" && (await provider.postStateToWebview()))
|
||||
} catch (error) {
|
||||
console.error(`Error submitting marketplace parameters: ${error}`)
|
||||
vscode.window.showErrorMessage(
|
||||
`Failed to install item "${item.name}":\n${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
console.error("Invalid payload for installMarketplaceItemWithParameters message:", message.payload)
|
||||
vscode.window.showErrorMessage(
|
||||
'Invalid "payload" received for installation: item or parameters missing.',
|
||||
)
|
||||
}
|
||||
}
|
||||
return true
|
||||
case "cancelMarketplaceInstall": {
|
||||
vscode.window.showInformationMessage("Marketplace installation cancelled.")
|
||||
return true
|
||||
}
|
||||
case "removeInstalledMarketplaceItem": {
|
||||
if (message.mpItem) {
|
||||
try {
|
||||
await marketplaceManager
|
||||
.removeInstalledMarketplaceItem(message.mpItem, message.mpInstallOptions)
|
||||
.then(async (r) => r === "$COMMIT" && (await provider.postStateToWebview()))
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(
|
||||
`Failed to remove item "${message.mpItem.name}":\n${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
console.error("Marketplace: removeInstalledMarketplaceItem called without `mpItem`")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -37,7 +37,26 @@ import { getCommand } from "../../utils/commands"
|
|||
|
||||
const ALLOWED_VSCODE_SETTINGS = new Set(["terminal.integrated.inheritEnv"])
|
||||
|
||||
export const webviewMessageHandler = async (provider: ClineProvider, message: WebviewMessage) => {
|
||||
import { MarketplaceManager } from "../../services/marketplace"
|
||||
import { handleMarketplaceMessages } from "./marketplaceMessageHandler"
|
||||
|
||||
const marketplaceMessages = new Set([
|
||||
"openExternal",
|
||||
"marketplaceSources",
|
||||
"fetchMarketplaceItems",
|
||||
"filterMarketplaceItems",
|
||||
"refreshMarketplaceSource",
|
||||
"installMarketplaceItem",
|
||||
"installMarketplaceItemWithParameters",
|
||||
"cancelMarketplaceInstall",
|
||||
"removeInstalledMarketplaceItem",
|
||||
])
|
||||
|
||||
export const webviewMessageHandler = async (
|
||||
provider: ClineProvider,
|
||||
message: WebviewMessage,
|
||||
marketplaceManager?: MarketplaceManager,
|
||||
) => {
|
||||
// Utility functions provided for concise get/update of global state via contextProxy API.
|
||||
const getGlobalState = <K extends keyof GlobalState>(key: K) => provider.contextProxy.getValue(key)
|
||||
const updateGlobalState = async <K extends keyof GlobalState>(key: K, value: GlobalState[K]) =>
|
||||
|
|
@ -1260,4 +1279,14 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
|
|||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (marketplaceManager && marketplaceMessages.has(message.type)) {
|
||||
try {
|
||||
console.log(`DEBUG: Routing ${message.type} message to marketplaceMessageHandler`)
|
||||
const result = await handleMarketplaceMessages(provider, message, marketplaceManager)
|
||||
console.log(`DEBUG: Marketplace message handled successfully: ${message.type}, result: ${result}`)
|
||||
} catch (error) {
|
||||
console.error(`DEBUG: Error handling marketplace message: ${error}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
21
src/exports/roo-code.d.ts
vendored
21
src/exports/roo-code.d.ts
vendored
|
|
@ -173,6 +173,13 @@ type GlobalSettings = {
|
|||
}
|
||||
| undefined
|
||||
enhancementApiConfigId?: string | undefined
|
||||
marketplaceSources?:
|
||||
| {
|
||||
url: string
|
||||
name?: string | undefined
|
||||
enabled: boolean
|
||||
}[]
|
||||
| undefined
|
||||
historyPreviewCollapsed?: boolean | undefined
|
||||
}
|
||||
|
||||
|
|
@ -905,6 +912,13 @@ type IpcMessage =
|
|||
}
|
||||
| undefined
|
||||
enhancementApiConfigId?: string | undefined
|
||||
marketplaceSources?:
|
||||
| {
|
||||
url: string
|
||||
name?: string | undefined
|
||||
enabled: boolean
|
||||
}[]
|
||||
| undefined
|
||||
historyPreviewCollapsed?: boolean | undefined
|
||||
}
|
||||
text: string
|
||||
|
|
@ -1380,6 +1394,13 @@ type TaskCommand =
|
|||
}
|
||||
| undefined
|
||||
enhancementApiConfigId?: string | undefined
|
||||
marketplaceSources?:
|
||||
| {
|
||||
url: string
|
||||
name?: string | undefined
|
||||
enabled: boolean
|
||||
}[]
|
||||
| undefined
|
||||
historyPreviewCollapsed?: boolean | undefined
|
||||
}
|
||||
text: string
|
||||
|
|
|
|||
|
|
@ -173,6 +173,13 @@ type GlobalSettings = {
|
|||
}
|
||||
| undefined
|
||||
enhancementApiConfigId?: string | undefined
|
||||
marketplaceSources?:
|
||||
| {
|
||||
url: string
|
||||
name?: string | undefined
|
||||
enabled: boolean
|
||||
}[]
|
||||
| undefined
|
||||
historyPreviewCollapsed?: boolean | undefined
|
||||
}
|
||||
|
||||
|
|
@ -919,6 +926,13 @@ type IpcMessage =
|
|||
}
|
||||
| undefined
|
||||
enhancementApiConfigId?: string | undefined
|
||||
marketplaceSources?:
|
||||
| {
|
||||
url: string
|
||||
name?: string | undefined
|
||||
enabled: boolean
|
||||
}[]
|
||||
| undefined
|
||||
historyPreviewCollapsed?: boolean | undefined
|
||||
}
|
||||
text: string
|
||||
|
|
@ -1396,6 +1410,13 @@ type TaskCommand =
|
|||
}
|
||||
| undefined
|
||||
enhancementApiConfigId?: string | undefined
|
||||
marketplaceSources?:
|
||||
| {
|
||||
url: string
|
||||
name?: string | undefined
|
||||
enabled: boolean
|
||||
}[]
|
||||
| undefined
|
||||
historyPreviewCollapsed?: boolean | undefined
|
||||
}
|
||||
text: string
|
||||
|
|
|
|||
100
src/i18n/locales/ca/marketplace.json
Normal file
100
src/i18n/locales/ca/marketplace.json
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
{
|
||||
"type-group": {
|
||||
"modes": "Modes",
|
||||
"mcps": "Servidors MCP",
|
||||
"prompts": "Prompts",
|
||||
"packages": "Paquets",
|
||||
"generic-type": "{{type}}s",
|
||||
"match": "coincidència"
|
||||
},
|
||||
"item-card": {
|
||||
"type-mode": "Mode",
|
||||
"type-mcp": "Servidor MCP",
|
||||
"type-prompt": "Prompt",
|
||||
"type-package": "Paquet",
|
||||
"type-other": "Altres",
|
||||
"by-author": "per {{author}}",
|
||||
"authors-profile": "Perfil de l'autor",
|
||||
"remove-tag-filter": "Eliminar filtre d'etiqueta: {{tag}}",
|
||||
"filter-by-tag": "Filtrar per etiqueta: {{tag}}",
|
||||
"component-details": "Detalls del component",
|
||||
"match-count": "{{count}} coincidènci{{count !== 1 ? 'es' : 'a'}}",
|
||||
"view": "Veure",
|
||||
"source": "Font"
|
||||
},
|
||||
"install-sidebar": {
|
||||
"title": "Instal·la {{itemName}}",
|
||||
"installButton": "Instal·lar",
|
||||
"cancelButton": "Cancel·lar"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "Cerca al mercat..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Tipus",
|
||||
"all": "Tots els tipus",
|
||||
"mode": "Mode",
|
||||
"mcp server": "Servidor MCP",
|
||||
"prompt": "Prompt",
|
||||
"package": "Paquet"
|
||||
},
|
||||
"sort": {
|
||||
"label": "Ordena per",
|
||||
"name": "Nom",
|
||||
"lastUpdated": "Última actualització"
|
||||
},
|
||||
"tags": {
|
||||
"label": "Etiquetes",
|
||||
"clear_one": "Esborra {{count}} etiqueta seleccionada",
|
||||
"clear_other": "Esborra {{count}} etiquetes seleccionades",
|
||||
"placeholder": "Cerca etiquetes...",
|
||||
"noResults": "No s'han trobat etiquetes.",
|
||||
"selected_one": "{{count}} etiqueta seleccionada",
|
||||
"selected_other": "{{count}} etiquetes seleccionades"
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
"title": "Fonts del mercat",
|
||||
"description": "Afegeix o gestiona fonts per als elements del mercat. Cada font és un repositori Git que conté definicions d'elements del mercat.",
|
||||
"errors": {
|
||||
"maxSources": "Màxim de {{max}} fonts permeses.",
|
||||
"emptyUrl": "La URL no pot estar buida.",
|
||||
"nonVisibleChars": "La URL conté caràcters no visibles.",
|
||||
"invalidGitUrl": "Format d'URL de Git no vàlid.",
|
||||
"duplicateUrl": "Ja existeix una font amb aquesta URL.",
|
||||
"nameTooLong": "El nom no pot superar els 20 caràcters.",
|
||||
"nonVisibleCharsName": "El nom conté caràcters no visibles.",
|
||||
"duplicateName": "Ja existeix una font amb aquest nom."
|
||||
},
|
||||
"add": {
|
||||
"namePlaceholder": "Nom opcional de la font (p. ex. 'El meu repositori privat')",
|
||||
"urlPlaceholder": "URL del repositori Git (p. ex. 'https://github.com/user/repo.git')",
|
||||
"urlFormats": "Formats compatibles: HTTPS, SSH o ruta de fitxer local.",
|
||||
"button": "Afegeix font"
|
||||
},
|
||||
"current": {
|
||||
"title": "Fonts actuals",
|
||||
"empty": "Encara no s'han afegit fonts del mercat.",
|
||||
"emptyHint": "Afegeix una font a dalt per explorar els elements del mercat.",
|
||||
"refresh": "Actualitza la font",
|
||||
"remove": "Elimina la font"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"browse": "Explora",
|
||||
"sources": "Fonts"
|
||||
},
|
||||
"title": "Mercat",
|
||||
"items": {
|
||||
"refresh": {
|
||||
"refreshing": "Actualitzant elements del mercat..."
|
||||
},
|
||||
"empty": {
|
||||
"noItems": "No s'han trobat elements del mercat.",
|
||||
"emptyHint": "Prova d'ajustar els filtres o els termes de cerca"
|
||||
},
|
||||
"count_one": "{{count}} element",
|
||||
"count_other": "{{count}} elements"
|
||||
}
|
||||
}
|
||||
100
src/i18n/locales/de/marketplace.json
Normal file
100
src/i18n/locales/de/marketplace.json
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
{
|
||||
"type-group": {
|
||||
"modes": "Modi",
|
||||
"mcps": "MCP-Server",
|
||||
"prompts": "Prompts",
|
||||
"packages": "Pakete",
|
||||
"generic-type": "{{type}}",
|
||||
"match": "Treffer"
|
||||
},
|
||||
"item-card": {
|
||||
"type-mode": "Modus",
|
||||
"type-mcp": "MCP-Server",
|
||||
"type-prompt": "Prompt",
|
||||
"type-package": "Paket",
|
||||
"type-other": "Sonstiges",
|
||||
"by-author": "von {{author}}",
|
||||
"authors-profile": "Autorenprofil",
|
||||
"remove-tag-filter": "Tag-Filter entfernen: {{tag}}",
|
||||
"filter-by-tag": "Nach Tag filtern: {{tag}}",
|
||||
"component-details": "Komponentendetails",
|
||||
"match-count": "{{count}} Treffer",
|
||||
"view": "Ansehen",
|
||||
"source": "Quelle"
|
||||
},
|
||||
"install-sidebar": {
|
||||
"title": "Installiere {{itemName}}",
|
||||
"installButton": "Installieren",
|
||||
"cancelButton": "Abbrechen"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "Marktplatz durchsuchen..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Typ",
|
||||
"all": "Alle Typen",
|
||||
"mode": "Modus",
|
||||
"mcp server": "MCP-Server",
|
||||
"prompt": "Prompt",
|
||||
"package": "Paket"
|
||||
},
|
||||
"sort": {
|
||||
"label": "Sortieren nach",
|
||||
"name": "Name",
|
||||
"lastUpdated": "Zuletzt aktualisiert"
|
||||
},
|
||||
"tags": {
|
||||
"label": "Tags",
|
||||
"clear_one": "{{count}} ausgewählten Tag löschen",
|
||||
"clear_other": "{{count}} ausgewählte Tags löschen",
|
||||
"placeholder": "Tags durchsuchen...",
|
||||
"noResults": "Keine Tags gefunden.",
|
||||
"selected_one": "{{count}} Tag ausgewählt",
|
||||
"selected_other": "{{count}} Tags ausgewählt"
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
"title": "Marktplatz-Quellen",
|
||||
"description": "Füge Quellen für Marktplatz-Items hinzu oder verwalte sie. Jede Quelle ist ein Git-Repository, das Marktplatz-Item-Definitionen enthält.",
|
||||
"errors": {
|
||||
"maxSources": "Maximal {{max}} Quellen erlaubt.",
|
||||
"emptyUrl": "URL darf nicht leer sein.",
|
||||
"nonVisibleChars": "URL enthält nicht sichtbare Zeichen.",
|
||||
"invalidGitUrl": "Ungültiges Git-URL-Format.",
|
||||
"duplicateUrl": "Quelle mit dieser URL existiert bereits.",
|
||||
"nameTooLong": "Name darf 20 Zeichen nicht überschreiten.",
|
||||
"nonVisibleCharsName": "Name enthält nicht sichtbare Zeichen.",
|
||||
"duplicateName": "Quelle mit diesem Namen existiert bereits."
|
||||
},
|
||||
"add": {
|
||||
"namePlaceholder": "Optionaler Quellname (z.B. 'Mein privates Repo')",
|
||||
"urlPlaceholder": "Git-Repository-URL (z.B. 'https://github.com/user/repo.git')",
|
||||
"urlFormats": "Unterstützte Formate: HTTPS, SSH oder lokaler Dateipfad.",
|
||||
"button": "Quelle hinzufügen"
|
||||
},
|
||||
"current": {
|
||||
"title": "Aktuelle Quellen",
|
||||
"empty": "Noch keine Marktplatz-Quellen hinzugefügt.",
|
||||
"emptyHint": "Füge oben eine Quelle hinzu, um Marktplatz-Items zu durchsuchen.",
|
||||
"refresh": "Quelle aktualisieren",
|
||||
"remove": "Quelle entfernen"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"browse": "Durchsuchen",
|
||||
"sources": "Quellen"
|
||||
},
|
||||
"title": "Marktplatz",
|
||||
"items": {
|
||||
"refresh": {
|
||||
"refreshing": "Marktplatz-Items werden aktualisiert..."
|
||||
},
|
||||
"empty": {
|
||||
"noItems": "Keine Marktplatz-Items gefunden.",
|
||||
"emptyHint": "Versuche, deine Filter oder Suchbegriffe anzupassen"
|
||||
},
|
||||
"count_one": "{{count}} Item",
|
||||
"count_other": "{{count}} Items"
|
||||
}
|
||||
}
|
||||
104
src/i18n/locales/en/marketplace.json
Normal file
104
src/i18n/locales/en/marketplace.json
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
{
|
||||
"type-group": {
|
||||
"modes": "Modes",
|
||||
"mcps": "MCP Servers",
|
||||
"prompts": "Prompts",
|
||||
"packages": "Packages",
|
||||
"generic-type": "{{type}}s",
|
||||
"match": "match"
|
||||
},
|
||||
"item-card": {
|
||||
"type-mode": "Mode",
|
||||
"type-mcp": "MCP Server",
|
||||
"type-prompt": "Prompt",
|
||||
"type-package": "Package",
|
||||
"type-other": "Other",
|
||||
"by-author": "by {{author}}",
|
||||
"authors-profile": "Author's Profile",
|
||||
"remove-tag-filter": "Remove tag filter: {{tag}}",
|
||||
"filter-by-tag": "Filter by tag: {{tag}}",
|
||||
"component-details": "Component Details",
|
||||
"match-count": "{{count}} match{{count !== 1 ? 'es' : ''}}",
|
||||
"view": "View",
|
||||
"source": "Source"
|
||||
},
|
||||
"install-sidebar": {
|
||||
"title": "Install {{itemName}}",
|
||||
"installButton": "Install",
|
||||
"cancelButton": "Cancel"
|
||||
},
|
||||
"install-sidebar": {
|
||||
"title": "Install {{itemName}}",
|
||||
"installButton": "Install",
|
||||
"cancelButton": "Cancel"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "Search marketplace..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Type",
|
||||
"all": "All Types",
|
||||
"mode": "Mode",
|
||||
"mcp server": "MCP Server",
|
||||
"prompt": "Prompt",
|
||||
"package": "Package"
|
||||
},
|
||||
"sort": {
|
||||
"label": "Sort By",
|
||||
"name": "Name",
|
||||
"lastUpdated": "Last Updated"
|
||||
},
|
||||
"tags": {
|
||||
"label": "Tags",
|
||||
"clear": "Clear {{count}} selected tag{{count !== 1 ? 's' : ''}}",
|
||||
"placeholder": "Search tags...",
|
||||
"noResults": "No tags found.",
|
||||
"selected": "{{count}} tag{{count !== 1 ? 's' : ''}} selected"
|
||||
},
|
||||
"sources": {
|
||||
"title": "Marketplace Sources",
|
||||
"description": "Add or manage sources for marketplace items. Each source is a Git repository containing marketplace item definitions.",
|
||||
"errors": {
|
||||
"maxSources": "Maximum of {{max}} sources allowed.",
|
||||
"emptyUrl": "URL cannot be empty.",
|
||||
"nonVisibleChars": "URL contains non-visible characters.",
|
||||
"invalidGitUrl": "Invalid Git URL format.",
|
||||
"duplicateUrl": "Source with this URL already exists.",
|
||||
"nameTooLong": "Name cannot exceed 20 characters.",
|
||||
"nonVisibleCharsName": "Name contains non-visible characters.",
|
||||
"duplicateName": "Source with this name already exists."
|
||||
},
|
||||
"add": {
|
||||
"namePlaceholder": "Optional source name (e.g. 'My Private Repo')",
|
||||
"urlPlaceholder": "Git repository URL (e.g. 'https://github.com/user/repo.git')",
|
||||
"urlFormats": "Supported formats: HTTPS, SSH, or local file path.",
|
||||
"button": "Add Source"
|
||||
},
|
||||
"current": {
|
||||
"title": "Current Sources",
|
||||
"empty": "No marketplace sources added yet.",
|
||||
"emptyHint": "Add a source above to browse marketplace items."
|
||||
},
|
||||
"current": {
|
||||
"refresh": "Refresh source",
|
||||
"remove": "Remove source"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"browse": "Browse",
|
||||
"sources": "Sources"
|
||||
},
|
||||
"title": "Marketplace"
|
||||
},
|
||||
"items": {
|
||||
"refresh": {
|
||||
"refreshing": "Refreshing marketplace items..."
|
||||
},
|
||||
"empty": {
|
||||
"noItems": "No marketplace items found.",
|
||||
"emptyHint": "Try adjusting your filters or search terms"
|
||||
},
|
||||
"count": "{{count}} item{{count !== 1 ? 's' : ''}}"
|
||||
}
|
||||
}
|
||||
100
src/i18n/locales/es/marketplace.json
Normal file
100
src/i18n/locales/es/marketplace.json
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
{
|
||||
"type-group": {
|
||||
"modes": "Modos",
|
||||
"mcps": "Servidores MCP",
|
||||
"prompts": "Prompts",
|
||||
"packages": "Paquetes",
|
||||
"generic-type": "{{type}}s",
|
||||
"match": "coincide"
|
||||
},
|
||||
"item-card": {
|
||||
"type-mode": "Modo",
|
||||
"type-mcp": "Servidor MCP",
|
||||
"type-prompt": "Prompt",
|
||||
"type-package": "Paquete",
|
||||
"type-other": "Otro",
|
||||
"by-author": "por {{author}}",
|
||||
"authors-profile": "Perfil del Autor",
|
||||
"remove-tag-filter": "Eliminar filtro de etiqueta: {{tag}}",
|
||||
"filter-by-tag": "Filtrar por etiqueta: {{tag}}",
|
||||
"component-details": "Detalles del Componente",
|
||||
"match-count": "{{count}} coincidencia{{count !== 1 ? 's' : ''}}",
|
||||
"view": "Ver",
|
||||
"source": "Fuente"
|
||||
},
|
||||
"install-sidebar": {
|
||||
"title": "Instalar {{itemName}}",
|
||||
"installButton": "Instalar",
|
||||
"cancelButton": "Cancelar"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "Buscar en el mercado..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Tipo",
|
||||
"all": "Todos los tipos",
|
||||
"mode": "Modo",
|
||||
"mcp server": "Servidor MCP",
|
||||
"prompt": "Prompt",
|
||||
"package": "Paquete"
|
||||
},
|
||||
"sort": {
|
||||
"label": "Ordenar por",
|
||||
"name": "Nombre",
|
||||
"lastUpdated": "Última actualización"
|
||||
},
|
||||
"tags": {
|
||||
"label": "Etiquetas",
|
||||
"clear_one": "Borrar {{count}} etiqueta seleccionada",
|
||||
"clear_other": "Borrar {{count}} etiquetas seleccionadas",
|
||||
"placeholder": "Buscar etiquetas...",
|
||||
"noResults": "No se encontraron etiquetas.",
|
||||
"selected_one": "{{count}} etiqueta seleccionada",
|
||||
"selected_other": "{{count}} etiquetas seleccionadas"
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
"title": "Fuentes del mercado",
|
||||
"description": "Añade o gestiona fuentes para los elementos del mercado. Cada fuente es un repositorio Git que contiene definiciones de elementos del mercado.",
|
||||
"errors": {
|
||||
"maxSources": "Máximo de {{max}} fuentes permitidas.",
|
||||
"emptyUrl": "La URL no puede estar vacía.",
|
||||
"nonVisibleChars": "La URL contiene caracteres no visibles.",
|
||||
"invalidGitUrl": "Formato de URL de Git no válido.",
|
||||
"duplicateUrl": "Ya existe una fuente con esta URL.",
|
||||
"nameTooLong": "El nombre no puede superar los 20 caracteres.",
|
||||
"nonVisibleCharsName": "El nombre contiene caracteres no visibles.",
|
||||
"duplicateName": "Ya existe una fuente con este nombre."
|
||||
},
|
||||
"add": {
|
||||
"namePlaceholder": "Nombre opcional de la fuente (p. ej. 'Mi repositorio privado')",
|
||||
"urlPlaceholder": "URL del repositorio Git (p. ej. 'https://github.com/user/repo.git')",
|
||||
"urlFormats": "Formatos compatibles: HTTPS, SSH o ruta de archivo local.",
|
||||
"button": "Añadir fuente"
|
||||
},
|
||||
"current": {
|
||||
"title": "Fuentes actuales",
|
||||
"empty": "Aún no se han añadido fuentes del mercado.",
|
||||
"emptyHint": "Añade una fuente arriba para explorar los elementos del mercado.",
|
||||
"refresh": "Actualizar fuente",
|
||||
"remove": "Eliminar fuente"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"browse": "Explorar",
|
||||
"sources": "Fuentes"
|
||||
},
|
||||
"title": "Mercado",
|
||||
"items": {
|
||||
"refresh": {
|
||||
"refreshing": "Actualizando elementos del mercado..."
|
||||
},
|
||||
"empty": {
|
||||
"noItems": "No se encontraron elementos del mercado.",
|
||||
"emptyHint": "Intenta ajustar tus filtros o términos de búsqueda"
|
||||
},
|
||||
"count_one": "{{count}} elemento",
|
||||
"count_other": "{{count}} elementos"
|
||||
}
|
||||
}
|
||||
100
src/i18n/locales/fr/marketplace.json
Normal file
100
src/i18n/locales/fr/marketplace.json
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
{
|
||||
"type-group": {
|
||||
"modes": "Modes",
|
||||
"mcps": "Serveurs MCP",
|
||||
"prompts": "Prompts",
|
||||
"packages": "Paquets",
|
||||
"generic-type": "{{type}}s",
|
||||
"match": "correspondance"
|
||||
},
|
||||
"item-card": {
|
||||
"type-mode": "Mode",
|
||||
"type-mcp": "Serveur MCP",
|
||||
"type-prompt": "Prompt",
|
||||
"type-package": "Paquet",
|
||||
"type-other": "Autre",
|
||||
"by-author": "par {{author}}",
|
||||
"authors-profile": "Profil de l'auteur",
|
||||
"remove-tag-filter": "Supprimer le filtre de tag : {{tag}}",
|
||||
"filter-by-tag": "Filtrer par tag : {{tag}}",
|
||||
"component-details": "Détails du composant",
|
||||
"match-count": "{{count}} correspondance{{count !== 1 ? 's' : ''}}",
|
||||
"view": "Voir",
|
||||
"source": "Source"
|
||||
},
|
||||
"install-sidebar": {
|
||||
"title": "Installer {{itemName}}",
|
||||
"installButton": "Installer",
|
||||
"cancelButton": "Annuler"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "Rechercher sur la place de marché..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Type",
|
||||
"all": "Tous les types",
|
||||
"mode": "Mode",
|
||||
"mcp server": "Serveur MCP",
|
||||
"prompt": "Prompt",
|
||||
"package": "Paquet"
|
||||
},
|
||||
"sort": {
|
||||
"label": "Trier par",
|
||||
"name": "Nom",
|
||||
"lastUpdated": "Dernière mise à jour"
|
||||
},
|
||||
"tags": {
|
||||
"label": "Tags",
|
||||
"clear_one": "Effacer {{count}} tag sélectionné",
|
||||
"clear_other": "Effacer {{count}} tags sélectionnés",
|
||||
"placeholder": "Rechercher des tags...",
|
||||
"noResults": "Aucun tag trouvé.",
|
||||
"selected_one": "{{count}} tag sélectionné",
|
||||
"selected_other": "{{count}} tags sélectionnés"
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
"title": "Sources de la place de marché",
|
||||
"description": "Ajoutez ou gérez les sources des éléments de la place de marché. Chaque source est un dépôt Git contenant des définitions d'éléments de la place de marché.",
|
||||
"errors": {
|
||||
"maxSources": "Maximum de {{max}} sources autorisées.",
|
||||
"emptyUrl": "L'URL ne peut pas être vide.",
|
||||
"nonVisibleChars": "L'URL contient des caractères non visibles.",
|
||||
"invalidGitUrl": "Format d'URL Git invalide.",
|
||||
"duplicateUrl": "Une source avec cette URL existe déjà.",
|
||||
"nameTooLong": "Le nom ne peut pas dépasser 20 caractères.",
|
||||
"nonVisibleCharsName": "Le nom contient des caractères non visibles.",
|
||||
"duplicateName": "Une source avec ce nom existe déjà."
|
||||
},
|
||||
"add": {
|
||||
"namePlaceholder": "Nom de source facultatif (par exemple, 'Mon dépôt privé')",
|
||||
"urlPlaceholder": "URL du dépôt Git (par exemple, 'https://github.com/user/repo.git')",
|
||||
"urlFormats": "Formats pris en charge : HTTPS, SSH ou chemin de fichier local.",
|
||||
"button": "Ajouter une source"
|
||||
},
|
||||
"current": {
|
||||
"title": "Sources actuelles",
|
||||
"empty": "Aucune source de place de marché ajoutée pour l'instant.",
|
||||
"emptyHint": "Ajoutez une source ci-dessus pour parcourir les éléments de la place de marché.",
|
||||
"refresh": "Actualiser la source",
|
||||
"remove": "Supprimer la source"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"browse": "Parcourir",
|
||||
"sources": "Sources"
|
||||
},
|
||||
"title": "Place de marché",
|
||||
"items": {
|
||||
"refresh": {
|
||||
"refreshing": "Actualisation des éléments de la place de marché..."
|
||||
},
|
||||
"empty": {
|
||||
"noItems": "Aucun élément de place de marché trouvé.",
|
||||
"emptyHint": "Essayez d'ajuster vos filtres ou termes de recherche"
|
||||
},
|
||||
"count_one": "{{count}} élément",
|
||||
"count_other": "{{count}} éléments"
|
||||
}
|
||||
}
|
||||
97
src/i18n/locales/hi/marketplace.json
Normal file
97
src/i18n/locales/hi/marketplace.json
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
{
|
||||
"type-group": {
|
||||
"modes": "मोड",
|
||||
"mcps": "एमसीपी सर्वर",
|
||||
"prompts": "प्रॉम्प्ट्स",
|
||||
"packages": "पैकेज",
|
||||
"generic-type": "{{type}}",
|
||||
"match": "मिलान"
|
||||
},
|
||||
"item-card": {
|
||||
"type-mode": "मोड",
|
||||
"type-mcp": "एमसीपी सर्वर",
|
||||
"type-prompt": "प्रॉम्प्ट",
|
||||
"type-package": "पैकेज",
|
||||
"type-other": "अन्य",
|
||||
"by-author": "लेखक: {{author}}",
|
||||
"authors-profile": "लेखक का प्रोफ़ाइल",
|
||||
"remove-tag-filter": "टैग फ़िल्टर हटाएं: {{tag}}",
|
||||
"filter-by-tag": "टैग से फ़िल्टर करें: {{tag}}",
|
||||
"component-details": "कंपोनेंट विवरण",
|
||||
"match-count": "{{count}} मिलान",
|
||||
"view": "देखें",
|
||||
"source": "स्रोत"
|
||||
},
|
||||
"install-sidebar": {
|
||||
"title": "{{itemName}} इंस्टॉल करें",
|
||||
"installButton": "इंस्टॉल करें",
|
||||
"cancelButton": "रद्द करें"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "मार्केटप्लेस खोजें..."
|
||||
},
|
||||
"type": {
|
||||
"label": "प्रकार",
|
||||
"all": "सभी प्रकार",
|
||||
"mode": "मोड",
|
||||
"mcp server": "एमसीपी सर्वर",
|
||||
"prompt": "प्रॉम्प्ट",
|
||||
"package": "पैकेज"
|
||||
},
|
||||
"sort": {
|
||||
"label": "इसके अनुसार क्रमबद्ध करें",
|
||||
"name": "नाम",
|
||||
"lastUpdated": "अंतिम अपडेट"
|
||||
},
|
||||
"tags": {
|
||||
"label": "टैग",
|
||||
"clear": "{{count}} चयनित टैग साफ़ करें",
|
||||
"placeholder": "टैग खोजें...",
|
||||
"noResults": "कोई टैग नहीं मिला।",
|
||||
"selected": "{{count}} टैग चयनित"
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
"title": "मार्केटप्लेस स्रोत",
|
||||
"description": "मार्केटप्लेस आइटम के लिए स्रोत जोड़ें या प्रबंधित करें। प्रत्येक स्रोत एक Git रिपॉजिटरी है जिसमें मार्केटप्लेस आइटम परिभाषाएँ होती हैं।",
|
||||
"errors": {
|
||||
"maxSources": "{{max}} स्रोतों की अधिकतम संख्या अनुमत है।",
|
||||
"emptyUrl": "URL खाली नहीं हो सकती।",
|
||||
"nonVisibleChars": "URL में गैर-दृश्य वर्ण हैं।",
|
||||
"invalidGitUrl": "अमान्य Git URL प्रारूप।",
|
||||
"duplicateUrl": "इस URL वाला स्रोत पहले से मौजूद है।",
|
||||
"nameTooLong": "नाम 20 वर्णों से अधिक नहीं हो सकता।",
|
||||
"nonVisibleCharsName": "नाम में गैर-दृश्य वर्ण हैं।",
|
||||
"duplicateName": "इस नाम वाला स्रोत पहले से मौजूद है।"
|
||||
},
|
||||
"add": {
|
||||
"namePlaceholder": "वैकल्पिक स्रोत नाम (जैसे 'मेरा निजी रेपो')",
|
||||
"urlPlaceholder": "Git रिपॉजिटरी URL (जैसे 'https://github.com/user/repo.git')",
|
||||
"urlFormats": "समर्थित प्रारूप: HTTPS, SSH, या स्थानीय फ़ाइल पथ।",
|
||||
"button": "स्रोत जोड़ें"
|
||||
},
|
||||
"current": {
|
||||
"title": "वर्तमान स्रोत",
|
||||
"empty": "अभी तक कोई मार्केटप्लेस स्रोत नहीं जोड़ा गया है।",
|
||||
"emptyHint": "मार्केटप्लेस आइटम ब्राउज़ करने के लिए ऊपर एक स्रोत जोड़ें।",
|
||||
"refresh": "स्रोत रीफ़्रेश करें",
|
||||
"remove": "स्रोत हटाएं"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"browse": "ब्राउज़ करें",
|
||||
"sources": "स्रोत"
|
||||
},
|
||||
"title": "मार्केटप्लेस",
|
||||
"items": {
|
||||
"refresh": {
|
||||
"refreshing": "मार्केटप्लेस आइटम रीफ़्रेश हो रहे हैं..."
|
||||
},
|
||||
"empty": {
|
||||
"noItems": "कोई मार्केटप्लेस आइटम नहीं मिला।",
|
||||
"emptyHint": "अपने फ़िल्टर या खोज शब्दों को समायोजित करने का प्रयास करें"
|
||||
},
|
||||
"count": "{{count}} आइटम"
|
||||
}
|
||||
}
|
||||
100
src/i18n/locales/it/marketplace.json
Normal file
100
src/i18n/locales/it/marketplace.json
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
{
|
||||
"type-group": {
|
||||
"modes": "Modalità",
|
||||
"mcps": "Server MCP",
|
||||
"prompts": "Prompt",
|
||||
"packages": "Pacchetti",
|
||||
"generic-type": "{{type}}",
|
||||
"match": "corrispondenza"
|
||||
},
|
||||
"item-card": {
|
||||
"type-mode": "Modalità",
|
||||
"type-mcp": "Server MCP",
|
||||
"type-prompt": "Prompt",
|
||||
"type-package": "Pacchetto",
|
||||
"type-other": "Altro",
|
||||
"by-author": "di {{author}}",
|
||||
"authors-profile": "Profilo dell'autore",
|
||||
"remove-tag-filter": "Rimuovi filtro tag: {{tag}}",
|
||||
"filter-by-tag": "Filtra per tag: {{tag}}",
|
||||
"component-details": "Dettagli componente",
|
||||
"match-count": "{{count}} corrispondenza{{count !== 1 ? 'e' : ''}}",
|
||||
"view": "Visualizza",
|
||||
"source": "Sorgente"
|
||||
},
|
||||
"install-sidebar": {
|
||||
"title": "Installa {{itemName}}",
|
||||
"installButton": "Installa",
|
||||
"cancelButton": "Annulla"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "Cerca nel marketplace..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Tipo",
|
||||
"all": "Tutti i tipi",
|
||||
"mode": "Modalità",
|
||||
"mcp server": "Server MCP",
|
||||
"prompt": "Prompt",
|
||||
"package": "Pacchetto"
|
||||
},
|
||||
"sort": {
|
||||
"label": "Ordina per",
|
||||
"name": "Nome",
|
||||
"lastUpdated": "Ultimo aggiornamento"
|
||||
},
|
||||
"tags": {
|
||||
"label": "Tag",
|
||||
"clear_one": "Cancella {{count}} tag selezionato",
|
||||
"clear_other": "Cancella {{count}} tag selezionati",
|
||||
"placeholder": "Cerca tag...",
|
||||
"noResults": "Nessun tag trovato.",
|
||||
"selected_one": "{{count}} tag selezionato",
|
||||
"selected_other": "{{count}} tag selezionati"
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
"title": "Sorgenti del marketplace",
|
||||
"description": "Aggiungi o gestisci le sorgenti per gli elementi del marketplace. Ogni sorgente è un repository Git contenente definizioni di elementi del marketplace.",
|
||||
"errors": {
|
||||
"maxSources": "Massimo {{max}} sorgenti consentite.",
|
||||
"emptyUrl": "L'URL non può essere vuoto.",
|
||||
"nonVisibleChars": "L'URL contiene caratteri non visibili.",
|
||||
"invalidGitUrl": "Formato URL Git non valido.",
|
||||
"duplicateUrl": "Esiste già una sorgente con questo URL.",
|
||||
"nameTooLong": "Il nome non può superare i 20 caratteri.",
|
||||
"nonVisibleCharsName": "Il nome contiene caratteri non visibili.",
|
||||
"duplicateName": "Esiste già una sorgente con questo nome."
|
||||
},
|
||||
"add": {
|
||||
"namePlaceholder": "Nome sorgente opzionale (es. 'Il mio repository privato')",
|
||||
"urlPlaceholder": "URL repository Git (es. 'https://github.com/user/repo.git')",
|
||||
"urlFormats": "Formati supportati: HTTPS, SSH o percorso file locale.",
|
||||
"button": "Aggiungi sorgente"
|
||||
},
|
||||
"current": {
|
||||
"title": "Sorgenti attuali",
|
||||
"empty": "Nessuna sorgente del marketplace aggiunta ancora.",
|
||||
"emptyHint": "Aggiungi una sorgente sopra per sfogliare gli elementi del marketplace.",
|
||||
"refresh": "Aggiorna sorgente",
|
||||
"remove": "Rimuovi sorgente"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"browse": "Sfoglia",
|
||||
"sources": "Sorgenti"
|
||||
},
|
||||
"title": "Marketplace",
|
||||
"items": {
|
||||
"refresh": {
|
||||
"refreshing": "Aggiornamento elementi del marketplace..."
|
||||
},
|
||||
"empty": {
|
||||
"noItems": "Nessun elemento del marketplace trovato.",
|
||||
"emptyHint": "Prova a regolare i filtri o i termini di ricerca"
|
||||
},
|
||||
"count_one": "{{count}} elemento",
|
||||
"count_other": "{{count}} elementi"
|
||||
}
|
||||
}
|
||||
97
src/i18n/locales/ja/marketplace.json
Normal file
97
src/i18n/locales/ja/marketplace.json
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
{
|
||||
"type-group": {
|
||||
"modes": "モード",
|
||||
"mcps": "MCPサーバー",
|
||||
"prompts": "プロンプト",
|
||||
"packages": "パッケージ",
|
||||
"generic-type": "{{type}}",
|
||||
"match": "一致"
|
||||
},
|
||||
"item-card": {
|
||||
"type-mode": "モード",
|
||||
"type-mcp": "MCPサーバー",
|
||||
"type-prompt": "プロンプト",
|
||||
"type-package": "パッケージ",
|
||||
"type-other": "その他",
|
||||
"by-author": "作成者:{{author}}",
|
||||
"authors-profile": "作成者のプロフィール",
|
||||
"remove-tag-filter": "タグフィルターを削除:{{tag}}",
|
||||
"filter-by-tag": "タグでフィルター:{{tag}}",
|
||||
"component-details": "コンポーネントの詳細",
|
||||
"match-count": "{{count}}件の一致",
|
||||
"view": "表示",
|
||||
"source": "ソース"
|
||||
},
|
||||
"install-sidebar": {
|
||||
"title": "{{itemName}}をインストール",
|
||||
"installButton": "インストール",
|
||||
"cancelButton": "キャンセル"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "マーケットプレイスを検索..."
|
||||
},
|
||||
"type": {
|
||||
"label": "タイプ",
|
||||
"all": "すべてのタイプ",
|
||||
"mode": "モード",
|
||||
"mcp server": "MCPサーバー",
|
||||
"prompt": "プロンプト",
|
||||
"package": "パッケージ"
|
||||
},
|
||||
"sort": {
|
||||
"label": "並べ替え",
|
||||
"name": "名前",
|
||||
"lastUpdated": "最終更新"
|
||||
},
|
||||
"tags": {
|
||||
"label": "タグ",
|
||||
"clear": "{{count}}個の選択されたタグをクリア",
|
||||
"placeholder": "タグを検索...",
|
||||
"noResults": "タグが見つかりませんでした。",
|
||||
"selected": "{{count}}個のタグを選択"
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
"title": "マーケットプレイスソース",
|
||||
"description": "マーケットプレイスアイテムのソースを追加または管理します。各ソースは、マーケットプレイスアイテムの定義を含むGitリポジトリです。",
|
||||
"errors": {
|
||||
"maxSources": "最大{{max}}個のソースが許可されています。",
|
||||
"emptyUrl": "URLは空にできません。",
|
||||
"nonVisibleChars": "URLに非表示文字が含まれています。",
|
||||
"invalidGitUrl": "無効なGit URL形式。",
|
||||
"duplicateUrl": "このURLを持つソースはすでに存在します。",
|
||||
"nameTooLong": "名前は20文字を超えることはできません。",
|
||||
"nonVisibleCharsName": "名前に非表示文字が含まれています。",
|
||||
"duplicateName": "この名前を持つソースはすでに存在します。"
|
||||
},
|
||||
"add": {
|
||||
"namePlaceholder": "オプションのソース名(例:'私のプライベートリポジトリ')",
|
||||
"urlPlaceholder": "GitリポジトリURL(例:'https://github.com/user/repo.git')",
|
||||
"urlFormats": "サポートされている形式:HTTPS、SSH、またはローカルファイルパス。",
|
||||
"button": "ソースを追加"
|
||||
},
|
||||
"current": {
|
||||
"title": "現在のソース",
|
||||
"empty": "まだマーケットプレイスソースが追加されていません。",
|
||||
"emptyHint": "マーケットプレイスアイテムを閲覧するには、上にソースを追加してください。",
|
||||
"refresh": "ソースを更新",
|
||||
"remove": "ソースを削除"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"browse": "閲覧",
|
||||
"sources": "ソース"
|
||||
},
|
||||
"title": "マーケットプレイス",
|
||||
"items": {
|
||||
"refresh": {
|
||||
"refreshing": "マーケットプレイスアイテムを更新中..."
|
||||
},
|
||||
"empty": {
|
||||
"noItems": "マーケットプレイスアイテムが見つかりませんでした。",
|
||||
"emptyHint": "フィルターまたは検索語句を調整してみてください"
|
||||
},
|
||||
"count": "{{count}}個のアイテム"
|
||||
}
|
||||
}
|
||||
97
src/i18n/locales/ko/marketplace.json
Normal file
97
src/i18n/locales/ko/marketplace.json
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
{
|
||||
"type-group": {
|
||||
"modes": "모드",
|
||||
"mcps": "MCP 서버",
|
||||
"prompts": "프롬프트",
|
||||
"packages": "패키지",
|
||||
"generic-type": "{{type}}",
|
||||
"match": "일치"
|
||||
},
|
||||
"item-card": {
|
||||
"type-mode": "모드",
|
||||
"type-mcp": "MCP 서버",
|
||||
"type-prompt": "프롬프트",
|
||||
"type-package": "패키지",
|
||||
"type-other": "기타",
|
||||
"by-author": "작성자: {{author}}",
|
||||
"authors-profile": "작성자 프로필",
|
||||
"remove-tag-filter": "태그 필터 제거: {{tag}}",
|
||||
"filter-by-tag": "태그로 필터링: {{tag}}",
|
||||
"component-details": "컴포넌트 상세정보",
|
||||
"match-count": "{{count}}개 일치",
|
||||
"view": "보기",
|
||||
"source": "소스"
|
||||
},
|
||||
"install-sidebar": {
|
||||
"title": "{{itemName}} 설치",
|
||||
"installButton": "설치",
|
||||
"cancelButton": "취소"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "마켓플레이스 검색..."
|
||||
},
|
||||
"type": {
|
||||
"label": "유형",
|
||||
"all": "모든 유형",
|
||||
"mode": "모드",
|
||||
"mcp server": "MCP 서버",
|
||||
"prompt": "프롬프트",
|
||||
"package": "패키지"
|
||||
},
|
||||
"sort": {
|
||||
"label": "정렬 기준",
|
||||
"name": "이름",
|
||||
"lastUpdated": "최종 업데이트"
|
||||
},
|
||||
"tags": {
|
||||
"label": "태그",
|
||||
"clear": "선택된 태그 {{count}}개 지우기",
|
||||
"placeholder": "태그 검색...",
|
||||
"noResults": "태그를 찾을 수 없습니다.",
|
||||
"selected": "태그 {{count}}개 선택됨"
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
"title": "마켓플레이스 소스",
|
||||
"description": "마켓플레이스 항목의 소스를 추가하거나 관리합니다. 각 소스는 마켓플레이스 항목 정의를 포함하는 Git 리포지토리입니다.",
|
||||
"errors": {
|
||||
"maxSources": "최대 {{max}}개의 소스가 허용됩니다.",
|
||||
"emptyUrl": "URL은 비워둘 수 없습니다.",
|
||||
"nonVisibleChars": "URL에 보이지 않는 문자가 포함되어 있습니다.",
|
||||
"invalidGitUrl": "잘못된 Git URL 형식입니다.",
|
||||
"duplicateUrl": "이 URL을 가진 소스가 이미 존재합니다.",
|
||||
"nameTooLong": "이름은 20자를 초과할 수 없습니다.",
|
||||
"nonVisibleCharsName": "이름에 보이지 않는 문자가 포함되어 있습니다.",
|
||||
"duplicateName": "이 이름을 가진 소스가 이미 존재합니다."
|
||||
},
|
||||
"add": {
|
||||
"namePlaceholder": "선택적 소스 이름 (예: '내 개인 리포지토리')",
|
||||
"urlPlaceholder": "Git 리포지토리 URL (예: 'https://github.com/user/repo.git')",
|
||||
"urlFormats": "지원되는 형식: HTTPS, SSH 또는 로컬 파일 경로.",
|
||||
"button": "소스 추가"
|
||||
},
|
||||
"current": {
|
||||
"title": "현재 소스",
|
||||
"empty": "아직 마켓플레이스 소스가 추가되지 않았습니다.",
|
||||
"emptyHint": "마켓플레이스 항목을 탐색하려면 위에 소스를 추가하세요.",
|
||||
"refresh": "소스 새로고침",
|
||||
"remove": "소스 제거"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"browse": "찾아보기",
|
||||
"sources": "소스"
|
||||
},
|
||||
"title": "마켓플레이스",
|
||||
"items": {
|
||||
"refresh": {
|
||||
"refreshing": "마켓플레이스 항목 새로고침 중..."
|
||||
},
|
||||
"empty": {
|
||||
"noItems": "마켓플레이스 항목을 찾을 수 없습니다.",
|
||||
"emptyHint": "필터 또는 검색어를 조정해 보세요"
|
||||
},
|
||||
"count": "{{count}}개 항목"
|
||||
}
|
||||
}
|
||||
103
src/i18n/locales/pl/marketplace.json
Normal file
103
src/i18n/locales/pl/marketplace.json
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
{
|
||||
"type-group": {
|
||||
"modes": "Tryby",
|
||||
"mcps": "Serwery MCP",
|
||||
"prompts": "Podpowiedzi",
|
||||
"packages": "Pakiety",
|
||||
"generic-type": "{{type}}y",
|
||||
"match": "dopasowanie"
|
||||
},
|
||||
"item-card": {
|
||||
"type-mode": "Tryb",
|
||||
"type-mcp": "Serwer MCP",
|
||||
"type-prompt": "Podpowiedź",
|
||||
"type-package": "Pakiet",
|
||||
"type-other": "Inne",
|
||||
"by-author": "autor: {{author}}",
|
||||
"authors-profile": "Profil autora",
|
||||
"remove-tag-filter": "Usuń filtr tagu: {{tag}}",
|
||||
"filter-by-tag": "Filtruj po tagu: {{tag}}",
|
||||
"component-details": "Szczegóły komponentu",
|
||||
"match-count": "{{count}} dopasowani{{count === 1 ? 'e' : count < 5 ? 'a' : 'ń'}}",
|
||||
"view": "Pokaż",
|
||||
"source": "Źródło"
|
||||
},
|
||||
"install-sidebar": {
|
||||
"title": "Zainstaluj {{itemName}}",
|
||||
"installButton": "Zainstaluj",
|
||||
"cancelButton": "Anuluj"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "Przeszukaj marketplace..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Typ",
|
||||
"all": "Wszystkie typy",
|
||||
"mode": "Tryb",
|
||||
"mcp server": "Serwer MCP",
|
||||
"prompt": "Podpowiedź",
|
||||
"package": "Pakiet"
|
||||
},
|
||||
"sort": {
|
||||
"label": "Sortuj według",
|
||||
"name": "Nazwa",
|
||||
"lastUpdated": "Ostatnia aktualizacja"
|
||||
},
|
||||
"tags": {
|
||||
"label": "Tagi",
|
||||
"clear_one": "Wyczyść {{count}} wybrany tag",
|
||||
"clear_few": "Wyczyść {{count}} wybrane tagi",
|
||||
"clear_many": "Wyczyść {{count}} wybranych tagów",
|
||||
"placeholder": "Szukaj tagów...",
|
||||
"noResults": "Nie znaleziono tagów.",
|
||||
"selected_one": "{{count}} wybrany tag",
|
||||
"selected_few": "{{count}} wybrane tagi",
|
||||
"selected_many": "{{count}} wybranych tagów"
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
"title": "Źródła marketplace",
|
||||
"description": "Dodaj lub zarządzaj źródłami elementów marketplace. Każde źródło to repozytorium Git zawierające definicje elementów marketplace.",
|
||||
"errors": {
|
||||
"maxSources": "Maksymalnie {{max}} źródeł dozwolonych.",
|
||||
"emptyUrl": "URL nie może być pusty.",
|
||||
"nonVisibleChars": "URL zawiera niewidoczne znaki.",
|
||||
"invalidGitUrl": "Nieprawidłowy format URL Git.",
|
||||
"duplicateUrl": "Źródło z tym URL już istnieje.",
|
||||
"nameTooLong": "Nazwa nie może przekraczać 20 znaków.",
|
||||
"nonVisibleCharsName": "Nazwa zawiera niewidoczne znaki.",
|
||||
"duplicateName": "Źródło z tą nazwą już istnieje."
|
||||
},
|
||||
"add": {
|
||||
"namePlaceholder": "Opcjonalna nazwa źródła (np. 'Moje prywatne repo')",
|
||||
"urlPlaceholder": "URL repozytorium Git (np. 'https://github.com/user/repo.git')",
|
||||
"urlFormats": "Obsługiwane formaty: HTTPS, SSH lub lokalna ścieżka pliku.",
|
||||
"button": "Dodaj źródło"
|
||||
},
|
||||
"current": {
|
||||
"title": "Aktualne źródła",
|
||||
"empty": "Nie dodano jeszcze żadnych źródeł marketplace.",
|
||||
"emptyHint": "Dodaj źródło powyżej, aby przeglądać elementy marketplace.",
|
||||
"refresh": "Odśwież źródło",
|
||||
"remove": "Usuń źródło"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"browse": "Przeglądaj",
|
||||
"sources": "Źródła"
|
||||
},
|
||||
"title": "Marketplace",
|
||||
"items": {
|
||||
"refresh": {
|
||||
"refreshing": "Odświeżanie elementów marketplace..."
|
||||
},
|
||||
"empty": {
|
||||
"noItems": "Nie znaleziono elementów marketplace.",
|
||||
"emptyHint": "Spróbuj dostosować filtry lub wyszukiwane hasła"
|
||||
},
|
||||
"count_one": "{{count}} element",
|
||||
"count_few": "{{count}} elementy",
|
||||
"count_many": "{{count}} elementów"
|
||||
}
|
||||
}
|
||||
100
src/i18n/locales/pt-BR/marketplace.json
Normal file
100
src/i18n/locales/pt-BR/marketplace.json
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
{
|
||||
"type-group": {
|
||||
"modes": "Modos",
|
||||
"mcps": "Servidores MCP",
|
||||
"prompts": "Prompts",
|
||||
"packages": "Pacotes",
|
||||
"generic-type": "{{type}}s",
|
||||
"match": "correspondência"
|
||||
},
|
||||
"item-card": {
|
||||
"type-mode": "Modo",
|
||||
"type-mcp": "Servidor MCP",
|
||||
"type-prompt": "Prompt",
|
||||
"type-package": "Pacote",
|
||||
"type-other": "Outro",
|
||||
"by-author": "por {{author}}",
|
||||
"authors-profile": "Perfil do Autor",
|
||||
"remove-tag-filter": "Remover filtro de tag: {{tag}}",
|
||||
"filter-by-tag": "Filtrar por tag: {{tag}}",
|
||||
"component-details": "Detalhes do Componente",
|
||||
"match-count": "{{count}} correspondência{{count !== 1 ? 's' : ''}}",
|
||||
"view": "Visualizar",
|
||||
"source": "Fonte"
|
||||
},
|
||||
"install-sidebar": {
|
||||
"title": "Instalar {{itemName}}",
|
||||
"installButton": "Instalar",
|
||||
"cancelButton": "Cancelar"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "Buscar no marketplace..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Tipo",
|
||||
"all": "Todos os tipos",
|
||||
"mode": "Modo",
|
||||
"mcp server": "Servidor MCP",
|
||||
"prompt": "Prompt",
|
||||
"package": "Pacote"
|
||||
},
|
||||
"sort": {
|
||||
"label": "Ordenar por",
|
||||
"name": "Nome",
|
||||
"lastUpdated": "Última atualização"
|
||||
},
|
||||
"tags": {
|
||||
"label": "Tags",
|
||||
"clear_one": "Limpar {{count}} tag selecionada",
|
||||
"clear_other": "Limpar {{count}} tags selecionadas",
|
||||
"placeholder": "Buscar tags...",
|
||||
"noResults": "Nenhuma tag encontrada.",
|
||||
"selected_one": "{{count}} tag selecionada",
|
||||
"selected_other": "{{count}} tags selecionadas"
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
"title": "Fontes do marketplace",
|
||||
"description": "Adicione ou gerencie fontes para os itens do marketplace. Cada fonte é um repositório Git contendo definições de itens do marketplace.",
|
||||
"errors": {
|
||||
"maxSources": "Máximo de {{max}} fontes permitidas.",
|
||||
"emptyUrl": "A URL não pode estar vazia.",
|
||||
"nonVisibleChars": "A URL contém caracteres não visíveis.",
|
||||
"invalidGitUrl": "Formato de URL Git inválido.",
|
||||
"duplicateUrl": "Uma fonte com esta URL já existe.",
|
||||
"nameTooLong": "O nome não pode exceder 20 caracteres.",
|
||||
"nonVisibleCharsName": "O nome contém caracteres não visíveis.",
|
||||
"duplicateName": "Uma fonte com este nome já existe."
|
||||
},
|
||||
"add": {
|
||||
"namePlaceholder": "Nome opcional da fonte (ex: 'Meu Repositório Privado')",
|
||||
"urlPlaceholder": "URL do repositório Git (ex: 'https://github.com/user/repo.git')",
|
||||
"urlFormats": "Formatos suportados: HTTPS, SSH ou caminho de arquivo local.",
|
||||
"button": "Adicionar fonte"
|
||||
},
|
||||
"current": {
|
||||
"title": "Fontes atuais",
|
||||
"empty": "Nenhuma fonte do marketplace adicionada ainda.",
|
||||
"emptyHint": "Adicione uma fonte acima para navegar pelos itens do marketplace.",
|
||||
"refresh": "Atualizar fonte",
|
||||
"remove": "Remover fonte"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"browse": "Navegar",
|
||||
"sources": "Fontes"
|
||||
},
|
||||
"title": "Marketplace",
|
||||
"items": {
|
||||
"refresh": {
|
||||
"refreshing": "Atualizando itens do marketplace..."
|
||||
},
|
||||
"empty": {
|
||||
"noItems": "Nenhum item do marketplace encontrado.",
|
||||
"emptyHint": "Tente ajustar seus filtros ou termos de busca"
|
||||
},
|
||||
"count_one": "{{count}} item",
|
||||
"count_other": "{{count}} itens"
|
||||
}
|
||||
}
|
||||
105
src/i18n/locales/ru/marketplace.json
Normal file
105
src/i18n/locales/ru/marketplace.json
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
{
|
||||
"type-group": {
|
||||
"modes": "Режимы",
|
||||
"mcps": "MCP-серверы",
|
||||
"prompts": "Промпты",
|
||||
"packages": "Пакеты",
|
||||
"generic-type": "{{type}}",
|
||||
"match": "совпадение"
|
||||
},
|
||||
"item-card": {
|
||||
"type-mode": "Режим",
|
||||
"type-mcp": "MCP-сервер",
|
||||
"type-prompt": "Промпт",
|
||||
"type-package": "Пакет",
|
||||
"type-other": "Другое",
|
||||
"by-author": "от {{author}}",
|
||||
"authors-profile": "Профиль автора",
|
||||
"remove-tag-filter": "Удалить фильтр по тегу: {{tag}}",
|
||||
"filter-by-tag": "Фильтровать по тегу: {{tag}}",
|
||||
"component-details": "Детали компонента",
|
||||
"match-count_one": "{{count}} совпадение",
|
||||
"match-count_few": "{{count}} совпадения",
|
||||
"match-count_many": "{{count}} совпадений",
|
||||
"view": "Просмотр",
|
||||
"source": "Источник"
|
||||
},
|
||||
"install-sidebar": {
|
||||
"title": "Установить {{itemName}}",
|
||||
"installButton": "Установить",
|
||||
"cancelButton": "Отмена"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "Поиск по маркетплейсу..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Тип",
|
||||
"all": "Все типы",
|
||||
"mode": "Режим",
|
||||
"mcp server": "MCP-сервер",
|
||||
"prompt": "Промпт",
|
||||
"package": "Пакет"
|
||||
},
|
||||
"sort": {
|
||||
"label": "Сортировать по",
|
||||
"name": "Имя",
|
||||
"lastUpdated": "Последнее обновление"
|
||||
},
|
||||
"tags": {
|
||||
"label": "Теги",
|
||||
"clear_one": "Очистить {{count}} выбранный тег",
|
||||
"clear_few": "Очистить {{count}} выбранных тега",
|
||||
"clear_many": "Очистить {{count}} выбранных тегов",
|
||||
"placeholder": "Поиск тегов...",
|
||||
"noResults": "Теги не найдены.",
|
||||
"selected_one": "{{count}} выбранный тег",
|
||||
"selected_few": "{{count}} выбранных тега",
|
||||
"selected_many": "{{count}} выбранных тегов"
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
"title": "Источники маркетплейса",
|
||||
"description": "Добавляйте или управляйте источниками элементов маркетплейса. Каждый источник — это репозиторий Git, содержащий определения элементов маркетплейса.",
|
||||
"errors": {
|
||||
"maxSources": "Разрешено не более {{max}} источников.",
|
||||
"emptyUrl": "URL не может быть пустым.",
|
||||
"nonVisibleChars": "URL содержит невидимые символы.",
|
||||
"invalidGitUrl": "Неверный формат URL Git.",
|
||||
"duplicateUrl": "Источник с таким URL уже существует.",
|
||||
"nameTooLong": "Имя не может превышать 20 символов.",
|
||||
"nonVisibleCharsName": "Имя содержит невидимые символы.",
|
||||
"duplicateName": "Источник с таким именем уже существует."
|
||||
},
|
||||
"add": {
|
||||
"namePlaceholder": "Необязательное имя источника (например, 'Мой приватный репозиторий')",
|
||||
"urlPlaceholder": "URL репозитория Git (например, 'https://github.com/user/repo.git')",
|
||||
"urlFormats": "Поддерживаемые форматы: HTTPS, SSH или локальный путь к файлу.",
|
||||
"button": "Добавить источник"
|
||||
},
|
||||
"current": {
|
||||
"title": "Текущие источники",
|
||||
"empty": "Источники маркетплейса еще не добавлены.",
|
||||
"emptyHint": "Добавьте источник выше, чтобы просмотреть элементы маркетплейса.",
|
||||
"refresh": "Обновить источник",
|
||||
"remove": "Удалить источник"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"browse": "Обзор",
|
||||
"sources": "Источники"
|
||||
},
|
||||
"title": "Маркетплейс",
|
||||
"items": {
|
||||
"refresh": {
|
||||
"refreshing": "Обновление элементов маркетплейса..."
|
||||
},
|
||||
"empty": {
|
||||
"noItems": "Элементы маркетплейса не найдены.",
|
||||
"emptyHint": "Попробуйте изменить фильтры или условия поиска"
|
||||
},
|
||||
"count_one": "{{count}} элемент",
|
||||
"count_few": "{{count}} элемента",
|
||||
"count_many": "{{count}} элементов"
|
||||
}
|
||||
}
|
||||
97
src/i18n/locales/tr/marketplace.json
Normal file
97
src/i18n/locales/tr/marketplace.json
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
{
|
||||
"type-group": {
|
||||
"modes": "Modlar",
|
||||
"mcps": "MCP Sunucuları",
|
||||
"prompts": "Komutlar",
|
||||
"packages": "Paketler",
|
||||
"generic-type": "{{type}}lar",
|
||||
"match": "eşleşme"
|
||||
},
|
||||
"item-card": {
|
||||
"type-mode": "Mod",
|
||||
"type-mcp": "MCP Sunucusu",
|
||||
"type-prompt": "Komut",
|
||||
"type-package": "Paket",
|
||||
"type-other": "Diğer",
|
||||
"by-author": "yazar: {{author}}",
|
||||
"authors-profile": "Yazar Profili",
|
||||
"remove-tag-filter": "Etiket filtresini kaldır: {{tag}}",
|
||||
"filter-by-tag": "Etikete göre filtrele: {{tag}}",
|
||||
"component-details": "Bileşen Detayları",
|
||||
"match-count": "{{count}} eşleşme",
|
||||
"view": "Görüntüle",
|
||||
"source": "Kaynak"
|
||||
},
|
||||
"install-sidebar": {
|
||||
"title": "{{itemName}} Yükle",
|
||||
"installButton": "Yükle",
|
||||
"cancelButton": "İptal"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "Marketplace ara..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Tip",
|
||||
"all": "Tüm Tipler",
|
||||
"mode": "Mod",
|
||||
"mcp server": "MCP Sunucusu",
|
||||
"prompt": "Komut",
|
||||
"package": "Paket"
|
||||
},
|
||||
"sort": {
|
||||
"label": "Sırala",
|
||||
"name": "Ad",
|
||||
"lastUpdated": "Son Güncelleme"
|
||||
},
|
||||
"tags": {
|
||||
"label": "Etiketler",
|
||||
"clear": "{{count}} seçili etiketi temizle",
|
||||
"placeholder": "Etiket ara...",
|
||||
"noResults": "Etiket bulunamadı.",
|
||||
"selected": "{{count}} etiket seçili"
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
"title": "Marketplace Kaynakları",
|
||||
"description": "Marketplace öğeleri için kaynak ekleyin veya yönetin. Her kaynak, marketplace öğe tanımlarını içeren bir Git deposudur.",
|
||||
"errors": {
|
||||
"maxSources": "Maksimum {{max}} kaynağa izin verilir.",
|
||||
"emptyUrl": "URL boş olamaz.",
|
||||
"nonVisibleChars": "URL görünmez karakterler içeriyor.",
|
||||
"invalidGitUrl": "Geçersiz Git URL formatı.",
|
||||
"duplicateUrl": "Bu URL'ye sahip bir kaynak zaten var.",
|
||||
"nameTooLong": "Ad 20 karakteri geçemez.",
|
||||
"nonVisibleCharsName": "Ad görünmez karakterler içeriyor.",
|
||||
"duplicateName": "Bu ada sahip bir kaynak zaten var."
|
||||
},
|
||||
"add": {
|
||||
"namePlaceholder": "İsteğe bağlı kaynak adı (örn. 'Özel Depom')",
|
||||
"urlPlaceholder": "Git deposu URL'si (örn. 'https://github.com/user/repo.git')",
|
||||
"urlFormats": "Desteklenen formatlar: HTTPS, SSH veya yerel dosya yolu.",
|
||||
"button": "Kaynak Ekle"
|
||||
},
|
||||
"current": {
|
||||
"title": "Mevcut Kaynaklar",
|
||||
"empty": "Henüz marketplace kaynağı eklenmedi.",
|
||||
"emptyHint": "Marketplace öğelerine göz atmak için yukarıdan bir kaynak ekleyin.",
|
||||
"refresh": "Kaynağı yenile",
|
||||
"remove": "Kaynağı kaldır"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"browse": "Göz At",
|
||||
"sources": "Kaynaklar"
|
||||
},
|
||||
"title": "Marketplace",
|
||||
"items": {
|
||||
"refresh": {
|
||||
"refreshing": "Marketplace öğeleri yenileniyor..."
|
||||
},
|
||||
"empty": {
|
||||
"noItems": "Marketplace öğesi bulunamadı.",
|
||||
"emptyHint": "Filtrelerinizi veya arama terimlerinizi ayarlamayı deneyin"
|
||||
},
|
||||
"count": "{{count}} öğe"
|
||||
}
|
||||
}
|
||||
97
src/i18n/locales/vi/marketplace.json
Normal file
97
src/i18n/locales/vi/marketplace.json
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
{
|
||||
"type-group": {
|
||||
"modes": "Chế độ",
|
||||
"mcps": "Máy chủ MCP",
|
||||
"prompts": "Gợi ý",
|
||||
"packages": "Gói",
|
||||
"generic-type": "{{type}}",
|
||||
"match": "phù hợp"
|
||||
},
|
||||
"item-card": {
|
||||
"type-mode": "Chế độ",
|
||||
"type-mcp": "Máy chủ MCP",
|
||||
"type-prompt": "Gợi ý",
|
||||
"type-package": "Gói",
|
||||
"type-other": "Khác",
|
||||
"by-author": "bởi {{author}}",
|
||||
"authors-profile": "Hồ sơ tác giả",
|
||||
"remove-tag-filter": "Xóa bộ lọc thẻ: {{tag}}",
|
||||
"filter-by-tag": "Lọc theo thẻ: {{tag}}",
|
||||
"component-details": "Chi tiết thành phần",
|
||||
"match-count": "{{count}} kết quả phù hợp",
|
||||
"view": "Xem",
|
||||
"source": "Nguồn"
|
||||
},
|
||||
"install-sidebar": {
|
||||
"title": "Cài đặt {{itemName}}",
|
||||
"installButton": "Cài đặt",
|
||||
"cancelButton": "Hủy"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "Tìm kiếm marketplace..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Loại",
|
||||
"all": "Tất cả các loại",
|
||||
"mode": "Chế độ",
|
||||
"mcp server": "Máy chủ MCP",
|
||||
"prompt": "Gợi ý",
|
||||
"package": "Gói"
|
||||
},
|
||||
"sort": {
|
||||
"label": "Sắp xếp theo",
|
||||
"name": "Tên",
|
||||
"lastUpdated": "Cập nhật lần cuối"
|
||||
},
|
||||
"tags": {
|
||||
"label": "Thẻ",
|
||||
"clear": "Xóa {{count}} thẻ đã chọn",
|
||||
"placeholder": "Tìm kiếm thẻ...",
|
||||
"noResults": "Không tìm thấy thẻ nào.",
|
||||
"selected": "{{count}} thẻ đã chọn"
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
"title": "Nguồn marketplace",
|
||||
"description": "Thêm hoặc quản lý các nguồn cho các mục marketplace. Mỗi nguồn là một kho lưu trữ Git chứa các định nghĩa mục marketplace.",
|
||||
"errors": {
|
||||
"maxSources": "Tối đa {{max}} nguồn được phép.",
|
||||
"emptyUrl": "URL không được để trống.",
|
||||
"nonVisibleChars": "URL chứa các ký tự không hiển thị.",
|
||||
"invalidGitUrl": "Định dạng URL Git không hợp lệ.",
|
||||
"duplicateUrl": "Nguồn với URL này đã tồn tại.",
|
||||
"nameTooLong": "Tên không được vượt quá 20 ký tự.",
|
||||
"nonVisibleCharsName": "Tên chứa các ký tự không hiển thị.",
|
||||
"duplicateName": "Nguồn với tên này đã tồn tại."
|
||||
},
|
||||
"add": {
|
||||
"namePlaceholder": "Tên nguồn tùy chọn (ví dụ: 'Kho lưu trữ riêng của tôi')",
|
||||
"urlPlaceholder": "URL kho lưu trữ Git (ví dụ: 'https://github.com/user/repo.git')",
|
||||
"urlFormats": "Các định dạng được hỗ trợ: HTTPS, SSH hoặc đường dẫn tệp cục bộ.",
|
||||
"button": "Thêm nguồn"
|
||||
},
|
||||
"current": {
|
||||
"title": "Nguồn hiện tại",
|
||||
"empty": "Chưa có nguồn marketplace nào được thêm.",
|
||||
"emptyHint": "Thêm nguồn ở trên để duyệt các mục marketplace.",
|
||||
"refresh": "Làm mới nguồn",
|
||||
"remove": "Xóa nguồn"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"browse": "Duyệt",
|
||||
"sources": "Nguồn"
|
||||
},
|
||||
"title": "Marketplace",
|
||||
"items": {
|
||||
"refresh": {
|
||||
"refreshing": "Đang làm mới các mục marketplace..."
|
||||
},
|
||||
"empty": {
|
||||
"noItems": "Không tìm thấy mục marketplace nào.",
|
||||
"emptyHint": "Thử điều chỉnh bộ lọc hoặc cụm từ tìm kiếm của bạn"
|
||||
},
|
||||
"count": "{{count}} mục"
|
||||
}
|
||||
}
|
||||
97
src/i18n/locales/zh-CN/marketplace.json
Normal file
97
src/i18n/locales/zh-CN/marketplace.json
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
{
|
||||
"type-group": {
|
||||
"modes": "模式",
|
||||
"mcps": "MCP服务器",
|
||||
"prompts": "提示",
|
||||
"packages": "包",
|
||||
"generic-type": "{{type}}",
|
||||
"match": "匹配"
|
||||
},
|
||||
"item-card": {
|
||||
"type-mode": "模式",
|
||||
"type-mcp": "MCP服务器",
|
||||
"type-prompt": "提示",
|
||||
"type-package": "包",
|
||||
"type-other": "其他",
|
||||
"by-author": "作者:{{author}}",
|
||||
"authors-profile": "作者主页",
|
||||
"remove-tag-filter": "移除标签过滤器:{{tag}}",
|
||||
"filter-by-tag": "按标签过滤:{{tag}}",
|
||||
"component-details": "组件详情",
|
||||
"match-count": "{{count}}个匹配",
|
||||
"view": "查看",
|
||||
"source": "源码"
|
||||
},
|
||||
"install-sidebar": {
|
||||
"title": "安装 {{itemName}}",
|
||||
"installButton": "安装",
|
||||
"cancelButton": "取消"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "搜索应用市场..."
|
||||
},
|
||||
"type": {
|
||||
"label": "类型",
|
||||
"all": "所有类型",
|
||||
"mode": "模式",
|
||||
"mcp server": "MCP 服务",
|
||||
"prompt": "提示词",
|
||||
"package": "包"
|
||||
},
|
||||
"sort": {
|
||||
"label": "排序方式",
|
||||
"name": "名称",
|
||||
"lastUpdated": "最后更新"
|
||||
},
|
||||
"tags": {
|
||||
"label": "标签",
|
||||
"clear": "清除 {{count}} 个已选标签",
|
||||
"placeholder": "搜索标签...",
|
||||
"noResults": "未找到标签。",
|
||||
"selected": "已选 {{count}} 个标签"
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
"title": "应用市场源",
|
||||
"description": "添加或管理应用市场项的来源。每个来源都是一个包含应用市场项定义的 Git 仓库。",
|
||||
"errors": {
|
||||
"maxSources": "最多允许 {{max}} 个来源。",
|
||||
"emptyUrl": "URL 不能为空。",
|
||||
"nonVisibleChars": "URL 包含不可见字符。",
|
||||
"invalidGitUrl": "无效的 Git URL 格式。",
|
||||
"duplicateUrl": "具有此 URL 的来源已存在。",
|
||||
"nameTooLong": "名称不能超过 20 个字符。",
|
||||
"nonVisibleCharsName": "名称包含不可见字符。",
|
||||
"duplicateName": "具有此名称的来源已存在。"
|
||||
},
|
||||
"add": {
|
||||
"namePlaceholder": "可选来源名称(例如 '我的私有仓库')",
|
||||
"urlPlaceholder": "Git 仓库 URL(例如 'https://github.com/user/repo.git')",
|
||||
"urlFormats": "支持的格式:HTTPS、SSH 或本地文件路径。",
|
||||
"button": "添加来源"
|
||||
},
|
||||
"current": {
|
||||
"title": "当前来源",
|
||||
"empty": "尚未添加应用市场来源。",
|
||||
"emptyHint": "在上方添加来源以浏览应用市场项。",
|
||||
"refresh": "刷新来源",
|
||||
"remove": "移除来源"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"browse": "浏览",
|
||||
"sources": "来源"
|
||||
},
|
||||
"title": "应用市场",
|
||||
"items": {
|
||||
"refresh": {
|
||||
"refreshing": "正在刷新应用市场项..."
|
||||
},
|
||||
"empty": {
|
||||
"noItems": "未找到应用市场项。",
|
||||
"emptyHint": "尝试调整您的过滤器或搜索词"
|
||||
},
|
||||
"count": "{{count}} 项"
|
||||
}
|
||||
}
|
||||
97
src/i18n/locales/zh-TW/marketplace.json
Normal file
97
src/i18n/locales/zh-TW/marketplace.json
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
{
|
||||
"type-group": {
|
||||
"modes": "模式",
|
||||
"mcps": "MCP伺服器",
|
||||
"prompts": "提示",
|
||||
"packages": "套件",
|
||||
"generic-type": "{{type}}",
|
||||
"match": "符合"
|
||||
},
|
||||
"item-card": {
|
||||
"type-mode": "模式",
|
||||
"type-mcp": "MCP伺服器",
|
||||
"type-prompt": "提示",
|
||||
"type-package": "套件",
|
||||
"type-other": "其他",
|
||||
"by-author": "作者:{{author}}",
|
||||
"authors-profile": "作者個人檔案",
|
||||
"remove-tag-filter": "移除標籤篩選:{{tag}}",
|
||||
"filter-by-tag": "依標籤篩選:{{tag}}",
|
||||
"component-details": "元件詳細資訊",
|
||||
"match-count": "{{count}}個符合",
|
||||
"view": "檢視",
|
||||
"source": "原始碼"
|
||||
},
|
||||
"install-sidebar": {
|
||||
"title": "安裝 {{itemName}}",
|
||||
"installButton": "安裝",
|
||||
"cancelButton": "取消"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "搜尋市集..."
|
||||
},
|
||||
"type": {
|
||||
"label": "類型",
|
||||
"all": "所有類型",
|
||||
"mode": "模式",
|
||||
"mcp server": "MCP 伺服器",
|
||||
"prompt": "提示",
|
||||
"package": "套件"
|
||||
},
|
||||
"sort": {
|
||||
"label": "排序依據",
|
||||
"name": "名稱",
|
||||
"lastUpdated": "上次更新"
|
||||
},
|
||||
"tags": {
|
||||
"label": "標籤",
|
||||
"clear": "清除 {{count}} 個選取的標籤",
|
||||
"placeholder": "搜尋標籤...",
|
||||
"noResults": "找不到標籤。",
|
||||
"selected": "已選取 {{count}} 個標籤"
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
"title": "市集來源",
|
||||
"description": "新增或管理市集項目的來源。每個來源都是一個包含市集項目定義的 Git 儲存庫。",
|
||||
"errors": {
|
||||
"maxSources": "最多允許 {{max}} 個來源。",
|
||||
"emptyUrl": "URL 不能為空。",
|
||||
"nonVisibleChars": "URL 包含不可見字元。",
|
||||
"invalidGitUrl": "無效的 Git URL 格式。",
|
||||
"duplicateUrl": "具有此 URL 的來源已存在。",
|
||||
"nameTooLong": "名稱不能超過 20 個字元。",
|
||||
"nonVisibleCharsName": "名稱包含不可見字元。",
|
||||
"duplicateName": "具有此名稱的來源已存在。"
|
||||
},
|
||||
"add": {
|
||||
"namePlaceholder": "選用來源名稱(例如 '我的私人儲存庫')",
|
||||
"urlPlaceholder": "Git 儲存庫 URL(例如 'https://github.com/user/repo.git')",
|
||||
"urlFormats": "支援的格式:HTTPS、SSH 或本機檔案路徑。",
|
||||
"button": "新增來源"
|
||||
},
|
||||
"current": {
|
||||
"title": "目前來源",
|
||||
"empty": "尚未新增市集來源。",
|
||||
"emptyHint": "在上方新增來源以瀏覽市集項目。",
|
||||
"refresh": "重新整理來源",
|
||||
"remove": "移除來源"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"browse": "瀏覽",
|
||||
"sources": "來源"
|
||||
},
|
||||
"title": "市集",
|
||||
"items": {
|
||||
"refresh": {
|
||||
"refreshing": "正在重新整理市集項目..."
|
||||
},
|
||||
"empty": {
|
||||
"noItems": "找不到市集項目。",
|
||||
"emptyHint": "嘗試調整您的篩選器或搜尋詞"
|
||||
},
|
||||
"count": "{{count}} 個項目"
|
||||
}
|
||||
}
|
||||
|
|
@ -90,6 +90,11 @@
|
|||
"title": "%command.history.title%",
|
||||
"icon": "$(history)"
|
||||
},
|
||||
{
|
||||
"command": "roo-cline.marketplaceButtonClicked",
|
||||
"title": "Marketplace",
|
||||
"icon": "$(extensions)"
|
||||
},
|
||||
{
|
||||
"command": "roo-cline.popoutButtonClicked",
|
||||
"title": "%command.openInEditor.title%",
|
||||
|
|
@ -232,6 +237,11 @@
|
|||
"command": "roo-cline.settingsButtonClicked",
|
||||
"group": "navigation@6",
|
||||
"when": "view == roo-cline.SidebarProvider"
|
||||
},
|
||||
{
|
||||
"command": "roo-cline.marketplaceButtonClicked",
|
||||
"group": "navigation@7",
|
||||
"when": "view == roo-cline.SidebarProvider"
|
||||
}
|
||||
],
|
||||
"editor/title": [
|
||||
|
|
@ -264,6 +274,11 @@
|
|||
"command": "roo-cline.settingsButtonClicked",
|
||||
"group": "navigation@6",
|
||||
"when": "activeWebviewPanelId == roo-cline.TabPanelProvider"
|
||||
},
|
||||
{
|
||||
"command": "roo-cline.marketplaceButtonClicked",
|
||||
"group": "navigation@7",
|
||||
"when": "activeWebviewPanelId == roo-cline.TabPanelProvider"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -75,6 +75,8 @@ export const commandIds = [
|
|||
|
||||
"focusInput",
|
||||
"acceptInput",
|
||||
|
||||
"marketplaceButtonClicked",
|
||||
] as const
|
||||
|
||||
export type CommandId = (typeof commandIds)[number]
|
||||
|
|
@ -800,6 +802,15 @@ export const globalSettingsSchema = z.object({
|
|||
customModePrompts: customModePromptsSchema.optional(),
|
||||
customSupportPrompts: customSupportPromptsSchema.optional(),
|
||||
enhancementApiConfigId: z.string().optional(),
|
||||
marketplaceSources: z
|
||||
.array(
|
||||
z.object({
|
||||
url: z.string(),
|
||||
name: z.string().optional(),
|
||||
enabled: z.boolean(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
historyPreviewCollapsed: z.boolean().optional(),
|
||||
})
|
||||
|
||||
|
|
@ -881,6 +892,7 @@ const globalSettingsRecord: GlobalSettingsRecord = {
|
|||
customSupportPrompts: undefined,
|
||||
enhancementApiConfigId: undefined,
|
||||
cachedChromeHostUrl: undefined,
|
||||
marketplaceSources: undefined,
|
||||
historyPreviewCollapsed: undefined,
|
||||
}
|
||||
|
||||
|
|
|
|||
317
src/services/marketplace/GitFetcher.ts
Normal file
317
src/services/marketplace/GitFetcher.ts
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
import * as fs from "fs/promises"
|
||||
import * as yaml from "js-yaml"
|
||||
import simpleGit, { SimpleGit } from "simple-git"
|
||||
import { MetadataScanner } from "./MetadataScanner"
|
||||
import { validateAnyMetadata } from "./schemas"
|
||||
import { LocalizationOptions, MarketplaceItem, MarketplaceRepository, RepositoryMetadata } from "./types"
|
||||
import { getUserLocale } from "./utils"
|
||||
|
||||
/**
|
||||
* Handles fetching and caching marketplace repositories
|
||||
*/
|
||||
export class GitFetcher {
|
||||
private readonly cacheDir: string
|
||||
private metadataScanner: MetadataScanner
|
||||
private git?: SimpleGit
|
||||
private localizationOptions: LocalizationOptions
|
||||
private activeGitInstances: Set<SimpleGit> = new Set()
|
||||
|
||||
constructor(context: vscode.ExtensionContext, localizationOptions?: LocalizationOptions) {
|
||||
this.cacheDir = path.join(context.globalStorageUri.fsPath, "marketplace-cache")
|
||||
this.localizationOptions = localizationOptions || {
|
||||
userLocale: getUserLocale(),
|
||||
fallbackLocale: "en",
|
||||
}
|
||||
this.metadataScanner = new MetadataScanner(undefined, this.localizationOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources
|
||||
*/
|
||||
dispose(): void {
|
||||
// Clean up all git instances
|
||||
this.activeGitInstances.forEach((git) => {
|
||||
try {
|
||||
// Force cleanup of git instance
|
||||
;(git as any)._executor = null
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
})
|
||||
this.activeGitInstances.clear()
|
||||
|
||||
// Clean up metadata scanner
|
||||
if (this.metadataScanner) {
|
||||
this.metadataScanner = null as any
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize git instance for a repository
|
||||
* @param repoDir Repository directory
|
||||
*/
|
||||
private initGit(repoDir: string): void {
|
||||
// Clean up old git instance if it exists
|
||||
if (this.git) {
|
||||
this.activeGitInstances.delete(this.git)
|
||||
try {
|
||||
// Force cleanup of git instance
|
||||
;(this.git as any)._executor = null
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
|
||||
// Create new git instance
|
||||
this.git = simpleGit(repoDir)
|
||||
this.activeGitInstances.add(this.git)
|
||||
|
||||
// Update MetadataScanner with new git instance
|
||||
const oldScanner = this.metadataScanner
|
||||
this.metadataScanner = new MetadataScanner(this.git, this.localizationOptions)
|
||||
|
||||
// Clean up old scanner
|
||||
if (oldScanner) {
|
||||
oldScanner.dispose?.()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch repository data
|
||||
* @param repoUrl Repository URL
|
||||
* @param forceRefresh Whether to bypass cache
|
||||
* @param sourceName Optional source repository name
|
||||
* @returns Repository data
|
||||
*/
|
||||
async fetchRepository(repoUrl: string, forceRefresh = false, sourceName?: string): Promise<MarketplaceRepository> {
|
||||
// Ensure cache directory exists
|
||||
await fs.mkdir(this.cacheDir, { recursive: true })
|
||||
|
||||
// Get repository directory name from URL
|
||||
const repoName = this.getRepositoryName(repoUrl)
|
||||
const repoDir = path.join(this.cacheDir, repoName)
|
||||
|
||||
// Clone or pull repository
|
||||
await this.cloneOrPullRepository(repoUrl, repoDir, forceRefresh)
|
||||
|
||||
// Initialize git for this repository
|
||||
this.initGit(repoDir)
|
||||
|
||||
// Find the registry dir
|
||||
const registryDir = await this.findRegistryDir(repoDir)
|
||||
|
||||
// Validate repository structure
|
||||
await this.validateRegistryStructure(registryDir)
|
||||
|
||||
// Parse repository metadata
|
||||
const metadata = await this.parseRepositoryMetadata(registryDir)
|
||||
|
||||
// Parse marketplace items
|
||||
// Get current branch using existing git instance
|
||||
const branch = (await this.git?.revparse(["--abbrev-ref", "HEAD"])) || "main"
|
||||
|
||||
const items = await this.parseMarketplaceItems(registryDir, repoUrl, sourceName || metadata.name)
|
||||
|
||||
return {
|
||||
metadata,
|
||||
items: items.map((item) => ({ ...item, defaultBranch: branch })),
|
||||
url: repoUrl,
|
||||
defaultBranch: branch,
|
||||
}
|
||||
}
|
||||
|
||||
async findRegistryDir(repoDir: string) {
|
||||
const isRoot = await fs
|
||||
.stat(path.join(repoDir, "metadata.en.yml"))
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
|
||||
if (isRoot) return repoDir
|
||||
|
||||
const isRegistrySubdir = await fs
|
||||
.stat(path.join(repoDir, "registry", "metadata.en.yml"))
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
|
||||
if (isRegistrySubdir) return path.join(repoDir, "registry")
|
||||
|
||||
throw new Error('Invalid repository structure: could not find "registry" metadata')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get repository name from URL
|
||||
* @param repoUrl Repository URL
|
||||
* @returns Repository name
|
||||
*/
|
||||
private getRepositoryName(repoUrl: string): string {
|
||||
const match = repoUrl.match(/\/([^/]+?)(?:\.git)?$/)
|
||||
if (!match) {
|
||||
throw new Error(`Invalid repository URL: ${repoUrl}`)
|
||||
}
|
||||
return match[1]
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone or pull repository
|
||||
* @param repoUrl Repository URL
|
||||
* @param repoDir Repository directory
|
||||
* @param forceRefresh Whether to force refresh
|
||||
*/
|
||||
/**
|
||||
* Clean up any git lock files in the repository
|
||||
* @param repoDir Repository directory
|
||||
*/
|
||||
private async cleanupGitLocks(repoDir: string): Promise<void> {
|
||||
const indexLockPath = path.join(repoDir, ".git", "index.lock")
|
||||
try {
|
||||
await fs.unlink(indexLockPath)
|
||||
} catch {
|
||||
// Ignore errors if file doesn't exist
|
||||
}
|
||||
}
|
||||
|
||||
private async cloneOrPullRepository(repoUrl: string, repoDir: string, forceRefresh: boolean): Promise<void> {
|
||||
try {
|
||||
// Clean up any existing git lock files first
|
||||
await this.cleanupGitLocks(repoDir)
|
||||
// Check if repository exists
|
||||
const gitDir = path.join(repoDir, ".git")
|
||||
let repoExists = await fs
|
||||
.stat(gitDir)
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
|
||||
if (repoExists && !forceRefresh) {
|
||||
try {
|
||||
// Pull latest changes
|
||||
const git = simpleGit(repoDir)
|
||||
// Force pull with overwrite
|
||||
await git.fetch("origin", "main")
|
||||
await git.raw(["reset", "--hard", "origin/main"])
|
||||
await git.raw(["clean", "-f", "-d"])
|
||||
} catch (error) {
|
||||
// Clean up git locks before retrying
|
||||
await this.cleanupGitLocks(repoDir)
|
||||
// If pull fails with specific errors that indicate repo corruption,
|
||||
// we should remove and re-clone
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
if (
|
||||
errorMessage.includes("not a git repository") ||
|
||||
errorMessage.includes("repository not found") ||
|
||||
errorMessage.includes("refusing to merge unrelated histories")
|
||||
) {
|
||||
await fs.rm(repoDir, { recursive: true, force: true })
|
||||
repoExists = false
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!repoExists || forceRefresh) {
|
||||
try {
|
||||
// Clean up any existing git lock files
|
||||
const indexLockPath = path.join(repoDir, ".git", "index.lock")
|
||||
try {
|
||||
await fs.unlink(indexLockPath)
|
||||
} catch {
|
||||
// Ignore errors if file doesn't exist
|
||||
}
|
||||
|
||||
// Always remove the directory before cloning
|
||||
await fs.rm(repoDir, { recursive: true, force: true })
|
||||
|
||||
// Add a small delay to ensure directory is fully cleaned up
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Verify directory is gone before proceeding
|
||||
const dirExists = await fs
|
||||
.stat(repoDir)
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
if (dirExists) {
|
||||
throw new Error("Failed to clean up directory before cloning")
|
||||
}
|
||||
|
||||
// Clone repository
|
||||
const git = simpleGit()
|
||||
// Clone with force options
|
||||
await git.clone(repoUrl, repoDir)
|
||||
// Reset to ensure clean state
|
||||
const repoGit = simpleGit(repoDir)
|
||||
await repoGit.raw(["clean", "-f", "-d"])
|
||||
await repoGit.raw(["reset", "--hard", "HEAD"])
|
||||
} catch (error) {
|
||||
// If clone fails, ensure we clean up any partially created directory
|
||||
try {
|
||||
await fs.rm(repoDir, { recursive: true, force: true })
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Get current branch using existing git instance
|
||||
// const branch =
|
||||
;(await this.git?.revparse(["--abbrev-ref", "HEAD"])) || "main"
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to clone/pull repository: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate registry structure
|
||||
* @param repoDir Registry directory
|
||||
*/
|
||||
private async validateRegistryStructure(repoDir: string): Promise<void> {
|
||||
// Check for metadata.en.yml
|
||||
const metadataPath = path.join(repoDir, "metadata.en.yml")
|
||||
try {
|
||||
await fs.stat(metadataPath)
|
||||
} catch {
|
||||
throw new Error("Registry is missing metadata.en.yml file")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse repository metadata
|
||||
* @param repoDir Repository directory
|
||||
* @returns Repository metadata
|
||||
*/
|
||||
private async parseRepositoryMetadata(repoDir: string): Promise<RepositoryMetadata> {
|
||||
const metadataPath = path.join(repoDir, "metadata.en.yml")
|
||||
const metadataContent = await fs.readFile(metadataPath, "utf-8")
|
||||
|
||||
try {
|
||||
const parsed = yaml.load(metadataContent) as Record<string, any>
|
||||
return validateAnyMetadata(parsed) as RepositoryMetadata
|
||||
} catch (error) {
|
||||
console.error("Failed to parse repository metadata:", error)
|
||||
return {
|
||||
name: "Unknown Repository",
|
||||
description: "Failed to load repository",
|
||||
version: "0.0.0",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse marketplace items
|
||||
* @param repoDir Repository directory
|
||||
* @param repoUrl Repository URL
|
||||
* @param sourceName Source repository name
|
||||
* @returns Array of marketplace items
|
||||
*/
|
||||
private async parseMarketplaceItems(
|
||||
repoDir: string,
|
||||
repoUrl: string,
|
||||
sourceName: string,
|
||||
): Promise<MarketplaceItem[]> {
|
||||
return this.metadataScanner.scanDirectory(repoDir, repoUrl, sourceName)
|
||||
}
|
||||
}
|
||||
205
src/services/marketplace/InstalledMetadataManager.ts
Normal file
205
src/services/marketplace/InstalledMetadataManager.ts
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
import * as fs from "fs/promises"
|
||||
import * as yaml from "js-yaml"
|
||||
import { z } from "zod"
|
||||
import { ensureSettingsDirectoryExists } from "../../utils/globalContext"
|
||||
|
||||
const ItemInstalledMetadataSchema = z.object({
|
||||
version: z.string(),
|
||||
modes: z.array(z.string()).optional(),
|
||||
mcps: z.array(z.string()).optional(),
|
||||
files: z.array(z.string()).optional(),
|
||||
})
|
||||
export type ItemInstalledMetadata = z.infer<typeof ItemInstalledMetadataSchema>
|
||||
|
||||
const ScopeInstalledMetadataSchema = z.record(ItemInstalledMetadataSchema)
|
||||
export type ScopeInstalledMetadata = z.infer<typeof ScopeInstalledMetadataSchema>
|
||||
|
||||
// Full metadata structure
|
||||
export interface FullInstallatedMetadata {
|
||||
project: ScopeInstalledMetadata
|
||||
global: ScopeInstalledMetadata
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages installed marketplace item metadata for both project and global scopes.
|
||||
*/
|
||||
export class InstalledMetadataManager {
|
||||
public fullMetadata: FullInstallatedMetadata = {
|
||||
project: {},
|
||||
global: {},
|
||||
}
|
||||
|
||||
constructor(private readonly context: vscode.ExtensionContext) {}
|
||||
|
||||
/**
|
||||
* Loads and validates metadata from a YAML file at the given path.
|
||||
*
|
||||
* Returns an empty object if the file doesn't exist or is invalid.
|
||||
*
|
||||
* Throws errors for issues other than file not found or validation errors.
|
||||
*/
|
||||
private async loadMetadataFile(filePath: string): Promise<ScopeInstalledMetadata> {
|
||||
try {
|
||||
const content = await fs.readFile(filePath, "utf-8")
|
||||
const data = yaml.load(content)
|
||||
|
||||
const validationResult = ScopeInstalledMetadataSchema.safeParse(data)
|
||||
|
||||
if (validationResult.success) {
|
||||
return validationResult.data
|
||||
} else {
|
||||
console.warn(
|
||||
`InstalledMetadataManager: Invalid metadata structure in ${filePath}. Validation errors:`,
|
||||
validationResult.error.flatten(),
|
||||
)
|
||||
return {} // Return empty for validation errors
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.code === "ENOENT") {
|
||||
return {} // File not found is expected
|
||||
}
|
||||
|
||||
// Re-throw unexpected errors (e.g., permissions issues, YAML parsing errors)
|
||||
console.error(`InstalledMetadataManager: Error reading or parsing metadata file ${filePath}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads project-specific installed metadata from .roo/.marketplace/metadata.yml.
|
||||
*/
|
||||
async reloadProject(): Promise<ScopeInstalledMetadata> {
|
||||
const metadataPath = await this.getMetadataFilePath("project")
|
||||
if (!metadataPath) {
|
||||
this.fullMetadata.project = {}
|
||||
} else {
|
||||
try {
|
||||
this.fullMetadata.project = await this.loadMetadataFile(metadataPath)
|
||||
console.debug("Project metadata reloaded:", this.fullMetadata.project)
|
||||
} catch (error) {
|
||||
console.error("InstalledMetadataManager: Failed to reload project metadata:", error)
|
||||
this.fullMetadata.project = {} // Reset on load failure
|
||||
}
|
||||
}
|
||||
return this.fullMetadata.project
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads global installed metadata from the extension's global storage.
|
||||
*/
|
||||
async reloadGlobal(): Promise<ScopeInstalledMetadata> {
|
||||
const metadataPath = await this.getMetadataFilePath("global")
|
||||
if (!metadataPath) {
|
||||
this.fullMetadata.global = {}
|
||||
} else {
|
||||
try {
|
||||
this.fullMetadata.global = await this.loadMetadataFile(metadataPath)
|
||||
console.debug("Global metadata reloaded:", this.fullMetadata.global)
|
||||
} catch (error) {
|
||||
console.error("InstalledMetadataManager: Failed to reload global metadata:", error)
|
||||
this.fullMetadata.global = {} // Reset on load failure
|
||||
}
|
||||
}
|
||||
return this.fullMetadata.global
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the metadata for a specific installed item.
|
||||
* @param scope The scope ('project' or 'global')
|
||||
* @param itemId The ID of the item
|
||||
* @returns The item's metadata or undefined if not found.
|
||||
*/
|
||||
getInstalledItem(scope: "project" | "global", itemId: string): ItemInstalledMetadata | undefined {
|
||||
return this.fullMetadata[scope]?.[itemId]
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the file path for the metadata file based on the scope.
|
||||
* @param scope The scope ('project' or 'global')
|
||||
* @returns The full file path or undefined if scope is project and no workspace is open.
|
||||
*/
|
||||
private async getMetadataFilePath(scope: "project" | "global"): Promise<string | undefined> {
|
||||
if (scope === "project") {
|
||||
if (!vscode.workspace.workspaceFolders?.length) {
|
||||
console.error("InstalledMetadataManager: Cannot get project metadata path, no workspace folder open.")
|
||||
return undefined
|
||||
}
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders[0].uri.fsPath
|
||||
return path.join(workspaceFolder, ".roo", ".marketplace", "metadata.yml")
|
||||
} else {
|
||||
// Global scope
|
||||
try {
|
||||
const globalSettingsPath = await ensureSettingsDirectoryExists(this.context)
|
||||
return path.join(globalSettingsPath, ".marketplace", "metadata.yml")
|
||||
} catch (error) {
|
||||
console.error("InstalledMetadataManager: Failed to get global settings directory path:", error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the metadata for a given scope to its corresponding YAML file.
|
||||
*
|
||||
* Throws an error if the file path cannot be determined or if saving fails.
|
||||
*
|
||||
* @param scope The scope ('project' or 'global')
|
||||
* @param metadata The metadata object to save.
|
||||
*/
|
||||
private async saveMetadataFile(scope: "project" | "global", metadata: ScopeInstalledMetadata): Promise<void> {
|
||||
const filePath = await this.getMetadataFilePath(scope)
|
||||
if (!filePath) {
|
||||
throw new Error(`InstalledMetadataManager: Could not determine metadata file path for scope '${scope}'.`)
|
||||
}
|
||||
|
||||
try {
|
||||
// Ensure the directory exists
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
||||
|
||||
// Serialize metadata to YAML
|
||||
const yamlContent = yaml.dump(metadata)
|
||||
|
||||
// Write to file
|
||||
await fs.writeFile(filePath, yamlContent, "utf-8")
|
||||
console.debug(`InstalledMetadataManager: Metadata saved successfully to ${filePath}`)
|
||||
} catch (error) {
|
||||
console.error(`InstalledMetadataManager: Error saving metadata file ${filePath}:`, error)
|
||||
throw error // Re-throw save errors
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds or updates metadata for an installed item and saves it.
|
||||
* @param scope The scope ('project' or 'global')
|
||||
* @param itemId The ID of the item
|
||||
* @param details The metadata details of the item
|
||||
*/
|
||||
async addInstalledItem(scope: "project" | "global", itemId: string, details: ItemInstalledMetadata): Promise<void> {
|
||||
// Add/update the item
|
||||
this.fullMetadata[scope][itemId] = details
|
||||
|
||||
// Save the updated metadata for the entire scope
|
||||
await this.saveMetadataFile(scope, this.fullMetadata[scope])
|
||||
console.log(`Installed item added/updated: ${scope}/${itemId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes metadata for an installed item and saves the changes.
|
||||
* @param scope The scope ('project' or 'global')
|
||||
* @param itemId The ID of the item
|
||||
*/
|
||||
async removeInstalledItem(scope: "project" | "global", itemId: string): Promise<void> {
|
||||
// Check if item exists
|
||||
if (this.fullMetadata[scope]?.[itemId]) {
|
||||
delete this.fullMetadata[scope][itemId]
|
||||
|
||||
// Save the updated metadata
|
||||
await this.saveMetadataFile(scope, this.fullMetadata[scope])
|
||||
console.log(`Installed item removed: ${scope}/${itemId}`)
|
||||
} else {
|
||||
console.warn(`InstalledMetadataManager: Item not found for removal: ${scope}/${itemId}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
784
src/services/marketplace/MarketplaceManager.ts
Normal file
784
src/services/marketplace/MarketplaceManager.ts
Normal file
|
|
@ -0,0 +1,784 @@
|
|||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
import * as fs from "fs/promises"
|
||||
import { GitFetcher } from "./GitFetcher"
|
||||
import {
|
||||
MarketplaceItem,
|
||||
MarketplaceRepository,
|
||||
MarketplaceSource,
|
||||
MarketplaceItemType,
|
||||
ComponentMetadata,
|
||||
LocalizationOptions,
|
||||
InstallMarketplaceItemOptions,
|
||||
RemoveInstalledMarketplaceItemOptions,
|
||||
} from "./types"
|
||||
import { getUserLocale } from "./utils"
|
||||
import { GlobalFileNames } from "../../shared/globalFileNames"
|
||||
import { assertsMpContext, createHookable, MarketplaceContext, registerMarketplaceHooks } from "roo-rocket"
|
||||
import { assertsBinarySha256, unpackFromUint8, extractRocketConfigFromUint8 } from "config-rocket/cli"
|
||||
import { getPanel } from "../../activate/registerCommands"
|
||||
import { ensureSettingsDirectoryExists } from "../../utils/globalContext"
|
||||
import { InstalledMetadataManager, ItemInstalledMetadata } from "./InstalledMetadataManager"
|
||||
|
||||
/**
|
||||
* Service for managing marketplace data
|
||||
*/
|
||||
export class MarketplaceManager {
|
||||
private currentItems: MarketplaceItem[] = []
|
||||
private originalItems: MarketplaceItem[] = []
|
||||
private static readonly CACHE_EXPIRY_MS = 3600000 // 1 hour
|
||||
|
||||
IMM: InstalledMetadataManager
|
||||
|
||||
private gitFetcher: GitFetcher
|
||||
private cache: Map<string, { data: MarketplaceRepository; timestamp: number }> = new Map()
|
||||
public isFetching = false
|
||||
|
||||
// Concurrency control
|
||||
private activeSourceOperations = new Set<string>() // Track active git operations per source
|
||||
private isMetadataScanActive = false // Track active metadata scanning
|
||||
private pendingOperations: Array<() => Promise<void>> = [] // Queue for pending operations
|
||||
|
||||
constructor(private readonly context: vscode.ExtensionContext) {
|
||||
const localizationOptions: LocalizationOptions = {
|
||||
userLocale: getUserLocale(),
|
||||
fallbackLocale: "en",
|
||||
}
|
||||
this.gitFetcher = new GitFetcher(context, localizationOptions)
|
||||
this.IMM = new InstalledMetadataManager(context)
|
||||
// Initial loading for the metadatas
|
||||
void this.IMM.reloadProject()
|
||||
void this.IMM.reloadGlobal()
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue an operation to run when no metadata scan is active
|
||||
*/
|
||||
private async queueOperation(operation: () => Promise<void>): Promise<void> {
|
||||
if (this.isMetadataScanActive) {
|
||||
return new Promise((resolve) => {
|
||||
this.pendingOperations.push(async () => {
|
||||
await operation()
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
this.isMetadataScanActive = true
|
||||
await operation()
|
||||
} finally {
|
||||
this.isMetadataScanActive = false
|
||||
|
||||
// Process any pending operations
|
||||
const nextOperation = this.pendingOperations.shift()
|
||||
if (nextOperation) {
|
||||
void this.queueOperation(nextOperation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getMarketplaceItems(
|
||||
enabledSources: MarketplaceSource[],
|
||||
): Promise<{ items: MarketplaceItem[]; errors?: string[] }> {
|
||||
const items: MarketplaceItem[] = []
|
||||
const errors: string[] = []
|
||||
|
||||
// Process sources sequentially with locking
|
||||
for (const source of enabledSources) {
|
||||
if (this.isSourceLocked(source.url)) {
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
this.lockSource(source.url)
|
||||
|
||||
// Queue metadata scanning operation
|
||||
await this.queueOperation(async () => {
|
||||
const repo = await this.getRepositoryData(source.url, false, source.name)
|
||||
|
||||
if (repo.items && repo.items.length > 0) {
|
||||
// Ensure each item is properly attributed to its source
|
||||
const itemsWithSource = repo.items.map((item) => ({
|
||||
...item,
|
||||
sourceName: source.name || this.getRepoNameFromUrl(source.url),
|
||||
sourceUrl: source.url,
|
||||
}))
|
||||
items.push(...itemsWithSource)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
console.error(`MarketplaceManager: Failed to fetch data from ${source.url}:`, error)
|
||||
errors.push(`Source ${source.url}: ${errorMessage}`)
|
||||
} finally {
|
||||
this.unlockSource(source.url)
|
||||
}
|
||||
}
|
||||
|
||||
// Store the current items
|
||||
this.currentItems = items
|
||||
// Preserve original unfiltered items
|
||||
this.originalItems = items
|
||||
|
||||
// Return both items and errors
|
||||
const result = {
|
||||
items,
|
||||
...(errors.length > 0 && { errors }),
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a source operation is in progress
|
||||
*/
|
||||
private isSourceLocked(url: string): boolean {
|
||||
return this.activeSourceOperations.has(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock a source for operations
|
||||
*/
|
||||
private lockSource(url: string): void {
|
||||
this.activeSourceOperations.add(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlock a source after operations complete
|
||||
*/
|
||||
private unlockSource(url: string): void {
|
||||
this.activeSourceOperations.delete(url)
|
||||
}
|
||||
|
||||
async getRepositoryData(
|
||||
url: string,
|
||||
forceRefresh: boolean = false,
|
||||
sourceName?: string,
|
||||
): Promise<MarketplaceRepository> {
|
||||
try {
|
||||
// Check cache first (unless force refresh is requested)
|
||||
const cached = this.cache.get(url)
|
||||
|
||||
if (!forceRefresh && cached && Date.now() - cached.timestamp < MarketplaceManager.CACHE_EXPIRY_MS) {
|
||||
return cached.data
|
||||
}
|
||||
|
||||
// Fetch fresh data with timeout protection
|
||||
const fetchPromise = this.gitFetcher.fetchRepository(url, forceRefresh, sourceName)
|
||||
|
||||
// Create a timeout promise
|
||||
let timeoutId: NodeJS.Timeout | undefined
|
||||
const timeoutPromise = new Promise<MarketplaceRepository>((_, reject) => {
|
||||
timeoutId = setTimeout(() => {
|
||||
reject(new Error(`Repository fetch timed out after 30 seconds: ${url}`))
|
||||
}, 30000) // 30 second timeout
|
||||
})
|
||||
|
||||
try {
|
||||
// Race the fetch against the timeout
|
||||
const result = await Promise.race([fetchPromise, timeoutPromise])
|
||||
|
||||
// Cache the result
|
||||
this.cache.set(url, { data: result, timestamp: Date.now() })
|
||||
|
||||
return result
|
||||
} finally {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`MarketplaceManager: Error fetching repository data for ${url}:`, error)
|
||||
|
||||
// Return empty repository data instead of throwing
|
||||
return {
|
||||
metadata: {
|
||||
name: "Unknown Repository",
|
||||
description: "Failed to load repository",
|
||||
version: "0.0.0",
|
||||
},
|
||||
items: [],
|
||||
url,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes a specific repository, bypassing the cache
|
||||
* @param url The repository URL to refresh
|
||||
* @param sourceName Optional name of the source
|
||||
* @returns The refreshed repository data
|
||||
*/
|
||||
async refreshRepository(url: string, sourceName?: string): Promise<MarketplaceRepository> {
|
||||
try {
|
||||
// Force a refresh by bypassing the cache
|
||||
const data = await this.getRepositoryData(url, true, sourceName)
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error(`MarketplaceManager: Failed to refresh repository ${url}:`, error)
|
||||
return {
|
||||
metadata: {
|
||||
name: "Unknown Repository",
|
||||
description: "Failed to load repository",
|
||||
version: "0.0.0",
|
||||
},
|
||||
items: [],
|
||||
url,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the in-memory cache
|
||||
*/
|
||||
clearCache(): void {
|
||||
this.cache.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up cache directories for repositories that are no longer in the configured sources
|
||||
* @param currentSources The current list of marketplace sources
|
||||
*/
|
||||
async cleanupCacheDirectories(currentSources: MarketplaceSource[]): Promise<void> {
|
||||
try {
|
||||
// Get the cache directory path
|
||||
const cacheDir = path.join(this.context.globalStorageUri.fsPath, "marketplace-cache")
|
||||
|
||||
// Check if cache directory exists
|
||||
try {
|
||||
await fs.stat(cacheDir)
|
||||
} catch (error) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get all subdirectories in the cache directory
|
||||
const entries = await fs.readdir(cacheDir, { withFileTypes: true })
|
||||
const cachedRepoDirs = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name)
|
||||
|
||||
// Get the list of repository names from current sources
|
||||
const currentRepoNames = currentSources.map((source) => this.getRepoNameFromUrl(source.url))
|
||||
|
||||
// Find directories to delete
|
||||
const dirsToDelete = cachedRepoDirs.filter((dir) => !currentRepoNames.includes(dir))
|
||||
|
||||
// Delete each directory that's no longer in the sources
|
||||
for (const dirName of dirsToDelete) {
|
||||
try {
|
||||
const dirPath = path.join(cacheDir, dirName)
|
||||
await fs.rm(dirPath, { recursive: true, force: true })
|
||||
} catch (error) {
|
||||
console.error(`MarketplaceManager: Failed to delete directory ${dirName}:`, error)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("MarketplaceManager: Error cleaning up cache directories:", error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a safe directory name from a Git URL
|
||||
* @param url The Git repository URL
|
||||
* @returns A sanitized directory name
|
||||
*/
|
||||
private getRepoNameFromUrl(url: string): string {
|
||||
// Extract repo name from URL and sanitize it
|
||||
const urlParts = url.split("/").filter((part) => part !== "")
|
||||
const repoName = urlParts[urlParts.length - 1].replace(/\.git$/, "")
|
||||
return repoName.replace(/[^a-zA-Z0-9-_]/g, "-")
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters marketplace items based on criteria
|
||||
* @param items The items to filter
|
||||
* @param filters The filter criteria
|
||||
* @returns Filtered items
|
||||
*/
|
||||
private static readonly MAX_CACHE_SIZE = 100
|
||||
private static readonly BATCH_SIZE = 100
|
||||
|
||||
private filterCache = new Map<
|
||||
string,
|
||||
{
|
||||
items: MarketplaceItem[]
|
||||
timestamp: number
|
||||
}
|
||||
>()
|
||||
|
||||
/**
|
||||
* Clear old entries from the filter cache
|
||||
*/
|
||||
private cleanupFilterCache(): void {
|
||||
if (this.filterCache.size > MarketplaceManager.MAX_CACHE_SIZE) {
|
||||
// Sort by timestamp and keep only the most recent entries
|
||||
const entries = Array.from(this.filterCache.entries())
|
||||
.sort(([, a], [, b]) => b.timestamp - a.timestamp)
|
||||
.slice(0, MarketplaceManager.MAX_CACHE_SIZE)
|
||||
|
||||
this.filterCache.clear()
|
||||
entries.forEach(([key, value]) => this.filterCache.set(key, value))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter items
|
||||
*/
|
||||
filterItems(
|
||||
items: MarketplaceItem[],
|
||||
filters: { type?: MarketplaceItemType; search?: string; tags?: string[] },
|
||||
): MarketplaceItem[] {
|
||||
// Create cache key from filters
|
||||
const cacheKey = JSON.stringify(filters)
|
||||
const cached = this.filterCache.get(cacheKey)
|
||||
if (cached) {
|
||||
return cached.items
|
||||
}
|
||||
|
||||
// Clean up old cache entries
|
||||
this.cleanupFilterCache()
|
||||
|
||||
// Process items in batches to avoid memory spikes
|
||||
const allFilteredItems: MarketplaceItem[] = []
|
||||
for (let i = 0; i < items.length; i += MarketplaceManager.BATCH_SIZE) {
|
||||
const batch = items.slice(i, Math.min(i + MarketplaceManager.BATCH_SIZE, items.length))
|
||||
const filteredBatch = this.processItemBatch(batch, filters)
|
||||
allFilteredItems.push(...filteredBatch)
|
||||
}
|
||||
|
||||
// Cache the results
|
||||
this.filterCache.set(cacheKey, {
|
||||
items: allFilteredItems,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
|
||||
return allFilteredItems
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a batch of items
|
||||
*/
|
||||
private processItemBatch(
|
||||
batch: MarketplaceItem[],
|
||||
filters: { type?: MarketplaceItemType; search?: string; tags?: string[] },
|
||||
): MarketplaceItem[] {
|
||||
// Helper functions
|
||||
const normalizeText = (text: string) => text.toLowerCase().replace(/\s+/g, " ").trim()
|
||||
const searchTerm = filters.search ? normalizeText(filters.search) : ""
|
||||
const containsSearchTerm = (text: string) => !searchTerm || normalizeText(text).includes(searchTerm)
|
||||
|
||||
return batch
|
||||
.map((item) => {
|
||||
const itemCopy = { ...item }
|
||||
|
||||
// Check parent item matches
|
||||
const itemMatches = {
|
||||
type: !filters.type || itemCopy.type === filters.type,
|
||||
search:
|
||||
!searchTerm || containsSearchTerm(itemCopy.name) || containsSearchTerm(itemCopy.description),
|
||||
tags:
|
||||
!filters.tags?.length ||
|
||||
(itemCopy.tags && filters.tags.some((tag) => itemCopy.tags!.includes(tag))),
|
||||
}
|
||||
|
||||
// Process subcomponents
|
||||
let hasMatchingSubcomponents = false
|
||||
if (itemCopy.items?.length) {
|
||||
itemCopy.items = itemCopy.items.map((subItem) => {
|
||||
const subMatches = {
|
||||
type: !filters.type || subItem.type === filters.type,
|
||||
search:
|
||||
!searchTerm ||
|
||||
(subItem.metadata &&
|
||||
(containsSearchTerm(subItem.metadata.name || "") ||
|
||||
containsSearchTerm(subItem.metadata.description || "") ||
|
||||
containsSearchTerm(subItem.type || ""))),
|
||||
tags:
|
||||
!filters.tags?.length ||
|
||||
(subItem.metadata?.tags &&
|
||||
filters.tags.some((tag) => subItem.metadata!.tags!.includes(tag))),
|
||||
}
|
||||
|
||||
const subItemMatched =
|
||||
subMatches.type &&
|
||||
(!searchTerm || subMatches.search) &&
|
||||
(!filters.tags?.length || subMatches.tags)
|
||||
|
||||
if (subItemMatched) {
|
||||
hasMatchingSubcomponents = true
|
||||
const matchReason: Record<string, boolean> = {
|
||||
nameMatch: searchTerm ? containsSearchTerm(subItem.metadata?.name || "") : true,
|
||||
descriptionMatch: searchTerm
|
||||
? containsSearchTerm(subItem.metadata?.description || "")
|
||||
: false,
|
||||
}
|
||||
|
||||
if (filters.type) {
|
||||
matchReason.typeMatch = subMatches.type
|
||||
}
|
||||
|
||||
subItem.matchInfo = {
|
||||
matched: true,
|
||||
matchReason,
|
||||
}
|
||||
} else {
|
||||
subItem.matchInfo = { matched: false }
|
||||
}
|
||||
|
||||
return subItem
|
||||
})
|
||||
}
|
||||
|
||||
const hasActiveFilters = filters.type || searchTerm || filters.tags?.length
|
||||
if (!hasActiveFilters) return itemCopy
|
||||
|
||||
const parentMatchesAll = itemMatches.type && itemMatches.search && itemMatches.tags
|
||||
const isPackageWithMatchingSubcomponent = itemCopy.type === "package" && hasMatchingSubcomponents
|
||||
|
||||
if (parentMatchesAll || isPackageWithMatchingSubcomponent) {
|
||||
const matchReason: Record<string, boolean> = {
|
||||
nameMatch: searchTerm ? containsSearchTerm(itemCopy.name) : false,
|
||||
descriptionMatch: searchTerm ? containsSearchTerm(itemCopy.description) : false,
|
||||
}
|
||||
|
||||
if (filters.type) {
|
||||
matchReason.typeMatch = itemMatches.type
|
||||
}
|
||||
|
||||
if (hasMatchingSubcomponents) {
|
||||
matchReason.hasMatchingSubcomponents = true
|
||||
}
|
||||
|
||||
// If this is a package and we're searching, also check if any subcomponent names match
|
||||
if (searchTerm && itemCopy.type === "package" && itemCopy.items?.length) {
|
||||
const subcomponentNameMatches = itemCopy.items.some(
|
||||
(subItem) => subItem.metadata && containsSearchTerm(subItem.metadata.name || ""),
|
||||
)
|
||||
if (subcomponentNameMatches) {
|
||||
matchReason.hasMatchingSubcomponents = true
|
||||
}
|
||||
}
|
||||
|
||||
itemCopy.matchInfo = {
|
||||
matched: true,
|
||||
matchReason,
|
||||
}
|
||||
return itemCopy
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
.filter((item): item is MarketplaceItem => item !== null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts marketplace items
|
||||
* @param items The items to sort
|
||||
* @param sortBy The field to sort by
|
||||
* @param sortOrder The sort order
|
||||
* @returns Sorted items
|
||||
*/
|
||||
sortItems(
|
||||
items: MarketplaceItem[],
|
||||
sortBy: keyof Pick<MarketplaceItem, "name" | "author" | "lastUpdated">,
|
||||
sortOrder: "asc" | "desc",
|
||||
sortSubcomponents: boolean = false,
|
||||
): MarketplaceItem[] {
|
||||
return [...items]
|
||||
.map((item) => {
|
||||
// Deep clone the item
|
||||
const clonedItem = { ...item }
|
||||
|
||||
// Sort or preserve subcomponents
|
||||
if (clonedItem.items && clonedItem.items.length > 0) {
|
||||
clonedItem.items = [...clonedItem.items]
|
||||
if (sortSubcomponents) {
|
||||
clonedItem.items.sort((a, b) => {
|
||||
const aValue = this.getSortValue(a, sortBy)
|
||||
const bValue = this.getSortValue(b, sortBy)
|
||||
const comparison = aValue.localeCompare(bValue)
|
||||
return sortOrder === "asc" ? comparison : -comparison
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return clonedItem
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const aValue = this.getSortValue(a, sortBy)
|
||||
const bValue = this.getSortValue(b, sortBy)
|
||||
const comparison = aValue.localeCompare(bValue)
|
||||
return sortOrder === "asc" ? comparison : -comparison
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current marketplace items
|
||||
* @returns The current items
|
||||
*/
|
||||
getCurrentItems(): MarketplaceItem[] {
|
||||
return this.currentItems
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates current items with filtered results
|
||||
* @param filters The filter criteria
|
||||
* @returns Filtered items
|
||||
*/
|
||||
updateWithFilteredItems(filters: {
|
||||
type?: MarketplaceItemType
|
||||
search?: string
|
||||
tags?: string[]
|
||||
}): MarketplaceItem[] {
|
||||
// If no filters, restore full list
|
||||
if (!filters.type && !filters.search && (!filters.tags || filters.tags.length === 0)) {
|
||||
this.currentItems = this.originalItems
|
||||
return this.currentItems
|
||||
}
|
||||
// Filter based on original items
|
||||
const filteredItems = this.filterItems(this.originalItems, filters)
|
||||
this.currentItems = filteredItems
|
||||
return filteredItems
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up resources used by the marketplace
|
||||
*/
|
||||
async cleanup(): Promise<void> {
|
||||
// Clean up cache directories for all sources
|
||||
const sources = Array.from(this.cache.keys()).map((url) => ({ url, enabled: true }))
|
||||
await this.cleanupCacheDirectories(sources)
|
||||
this.clearCache()
|
||||
// Clear filter cache
|
||||
this.filterCache.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get the sort value for an item
|
||||
*/
|
||||
private getSortValue(
|
||||
item:
|
||||
| MarketplaceItem
|
||||
| { type: MarketplaceItemType; path: string; metadata?: ComponentMetadata; lastUpdated?: string },
|
||||
sortBy: keyof Pick<MarketplaceItem, "name" | "author" | "lastUpdated">,
|
||||
): string {
|
||||
if ("metadata" in item && item.metadata) {
|
||||
// Handle subcomponent
|
||||
switch (sortBy) {
|
||||
case "name":
|
||||
return item.metadata.name
|
||||
case "author":
|
||||
return ""
|
||||
case "lastUpdated":
|
||||
return item.lastUpdated || ""
|
||||
default:
|
||||
return item.metadata.name
|
||||
}
|
||||
} else {
|
||||
// Handle parent item
|
||||
const parentItem = item as MarketplaceItem
|
||||
switch (sortBy) {
|
||||
case "name":
|
||||
return parentItem.name
|
||||
case "author":
|
||||
return parentItem.author || ""
|
||||
case "lastUpdated":
|
||||
return parentItem.lastUpdated || ""
|
||||
default:
|
||||
return parentItem.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the cwd for the specified target scope
|
||||
*/
|
||||
async resolveScopeCwd(target: InstallMarketplaceItemOptions["target"]): Promise<string> {
|
||||
if (target === "project" && !vscode.workspace.workspaceFolders?.length)
|
||||
throw new Error("Cannot load current workspace folder")
|
||||
|
||||
return target === "project"
|
||||
? vscode.workspace.workspaceFolders![0].uri.fsPath
|
||||
: await ensureSettingsDirectoryExists(this.context)
|
||||
}
|
||||
|
||||
async installMarketplaceItem(
|
||||
item: MarketplaceItem,
|
||||
options?: InstallMarketplaceItemOptions,
|
||||
): Promise<"$COMMIT" | any> {
|
||||
// Temporary added due to hosting with _doInstall
|
||||
const _IMM = this.IMM
|
||||
|
||||
const { target = "project", parameters } = options || {}
|
||||
|
||||
vscode.window.showInformationMessage(`Installing item: "${item.name}"`)
|
||||
|
||||
const cwd = await this.resolveScopeCwd(target)
|
||||
|
||||
if (!item.binaryUrl || !item.binaryHash) throw new Error("Item does not have a binary URL or hash.")
|
||||
|
||||
// Creates `mpContext` to delegate context to `roo-rocket`
|
||||
const mpContext: MarketplaceContext =
|
||||
target === "project"
|
||||
? { target }
|
||||
: {
|
||||
target,
|
||||
globalFileNames: {
|
||||
mcp: GlobalFileNames.mcpSettings,
|
||||
mode: GlobalFileNames.customModes,
|
||||
},
|
||||
}
|
||||
assertsMpContext(mpContext)
|
||||
|
||||
// Fetch the binary
|
||||
const binaryUint8 = await fetchBinary(item.binaryUrl)
|
||||
|
||||
// `parameters` only exists in flows where we already check everything and then requires parameters input
|
||||
// so we can optimize and skip the latter checks
|
||||
if (parameters) return await _doInstall()
|
||||
|
||||
// Check binary integrity
|
||||
await assertsBinarySha256(binaryUint8, item.binaryHash)
|
||||
|
||||
// Extract config and check if it has prompt parameters.
|
||||
const config = await extractRocketConfigFromUint8(binaryUint8)
|
||||
const configHavePromptParameters = config?.parameters?.some((param) => param.resolver.operation === "prompt")
|
||||
if (configHavePromptParameters) {
|
||||
vscode.window.showInformationMessage(`"${item.name}" is configurable, opening UI form...`)
|
||||
|
||||
const panel = getPanel()
|
||||
if (panel) {
|
||||
panel.webview.postMessage({
|
||||
type: "openMarketplaceInstallSidebarWithConfig",
|
||||
payload: {
|
||||
item,
|
||||
config,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
throw new Error("Could not open UI form: Webview panel not found.")
|
||||
}
|
||||
return false // Stop installation process here, wait for parameters from frontend
|
||||
}
|
||||
|
||||
return await _doInstall()
|
||||
async function _doInstall() {
|
||||
// Create a custom hookable instance to support global installations
|
||||
const customHookable = createHookable()
|
||||
registerMarketplaceHooks(customHookable, mpContext)
|
||||
|
||||
// Register hook to set parameters if provided
|
||||
if (parameters)
|
||||
customHookable.hook("onParameter", ({ parameter, resolvedParameters }) => {
|
||||
if (parameter.id in parameters)
|
||||
return (resolvedParameters[parameter.id] = parameters[parameter.id as keyof typeof parameters])
|
||||
|
||||
// If there is unresolved prompt operation, throw error or else it would hang the installation
|
||||
if (parameter.resolver.operation === "prompt") throw new Error("Unexpected prompt operation")
|
||||
})
|
||||
|
||||
// Register hooks to build `ItemInstalledMetadata`
|
||||
const itemInstalledMetadata: ItemInstalledMetadata = {
|
||||
version: item.version,
|
||||
modes: [],
|
||||
mcps: [],
|
||||
files: [],
|
||||
}
|
||||
customHookable.hook("onFileOutput", ({ filePath, data }) => {
|
||||
if (filePath.endsWith("/.roomodes")) {
|
||||
const parsedData = JSON.parse(data)
|
||||
if (parsedData?.customModes?.length) {
|
||||
parsedData.customModes.forEach((mode: any) => {
|
||||
itemInstalledMetadata.modes?.push(mode.slug)
|
||||
})
|
||||
}
|
||||
} else if (filePath.endsWith("/.roo/mcp.json")) {
|
||||
const parsedData = JSON.parse(data)
|
||||
const mcpSlugs = Object.keys(parsedData?.mcpServers ?? {})
|
||||
if (mcpSlugs.length) {
|
||||
mcpSlugs.forEach((mcpSlug: any) => {
|
||||
itemInstalledMetadata.mcps?.push(mcpSlug)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
itemInstalledMetadata.files?.push(path.relative(cwd, filePath))
|
||||
}
|
||||
})
|
||||
|
||||
vscode.window.showInformationMessage(`"${item.name}" is unpacking...`)
|
||||
await unpackFromUint8(binaryUint8, {
|
||||
hookable: customHookable,
|
||||
nonAssemblyBehavior: true,
|
||||
cwd,
|
||||
}).then(() => {
|
||||
_IMM.addInstalledItem("project", item.id, itemInstalledMetadata)
|
||||
})
|
||||
vscode.window.showInformationMessage(`"${item.name}" installed successfully`)
|
||||
|
||||
return "$COMMIT"
|
||||
}
|
||||
}
|
||||
|
||||
async removeInstalledMarketplaceItem(
|
||||
item: MarketplaceItem,
|
||||
options?: RemoveInstalledMarketplaceItemOptions,
|
||||
): Promise<"$COMMIT" | any> {
|
||||
const { target = "project" } = options || {}
|
||||
|
||||
vscode.window.showInformationMessage(`Removing item: "${item.name}"`)
|
||||
|
||||
const cwd = await this.resolveScopeCwd(target)
|
||||
const modesFilePath = path.join(cwd, target === "project" ? ".roomodes" : GlobalFileNames.customModes)
|
||||
const mcpsFilePath = path.join(cwd, target === "project" ? ".roo/mcp.json" : GlobalFileNames.mcpSettings)
|
||||
|
||||
const itemInstalledMetadata = this.IMM.getInstalledItem(target, item.id)
|
||||
if (itemInstalledMetadata) {
|
||||
if (itemInstalledMetadata.modes) {
|
||||
if (await fs.access(modesFilePath).catch(() => true))
|
||||
vscode.window.showWarningMessage(`"${item.name}": modes file not found`)
|
||||
else {
|
||||
const parsedModesFile = JSON.parse(await fs.readFile(modesFilePath, "utf-8"))
|
||||
parsedModesFile.customModes = parsedModesFile.customModes.filter(
|
||||
(m: any) => !itemInstalledMetadata.modes!.includes(m.slug),
|
||||
)
|
||||
await fs.writeFile(modesFilePath, JSON.stringify(parsedModesFile, null, 2), "utf-8")
|
||||
}
|
||||
}
|
||||
if (itemInstalledMetadata.mcps) {
|
||||
if (await fs.access(mcpsFilePath).catch(() => true))
|
||||
vscode.window.showWarningMessage(`"${item.name}": mcps file not found`)
|
||||
else {
|
||||
const parsedMcpsFile = JSON.parse(await fs.readFile(mcpsFilePath, "utf-8"))
|
||||
itemInstalledMetadata.mcps.forEach((mcp) => {
|
||||
delete parsedMcpsFile.mcpServers[mcp]
|
||||
})
|
||||
await fs.writeFile(mcpsFilePath, JSON.stringify(parsedMcpsFile, null, 2), "utf-8")
|
||||
}
|
||||
}
|
||||
if (itemInstalledMetadata.files) {
|
||||
for (const file of itemInstalledMetadata.files) {
|
||||
try {
|
||||
await fs.rm(path.join(cwd, file))
|
||||
} catch (error) {
|
||||
vscode.window.showWarningMessage(
|
||||
`"${item.name}": failed to remove file "${file}": ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.IMM.removeInstalledItem(target, item.id)
|
||||
vscode.window.showInformationMessage(`"${item.name}" removed successfully`)
|
||||
return "$COMMIT"
|
||||
} else {
|
||||
throw new Error(`is not installed in scope "${target}"`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchBinary(url: string) {
|
||||
const res = await fetch(url)
|
||||
if (!res.ok) throw new Error(`Failed to download binary from ${url}`)
|
||||
|
||||
return new Uint8Array(await res.arrayBuffer())
|
||||
}
|
||||
409
src/services/marketplace/MetadataScanner.ts
Normal file
409
src/services/marketplace/MetadataScanner.ts
Normal file
|
|
@ -0,0 +1,409 @@
|
|||
import * as path from "path"
|
||||
import * as fs from "fs/promises"
|
||||
import * as vscode from "vscode"
|
||||
import * as yaml from "js-yaml"
|
||||
import { SimpleGit } from "simple-git"
|
||||
import { validateAnyMetadata } from "./schemas"
|
||||
import {
|
||||
ComponentMetadata,
|
||||
MarketplaceItemType,
|
||||
LocalizationOptions,
|
||||
LocalizedMetadata,
|
||||
MarketplaceItem,
|
||||
PackageMetadata,
|
||||
} from "./types"
|
||||
import { getUserLocale } from "./utils"
|
||||
|
||||
/**
|
||||
* Handles component discovery and metadata loading
|
||||
*/
|
||||
export class MetadataScanner {
|
||||
private git?: SimpleGit
|
||||
private localizationOptions: LocalizationOptions
|
||||
private originalRootDir: string | null = null
|
||||
private static readonly MAX_DEPTH = 5 // Maximum directory depth
|
||||
private static readonly BATCH_SIZE = 50 // Number of items to process at once
|
||||
private static readonly CONCURRENT_SCANS = 3 // Number of concurrent directory scans
|
||||
private isDisposed = false
|
||||
|
||||
constructor(git?: SimpleGit, localizationOptions?: LocalizationOptions) {
|
||||
this.git = git
|
||||
this.localizationOptions = localizationOptions || {
|
||||
userLocale: getUserLocale(),
|
||||
fallbackLocale: "en",
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources
|
||||
*/
|
||||
dispose(): void {
|
||||
if (this.isDisposed) {
|
||||
return
|
||||
}
|
||||
|
||||
// Clean up git instance reference
|
||||
this.git = undefined
|
||||
|
||||
// Clear any other references
|
||||
this.originalRootDir = null
|
||||
this.localizationOptions = null as any
|
||||
|
||||
this.isDisposed = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Generator function to yield items in batches
|
||||
*/
|
||||
private async *scanDirectoryBatched(
|
||||
rootDir: string,
|
||||
repoUrl: string,
|
||||
sourceName?: string,
|
||||
depth: number = 0,
|
||||
): AsyncGenerator<MarketplaceItem[]> {
|
||||
if (depth > MetadataScanner.MAX_DEPTH) {
|
||||
return
|
||||
}
|
||||
|
||||
const batch: MarketplaceItem[] = []
|
||||
const entries = await fs.readdir(rootDir, { withFileTypes: true })
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue
|
||||
|
||||
const componentDir = path.join(rootDir, entry.name)
|
||||
const metadata = await this.loadComponentMetadata(componentDir)
|
||||
const localizedMetadata = metadata ? this.getLocalizedMetadata(metadata) : null
|
||||
|
||||
if (localizedMetadata) {
|
||||
const item = await this.createMarketplaceItem(
|
||||
localizedMetadata,
|
||||
componentDir,
|
||||
repoUrl,
|
||||
this.originalRootDir || rootDir,
|
||||
sourceName,
|
||||
)
|
||||
|
||||
if (item) {
|
||||
// If this is a package, scan for subcomponents
|
||||
if (this.isPackageMetadata(localizedMetadata)) {
|
||||
await this.scanPackageSubcomponents(componentDir, item)
|
||||
}
|
||||
|
||||
batch.push(item)
|
||||
if (batch.length >= MetadataScanner.BATCH_SIZE) {
|
||||
yield batch.splice(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only scan subdirectories if no metadata was found
|
||||
if (!localizedMetadata) {
|
||||
const subGenerator = this.scanDirectoryBatched(componentDir, repoUrl, sourceName, depth + 1)
|
||||
for await (const subBatch of subGenerator) {
|
||||
batch.push(...subBatch)
|
||||
if (batch.length >= MetadataScanner.BATCH_SIZE) {
|
||||
yield batch.splice(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (batch.length > 0) {
|
||||
yield batch
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans a directory for components
|
||||
* @param rootDir The root directory to scan
|
||||
* @param repoUrl The repository URL
|
||||
* @param sourceName Optional source repository name
|
||||
* @returns Array of discovered items
|
||||
*/
|
||||
/**
|
||||
* Scan a directory and return items in batches
|
||||
*/
|
||||
async scanDirectory(
|
||||
rootDir: string,
|
||||
repoUrl: string,
|
||||
sourceName?: string,
|
||||
isRecursiveCall: boolean = false,
|
||||
): Promise<MarketplaceItem[]> {
|
||||
// Only set originalRootDir on the first call
|
||||
if (!isRecursiveCall && !this.originalRootDir) {
|
||||
this.originalRootDir = rootDir
|
||||
}
|
||||
|
||||
const items: MarketplaceItem[] = []
|
||||
const generator = this.scanDirectoryBatched(rootDir, repoUrl, sourceName)
|
||||
|
||||
for await (const batch of generator) {
|
||||
items.push(...batch)
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets localized metadata with fallback
|
||||
* @param metadata The localized metadata object
|
||||
* @returns The metadata in the user's locale or fallback locale, or null if neither is available
|
||||
*/
|
||||
private getLocalizedMetadata(metadata: LocalizedMetadata<ComponentMetadata>): ComponentMetadata | null {
|
||||
const { userLocale, fallbackLocale } = this.localizationOptions
|
||||
|
||||
// First try user's locale
|
||||
if (metadata[userLocale]) {
|
||||
return metadata[userLocale]
|
||||
}
|
||||
|
||||
// Fall back to fallbackLocale (typically English)
|
||||
if (metadata[fallbackLocale]) {
|
||||
return metadata[fallbackLocale]
|
||||
}
|
||||
|
||||
// No suitable metadata found
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads metadata for a component
|
||||
* @param componentDir The component directory
|
||||
* @returns Localized metadata or null if no metadata found
|
||||
*/
|
||||
private async loadComponentMetadata(componentDir: string): Promise<LocalizedMetadata<ComponentMetadata> | null> {
|
||||
const metadata: LocalizedMetadata<ComponentMetadata> = {}
|
||||
try {
|
||||
const entries = await fs.readdir(componentDir, { withFileTypes: true })
|
||||
|
||||
// Look for metadata.{locale}.yml files
|
||||
for (const entry of entries) {
|
||||
if (!entry.isFile()) continue
|
||||
|
||||
const match = entry.name.match(/^metadata\.([a-z]{2})\.yml$/)
|
||||
if (!match) continue
|
||||
|
||||
const locale = match[1]
|
||||
const metadataPath = path.join(componentDir, entry.name)
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(metadataPath, "utf-8")
|
||||
const parsed = yaml.load(content) as Record<string, any>
|
||||
|
||||
// Add type field if missing but has a parent directory indicating type
|
||||
if (!parsed.type) {
|
||||
const parentDir = path.basename(componentDir)
|
||||
if (parentDir === "mcps") {
|
||||
parsed.type = "mcp"
|
||||
}
|
||||
}
|
||||
|
||||
metadata[locale] = validateAnyMetadata(parsed) as ComponentMetadata
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
console.error(`Error loading metadata from ${metadataPath}:`, error)
|
||||
|
||||
// Show validation errors to user
|
||||
if (errorMessage.includes("Invalid metadata:")) {
|
||||
vscode.window.showErrorMessage(
|
||||
`Invalid metadata in ${path.basename(metadataPath)}: ${errorMessage.replace("Invalid metadata:", "").trim()}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error reading directory ${componentDir}:`, error)
|
||||
}
|
||||
|
||||
return Object.keys(metadata).length > 0 ? metadata : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a MarketplaceItem from component metadata
|
||||
* @param metadata The component metadata
|
||||
* @param componentDir The component directory
|
||||
* @param repoUrl The repository URL
|
||||
* @param sourceName Optional source repository name
|
||||
* @returns MarketplaceItem or null if invalid
|
||||
*/
|
||||
private async createMarketplaceItem(
|
||||
metadata: ComponentMetadata,
|
||||
componentDir: string,
|
||||
repoUrl: string,
|
||||
rootDir: string,
|
||||
sourceName?: string,
|
||||
): Promise<MarketplaceItem | null> {
|
||||
// Skip if no type or invalid type
|
||||
if (!metadata.type || !this.isValidMarketplaceItemType(metadata.type)) {
|
||||
return null
|
||||
}
|
||||
// Always use the original root directory for path calculations
|
||||
const effectiveRootDir = this.originalRootDir || rootDir
|
||||
// Always calculate path relative to the original root directory
|
||||
const relativePath = path.relative(effectiveRootDir, componentDir).replace(/\\/g, "/")
|
||||
// Don't encode spaces in URL to match test expectations
|
||||
const urlPath = relativePath
|
||||
.split("/")
|
||||
.map((part) => encodeURIComponent(part))
|
||||
.join("/")
|
||||
// Create the item with the correct path and URL
|
||||
return {
|
||||
id: metadata.id || `${metadata.type}#${relativePath || metadata.name}`,
|
||||
name: metadata.name,
|
||||
description: metadata.description,
|
||||
type: metadata.type,
|
||||
version: metadata.version,
|
||||
binaryUrl: metadata.binaryUrl,
|
||||
binaryHash: metadata.binaryHash,
|
||||
tags: metadata.tags,
|
||||
url: `${repoUrl}/tree/main/${urlPath}`,
|
||||
repoUrl,
|
||||
sourceName,
|
||||
path: relativePath,
|
||||
lastUpdated: await this.getLastModifiedDate(componentDir),
|
||||
items: [], // Initialize empty items array for all components
|
||||
author: metadata.author,
|
||||
authorUrl: metadata.authorUrl,
|
||||
sourceUrl: metadata.sourceUrl,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the last modified date for a component using git history
|
||||
* @param componentDir The component directory
|
||||
* @returns ISO date string
|
||||
*/
|
||||
private async getLastModifiedDate(componentDir: string): Promise<string> {
|
||||
if (this.git) {
|
||||
try {
|
||||
// Get the latest commit date for the directory and its contents
|
||||
const result = await this.git.raw([
|
||||
"log",
|
||||
"-1",
|
||||
"--format=%aI", // ISO 8601 format
|
||||
"--",
|
||||
componentDir,
|
||||
])
|
||||
if (result) {
|
||||
return result.trim()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error getting git history for ${componentDir}:`, error)
|
||||
// Fall through to fs.stat fallback
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to fs.stat if git is not available or fails
|
||||
try {
|
||||
const stats = await fs.stat(componentDir)
|
||||
return stats.mtime.toISOString()
|
||||
} catch {
|
||||
return new Date().toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively scans a package directory for subcomponents
|
||||
* @param packageDir The package directory to scan
|
||||
* @param packageItem The package item to add subcomponents to
|
||||
*/
|
||||
private async scanPackageSubcomponents(
|
||||
packageDir: string,
|
||||
packageItem: MarketplaceItem,
|
||||
parentPath: string = "",
|
||||
): Promise<void> {
|
||||
try {
|
||||
// First check for explicitly listed items in package metadata
|
||||
const metadataPath = path.join(packageDir, "metadata.en.yml")
|
||||
try {
|
||||
const content = await fs.readFile(metadataPath, "utf-8")
|
||||
const parsed = yaml.load(content) as PackageMetadata
|
||||
|
||||
if (parsed.items) {
|
||||
for (const item of parsed.items) {
|
||||
// For relative paths starting with ../, resolve from package directory
|
||||
const itemPath = path.join(packageDir, item.path)
|
||||
const subMetadata = await this.loadComponentMetadata(itemPath)
|
||||
if (subMetadata) {
|
||||
const localizedSubMetadata = this.getLocalizedMetadata(subMetadata)
|
||||
if (localizedSubMetadata) {
|
||||
packageItem.items = packageItem.items || []
|
||||
packageItem.items.push({
|
||||
type: localizedSubMetadata.type,
|
||||
path: item.path,
|
||||
metadata: localizedSubMetadata,
|
||||
lastUpdated: await this.getLastModifiedDate(itemPath),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors reading metadata.en.yml - we'll still scan subdirectories
|
||||
}
|
||||
|
||||
// Then scan subdirectories for implicit components
|
||||
const entries = await fs.readdir(packageDir, { withFileTypes: true })
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue
|
||||
|
||||
const subPath = path.join(packageDir, entry.name)
|
||||
const relativePath = parentPath ? `${parentPath}/${entry.name}` : entry.name
|
||||
|
||||
// Try to load metadata directly
|
||||
const subMetadata = await this.loadComponentMetadata(subPath)
|
||||
if (!subMetadata) {
|
||||
// If no metadata found, recurse into directory
|
||||
await this.scanPackageSubcomponents(subPath, packageItem, relativePath)
|
||||
continue
|
||||
}
|
||||
|
||||
// Get localized metadata with fallback
|
||||
const localizedSubMetadata = this.getLocalizedMetadata(subMetadata)
|
||||
if (!localizedSubMetadata) {
|
||||
// If no localized metadata, recurse into directory
|
||||
await this.scanPackageSubcomponents(subPath, packageItem, relativePath)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this component is already listed
|
||||
const isListed = packageItem.items?.some((i) => i.path === relativePath)
|
||||
if (!isListed) {
|
||||
// Initialize items array if needed
|
||||
packageItem.items = packageItem.items || []
|
||||
|
||||
// Add new subcomponent
|
||||
packageItem.items.push({
|
||||
type: localizedSubMetadata.type,
|
||||
path: relativePath,
|
||||
metadata: localizedSubMetadata,
|
||||
lastUpdated: await this.getLastModifiedDate(subPath),
|
||||
})
|
||||
}
|
||||
|
||||
// Don't recurse into directories that have valid metadata
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error scanning package subcomponents in ${packageDir}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard for component types
|
||||
* @param type The type to check
|
||||
* @returns Whether the type is valid
|
||||
*/
|
||||
private isValidMarketplaceItemType(type: string): type is MarketplaceItemType {
|
||||
return ["role", "mcp", "storage", "mode", "prompt", "package"].includes(type)
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard for package metadata
|
||||
* @param metadata The metadata to check
|
||||
* @returns Whether the metadata is for a package
|
||||
*/
|
||||
private isPackageMetadata(metadata: ComponentMetadata): metadata is PackageMetadata {
|
||||
return metadata.type === "package"
|
||||
}
|
||||
}
|
||||
378
src/services/marketplace/__tests__/GitFetcher.test.ts
Normal file
378
src/services/marketplace/__tests__/GitFetcher.test.ts
Normal file
|
|
@ -0,0 +1,378 @@
|
|||
import * as vscode from "vscode"
|
||||
import { GitFetcher } from "../GitFetcher"
|
||||
import * as fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import simpleGit, { SimpleGit } from "simple-git"
|
||||
|
||||
// Mock simpleGit
|
||||
jest.mock("simple-git", () => {
|
||||
const mockGit = {
|
||||
clone: jest.fn(),
|
||||
pull: jest.fn(),
|
||||
revparse: jest.fn(),
|
||||
fetch: jest.fn(),
|
||||
clean: jest.fn(),
|
||||
raw: jest.fn(),
|
||||
}
|
||||
return jest.fn(() => mockGit)
|
||||
})
|
||||
|
||||
// Mock fs/promises
|
||||
jest.mock("fs/promises", () => ({
|
||||
mkdir: jest.fn(),
|
||||
stat: jest.fn(),
|
||||
rm: jest.fn(),
|
||||
unlink: jest.fn(),
|
||||
readdir: jest.fn().mockResolvedValue([]),
|
||||
readFile: jest.fn().mockResolvedValue(`
|
||||
name: Test Repository
|
||||
description: Test Description
|
||||
version: 1.0.0
|
||||
`),
|
||||
}))
|
||||
|
||||
// Mock child_process.exec for path with spaces tests
|
||||
jest.mock("child_process", () => ({
|
||||
exec: jest.fn(),
|
||||
}))
|
||||
|
||||
// Mock promisify
|
||||
jest.mock("util", () => ({
|
||||
promisify: jest.fn(),
|
||||
}))
|
||||
|
||||
// Mock vscode
|
||||
const mockContext = {
|
||||
globalStorageUri: {
|
||||
fsPath: path.join(process.cwd(), "mock-storage-path"),
|
||||
},
|
||||
} as vscode.ExtensionContext
|
||||
|
||||
// Create mock Dirent objects
|
||||
// const createMockDirent = (name: string, isDir: boolean): Dirent => {
|
||||
// return {
|
||||
// name,
|
||||
// isDirectory: () => isDir,
|
||||
// isFile: () => !isDir,
|
||||
// isBlockDevice: () => false,
|
||||
// isCharacterDevice: () => false,
|
||||
// isFIFO: () => false,
|
||||
// isSocket: () => false,
|
||||
// isSymbolicLink: () => false,
|
||||
// // These are readonly in the real Dirent
|
||||
// path: "",
|
||||
// parentPath: "",
|
||||
// } as Dirent
|
||||
// }
|
||||
|
||||
describe("GitFetcher", () => {
|
||||
let gitFetcher: GitFetcher
|
||||
const mockSimpleGit = simpleGit as jest.MockedFunction<typeof simpleGit>
|
||||
const testRepoUrl = "https://github.com/test/repo"
|
||||
const testRepoDir = path.join(mockContext.globalStorageUri.fsPath, "marketplace-cache", "repo")
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
gitFetcher = new GitFetcher(mockContext)
|
||||
|
||||
// Reset fs mock defaults
|
||||
;(fs.mkdir as jest.Mock).mockResolvedValue(undefined)
|
||||
;(fs.rm as jest.Mock).mockImplementation((pathToRemove: string, options?: any) => {
|
||||
// Always require recursive and force options
|
||||
if (!options?.recursive || !options?.force) {
|
||||
return Promise.reject(new Error("Invalid rm call: missing recursive or force options"))
|
||||
}
|
||||
// Allow any path under marketplace-cache directory
|
||||
const normalizedPath = path.normalize(pathToRemove)
|
||||
const normalizedCachePath = path.normalize(
|
||||
path.join(mockContext.globalStorageUri.fsPath, "marketplace-cache"),
|
||||
)
|
||||
if (normalizedPath.startsWith(normalizedCachePath)) {
|
||||
return Promise.resolve(undefined)
|
||||
}
|
||||
return Promise.reject(new Error(`Invalid rm call: path ${pathToRemove} not in marketplace-cache`))
|
||||
})
|
||||
|
||||
// Setup fs.stat mock for repository structure validation
|
||||
;(fs.stat as jest.Mock).mockImplementation((path: string) => {
|
||||
if (path.endsWith(".git")) return Promise.reject(new Error("ENOENT"))
|
||||
if (path.endsWith("metadata.en.yml")) return Promise.resolve(true)
|
||||
if (path.endsWith("README.md")) return Promise.resolve(true)
|
||||
return Promise.reject(new Error("ENOENT"))
|
||||
})
|
||||
|
||||
// Setup default git mock behavior
|
||||
const mockGit = {
|
||||
clone: jest.fn().mockResolvedValue(undefined),
|
||||
pull: jest.fn().mockResolvedValue(undefined),
|
||||
revparse: jest.fn().mockResolvedValue("main"),
|
||||
// Add other required SimpleGit methods with no-op implementations
|
||||
addAnnotatedTag: jest.fn(),
|
||||
addConfig: jest.fn(),
|
||||
applyPatch: jest.fn(),
|
||||
listConfig: jest.fn(),
|
||||
addRemote: jest.fn(),
|
||||
addTag: jest.fn(),
|
||||
branch: jest.fn(),
|
||||
branchLocal: jest.fn(),
|
||||
checkout: jest.fn(),
|
||||
checkoutBranch: jest.fn(),
|
||||
checkoutLatestTag: jest.fn(),
|
||||
checkoutLocalBranch: jest.fn(),
|
||||
clean: jest.fn(),
|
||||
clearQueue: jest.fn(),
|
||||
commit: jest.fn(),
|
||||
cwd: jest.fn(),
|
||||
deleteLocalBranch: jest.fn(),
|
||||
deleteLocalBranches: jest.fn(),
|
||||
diff: jest.fn(),
|
||||
diffSummary: jest.fn(),
|
||||
exec: jest.fn(),
|
||||
fetch: jest.fn(),
|
||||
getRemotes: jest.fn(),
|
||||
init: jest.fn(),
|
||||
log: jest.fn(),
|
||||
merge: jest.fn(),
|
||||
mirror: jest.fn(),
|
||||
push: jest.fn(),
|
||||
pushTags: jest.fn(),
|
||||
raw: jest.fn(),
|
||||
rebase: jest.fn(),
|
||||
remote: jest.fn(),
|
||||
removeRemote: jest.fn(),
|
||||
reset: jest.fn(),
|
||||
revert: jest.fn(),
|
||||
show: jest.fn(),
|
||||
stash: jest.fn(),
|
||||
status: jest.fn(),
|
||||
subModule: jest.fn(),
|
||||
tag: jest.fn(),
|
||||
tags: jest.fn(),
|
||||
updateServerInfo: jest.fn(),
|
||||
} as unknown as SimpleGit
|
||||
mockSimpleGit.mockReturnValue(mockGit)
|
||||
})
|
||||
|
||||
describe("fetchRepository", () => {
|
||||
it("should successfully clone a new repository", async () => {
|
||||
await expect(gitFetcher.fetchRepository(testRepoUrl)).resolves.toBeDefined()
|
||||
|
||||
const mockGit = mockSimpleGit()
|
||||
expect(mockGit.clone).toHaveBeenCalledWith(testRepoUrl, testRepoDir)
|
||||
expect(mockGit.raw).toHaveBeenCalledWith(["clean", "-f", "-d"])
|
||||
expect(mockGit.raw).toHaveBeenCalledWith(["reset", "--hard", "HEAD"])
|
||||
})
|
||||
|
||||
it("should pull existing repository", async () => {
|
||||
// Mock repository exists
|
||||
;(fs.stat as jest.Mock).mockImplementation((path: string) => {
|
||||
if (path.endsWith(".git")) return Promise.resolve(true)
|
||||
if (path.endsWith("metadata.en.yml")) return Promise.resolve(true)
|
||||
if (path.endsWith("README.md")) return Promise.resolve(true)
|
||||
return Promise.reject(new Error("ENOENT"))
|
||||
})
|
||||
|
||||
await gitFetcher.fetchRepository(testRepoUrl)
|
||||
|
||||
const mockGit = mockSimpleGit()
|
||||
expect(mockGit.fetch).toHaveBeenCalledWith("origin", "main")
|
||||
expect(mockGit.raw).toHaveBeenCalledWith(["reset", "--hard", "origin/main"])
|
||||
expect(mockGit.raw).toHaveBeenCalledWith(["clean", "-f", "-d"])
|
||||
expect(mockGit.clone).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should handle clone failures", async () => {
|
||||
const mockGit = {
|
||||
...mockSimpleGit(),
|
||||
clone: jest.fn().mockRejectedValue(new Error("fatal: repository not found")),
|
||||
pull: jest.fn(),
|
||||
revparse: jest.fn(),
|
||||
} as unknown as SimpleGit
|
||||
mockSimpleGit.mockReturnValue(mockGit)
|
||||
|
||||
await expect(gitFetcher.fetchRepository(testRepoUrl)).rejects.toThrow(/Failed to clone\/pull repository/)
|
||||
|
||||
// Verify cleanup was called
|
||||
expect(fs.rm).toHaveBeenCalledWith(testRepoDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it("should handle pull failures and re-clone", async () => {
|
||||
// Mock repository exists
|
||||
;(fs.stat as jest.Mock).mockImplementation((path: string) => {
|
||||
if (path.endsWith(".git")) return Promise.resolve(true)
|
||||
if (path.endsWith("metadata.en.yml")) return Promise.resolve(true)
|
||||
if (path.endsWith("README.md")) return Promise.resolve(true)
|
||||
return Promise.reject(new Error("ENOENT"))
|
||||
})
|
||||
|
||||
// Reset fs.rm mock to track calls
|
||||
;(fs.rm as jest.Mock).mockReset()
|
||||
;(fs.rm as jest.Mock).mockImplementation((path: string, options?: any) => {
|
||||
if (path === testRepoDir && options?.recursive && options?.force) {
|
||||
return Promise.resolve(undefined)
|
||||
}
|
||||
return Promise.reject(new Error("Invalid rm call"))
|
||||
})
|
||||
|
||||
const mockGit = {
|
||||
clone: jest.fn().mockResolvedValue(undefined),
|
||||
pull: jest.fn().mockRejectedValue(new Error("not a git repository")),
|
||||
revparse: jest.fn().mockResolvedValue("main"),
|
||||
fetch: jest.fn().mockRejectedValue(new Error("not a git repository")),
|
||||
clean: jest.fn(),
|
||||
raw: jest.fn(),
|
||||
} as unknown as SimpleGit
|
||||
mockSimpleGit.mockReturnValue(mockGit)
|
||||
|
||||
await gitFetcher.fetchRepository(testRepoUrl)
|
||||
|
||||
// Verify directory was removed and repository was re-cloned
|
||||
// First rm call is for cleanup before clone
|
||||
expect(fs.rm).toHaveBeenCalledWith(testRepoDir, { recursive: true, force: true })
|
||||
// Second rm call is after pull failure
|
||||
expect(fs.rm).toHaveBeenCalledWith(testRepoDir, { recursive: true, force: true })
|
||||
expect(mockGit.clone).toHaveBeenCalledWith(testRepoUrl, testRepoDir)
|
||||
expect(mockGit.raw).toHaveBeenCalledWith(["clean", "-f", "-d"])
|
||||
expect(mockGit.raw).toHaveBeenCalledWith(["reset", "--hard", "HEAD"])
|
||||
})
|
||||
|
||||
it("should handle missing metadata.yml", async () => {
|
||||
// Mock repository exists but missing metadata
|
||||
;(fs.stat as jest.Mock).mockImplementation((path: string) => {
|
||||
if (path.endsWith("metadata.en.yml")) return Promise.reject(new Error("ENOENT"))
|
||||
return Promise.resolve(true)
|
||||
})
|
||||
|
||||
await expect(gitFetcher.fetchRepository(testRepoUrl)).rejects.toThrow(
|
||||
'Invalid repository structure: could not find "registry" metadata',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Git Lock File Handling", () => {
|
||||
it("should clean up index.lock file before operations", async () => {
|
||||
// Mock repository exists
|
||||
;(fs.stat as jest.Mock).mockImplementation((path: string) => {
|
||||
if (path.endsWith(".git")) return Promise.resolve(true)
|
||||
if (path.endsWith("metadata.en.yml")) return Promise.resolve(true)
|
||||
if (path.endsWith("README.md")) return Promise.resolve(true)
|
||||
return Promise.reject(new Error("ENOENT"))
|
||||
})
|
||||
|
||||
await gitFetcher.fetchRepository(testRepoUrl)
|
||||
|
||||
// Verify lock file cleanup was attempted
|
||||
expect(fs.unlink).toHaveBeenCalledWith(expect.stringContaining("index.lock"))
|
||||
})
|
||||
|
||||
it("should handle missing lock file gracefully", async () => {
|
||||
// Mock unlink to fail as if file doesn't exist
|
||||
;(fs.unlink as jest.Mock).mockRejectedValue(new Error("ENOENT"))
|
||||
|
||||
await gitFetcher.fetchRepository(testRepoUrl)
|
||||
|
||||
// Operation should succeed despite lock file not existing
|
||||
const mockGit = mockSimpleGit()
|
||||
expect(mockGit.clone).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should clean up lock file when pull fails", async () => {
|
||||
// Mock repository exists
|
||||
;(fs.stat as jest.Mock).mockImplementation((path: string) => {
|
||||
if (path.endsWith(".git")) return Promise.resolve(true)
|
||||
if (path.endsWith("metadata.en.yml")) return Promise.resolve(true)
|
||||
if (path.endsWith("README.md")) return Promise.resolve(true)
|
||||
return Promise.reject(new Error("ENOENT"))
|
||||
})
|
||||
|
||||
const mockGit = {
|
||||
clone: jest.fn().mockResolvedValue(undefined),
|
||||
pull: jest.fn().mockRejectedValue(new Error("not a git repository")),
|
||||
revparse: jest.fn().mockResolvedValue("main"),
|
||||
fetch: jest.fn().mockRejectedValue(new Error("not a git repository")),
|
||||
clean: jest.fn(),
|
||||
raw: jest.fn(),
|
||||
} as unknown as SimpleGit
|
||||
mockSimpleGit.mockReturnValue(mockGit)
|
||||
|
||||
await gitFetcher.fetchRepository(testRepoUrl)
|
||||
|
||||
// Verify lock file cleanup was attempted after pull failure
|
||||
expect(fs.unlink).toHaveBeenCalledWith(expect.stringContaining("index.lock"))
|
||||
})
|
||||
})
|
||||
|
||||
describe("Repository Structure Validation", () => {
|
||||
// Helper function to access private method
|
||||
const validateRegistryStructure = async (repoDir: string) => {
|
||||
return (gitFetcher as any).validateRegistryStructure(repoDir)
|
||||
}
|
||||
|
||||
describe("metadata.en.yml validation", () => {
|
||||
it("should throw error when metadata.en.yml is missing", async () => {
|
||||
// Mock fs.stat to simulate missing file
|
||||
;(fs.stat as jest.Mock).mockImplementation((path: string) => {
|
||||
if (path.endsWith("metadata.en.yml")) return Promise.reject(new Error("File not found"))
|
||||
return Promise.resolve({} as any)
|
||||
})
|
||||
|
||||
// Call the method and expect it to throw
|
||||
await expect(validateRegistryStructure("/mock/repo")).rejects.toThrow(
|
||||
"Registry is missing metadata.en.yml file",
|
||||
)
|
||||
})
|
||||
|
||||
it("should pass when metadata.en.yml exists", async () => {
|
||||
// Mock fs.stat to simulate existing file
|
||||
;(fs.stat as jest.Mock).mockImplementation(() => {
|
||||
return Promise.resolve({} as any)
|
||||
})
|
||||
|
||||
// Call the method and expect it not to throw
|
||||
await expect(validateRegistryStructure("/mock/repo")).resolves.not.toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("Git Command Handling with Special Paths", () => {
|
||||
beforeEach(() => {
|
||||
// Reset fs.stat mock to default behavior
|
||||
;(fs.stat as jest.Mock).mockImplementation((path: string) => {
|
||||
if (path.endsWith(".git")) return Promise.reject(new Error("ENOENT"))
|
||||
if (path.endsWith("metadata.en.yml")) return Promise.resolve(true)
|
||||
if (path.endsWith("README.md")) return Promise.resolve(true)
|
||||
return Promise.reject(new Error("ENOENT"))
|
||||
})
|
||||
|
||||
// No need to reset fs.rm mock here as it's handled in beforeEach
|
||||
})
|
||||
|
||||
it("should handle paths with spaces when cloning", async () => {
|
||||
const url = "https://github.com/example/repo"
|
||||
|
||||
// Create a new GitFetcher instance
|
||||
const gitFetcher = new GitFetcher(mockContext)
|
||||
|
||||
// Attempt to fetch repository
|
||||
await gitFetcher.fetchRepository(url)
|
||||
|
||||
// Verify that simpleGit's clone was called with the correct arguments
|
||||
const mockGit = mockSimpleGit()
|
||||
expect(mockGit.clone).toHaveBeenCalledWith(url, expect.stringContaining("marketplace-cache"))
|
||||
})
|
||||
|
||||
it("should handle paths with special characters when cloning", async () => {
|
||||
const url = "https://github.com/example/repo-name"
|
||||
|
||||
// Create a new GitFetcher instance
|
||||
const gitFetcher = new GitFetcher(mockContext)
|
||||
|
||||
// Attempt to fetch repository
|
||||
await gitFetcher.fetchRepository(url)
|
||||
|
||||
// Verify that simpleGit's clone was called with the correct arguments
|
||||
const mockGit = mockSimpleGit()
|
||||
expect(mockGit.clone).toHaveBeenCalledWith(url, expect.stringContaining("marketplace-cache"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import { isValidGitRepositoryUrl } from "../../../shared/MarketplaceValidation"
|
||||
|
||||
describe("Git URL Validation", () => {
|
||||
test("validates multi-segment domain SSH URL", () => {
|
||||
const url = "git@git.lab.company.com:team-name/project-name.git"
|
||||
expect(isValidGitRepositoryUrl(url)).toBe(true)
|
||||
})
|
||||
})
|
||||
764
src/services/marketplace/__tests__/MarketplaceManager.test.ts
Normal file
764
src/services/marketplace/__tests__/MarketplaceManager.test.ts
Normal file
|
|
@ -0,0 +1,764 @@
|
|||
import { MarketplaceManager } from "../MarketplaceManager"
|
||||
import { MarketplaceItem, MarketplaceSource, MarketplaceRepository, MarketplaceItemType } from "../types"
|
||||
import { MetadataScanner } from "../MetadataScanner"
|
||||
import { GitFetcher } from "../GitFetcher"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
describe("MarketplaceManager", () => {
|
||||
describe("filterItems", () => {
|
||||
// Create a mock context with required properties
|
||||
const mockContext = {
|
||||
globalStorageUri: {
|
||||
fsPath: path.resolve(__dirname, "../../../../mock/settings/path"),
|
||||
},
|
||||
extensionPath: path.resolve(__dirname, "../../../../"),
|
||||
subscriptions: [],
|
||||
workspaceState: {
|
||||
get: jest.fn(),
|
||||
update: jest.fn(),
|
||||
},
|
||||
globalState: {
|
||||
get: jest.fn(),
|
||||
update: jest.fn(),
|
||||
},
|
||||
asAbsolutePath: jest.fn((p) => p),
|
||||
storagePath: "",
|
||||
logPath: "",
|
||||
extensionUri: { fsPath: "" },
|
||||
environmentVariableCollection: {},
|
||||
extensionMode: 1,
|
||||
storageUri: { fsPath: "" },
|
||||
} as unknown as vscode.ExtensionContext
|
||||
|
||||
let manager: MarketplaceManager
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a new manager instance with the mock context for each test
|
||||
manager = new MarketplaceManager(mockContext)
|
||||
})
|
||||
|
||||
it("should correctly filter items by search term", () => {
|
||||
const items: MarketplaceItem[] = [
|
||||
{
|
||||
id: "test-item-1",
|
||||
name: "Test Item 1",
|
||||
description: "First test item",
|
||||
version: "zxc",
|
||||
type: "mode",
|
||||
url: "test1",
|
||||
repoUrl: "test1",
|
||||
},
|
||||
{
|
||||
id: "another-item",
|
||||
name: "Another Item",
|
||||
description: "Second item",
|
||||
version: "zxc",
|
||||
type: "mode",
|
||||
url: "test2",
|
||||
repoUrl: "test2",
|
||||
},
|
||||
]
|
||||
|
||||
const filtered = manager.filterItems(items, { search: "test" })
|
||||
expect(filtered).toHaveLength(1)
|
||||
expect(filtered[0].name).toBe("Test Item 1")
|
||||
expect(filtered[0].matchInfo?.matched).toBe(true)
|
||||
})
|
||||
|
||||
it("should correctly filter items by type", () => {
|
||||
const items: MarketplaceItem[] = [
|
||||
{
|
||||
id: "mode-item",
|
||||
name: "Mode Item",
|
||||
description: "A mode",
|
||||
version: "zxc",
|
||||
type: "mode",
|
||||
url: "test1",
|
||||
repoUrl: "test1",
|
||||
},
|
||||
{
|
||||
id: "server-item",
|
||||
name: "Server Item",
|
||||
description: "A server",
|
||||
version: "zxc",
|
||||
type: "mcp",
|
||||
url: "test2",
|
||||
repoUrl: "test2",
|
||||
},
|
||||
]
|
||||
|
||||
const filtered = manager.filterItems(items, { type: "mode" })
|
||||
expect(filtered).toHaveLength(1)
|
||||
expect(filtered[0].name).toBe("Mode Item")
|
||||
expect(filtered[0].matchInfo?.matchReason?.typeMatch).toBe(true)
|
||||
})
|
||||
|
||||
it("should preserve original items when filtering", () => {
|
||||
const items: MarketplaceItem[] = [
|
||||
{
|
||||
id: "test-item-1",
|
||||
name: "Test Item 1",
|
||||
description: "First test item",
|
||||
version: "zxc",
|
||||
type: "mode",
|
||||
url: "test1",
|
||||
repoUrl: "test1",
|
||||
},
|
||||
{
|
||||
id: "another-item",
|
||||
name: "Another Item",
|
||||
description: "Second item",
|
||||
version: "zxc",
|
||||
type: "mode",
|
||||
url: "test2",
|
||||
repoUrl: "test2",
|
||||
},
|
||||
]
|
||||
|
||||
const originalItemsJson = JSON.stringify(items)
|
||||
manager.filterItems(items, { search: "test" })
|
||||
expect(JSON.stringify(items)).toBe(originalItemsJson)
|
||||
})
|
||||
})
|
||||
|
||||
let manager: MarketplaceManager
|
||||
|
||||
beforeEach(() => {
|
||||
const context = {
|
||||
globalStorageUri: { fsPath: path.resolve(__dirname, "../../../../mock/settings/path") },
|
||||
} as vscode.ExtensionContext
|
||||
manager = new MarketplaceManager(context)
|
||||
})
|
||||
|
||||
describe("Type Filter Behavior", () => {
|
||||
let typeFilterTestItems: MarketplaceItem[]
|
||||
|
||||
test("should include package with MCP server subcomponent when filtering by type 'mcp'", () => {
|
||||
const items: MarketplaceItem[] = [
|
||||
{
|
||||
id: "data-platform-package",
|
||||
name: "Data Platform Package",
|
||||
description: "A package containing MCP servers",
|
||||
version: "zxc",
|
||||
type: "package" as MarketplaceItemType,
|
||||
url: "test/package",
|
||||
repoUrl: "https://example.com",
|
||||
items: [
|
||||
{
|
||||
type: "mcp" as MarketplaceItemType,
|
||||
path: "test/server",
|
||||
metadata: {
|
||||
name: "Data Validator",
|
||||
description: "An MCP server",
|
||||
version: "1.0.0",
|
||||
type: "mcp" as MarketplaceItemType,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "standalone-server",
|
||||
name: "Standalone Server",
|
||||
description: "A standalone MCP server",
|
||||
version: "zxc",
|
||||
type: "mcp" as MarketplaceItemType,
|
||||
url: "test/server",
|
||||
repoUrl: "https://example.com",
|
||||
},
|
||||
]
|
||||
|
||||
const filtered = manager.filterItems(items, { type: "mcp" })
|
||||
expect(filtered.length).toBe(2)
|
||||
expect(filtered.map((item) => item.name)).toContain("Data Platform Package")
|
||||
expect(filtered.map((item) => item.name)).toContain("Standalone Server")
|
||||
|
||||
// Verify package is included because of its MCP server subcomponent
|
||||
const pkg = filtered.find((item) => item.name === "Data Platform Package")
|
||||
expect(pkg?.matchInfo?.matched).toBe(true)
|
||||
expect(pkg?.matchInfo?.matchReason?.hasMatchingSubcomponents).toBe(true)
|
||||
expect(pkg?.items?.[0].matchInfo?.matched).toBe(true)
|
||||
expect(pkg?.items?.[0].matchInfo?.matchReason?.typeMatch).toBe(true)
|
||||
})
|
||||
|
||||
test("should include package when filtering by subcomponent type", () => {
|
||||
const items: MarketplaceItem[] = [
|
||||
{
|
||||
id: "data-platform-package",
|
||||
name: "Data Platform Package",
|
||||
description: "A package containing MCP servers",
|
||||
version: "zxc",
|
||||
type: "package" as MarketplaceItemType,
|
||||
url: "test/package",
|
||||
repoUrl: "https://example.com",
|
||||
items: [
|
||||
{
|
||||
type: "mcp" as MarketplaceItemType,
|
||||
path: "test/server",
|
||||
metadata: {
|
||||
name: "Data Validator",
|
||||
description: "An MCP server",
|
||||
version: "1.0.0",
|
||||
type: "mcp" as MarketplaceItemType,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const filtered = manager.filterItems(items, { type: "mcp" })
|
||||
expect(filtered.length).toBe(1)
|
||||
expect(filtered[0].name).toBe("Data Platform Package")
|
||||
expect(filtered[0].matchInfo?.matched).toBe(true)
|
||||
expect(filtered[0].items?.[0].matchInfo?.matched).toBe(true)
|
||||
expect(filtered[0].items?.[0].matchInfo?.matchReason?.typeMatch).toBe(true)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
// Create test items
|
||||
typeFilterTestItems = [
|
||||
{
|
||||
id: "test-package",
|
||||
name: "Test Package",
|
||||
description: "A test package",
|
||||
version: "zxc",
|
||||
type: "package",
|
||||
url: "test/package",
|
||||
repoUrl: "https://example.com",
|
||||
items: [
|
||||
{
|
||||
type: "mode",
|
||||
path: "test/mode",
|
||||
metadata: {
|
||||
name: "Test Mode",
|
||||
description: "A test mode",
|
||||
version: "1.0.0",
|
||||
type: "mode",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "mcp",
|
||||
path: "test/server",
|
||||
metadata: {
|
||||
name: "Test Server",
|
||||
description: "A test server",
|
||||
version: "1.0.0",
|
||||
type: "mcp",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "test-mode",
|
||||
name: "Test Mode",
|
||||
description: "A standalone test mode",
|
||||
version: "zxc",
|
||||
type: "mode",
|
||||
url: "test/standalone-mode",
|
||||
repoUrl: "https://example.com",
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
// Concurrency Control tests moved to their own describe block
|
||||
|
||||
test("should include package when filtering by its own type", () => {
|
||||
// Filter by package type
|
||||
const filtered = manager.filterItems(typeFilterTestItems, { type: "package" })
|
||||
|
||||
// Should include the package
|
||||
expect(filtered.length).toBe(1)
|
||||
expect(filtered[0].name).toBe("Test Package")
|
||||
expect(filtered[0].matchInfo?.matched).toBe(true)
|
||||
expect(filtered[0].matchInfo?.matchReason?.typeMatch).toBe(true)
|
||||
})
|
||||
|
||||
// Note: The test "should include package when filtering by subcomponent type" is already covered by
|
||||
// the test "should work with type filter and localization together" in the filterItems with subcomponents section
|
||||
|
||||
test("should not include package when filtering by type with no matching subcomponents", () => {
|
||||
// Create a package with no matching subcomponents
|
||||
const noMatchPackage: MarketplaceItem = {
|
||||
id: "no-match-package",
|
||||
name: "No Match Package",
|
||||
description: "A package with no matching subcomponents",
|
||||
version: "zxc",
|
||||
type: "package",
|
||||
url: "test/no-match",
|
||||
repoUrl: "https://example.com",
|
||||
items: [
|
||||
{
|
||||
type: "prompt",
|
||||
path: "test/prompt",
|
||||
metadata: {
|
||||
name: "Test Prompt",
|
||||
description: "A test prompt",
|
||||
version: "1.0.0",
|
||||
type: "prompt",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
// Filter by mode type
|
||||
const filtered = manager.filterItems([noMatchPackage], { type: "mode" })
|
||||
|
||||
// Should not include the package
|
||||
expect(filtered.length).toBe(0)
|
||||
})
|
||||
|
||||
test("should handle package with no subcomponents", () => {
|
||||
// Create a package with no subcomponents
|
||||
const noSubcomponentsPackage: MarketplaceItem = {
|
||||
id: "no-subcomponents-package",
|
||||
name: "No Subcomponents Package",
|
||||
description: "A package with no subcomponents",
|
||||
version: "zxc",
|
||||
type: "package",
|
||||
url: "test/no-subcomponents",
|
||||
repoUrl: "https://example.com",
|
||||
}
|
||||
|
||||
// Filter by mode type
|
||||
const filtered = manager.filterItems([noSubcomponentsPackage], { type: "mode" })
|
||||
|
||||
// Should not include the package
|
||||
expect(filtered.length).toBe(0)
|
||||
})
|
||||
|
||||
describe("Consistency with Search Term Behavior", () => {
|
||||
let consistencyTestItems: MarketplaceItem[]
|
||||
|
||||
beforeEach(() => {
|
||||
// Create test items
|
||||
consistencyTestItems = [
|
||||
{
|
||||
id: "test-package",
|
||||
name: "Test Package",
|
||||
description: "A test package",
|
||||
version: "zxc",
|
||||
type: "package",
|
||||
url: "test/package",
|
||||
repoUrl: "https://example.com",
|
||||
items: [
|
||||
{
|
||||
type: "mode",
|
||||
path: "test/mode",
|
||||
metadata: {
|
||||
name: "Test Mode",
|
||||
description: "A test mode",
|
||||
version: "1.0.0",
|
||||
type: "mode",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
test("should behave consistently with search term for packages", () => {
|
||||
// Filter by type
|
||||
const typeFiltered = manager.filterItems(consistencyTestItems, { type: "package" })
|
||||
|
||||
// Filter by search term that matches the package
|
||||
const searchFiltered = manager.filterItems(consistencyTestItems, { search: "test package" })
|
||||
|
||||
// Both should include the package
|
||||
expect(typeFiltered.length).toBe(1)
|
||||
expect(searchFiltered.length).toBe(1)
|
||||
|
||||
// Both should mark the package as matched
|
||||
expect(typeFiltered[0].matchInfo?.matched).toBe(true)
|
||||
expect(searchFiltered[0].matchInfo?.matched).toBe(true)
|
||||
})
|
||||
|
||||
test("should behave consistently with search term for subcomponents", () => {
|
||||
// Filter by type that matches a subcomponent
|
||||
const typeFiltered = manager.filterItems(consistencyTestItems, { type: "mode" })
|
||||
|
||||
// Filter by search term that matches a subcomponent
|
||||
const searchFiltered = manager.filterItems(consistencyTestItems, { search: "test mode" })
|
||||
|
||||
// Both should include the package
|
||||
expect(typeFiltered.length).toBe(1)
|
||||
expect(searchFiltered.length).toBe(1)
|
||||
|
||||
// Both should mark the package as matched
|
||||
expect(typeFiltered[0].matchInfo?.matched).toBe(true)
|
||||
expect(searchFiltered[0].matchInfo?.matched).toBe(true)
|
||||
|
||||
// Both should mark the subcomponent as matched
|
||||
expect(typeFiltered[0].items?.[0].matchInfo?.matched).toBe(true)
|
||||
expect(searchFiltered[0].items?.[0].matchInfo?.matched).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("sortItems with subcomponents", () => {
|
||||
const testItems: MarketplaceItem[] = [
|
||||
{
|
||||
id: "b-package",
|
||||
name: "B Package",
|
||||
description: "Package B",
|
||||
type: "package",
|
||||
version: "1.0.0",
|
||||
url: "/test/b",
|
||||
repoUrl: "https://example.com",
|
||||
items: [
|
||||
{
|
||||
type: "mode",
|
||||
path: "modes/y",
|
||||
metadata: {
|
||||
name: "Y Mode",
|
||||
description: "Mode Y",
|
||||
type: "mode",
|
||||
version: "1.0.0",
|
||||
},
|
||||
lastUpdated: "2025-04-13T09:00:00-07:00",
|
||||
},
|
||||
{
|
||||
type: "mode",
|
||||
path: "modes/x",
|
||||
metadata: {
|
||||
name: "X Mode",
|
||||
description: "Mode X",
|
||||
type: "mode",
|
||||
version: "1.0.0",
|
||||
},
|
||||
lastUpdated: "2025-04-13T09:00:00-07:00",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "a-package",
|
||||
name: "A Package",
|
||||
description: "Package A",
|
||||
type: "package",
|
||||
version: "1.0.0",
|
||||
url: "/test/a",
|
||||
repoUrl: "https://example.com",
|
||||
items: [
|
||||
{
|
||||
type: "mode",
|
||||
path: "modes/z",
|
||||
metadata: {
|
||||
name: "Z Mode",
|
||||
description: "Mode Z",
|
||||
type: "mode",
|
||||
version: "1.0.0",
|
||||
},
|
||||
lastUpdated: "2025-04-13T08:00:00-07:00",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
it("should sort parent items while preserving subcomponents", () => {
|
||||
const sorted = manager.sortItems(testItems, "name", "asc")
|
||||
expect(sorted[0].name).toBe("A Package")
|
||||
expect(sorted[1].name).toBe("B Package")
|
||||
expect(sorted[0].items![0].metadata!.name).toBe("Z Mode")
|
||||
expect(sorted[1].items![0].metadata!.name).toBe("Y Mode")
|
||||
})
|
||||
|
||||
it("should sort subcomponents within parents", () => {
|
||||
const sorted = manager.sortItems(testItems, "name", "asc", true)
|
||||
expect(sorted[1].items![0].metadata!.name).toBe("X Mode")
|
||||
expect(sorted[1].items![1].metadata!.name).toBe("Y Mode")
|
||||
})
|
||||
|
||||
it("should preserve subcomponent order when sortSubcomponents is false", () => {
|
||||
const sorted = manager.sortItems(testItems, "name", "asc", false)
|
||||
expect(sorted[1].items![0].metadata!.name).toBe("Y Mode")
|
||||
expect(sorted[1].items![1].metadata!.name).toBe("X Mode")
|
||||
})
|
||||
|
||||
it("should handle empty subcomponents when sorting", () => {
|
||||
const itemsWithEmpty = [
|
||||
...testItems,
|
||||
{
|
||||
id: "c-package",
|
||||
name: "C Package",
|
||||
description: "Package C",
|
||||
type: "package" as const,
|
||||
version: "1.0.0",
|
||||
url: "/test/c",
|
||||
repoUrl: "https://example.com",
|
||||
items: [],
|
||||
} as MarketplaceItem,
|
||||
]
|
||||
const sorted = manager.sortItems(itemsWithEmpty, "name", "asc")
|
||||
expect(sorted[2].name).toBe("C Package")
|
||||
expect(sorted[2].items).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("filterItems with real data", () => {
|
||||
it("should return all subcomponents with match info", () => {
|
||||
const testItems: MarketplaceItem[] = [
|
||||
{
|
||||
id: "data-platform-package",
|
||||
name: "Data Platform Package",
|
||||
description: "A test platform",
|
||||
type: "package",
|
||||
version: "1.0.0",
|
||||
url: "/test/data-platform",
|
||||
repoUrl: "https://example.com",
|
||||
items: [
|
||||
{
|
||||
type: "mcp",
|
||||
path: "mcps/data-validator",
|
||||
metadata: {
|
||||
name: "Data Validator",
|
||||
description: "An MCP server for validating data quality",
|
||||
type: "mcp",
|
||||
version: "1.0.0",
|
||||
},
|
||||
lastUpdated: "2025-04-13T10:00:00-07:00",
|
||||
},
|
||||
{
|
||||
type: "mode",
|
||||
path: "modes/task-runner",
|
||||
metadata: {
|
||||
name: "Task Runner",
|
||||
description: "A mode for running tasks",
|
||||
type: "mode",
|
||||
version: "1.0.0",
|
||||
},
|
||||
lastUpdated: "2025-04-13T10:00:00-07:00",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
// Search for "data validator"
|
||||
const filtered = manager.filterItems(testItems, { search: "data validator" })
|
||||
|
||||
// Verify package is returned
|
||||
expect(filtered.length).toBe(1)
|
||||
const pkg = filtered[0]
|
||||
|
||||
// Verify all subcomponents are returned
|
||||
expect(pkg.items?.length).toBe(2)
|
||||
|
||||
// Verify matching subcomponent has correct matchInfo
|
||||
const validator = pkg.items?.find((item) => item.metadata?.name === "Data Validator")
|
||||
expect(validator?.matchInfo).toEqual({
|
||||
matched: true,
|
||||
matchReason: {
|
||||
nameMatch: true,
|
||||
descriptionMatch: false,
|
||||
},
|
||||
})
|
||||
|
||||
// Verify non-matching subcomponent has correct matchInfo
|
||||
const runner = pkg.items?.find((item) => item.metadata?.name === "Task Runner")
|
||||
expect(runner?.matchInfo).toEqual({
|
||||
matched: false,
|
||||
})
|
||||
|
||||
// Verify package has matchInfo indicating it contains matches
|
||||
expect(pkg.matchInfo).toEqual({
|
||||
matched: true,
|
||||
matchReason: {
|
||||
nameMatch: false,
|
||||
descriptionMatch: false,
|
||||
hasMatchingSubcomponents: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("Source Attribution", () => {
|
||||
let manager: MarketplaceManager
|
||||
|
||||
beforeEach(() => {
|
||||
const mockContext = {
|
||||
globalStorageUri: { fsPath: "/test/path" },
|
||||
} as vscode.ExtensionContext
|
||||
manager = new MarketplaceManager(mockContext)
|
||||
})
|
||||
|
||||
it("should maintain source attribution for items", async () => {
|
||||
const sources: MarketplaceSource[] = [
|
||||
{ url: "https://github.com/test/repo1", name: "Source 1", enabled: true },
|
||||
{ url: "https://github.com/test/repo2", name: "Source 2", enabled: true },
|
||||
]
|
||||
|
||||
// Mock getRepositoryData to return different items for each source
|
||||
jest.spyOn(manager as any, "getRepositoryData")
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
metadata: { name: "test", description: "test", version: "1.0.0" },
|
||||
items: [
|
||||
{
|
||||
id: "item-1",
|
||||
name: "Item 1",
|
||||
type: "mode",
|
||||
description: "Test item",
|
||||
url: "test1",
|
||||
repoUrl: "https://github.com/test/repo1",
|
||||
},
|
||||
],
|
||||
url: sources[0].url,
|
||||
}),
|
||||
)
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
metadata: { name: "test", description: "test", version: "1.0.0" },
|
||||
items: [],
|
||||
url: sources[1].url,
|
||||
}),
|
||||
)
|
||||
|
||||
const result = await manager.getMarketplaceItems(sources)
|
||||
|
||||
// Verify items maintain their source attribution
|
||||
expect(result.items).toHaveLength(1)
|
||||
expect(result.items[0].sourceName).toBe("Source 1")
|
||||
expect(result.items[0].sourceUrl).toBe("https://github.com/test/repo1")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Concurrency Control", () => {
|
||||
let manager: MarketplaceManager
|
||||
|
||||
beforeEach(() => {
|
||||
const mockContext = {
|
||||
globalStorageUri: { fsPath: "/test/path" },
|
||||
} as vscode.ExtensionContext
|
||||
manager = new MarketplaceManager(mockContext)
|
||||
})
|
||||
|
||||
it("should not allow concurrent operations on the same source", async () => {
|
||||
const source: MarketplaceSource = {
|
||||
url: "https://github.com/test/repo",
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
// Mock getRepositoryData to return a resolved promise immediately
|
||||
const getRepoSpy = jest.spyOn(manager as any, "getRepositoryData").mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
metadata: { name: "test", description: "test", version: "1.0.0" },
|
||||
items: [],
|
||||
url: source.url,
|
||||
} as MarketplaceRepository),
|
||||
)
|
||||
|
||||
// Start two concurrent operations
|
||||
const operation1 = manager.getMarketplaceItems([source])
|
||||
const operation2 = manager.getMarketplaceItems([source])
|
||||
|
||||
// Wait for both to complete
|
||||
// const [result1, result2] =
|
||||
await Promise.all([operation1, operation2])
|
||||
|
||||
// Verify getRepositoryData was only called once
|
||||
expect(getRepoSpy).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Clean up
|
||||
getRepoSpy.mockRestore()
|
||||
})
|
||||
|
||||
it("should not allow metadata scanning during git operations", async () => {
|
||||
try {
|
||||
const source1: MarketplaceSource = {
|
||||
url: "https://github.com/test/repo1",
|
||||
enabled: true,
|
||||
}
|
||||
const source2: MarketplaceSource = {
|
||||
url: "https://github.com/test/repo2",
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
let isGitOperationActive = false
|
||||
let metadataScanDuringGit = false
|
||||
|
||||
// Mock git operation to resolve immediately
|
||||
// const fetchRepoSpy =
|
||||
jest.spyOn(GitFetcher.prototype, "fetchRepository").mockImplementation(async () => {
|
||||
isGitOperationActive = true
|
||||
isGitOperationActive = false
|
||||
return {
|
||||
metadata: { name: "test", description: "test", version: "1.0.0" },
|
||||
items: [],
|
||||
url: source1.url,
|
||||
}
|
||||
})
|
||||
|
||||
// Mock metadata scanner to check if git operation is active
|
||||
// const scanDirSpy =
|
||||
jest.spyOn(MetadataScanner.prototype, "scanDirectory").mockImplementation(async () => {
|
||||
if (isGitOperationActive) {
|
||||
metadataScanDuringGit = true
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
// Process both sources
|
||||
await manager.getMarketplaceItems([source1, source2])
|
||||
|
||||
// Verify metadata scanning didn't occur during git operations
|
||||
expect(metadataScanDuringGit).toBe(false)
|
||||
} finally {
|
||||
jest.clearAllTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it("should queue metadata scans and process them sequentially", async () => {
|
||||
const sources: MarketplaceSource[] = [
|
||||
{ url: "https://github.com/test/repo1", enabled: true },
|
||||
{ url: "https://github.com/test/repo2", enabled: true },
|
||||
{ url: "https://github.com/test/repo3", enabled: true },
|
||||
]
|
||||
|
||||
let activeScans = 0
|
||||
let maxConcurrentScans = 0
|
||||
|
||||
// Create a mock MetadataScanner that resolves immediately
|
||||
const mockScanner = new MetadataScanner()
|
||||
const scanDirectorySpy = jest.spyOn(mockScanner, "scanDirectory").mockImplementation(async () => {
|
||||
activeScans++
|
||||
maxConcurrentScans = Math.max(maxConcurrentScans, activeScans)
|
||||
activeScans--
|
||||
return Promise.resolve([])
|
||||
})
|
||||
|
||||
// Create a mock GitFetcher that uses our mock scanner
|
||||
const mockGitFetcher = new GitFetcher({
|
||||
globalStorageUri: { fsPath: "/test/path" },
|
||||
} as vscode.ExtensionContext)
|
||||
|
||||
// Replace GitFetcher's metadataScanner with our mock
|
||||
;(mockGitFetcher as any).metadataScanner = mockScanner
|
||||
|
||||
// Mock GitFetcher's fetchRepository to trigger metadata scanning
|
||||
const fetchRepoSpy = jest
|
||||
.spyOn(mockGitFetcher, "fetchRepository")
|
||||
.mockImplementation(async (repoUrl: string) => {
|
||||
// Call scanDirectory through our mock scanner
|
||||
await mockScanner.scanDirectory("/test/path", repoUrl)
|
||||
|
||||
return Promise.resolve({
|
||||
metadata: { name: "test", description: "test", version: "1.0.0" },
|
||||
items: [],
|
||||
url: repoUrl,
|
||||
})
|
||||
})
|
||||
|
||||
// Replace the GitFetcher instance in the manager
|
||||
;(manager as any).gitFetcher = mockGitFetcher
|
||||
|
||||
// Process all sources
|
||||
await manager.getMarketplaceItems(sources)
|
||||
|
||||
// Verify scans were called and only one was active at a time
|
||||
expect(scanDirectorySpy).toHaveBeenCalledTimes(sources.length)
|
||||
expect(maxConcurrentScans).toBe(1)
|
||||
|
||||
// Clean up
|
||||
scanDirectorySpy.mockRestore()
|
||||
fetchRepoSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,237 @@
|
|||
import {
|
||||
isValidGitRepositoryUrl,
|
||||
validateSourceUrl,
|
||||
validateSourceName,
|
||||
validateSourceDuplicates,
|
||||
validateSource,
|
||||
validateSources,
|
||||
} from "../../../shared/MarketplaceValidation"
|
||||
import { MarketplaceSource } from "../types"
|
||||
|
||||
describe("MarketplaceSourceValidation", () => {
|
||||
describe("isValidGitRepositoryUrl", () => {
|
||||
const validUrls = [
|
||||
"https://github.com/username/repo",
|
||||
"https://gitlab.com/username/repo",
|
||||
"https://bitbucket.org/username/repo",
|
||||
|
||||
// Custom/self-hosted domains
|
||||
"https://git.company.com/username/repo",
|
||||
"https://git.internal.dev/username/repo.git",
|
||||
"git@git.company.com:username/repo.git",
|
||||
"git://git.internal.dev/username/repo.git",
|
||||
|
||||
// Subdomains and longer TLDs
|
||||
"https://git.dev.company.co.uk/username/repo",
|
||||
"git@git.dev.internal.company.com:username/repo.git",
|
||||
]
|
||||
|
||||
const invalidUrls = [
|
||||
"",
|
||||
" ",
|
||||
"not-a-url",
|
||||
"https://example.com", // Missing username/repo parts
|
||||
"git@example.com", // Missing repo part
|
||||
"https://git.company.com/repo", // Missing username part
|
||||
"git://example.com/repo", // Missing username part
|
||||
"https://git.company.com/", // Missing both username and repo
|
||||
]
|
||||
|
||||
test.each(validUrls)("should accept valid URL: %s", (url) => {
|
||||
expect(isValidGitRepositoryUrl(url)).toBe(true)
|
||||
})
|
||||
|
||||
test.each(invalidUrls)("should reject invalid URL: %s", (url) => {
|
||||
expect(isValidGitRepositoryUrl(url)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("validateSourceUrl", () => {
|
||||
test("should accept valid URLs", () => {
|
||||
const errors = validateSourceUrl("https://github.com/username/repo")
|
||||
expect(errors).toHaveLength(0)
|
||||
})
|
||||
|
||||
test("should reject empty URL", () => {
|
||||
const errors = validateSourceUrl("")
|
||||
expect(errors).toHaveLength(1)
|
||||
expect(errors[0]).toEqual({
|
||||
field: "url",
|
||||
message: "URL cannot be empty",
|
||||
})
|
||||
})
|
||||
|
||||
test("should reject invalid URL format", () => {
|
||||
const errors = validateSourceUrl("not-a-url")
|
||||
expect(errors).toHaveLength(1)
|
||||
expect(errors[0]).toEqual({
|
||||
field: "url",
|
||||
message: "URL must be a valid Git repository URL (e.g., https://git.example.com/username/repo)",
|
||||
})
|
||||
})
|
||||
|
||||
test("should reject URLs with non-visible characters", () => {
|
||||
const errors = validateSourceUrl("https://github.com/username/repo\t")
|
||||
expect(errors).toHaveLength(1)
|
||||
expect(errors[0]).toEqual({
|
||||
field: "url",
|
||||
message: "URL contains non-visible characters other than spaces",
|
||||
})
|
||||
})
|
||||
|
||||
test("should reject non-Git repository URLs", () => {
|
||||
const errors = validateSourceUrl("https://example.com/path")
|
||||
expect(errors).toHaveLength(1)
|
||||
expect(errors[0]).toEqual({
|
||||
field: "url",
|
||||
message: "URL must be a valid Git repository URL (e.g., https://git.example.com/username/repo)",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("validateSourceName", () => {
|
||||
test("should accept valid names", () => {
|
||||
const errors = validateSourceName("Valid Name")
|
||||
expect(errors).toHaveLength(0)
|
||||
})
|
||||
|
||||
test("should accept undefined name", () => {
|
||||
const errors = validateSourceName(undefined)
|
||||
expect(errors).toHaveLength(0)
|
||||
})
|
||||
|
||||
test("should reject names longer than 20 characters", () => {
|
||||
const errors = validateSourceName("This name is way too long to be valid")
|
||||
expect(errors).toHaveLength(1)
|
||||
expect(errors[0]).toEqual({
|
||||
field: "name",
|
||||
message: "Name must be 20 characters or less",
|
||||
})
|
||||
})
|
||||
|
||||
test("should reject names with non-visible characters", () => {
|
||||
const errors = validateSourceName("Invalid\tName")
|
||||
expect(errors).toHaveLength(1)
|
||||
expect(errors[0]).toEqual({
|
||||
field: "name",
|
||||
message: "Name contains non-visible characters other than spaces",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("validateSourceDuplicates", () => {
|
||||
const existingSources: MarketplaceSource[] = [
|
||||
{ url: "https://git.company.com/user1/repo1", name: "Source 1", enabled: true },
|
||||
{ url: "https://git.company.com/user2/repo2", name: "Source 2", enabled: true },
|
||||
]
|
||||
|
||||
test("should accept unique sources", () => {
|
||||
const newSource: MarketplaceSource = {
|
||||
url: "https://git.company.com/user3/repo3",
|
||||
name: "Source 3",
|
||||
enabled: true,
|
||||
}
|
||||
const errors = validateSourceDuplicates(existingSources, newSource)
|
||||
expect(errors).toHaveLength(0)
|
||||
})
|
||||
|
||||
test("should reject duplicate URLs (case insensitive)", () => {
|
||||
const newSource: MarketplaceSource = {
|
||||
url: "HTTPS://GIT.COMPANY.COM/USER1/REPO1",
|
||||
name: "Different Name",
|
||||
enabled: true,
|
||||
}
|
||||
const errors = validateSourceDuplicates(existingSources, newSource)
|
||||
expect(errors).toHaveLength(1)
|
||||
expect(errors[0].field).toBe("url")
|
||||
expect(errors[0].message).toContain("duplicate")
|
||||
})
|
||||
|
||||
test("should reject duplicate names (case insensitive)", () => {
|
||||
const newSource: MarketplaceSource = {
|
||||
url: "https://git.company.com/user3/repo3",
|
||||
name: "SOURCE 1",
|
||||
enabled: true,
|
||||
}
|
||||
const errors = validateSourceDuplicates(existingSources, newSource)
|
||||
expect(errors).toHaveLength(1)
|
||||
expect(errors[0].field).toBe("name")
|
||||
expect(errors[0].message).toContain("duplicate")
|
||||
})
|
||||
|
||||
test("should detect duplicates within source list", () => {
|
||||
const sourcesWithDuplicates: MarketplaceSource[] = [
|
||||
{ url: "https://git.company.com/user1/repo1", name: "Source 1", enabled: true },
|
||||
{ url: "https://git.company.com/user1/repo1", name: "Source 2", enabled: true }, // Duplicate URL
|
||||
{ url: "https://git.company.com/user3/repo3", name: "Source 1", enabled: true }, // Duplicate name
|
||||
]
|
||||
const errors = validateSourceDuplicates(sourcesWithDuplicates)
|
||||
expect(errors).toHaveLength(4) // Two URL duplicates (bidirectional) and two name duplicates (bidirectional)
|
||||
|
||||
// Check for URL duplicates
|
||||
const urlErrors = errors.filter((e) => e.field === "url")
|
||||
expect(urlErrors).toHaveLength(2)
|
||||
expect(urlErrors[0].message).toContain("Source #1 has a duplicate URL with Source #2")
|
||||
expect(urlErrors[1].message).toContain("Source #2 has a duplicate URL with Source #1")
|
||||
|
||||
// Check for name duplicates
|
||||
const nameErrors = errors.filter((e) => e.field === "name")
|
||||
expect(nameErrors).toHaveLength(2)
|
||||
expect(nameErrors[0].message).toContain("Source #1 has a duplicate name with Source #3")
|
||||
expect(nameErrors[1].message).toContain("Source #3 has a duplicate name with Source #1")
|
||||
})
|
||||
})
|
||||
|
||||
describe("validateSource", () => {
|
||||
const existingSources: MarketplaceSource[] = [
|
||||
{ url: "https://git.company.com/user1/repo1", name: "Source 1", enabled: true },
|
||||
]
|
||||
|
||||
test("should accept valid source", () => {
|
||||
const source: MarketplaceSource = {
|
||||
url: "https://git.company.com/user2/repo2",
|
||||
name: "Source 2",
|
||||
enabled: true,
|
||||
}
|
||||
const errors = validateSource(source, existingSources)
|
||||
expect(errors).toHaveLength(0)
|
||||
})
|
||||
|
||||
test("should accumulate multiple validation errors", () => {
|
||||
const source: MarketplaceSource = {
|
||||
url: "https://git.company.com/user1/repo1", // Duplicate URL
|
||||
name: "This name is way too long to be valid\t", // Too long and has tab
|
||||
enabled: true,
|
||||
}
|
||||
const errors = validateSource(source, existingSources)
|
||||
expect(errors.length).toBeGreaterThan(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("validateSources", () => {
|
||||
test("should accept valid source list", () => {
|
||||
const sources: MarketplaceSource[] = [
|
||||
{ url: "https://git.company.com/user1/repo1", name: "Source 1", enabled: true },
|
||||
{ url: "https://git.company.com/user2/repo2", name: "Source 2", enabled: true },
|
||||
]
|
||||
const errors = validateSources(sources)
|
||||
expect(errors).toHaveLength(0)
|
||||
})
|
||||
|
||||
test("should detect multiple issues across sources", () => {
|
||||
const sources: MarketplaceSource[] = [
|
||||
{ url: "https://git.company.com/user1/repo1", name: "Source 1", enabled: true },
|
||||
{ url: "https://git.company.com/user1/repo1", name: "Source 1", enabled: true }, // Duplicate URL and name
|
||||
{ url: "invalid-url", name: "This name is way too long\t", enabled: true }, // Invalid URL and name
|
||||
]
|
||||
const errors = validateSources(sources)
|
||||
expect(errors.length).toBeGreaterThan(2)
|
||||
})
|
||||
|
||||
test("should include source index in error messages", () => {
|
||||
const sources: MarketplaceSource[] = [{ url: "invalid-url", name: "Source 1", enabled: true }]
|
||||
const errors = validateSources(sources)
|
||||
expect(errors[0].message).toContain("Source #1")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import * as path from "path"
|
||||
import { GitFetcher } from "../GitFetcher"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
describe("MetadataScanner External References", () => {
|
||||
// TODO: remove this note
|
||||
// This test is expected to fail until we update the registry with the new wordings (`mcp server` => `mcp`)
|
||||
it.skip("should find all subcomponents in Project Manager package including external references", async () => {
|
||||
// Create a GitFetcher instance using the project's mock settings directory
|
||||
const mockContext = {
|
||||
globalStorageUri: { fsPath: path.resolve(__dirname, "../../../../mock/settings") },
|
||||
} as vscode.ExtensionContext
|
||||
const gitFetcher = new GitFetcher(mockContext)
|
||||
|
||||
// Fetch the marketplace repository
|
||||
const repoUrl = "https://github.com/RooVetGit/Roo-Code-Marketplace"
|
||||
const repo = await gitFetcher.fetchRepository(repoUrl)
|
||||
|
||||
// Find the Project Manager package
|
||||
const projectManager = repo.items.find((item) => item.name === "Project Manager Package")
|
||||
expect(projectManager).toBeDefined()
|
||||
expect(projectManager?.type).toBe("package")
|
||||
|
||||
// Verify it has exactly 2 subcomponents
|
||||
expect(projectManager?.items).toBeDefined()
|
||||
expect(projectManager?.items?.length).toBe(2)
|
||||
|
||||
// Verify one is a mode and one is an MCP server
|
||||
const hasMode = projectManager?.items?.some((item) => item.type === "mode")
|
||||
const hasMcpServer = projectManager?.items?.some((item) => item.type === "mcp")
|
||||
expect(hasMode).toBe(true)
|
||||
expect(hasMcpServer).toBe(true)
|
||||
|
||||
// Verify the MCP server is the Smartsheet component
|
||||
const smartsheet = projectManager?.items?.find(
|
||||
(item) => item.metadata?.name === "Smartsheet MCP - Project Management",
|
||||
)
|
||||
expect(smartsheet).toBeDefined()
|
||||
expect(smartsheet?.type).toBe("mcp")
|
||||
})
|
||||
})
|
||||
157
src/services/marketplace/__tests__/MetadataScanner.test.ts
Normal file
157
src/services/marketplace/__tests__/MetadataScanner.test.ts
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
jest.mock("fs/promises", () => {
|
||||
const mockStat = jest.fn()
|
||||
const mockReaddir = jest.fn()
|
||||
const mockReadFile = jest.fn()
|
||||
return {
|
||||
stat: mockStat,
|
||||
readdir: mockReaddir,
|
||||
readFile: mockReadFile,
|
||||
}
|
||||
})
|
||||
|
||||
import * as path from "path"
|
||||
import { jest } from "@jest/globals"
|
||||
import { Dirent, Stats } from "fs"
|
||||
import { MetadataScanner } from "../MetadataScanner"
|
||||
import { SimpleGit } from "simple-git"
|
||||
import * as fs from "fs/promises"
|
||||
|
||||
// Helper function to normalize paths for test assertions
|
||||
const normalizePath = (p: string) => p.replace(/\\/g, "/")
|
||||
|
||||
// Create mock git functions with proper types
|
||||
const mockGitRaw = jest.fn<() => Promise<string>>()
|
||||
const mockGitRevparse = jest.fn<() => Promise<string>>()
|
||||
|
||||
describe("MetadataScanner", () => {
|
||||
let metadataScanner: MetadataScanner
|
||||
const mockBasePath = "/test/repo"
|
||||
const mockRepoUrl = "https://example.com/repo"
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset all mocks
|
||||
jest.clearAllMocks()
|
||||
|
||||
// Create mock git instance with default date
|
||||
const mockGit = {
|
||||
raw: mockGitRaw.mockResolvedValue("2025-04-13T09:00:00-07:00"),
|
||||
revparse: mockGitRevparse.mockResolvedValue("main"),
|
||||
} as unknown as SimpleGit
|
||||
|
||||
// Initialize MetadataScanner with mock git
|
||||
metadataScanner = new MetadataScanner(mockGit)
|
||||
})
|
||||
|
||||
describe("Basic Metadata Scanning", () => {
|
||||
it("should discover components with English metadata", async () => {
|
||||
// Setup mock implementations
|
||||
const mockStats = {
|
||||
isDirectory: () => true,
|
||||
isFile: () => true,
|
||||
mtime: new Date(),
|
||||
} as Stats
|
||||
|
||||
// Mock fs.promises methods using type assertions
|
||||
const mockedFs = jest.mocked(fs)
|
||||
mockedFs.stat.mockResolvedValue(mockStats)
|
||||
|
||||
// Define specific Dirent objects
|
||||
const componentDirDirent: Dirent = {
|
||||
name: "component1",
|
||||
isDirectory: () => true,
|
||||
isFile: () => false,
|
||||
} as Dirent
|
||||
const metadataFileDirent: Dirent = {
|
||||
name: "metadata.en.yml",
|
||||
isDirectory: () => false,
|
||||
isFile: () => true,
|
||||
} as Dirent
|
||||
|
||||
// Refined mock implementation for fs.readdir
|
||||
;(mockedFs.readdir as any).mockImplementation(async (p: string, options?: any) => {
|
||||
const normalizedP = normalizePath(p)
|
||||
const normalizedBasePath = normalizePath(mockBasePath)
|
||||
const normalizedComponentPath = normalizePath(path.join(mockBasePath, "component1"))
|
||||
|
||||
if (normalizedP === normalizedBasePath) {
|
||||
// For the base path, return only the component directory
|
||||
const baseDirents = [componentDirDirent]
|
||||
return options?.withFileTypes ? baseDirents : baseDirents.map((d) => d.name)
|
||||
} else if (normalizedP === normalizedComponentPath) {
|
||||
// For the component1 directory, return only the metadata file
|
||||
const componentDirents = [metadataFileDirent]
|
||||
return options?.withFileTypes ? componentDirents : componentDirents.map((d) => d.name)
|
||||
} else {
|
||||
// For any other path (deeper recursion), return empty
|
||||
return options?.withFileTypes ? [] : []
|
||||
}
|
||||
})
|
||||
|
||||
mockedFs.readFile.mockResolvedValue(
|
||||
Buffer.from(`
|
||||
name: Test Component
|
||||
description: A test component
|
||||
type: mcp
|
||||
version: 1.0.0
|
||||
sourceUrl: https://example.com/component1
|
||||
`),
|
||||
)
|
||||
|
||||
const items = await metadataScanner.scanDirectory(mockBasePath, mockRepoUrl)
|
||||
|
||||
expect(items).toHaveLength(1)
|
||||
expect(items[0].name).toBe("Test Component")
|
||||
expect(items[0].type).toBe("mcp")
|
||||
expect(items[0].url).toBe("https://example.com/repo/tree/main/component1")
|
||||
expect(items[0].path).toBe("component1")
|
||||
expect(items[0].sourceUrl).toBe("https://example.com/component1")
|
||||
})
|
||||
it("should handle missing sourceUrl in metadata", async () => {
|
||||
const mockDirents = [
|
||||
{
|
||||
name: "component2",
|
||||
isDirectory: () => true,
|
||||
isFile: () => false,
|
||||
},
|
||||
{
|
||||
name: "metadata.en.yml",
|
||||
isDirectory: () => false,
|
||||
isFile: () => true,
|
||||
},
|
||||
] as Dirent[]
|
||||
|
||||
const mockEmptyDirents = [] as Dirent[]
|
||||
const mockStats = {
|
||||
isDirectory: () => true,
|
||||
isFile: () => true,
|
||||
mtime: new Date(),
|
||||
} as Stats
|
||||
|
||||
const mockedFs = jest.mocked(fs)
|
||||
mockedFs.stat.mockResolvedValue(mockStats)
|
||||
;(mockedFs.readdir as any).mockImplementation(async (path: any, options?: any) => {
|
||||
if (path.toString().includes("/component2/")) {
|
||||
return options?.withFileTypes ? mockEmptyDirents : []
|
||||
}
|
||||
return options?.withFileTypes ? mockDirents : mockDirents.map((d) => d.name)
|
||||
})
|
||||
mockedFs.readFile.mockResolvedValue(
|
||||
Buffer.from(`
|
||||
name: Test Component 2
|
||||
description: A test component without sourceUrl
|
||||
type: mcp
|
||||
version: 1.0.0
|
||||
`),
|
||||
)
|
||||
|
||||
const items = await metadataScanner.scanDirectory(mockBasePath, mockRepoUrl)
|
||||
|
||||
expect(items).toHaveLength(1)
|
||||
expect(items[0].name).toBe("Test Component 2")
|
||||
expect(items[0].type).toBe("mcp")
|
||||
expect(items[0].url).toBe("https://example.com/repo/tree/main/component2")
|
||||
expect(items[0].path).toBe("component2")
|
||||
expect(items[0].sourceUrl).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
133
src/services/marketplace/__tests__/schemas.test.ts
Normal file
133
src/services/marketplace/__tests__/schemas.test.ts
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import {
|
||||
validateMetadata,
|
||||
validateAnyMetadata,
|
||||
repositoryMetadataSchema,
|
||||
componentMetadataSchema,
|
||||
packageMetadataSchema,
|
||||
} from "../schemas"
|
||||
|
||||
describe("Schema Validation", () => {
|
||||
describe("validateMetadata", () => {
|
||||
it("should validate repository metadata", () => {
|
||||
const data = {
|
||||
name: "Test Repository",
|
||||
description: "A test repository",
|
||||
version: "1.0.0",
|
||||
tags: ["test"],
|
||||
}
|
||||
|
||||
expect(() => validateMetadata(data, repositoryMetadataSchema)).not.toThrow()
|
||||
})
|
||||
|
||||
it("should validate component metadata", () => {
|
||||
const data = {
|
||||
name: "Test Component",
|
||||
description: "A test component",
|
||||
version: "1.0.0",
|
||||
type: "mcp",
|
||||
tags: ["test"],
|
||||
}
|
||||
|
||||
expect(() => validateMetadata(data, componentMetadataSchema)).not.toThrow()
|
||||
})
|
||||
|
||||
it("should validate package metadata", () => {
|
||||
const data = {
|
||||
name: "Test Package",
|
||||
description: "A test package",
|
||||
version: "1.0.0",
|
||||
type: "package",
|
||||
items: [{ type: "mcp", path: "../external/server" }],
|
||||
}
|
||||
|
||||
expect(() => validateMetadata(data, packageMetadataSchema)).not.toThrow()
|
||||
})
|
||||
|
||||
it("should throw error for missing required fields", () => {
|
||||
const data = {
|
||||
description: "Missing name",
|
||||
version: "1.0.0",
|
||||
}
|
||||
|
||||
expect(() => validateMetadata(data, repositoryMetadataSchema)).toThrow("name: Name is required")
|
||||
})
|
||||
|
||||
it("should throw error for invalid version format", () => {
|
||||
const data = {
|
||||
name: "Test",
|
||||
description: "Test",
|
||||
version: "invalid",
|
||||
}
|
||||
|
||||
expect(() => validateMetadata(data, repositoryMetadataSchema)).toThrow(
|
||||
"version: Version must be in semver format",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("validateAnyMetadata", () => {
|
||||
it("should auto-detect and validate repository metadata", () => {
|
||||
const data = {
|
||||
name: "Test Repository",
|
||||
description: "A test repository",
|
||||
version: "1.0.0",
|
||||
}
|
||||
|
||||
expect(() => validateAnyMetadata(data)).not.toThrow()
|
||||
})
|
||||
|
||||
it("should auto-detect and validate component metadata", () => {
|
||||
const data = {
|
||||
name: "Test Component",
|
||||
description: "A test component",
|
||||
version: "1.0.0",
|
||||
type: "mcp",
|
||||
}
|
||||
|
||||
expect(() => validateAnyMetadata(data)).not.toThrow()
|
||||
})
|
||||
|
||||
it("should auto-detect and validate package metadata", () => {
|
||||
const data = {
|
||||
name: "Test Package",
|
||||
description: "A test package",
|
||||
version: "1.0.0",
|
||||
type: "package",
|
||||
items: [{ type: "mcp", path: "../external/server" }],
|
||||
}
|
||||
|
||||
expect(() => validateAnyMetadata(data)).not.toThrow()
|
||||
})
|
||||
|
||||
it("should throw error for unknown component type", () => {
|
||||
const data = {
|
||||
name: "Test",
|
||||
description: "Test",
|
||||
version: "1.0.0",
|
||||
type: "unknown",
|
||||
}
|
||||
|
||||
expect(() => validateAnyMetadata(data)).toThrow("Unknown component type: unknown")
|
||||
})
|
||||
|
||||
it("should throw error for invalid external item reference", () => {
|
||||
const data = {
|
||||
name: "Test Package",
|
||||
description: "Test package",
|
||||
version: "1.0.0",
|
||||
type: "package",
|
||||
items: [{ type: "unknown", path: "../external/server" }],
|
||||
}
|
||||
|
||||
expect(() => validateAnyMetadata(data)).toThrow('type: Invalid value "unknown"')
|
||||
})
|
||||
|
||||
it("should throw error for non-object input", () => {
|
||||
expect(() => validateAnyMetadata("not an object")).toThrow("Invalid metadata: must be an object")
|
||||
})
|
||||
|
||||
it("should throw error for null input", () => {
|
||||
expect(() => validateAnyMetadata(null)).toThrow("Invalid metadata: must be an object")
|
||||
})
|
||||
})
|
||||
})
|
||||
22
src/services/marketplace/constants.ts
Normal file
22
src/services/marketplace/constants.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* Constants for the marketplace
|
||||
*/
|
||||
|
||||
/**
|
||||
* Default marketplace repository URL
|
||||
*/
|
||||
export const DEFAULT_PACKAGE_MANAGER_REPO_URL = "https://github.com/RooVetGit/Roo-Code-Marketplace"
|
||||
|
||||
/**
|
||||
* Default marketplace repository name
|
||||
*/
|
||||
export const DEFAULT_PACKAGE_MANAGER_REPO_NAME = "Roo Code"
|
||||
|
||||
/**
|
||||
* Default marketplace source
|
||||
*/
|
||||
export const DEFAULT_MARKETPLACE_SOURCE = {
|
||||
url: DEFAULT_PACKAGE_MANAGER_REPO_URL,
|
||||
name: DEFAULT_PACKAGE_MANAGER_REPO_NAME,
|
||||
enabled: true,
|
||||
}
|
||||
4
src/services/marketplace/index.ts
Normal file
4
src/services/marketplace/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export * from "./GitFetcher"
|
||||
export * from "./MarketplaceManager"
|
||||
export * from "./types"
|
||||
export * from "../../shared/MarketplaceValidation"
|
||||
178
src/services/marketplace/schemas.ts
Normal file
178
src/services/marketplace/schemas.ts
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
import { z } from "zod"
|
||||
|
||||
/**
|
||||
* Base metadata schema with common fields
|
||||
*/
|
||||
export const baseMetadataSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
name: z.string().min(1, "Name is required"),
|
||||
description: z.string(),
|
||||
version: z.string(),
|
||||
binaryUrl: z.string().url("Binary URL must be a valid URL").optional(),
|
||||
binaryHash: z.string().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
author: z.string().optional(),
|
||||
authorUrl: z.string().url("Author URL must be a valid URL").optional(),
|
||||
sourceUrl: z.string().url("Source URL must be a valid URL").optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Component type validation
|
||||
*/
|
||||
export const marketplaceItemTypeSchema = z.enum(["mode", "prompt", "package", "mcp"] as const)
|
||||
|
||||
/**
|
||||
* Repository metadata schema
|
||||
*/
|
||||
export const repositoryMetadataSchema = baseMetadataSchema
|
||||
|
||||
/**
|
||||
* Component metadata schema
|
||||
*/
|
||||
export const componentMetadataSchema = baseMetadataSchema.extend({
|
||||
type: marketplaceItemTypeSchema,
|
||||
})
|
||||
|
||||
/**
|
||||
* External item reference schema
|
||||
*/
|
||||
export const externalItemSchema = z.object({
|
||||
type: marketplaceItemTypeSchema,
|
||||
path: z.string().min(1, "Path is required"),
|
||||
})
|
||||
|
||||
/**
|
||||
* Package metadata schema
|
||||
*/
|
||||
export const packageMetadataSchema = componentMetadataSchema.extend({
|
||||
type: z.literal("package"),
|
||||
items: z.array(externalItemSchema).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Validate parsed YAML against a schema
|
||||
* @param data Data to validate
|
||||
* @param schema Schema to validate against
|
||||
* @returns Validated data
|
||||
* @throws Error if validation fails
|
||||
*/
|
||||
export function validateMetadata<T>(data: unknown, schema: z.ZodType<T>): T {
|
||||
try {
|
||||
return schema.parse(data)
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
const issues = error.issues
|
||||
.map((issue) => {
|
||||
const path = issue.path.join(".")
|
||||
// Format error messages to match expected format
|
||||
if (issue.message === "Required") {
|
||||
if (path === "name") {
|
||||
return "name: Name is required"
|
||||
}
|
||||
return path ? `${path}: ${path.split(".").pop()} is required` : "Required field missing"
|
||||
}
|
||||
if (issue.code === "invalid_enum_value") {
|
||||
return path ? `${path}: Invalid value "${issue.received}"` : `Invalid value "${issue.received}"`
|
||||
}
|
||||
return path ? `${path}: ${issue.message}` : issue.message
|
||||
})
|
||||
.join("\n")
|
||||
throw new Error(issues)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine metadata type and validate
|
||||
* @param data Data to validate
|
||||
* @returns Validated metadata
|
||||
* @throws Error if validation fails
|
||||
*/
|
||||
export function validateAnyMetadata(data: unknown) {
|
||||
// Try to determine the type of metadata
|
||||
if (typeof data === "object" && data !== null) {
|
||||
const obj = data as Record<string, unknown>
|
||||
|
||||
if ("type" in obj) {
|
||||
const type = obj.type
|
||||
switch (type) {
|
||||
case "package":
|
||||
return validateMetadata(data, packageMetadataSchema)
|
||||
case "mode":
|
||||
case "mcp":
|
||||
case "prompt":
|
||||
case "role":
|
||||
case "storage":
|
||||
return validateMetadata(data, componentMetadataSchema)
|
||||
default:
|
||||
throw new Error(`Unknown component type: ${String(type)}`)
|
||||
}
|
||||
} else {
|
||||
// No type field, assume repository metadata
|
||||
return validateMetadata(data, repositoryMetadataSchema)
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Invalid metadata: must be an object")
|
||||
}
|
||||
|
||||
/**
|
||||
* Schema for a single marketplace item parameter
|
||||
*/
|
||||
export const parameterSchema = z.record(z.string(), z.any())
|
||||
|
||||
/**
|
||||
* Schema for a marketplace item
|
||||
*/
|
||||
export const marketplaceItemSchema = baseMetadataSchema.extend({
|
||||
id: z.string(),
|
||||
type: marketplaceItemTypeSchema,
|
||||
url: z.string(),
|
||||
repoUrl: z.string(),
|
||||
sourceName: z.string().optional(),
|
||||
lastUpdated: z.string().optional(),
|
||||
defaultBranch: z.string().optional(),
|
||||
path: z.string().optional(),
|
||||
items: z
|
||||
.array(
|
||||
z.object({
|
||||
type: marketplaceItemTypeSchema,
|
||||
path: z.string(),
|
||||
metadata: componentMetadataSchema.optional(),
|
||||
lastUpdated: z.string().optional(),
|
||||
matchInfo: z
|
||||
.object({
|
||||
// Assuming MatchInfo is an object, adjust if needed
|
||||
matched: z.boolean(),
|
||||
matchReason: z
|
||||
.object({
|
||||
nameMatch: z.boolean().optional(),
|
||||
descriptionMatch: z.boolean().optional(),
|
||||
tagMatch: z.boolean().optional(),
|
||||
typeMatch: z.boolean().optional(),
|
||||
hasMatchingSubcomponents: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
matchInfo: z
|
||||
.object({
|
||||
// Assuming MatchInfo is an object, adjust if needed
|
||||
matched: z.boolean(),
|
||||
matchReason: z
|
||||
.object({
|
||||
nameMatch: z.boolean().optional(),
|
||||
descriptionMatch: z.boolean().optional(),
|
||||
tagMatch: z.boolean().optional(),
|
||||
typeMatch: z.boolean().optional(),
|
||||
hasMatchingSubcomponents: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
parameters: z.record(z.string(), z.any()).optional(),
|
||||
})
|
||||
159
src/services/marketplace/types.ts
Normal file
159
src/services/marketplace/types.ts
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
import { RocketConfig } from "config-rocket"
|
||||
|
||||
/**
|
||||
* Information about why an item matched search/filter criteria
|
||||
*/
|
||||
export interface MatchInfo {
|
||||
matched: boolean
|
||||
matchReason?: {
|
||||
nameMatch?: boolean
|
||||
descriptionMatch?: boolean
|
||||
tagMatch?: boolean
|
||||
typeMatch?: boolean
|
||||
hasMatchingSubcomponents?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Supported component types
|
||||
*/
|
||||
export type MarketplaceItemType = "mode" | "prompt" | "package" | "mcp"
|
||||
|
||||
/**
|
||||
* Base metadata interface
|
||||
*/
|
||||
export interface BaseMetadata {
|
||||
id?: string
|
||||
name: string
|
||||
description: string
|
||||
version: string
|
||||
binaryUrl?: string
|
||||
binaryHash?: string
|
||||
tags?: string[]
|
||||
author?: string
|
||||
authorUrl?: string
|
||||
sourceUrl?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Repository root metadata
|
||||
*/
|
||||
export interface RepositoryMetadata extends BaseMetadata {}
|
||||
|
||||
/**
|
||||
* Component metadata with type
|
||||
*/
|
||||
export interface ComponentMetadata extends BaseMetadata {
|
||||
type: MarketplaceItemType
|
||||
}
|
||||
|
||||
/**
|
||||
* Package metadata with optional subcomponents
|
||||
*/
|
||||
export interface PackageMetadata extends ComponentMetadata {
|
||||
type: "package"
|
||||
items?: {
|
||||
type: MarketplaceItemType
|
||||
path: string
|
||||
metadata?: ComponentMetadata
|
||||
}[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Subcomponent metadata with parent reference
|
||||
*/
|
||||
export interface SubcomponentMetadata extends ComponentMetadata {
|
||||
parentPackage: {
|
||||
name: string
|
||||
path: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an individual parsed marketplace item
|
||||
*/
|
||||
export interface MarketplaceItem {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
type: MarketplaceItemType
|
||||
url: string
|
||||
repoUrl: string
|
||||
sourceName?: string
|
||||
author?: string
|
||||
authorUrl?: string
|
||||
tags?: string[]
|
||||
version: string
|
||||
binaryUrl?: string
|
||||
binaryHash?: string
|
||||
lastUpdated?: string
|
||||
sourceUrl?: string
|
||||
defaultBranch?: string
|
||||
path?: string // Add path to main item
|
||||
items?: {
|
||||
type: MarketplaceItemType
|
||||
path: string
|
||||
metadata?: ComponentMetadata
|
||||
lastUpdated?: string
|
||||
matchInfo?: MatchInfo // Add match information for subcomponents
|
||||
}[]
|
||||
matchInfo?: MatchInfo // Add match information for the package itself
|
||||
config?: RocketConfig // Revert to using RocketConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a Git repository source for marketplace items
|
||||
*/
|
||||
export interface MarketplaceSource {
|
||||
url: string
|
||||
name?: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a repository with its metadata and items
|
||||
*/
|
||||
export interface MarketplaceRepository {
|
||||
metadata: RepositoryMetadata
|
||||
items: MarketplaceItem[]
|
||||
url: string
|
||||
error?: string
|
||||
defaultBranch?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility type for metadata files with locale
|
||||
*/
|
||||
export type LocalizedMetadata<T> = {
|
||||
[locale: string]: T
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for localization handling
|
||||
*/
|
||||
export interface LocalizationOptions {
|
||||
userLocale: string
|
||||
fallbackLocale: string
|
||||
}
|
||||
|
||||
export interface InstallMarketplaceItemOptions {
|
||||
/**
|
||||
* Specify the target scope
|
||||
*
|
||||
* @default 'project'
|
||||
*/
|
||||
target?: "global" | "project"
|
||||
/**
|
||||
* Parameters provided by the user for configurable marketplace items
|
||||
*/
|
||||
parameters?: Record<string, any>
|
||||
}
|
||||
|
||||
export interface RemoveInstalledMarketplaceItemOptions {
|
||||
/**
|
||||
* Specify the target scope
|
||||
*
|
||||
* @default 'project'
|
||||
*/
|
||||
target?: "global" | "project"
|
||||
}
|
||||
13
src/services/marketplace/utils.ts
Normal file
13
src/services/marketplace/utils.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import * as vscode from "vscode"
|
||||
|
||||
/**
|
||||
* Gets the user's locale from VS Code environment
|
||||
* @returns The user's locale code (e.g., 'en', 'fr')
|
||||
*/
|
||||
export function getUserLocale(): string {
|
||||
// Get from VS Code API
|
||||
const vscodeLocale = vscode.env.language
|
||||
|
||||
// Extract just the language part (e.g., "en-US" -> "en")
|
||||
return vscodeLocale.split("-")[0].toLowerCase()
|
||||
}
|
||||
|
|
@ -16,6 +16,8 @@ import {
|
|||
import { McpServer } from "./mcp"
|
||||
import { Mode } from "./modes"
|
||||
import { RouterModels } from "./api"
|
||||
import { MarketplaceItem, MarketplaceSource } from "../services/marketplace/types"
|
||||
import { FullInstallatedMetadata } from "../services/marketplace/InstalledMetadataManager"
|
||||
|
||||
export type { ProviderSettingsEntry, ToolProgressStatus }
|
||||
|
||||
|
|
@ -70,13 +72,18 @@ export interface ExtensionMessage {
|
|||
| "commandExecutionStatus"
|
||||
| "vsCodeSetting"
|
||||
| "condenseTaskContextResponse"
|
||||
| "openMarketplaceInstallSidebarWithConfig"
|
||||
| "repositoryRefreshComplete"
|
||||
text?: string
|
||||
payload?: any // Add a generic payload for now, can refine later
|
||||
// Expected payload for "openMarketplaceInstallSidebarWithConfig": { item: MarketplaceItem, config: RocketConfig | undefined }
|
||||
action?:
|
||||
| "chatButtonClicked"
|
||||
| "mcpButtonClicked"
|
||||
| "settingsButtonClicked"
|
||||
| "historyButtonClicked"
|
||||
| "promptsButtonClicked"
|
||||
| "marketplaceButtonClicked"
|
||||
| "didBecomeVisible"
|
||||
| "focusInput"
|
||||
invoke?: "newChat" | "sendMessage" | "primaryButtonClick" | "secondaryButtonClick" | "setChatBoxMessage"
|
||||
|
|
@ -108,6 +115,8 @@ export interface ExtensionMessage {
|
|||
error?: string
|
||||
setting?: string
|
||||
value?: any
|
||||
items?: MarketplaceItem[]
|
||||
url?: string // For repositoryRefreshComplete
|
||||
}
|
||||
|
||||
export type ExtensionState = Pick<
|
||||
|
|
@ -207,6 +216,9 @@ export type ExtensionState = Pick<
|
|||
settingsImportedAt?: number
|
||||
historyPreviewCollapsed?: boolean
|
||||
autoCondenseContextPercent: number
|
||||
marketplaceSources?: MarketplaceSource[]
|
||||
marketplaceItems?: MarketplaceItem[]
|
||||
marketplaceInstalledMetadata?: FullInstallatedMetadata
|
||||
}
|
||||
|
||||
export type { ClineMessage, ClineAsk, ClineSay }
|
||||
|
|
|
|||
254
src/shared/MarketplaceValidation.ts
Normal file
254
src/shared/MarketplaceValidation.ts
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
/**
|
||||
* Shared validation utilities for marketplace sources
|
||||
*/
|
||||
import { MarketplaceSource } from "../services/marketplace/types"
|
||||
|
||||
/**
|
||||
* Error type for marketplace source validation
|
||||
*/
|
||||
export interface ValidationError {
|
||||
field: string
|
||||
message: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a URL is a valid Git repository URL
|
||||
* @param url The URL to validate
|
||||
* @returns True if the URL is a valid Git repository URL, false otherwise
|
||||
*/
|
||||
export function isValidGitRepositoryUrl(url: string): boolean {
|
||||
// Trim the URL to remove any leading/trailing whitespace
|
||||
const trimmedUrl = url.trim()
|
||||
|
||||
// HTTPS pattern (GitHub, GitLab, Bitbucket, etc.)
|
||||
// Examples:
|
||||
// - https://github.com/username/repo
|
||||
// - https://github.com/username/repo.git
|
||||
// - https://gitlab.com/username/repo
|
||||
// - https://bitbucket.org/username/repo
|
||||
const httpsPattern =
|
||||
/^https?:\/\/(?:[a-zA-Z0-9_-]+\.)*[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+(?:\/[^/]+)*(?:\.git)?$/
|
||||
|
||||
// SSH pattern
|
||||
// Examples:
|
||||
// - git@github.com:username/repo.git
|
||||
// - git@gitlab.com:username/repo.git
|
||||
const sshPattern = /^git@(?:[a-zA-Z0-9_-]+\.)*[a-zA-Z0-9_-]+:([a-zA-Z0-9_-]+)\/([a-zA-Z0-9_-]+)(?:\.git)?$/
|
||||
|
||||
// Git protocol pattern
|
||||
// Examples:
|
||||
// - git://github.com/username/repo.git
|
||||
const gitProtocolPattern = /^git:\/\/(?:[a-zA-Z0-9_-]+\.)*[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+(?:\.git)?$/
|
||||
|
||||
return httpsPattern.test(trimmedUrl) || sshPattern.test(trimmedUrl) || gitProtocolPattern.test(trimmedUrl)
|
||||
}
|
||||
|
||||
export function validateSourceUrl(url: string): ValidationError[] {
|
||||
const errors: ValidationError[] = []
|
||||
|
||||
// Check if URL is empty
|
||||
if (!url) {
|
||||
errors.push({
|
||||
field: "url",
|
||||
message: "URL cannot be empty",
|
||||
})
|
||||
return errors // Return early if URL is empty
|
||||
}
|
||||
|
||||
// Check for non-visible characters (except spaces)
|
||||
const nonVisibleCharRegex = /[^\S ]/
|
||||
if (nonVisibleCharRegex.test(url)) {
|
||||
errors.push({
|
||||
field: "url",
|
||||
message: "URL contains non-visible characters other than spaces",
|
||||
})
|
||||
}
|
||||
|
||||
// Check if URL is a valid Git repository URL
|
||||
if (!isValidGitRepositoryUrl(url)) {
|
||||
errors.push({
|
||||
field: "url",
|
||||
message: "URL must be a valid Git repository URL (e.g., https://git.example.com/username/repo)",
|
||||
})
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
export function validateSourceName(name?: string): ValidationError[] {
|
||||
const errors: ValidationError[] = []
|
||||
|
||||
// Skip validation if name is not provided
|
||||
if (!name) {
|
||||
return errors
|
||||
}
|
||||
|
||||
// Check name length
|
||||
if (name.length > 20) {
|
||||
errors.push({
|
||||
field: "name",
|
||||
message: "Name must be 20 characters or less",
|
||||
})
|
||||
}
|
||||
|
||||
// Check for non-visible characters (except spaces)
|
||||
const nonVisibleCharRegex = /[^\S ]/
|
||||
if (nonVisibleCharRegex.test(name)) {
|
||||
errors.push({
|
||||
field: "name",
|
||||
message: "Name contains non-visible characters other than spaces",
|
||||
})
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
// Cache for normalized strings to avoid repeated operations
|
||||
const normalizeCache = new Map<string, string>()
|
||||
|
||||
function normalizeString(str: string): string {
|
||||
const cached = normalizeCache.get(str)
|
||||
if (cached) return cached
|
||||
|
||||
const normalized = str.toLowerCase().replace(/\s+/g, "")
|
||||
normalizeCache.set(str, normalized)
|
||||
return normalized
|
||||
}
|
||||
|
||||
export function validateSourceDuplicates(
|
||||
sources: MarketplaceSource[],
|
||||
newSource?: MarketplaceSource,
|
||||
): ValidationError[] {
|
||||
const errors: ValidationError[] = []
|
||||
|
||||
// Process existing sources
|
||||
const seen = new Set<string>()
|
||||
|
||||
// Check for duplicates within existing sources
|
||||
for (let i = 0; i < sources.length; i++) {
|
||||
const source = sources[i]
|
||||
const normalizedUrl = normalizeString(source.url)
|
||||
const normalizedName = source.name ? normalizeString(source.name) : null
|
||||
|
||||
// Check for URL duplicates
|
||||
for (let j = i + 1; j < sources.length; j++) {
|
||||
const otherSource = sources[j]
|
||||
const otherUrl = normalizeString(otherSource.url)
|
||||
|
||||
if (normalizedUrl === otherUrl) {
|
||||
const key = `url:${i}:${j}`
|
||||
if (!seen.has(key)) {
|
||||
errors.push({
|
||||
field: "url",
|
||||
message: `Source #${i + 1} has a duplicate URL with Source #${j + 1}`,
|
||||
})
|
||||
errors.push({
|
||||
field: "url",
|
||||
message: `Source #${j + 1} has a duplicate URL with Source #${i + 1}`,
|
||||
})
|
||||
seen.add(key)
|
||||
seen.add(`url:${j}:${i}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for name duplicates if both have names
|
||||
if (normalizedName && otherSource.name) {
|
||||
const otherName = normalizeString(otherSource.name)
|
||||
if (normalizedName === otherName) {
|
||||
const key = `name:${i}:${j}`
|
||||
if (!seen.has(key)) {
|
||||
errors.push({
|
||||
field: "name",
|
||||
message: `Source #${i + 1} has a duplicate name with Source #${j + 1}`,
|
||||
})
|
||||
errors.push({
|
||||
field: "name",
|
||||
message: `Source #${j + 1} has a duplicate name with Source #${i + 1}`,
|
||||
})
|
||||
seen.add(key)
|
||||
seen.add(`name:${j}:${i}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check new source against existing sources if provided
|
||||
if (newSource) {
|
||||
const normalizedNewUrl = normalizeString(newSource.url)
|
||||
const normalizedNewName = newSource.name ? normalizeString(newSource.name) : null
|
||||
|
||||
// Check for duplicates with existing sources
|
||||
for (let i = 0; i < sources.length; i++) {
|
||||
const source = sources[i]
|
||||
const sourceUrl = normalizeString(source.url)
|
||||
|
||||
if (sourceUrl === normalizedNewUrl) {
|
||||
errors.push({
|
||||
field: "url",
|
||||
message: `URL is a duplicate of Source #${i + 1}`,
|
||||
})
|
||||
}
|
||||
|
||||
if (source.name && normalizedNewName) {
|
||||
const sourceName = normalizeString(source.name)
|
||||
if (sourceName === normalizedNewName) {
|
||||
errors.push({
|
||||
field: "name",
|
||||
message: `Name is a duplicate of Source #${i + 1}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
export function validateSource(
|
||||
source: MarketplaceSource,
|
||||
existingSources: MarketplaceSource[] = [],
|
||||
): ValidationError[] {
|
||||
// Combine all validation errors
|
||||
return [
|
||||
...validateSourceUrl(source.url),
|
||||
...validateSourceName(source.name),
|
||||
...validateSourceDuplicates(existingSources, source),
|
||||
]
|
||||
}
|
||||
|
||||
export function validateSources(sources: MarketplaceSource[]): ValidationError[] {
|
||||
// Pre-allocate maximum possible size for errors array
|
||||
const errors: ValidationError[] = new Array(sources.length * 2 + (sources.length * (sources.length - 1)) / 2)
|
||||
let errorIndex = 0
|
||||
|
||||
// Validate each source individually
|
||||
for (let i = 0; i < sources.length; i++) {
|
||||
const source = sources[i]
|
||||
const urlErrors = validateSourceUrl(source.url)
|
||||
const nameErrors = validateSourceName(source.name)
|
||||
|
||||
// Add index to error messages
|
||||
for (const error of urlErrors) {
|
||||
errors[errorIndex++] = {
|
||||
field: error.field,
|
||||
message: `Source #${i + 1}: ${error.message}`,
|
||||
}
|
||||
}
|
||||
for (const error of nameErrors) {
|
||||
errors[errorIndex++] = {
|
||||
field: error.field,
|
||||
message: `Source #${i + 1}: ${error.message}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for duplicates across all sources
|
||||
const duplicateErrors = validateSourceDuplicates(sources)
|
||||
for (const error of duplicateErrors) {
|
||||
errors[errorIndex++] = error
|
||||
}
|
||||
|
||||
// Trim array to actual size
|
||||
return errors.slice(0, errorIndex)
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import { z } from "zod"
|
|||
|
||||
import { ProviderSettings } from "./api"
|
||||
import { Mode, PromptComponent, ModeConfig } from "./modes"
|
||||
import { InstallMarketplaceItemOptions, MarketplaceItem, MarketplaceSource } from "../services/marketplace/types"
|
||||
|
||||
export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse"
|
||||
|
||||
|
|
@ -133,6 +134,19 @@ export interface WebviewMessage {
|
|||
| "toggleApiConfigPin"
|
||||
| "setHistoryPreviewCollapsed"
|
||||
| "condenseTaskContextRequest"
|
||||
| "repositoryRefreshComplete"
|
||||
| "setHistoryPreviewCollapsed"
|
||||
| "openExternal"
|
||||
| "marketplaceSources"
|
||||
| "fetchMarketplaceItems"
|
||||
| "filterMarketplaceItems"
|
||||
| "marketplaceButtonClicked"
|
||||
| "refreshMarketplaceSource"
|
||||
| "installMarketplaceItem"
|
||||
| "installMarketplaceItemWithParameters"
|
||||
| "cancelMarketplaceInstall"
|
||||
| "removeInstalledMarketplaceItem"
|
||||
| "openMarketplaceInstallSidebarWithConfig"
|
||||
text?: string
|
||||
disabled?: boolean
|
||||
askResponse?: ClineAskResponse
|
||||
|
|
@ -162,6 +176,12 @@ export interface WebviewMessage {
|
|||
hasSystemPromptOverride?: boolean
|
||||
terminalOperation?: "continue" | "abort"
|
||||
historyPreviewCollapsed?: boolean
|
||||
sources?: MarketplaceSource[]
|
||||
filters?: { type?: string; search?: string; tags?: string[] }
|
||||
url?: string // For openExternal
|
||||
mpItem?: MarketplaceItem
|
||||
mpInstallOptions?: InstallMarketplaceItemOptions
|
||||
config?: Record<string, any> // Add config to the payload
|
||||
}
|
||||
|
||||
export const checkoutDiffPayloadSchema = z.object({
|
||||
|
|
@ -181,4 +201,25 @@ export const checkoutRestorePayloadSchema = z.object({
|
|||
|
||||
export type CheckpointRestorePayload = z.infer<typeof checkoutRestorePayloadSchema>
|
||||
|
||||
export type WebViewMessagePayload = CheckpointDiffPayload | CheckpointRestorePayload
|
||||
import { marketplaceItemSchema } from "../services/marketplace/schemas"
|
||||
|
||||
export const installMarketplaceItemWithParametersPayloadSchema = z.object({
|
||||
item: marketplaceItemSchema.strict(),
|
||||
parameters: z.record(z.string(), z.any()),
|
||||
})
|
||||
|
||||
export type InstallMarketplaceItemWithParametersPayload = z.infer<
|
||||
typeof installMarketplaceItemWithParametersPayloadSchema
|
||||
>
|
||||
|
||||
export const cancelMarketplaceInstallPayloadSchema = z.object({
|
||||
itemId: z.string(),
|
||||
})
|
||||
|
||||
export type CancelMarketplaceInstallPayload = z.infer<typeof cancelMarketplaceInstallPayloadSchema>
|
||||
|
||||
export type WebViewMessagePayload =
|
||||
| CheckpointDiffPayload
|
||||
| CheckpointRestorePayload
|
||||
| InstallMarketplaceItemWithParametersPayload
|
||||
| CancelMarketplaceInstallPayload
|
||||
|
|
|
|||
34
src/shared/__tests__/MarketplaceValidation.test.ts
Normal file
34
src/shared/__tests__/MarketplaceValidation.test.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { isValidGitRepositoryUrl } from "../MarketplaceValidation"
|
||||
|
||||
describe("Git URL Validation", () => {
|
||||
const validUrls = [
|
||||
"https://github.com/user/repo",
|
||||
"https://gitlab.com/group/repo",
|
||||
"https://git.internal.company.com/team/repo",
|
||||
"git@github.com:user/repo.git",
|
||||
"git@git.internal.company.com:team/repo",
|
||||
"git://gitlab.com/group/repo.git",
|
||||
"git://git.internal.company.com/team-name/repo-name",
|
||||
"https://github.com/org-name/repo-name",
|
||||
"git@gitlab.com:group-name/project-name.git",
|
||||
]
|
||||
|
||||
const invalidUrls = [
|
||||
"not-a-url",
|
||||
"http://single/repo",
|
||||
"https://github.com/no-repo",
|
||||
"git@github.com/wrong-format",
|
||||
"git://invalid@domain:repo",
|
||||
"git@domain:no-slash",
|
||||
"git@domain:/invalid-start",
|
||||
"git@domain:group//repo",
|
||||
]
|
||||
|
||||
test.each(validUrls)("should accept valid git URL: %s", (url) => {
|
||||
expect(isValidGitRepositoryUrl(url)).toBe(true)
|
||||
})
|
||||
|
||||
test.each(invalidUrls)("should reject invalid git URL: %s", (url) => {
|
||||
expect(isValidGitRepositoryUrl(url)).toBe(false)
|
||||
})
|
||||
})
|
||||
13
src/utils/globalContext.ts
Normal file
13
src/utils/globalContext.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { mkdir } from "fs/promises"
|
||||
import { join } from "path"
|
||||
import { ExtensionContext } from "vscode"
|
||||
|
||||
export async function getGlobalFsPath(context: ExtensionContext): Promise<string> {
|
||||
return context.globalStorageUri.fsPath
|
||||
}
|
||||
|
||||
export async function ensureSettingsDirectoryExists(context: ExtensionContext): Promise<string> {
|
||||
const settingsDir = join(context.globalStorageUri.fsPath, "settings")
|
||||
await mkdir(settingsDir, { recursive: true })
|
||||
return settingsDir
|
||||
}
|
||||
8
src/utils/url.ts
Normal file
8
src/utils/url.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export const isValidUrl = (urlString: string): boolean => {
|
||||
try {
|
||||
new URL(urlString)
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@
|
|||
"clean": "rimraf build tsconfig.tsbuildinfo .turbo"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-accordion": "^1.2.10",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||
"@radix-ui/react-checkbox": "^1.1.5",
|
||||
"@radix-ui/react-collapsible": "^1.1.3",
|
||||
|
|
@ -59,6 +60,7 @@
|
|||
"rehype-highlight": "^7.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remove-markdown": "^0.6.0",
|
||||
"rocket-config": "^1.0.7",
|
||||
"shell-quote": "^1.8.2",
|
||||
"shiki": "^3.2.1",
|
||||
"source-map": "^0.7.4",
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { useCallback, useEffect, useRef, useState } from "react"
|
||||
import { useCallback, useEffect, useRef, useState, useMemo } from "react"
|
||||
import { useEvent } from "react-use"
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
||||
|
||||
import { ExtensionMessage } from "@roo/shared/ExtensionMessage"
|
||||
import TranslationProvider from "./i18n/TranslationContext"
|
||||
import { MarketplaceViewStateManager } from "./components/marketplace/MarketplaceViewStateManager"
|
||||
|
||||
import { vscode } from "./utils/vscode"
|
||||
import { telemetryClient } from "./utils/TelemetryClient"
|
||||
|
|
@ -13,10 +14,11 @@ import HistoryView from "./components/history/HistoryView"
|
|||
import SettingsView, { SettingsViewRef } from "./components/settings/SettingsView"
|
||||
import WelcomeView from "./components/welcome/WelcomeView"
|
||||
import McpView from "./components/mcp/McpView"
|
||||
import { MarketplaceView } from "./components/marketplace/MarketplaceView"
|
||||
import PromptsView from "./components/prompts/PromptsView"
|
||||
import { HumanRelayDialog } from "./components/human-relay/HumanRelayDialog"
|
||||
|
||||
type Tab = "settings" | "history" | "mcp" | "prompts" | "chat"
|
||||
type Tab = "settings" | "history" | "mcp" | "prompts" | "chat" | "marketplace"
|
||||
|
||||
const tabsByMessageAction: Partial<Record<NonNullable<ExtensionMessage["action"]>, Tab>> = {
|
||||
chatButtonClicked: "chat",
|
||||
|
|
@ -24,12 +26,16 @@ const tabsByMessageAction: Partial<Record<NonNullable<ExtensionMessage["action"]
|
|||
promptsButtonClicked: "prompts",
|
||||
mcpButtonClicked: "mcp",
|
||||
historyButtonClicked: "history",
|
||||
marketplaceButtonClicked: "marketplace",
|
||||
}
|
||||
|
||||
const App = () => {
|
||||
const { didHydrateState, showWelcome, shouldShowAnnouncement, telemetrySetting, telemetryKey, machineId } =
|
||||
useExtensionState()
|
||||
|
||||
// Create a persistent state manager
|
||||
const marketplaceStateManager = useMemo(() => new MarketplaceViewStateManager(), [])
|
||||
|
||||
const [showAnnouncement, setShowAnnouncement] = useState(false)
|
||||
const [tab, setTab] = useState<Tab>("chat")
|
||||
|
||||
|
|
@ -118,6 +124,9 @@ const App = () => {
|
|||
{tab === "settings" && (
|
||||
<SettingsView ref={settingsRef} onDone={() => setTab("chat")} targetSection={currentSection} />
|
||||
)}
|
||||
{tab === "marketplace" && (
|
||||
<MarketplaceView stateManager={marketplaceStateManager} onDone={() => switchTab("chat")} />
|
||||
)}
|
||||
<ChatView
|
||||
ref={chatViewRef}
|
||||
isHidden={tab !== "chat"}
|
||||
|
|
|
|||
|
|
@ -6,3 +6,6 @@ export const Loader = () => React.createElement("div")
|
|||
export const X = () => React.createElement("div")
|
||||
export const Edit = () => React.createElement("div")
|
||||
export const Database = (props: any) => React.createElement("span", { "data-testid": "database-icon", ...props })
|
||||
export const MoreVertical = () => React.createElement("div", {}, "VerticalMenu")
|
||||
export const ExternalLink = () => React.createElement("div")
|
||||
export const Download = () => React.createElement("div")
|
||||
|
|
|
|||
90
webview-ui/src/components/marketplace/InstallSidebar.tsx
Normal file
90
webview-ui/src/components/marketplace/InstallSidebar.tsx
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import React, { useState } from "react"
|
||||
import { VSCodeButton, VSCodeTextField, VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
|
||||
import { MarketplaceItem } from "../../../../src/services/marketplace/types"
|
||||
import { RocketConfig } from "config-rocket"
|
||||
|
||||
interface MarketplaceInstallSidebarProps {
|
||||
item: MarketplaceItem
|
||||
config: RocketConfig
|
||||
onClose?: () => void
|
||||
onSubmit?: (item: MarketplaceItem, parameters: Record<string, any>) => void
|
||||
}
|
||||
|
||||
const InstallSidebar: React.FC<MarketplaceInstallSidebarProps> = ({ item, config, onClose, onSubmit }) => {
|
||||
const initialUserParameters = config.parameters!.reduce(
|
||||
(acc, param) => {
|
||||
if (param.resolver.operation === "prompt")
|
||||
acc[param.id] = param.resolver.initial ?? (param.resolver.type === "confirm" ? true : "")
|
||||
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, any>,
|
||||
)
|
||||
const [userParameters, setUserParameters] = useState<Record<string, any>>(initialUserParameters)
|
||||
|
||||
const handleParameterChange = (name: string, value: any) => {
|
||||
setUserParameters({ ...userParameters, [name]: value })
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (onSubmit && item) {
|
||||
onSubmit(item, userParameters)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 flex justify-end"
|
||||
onClick={onClose} // Close sidebar when clicking outside
|
||||
>
|
||||
<div
|
||||
className="flex flex-col p-4 bg-vscode-sideBar-background text-vscode-foreground h-full w-3/4 shadow-lg" // Adjust width and add shadow
|
||||
onClick={(e) => e.stopPropagation()}>
|
||||
<h2 className="text-xl font-bold mb-4">Install {item.name}</h2>
|
||||
<div className="flex-grow overflow-y-auto space-y-4">
|
||||
{config.parameters?.map((param) => {
|
||||
// Only render prompt parameters
|
||||
if (param.resolver.operation !== "prompt") return null
|
||||
|
||||
return (
|
||||
<div key={param.id} className="flex flex-col">
|
||||
<label htmlFor={param.id} className="text-sm font-semibold mb-1">
|
||||
{param.resolver.label || param.id}{" "}
|
||||
{/* Use label from resolver if available, otherwise name */}
|
||||
</label>
|
||||
{/* Render input based on param.resolver.type */}
|
||||
{param.resolver.type === "text" && (
|
||||
<VSCodeTextField
|
||||
id={param.id}
|
||||
value={userParameters[param.id]}
|
||||
onChange={(e) =>
|
||||
handleParameterChange(param.id, (e.target as HTMLInputElement).value)
|
||||
}
|
||||
className="w-full"></VSCodeTextField>
|
||||
)}
|
||||
{param.resolver.type === "confirm" && (
|
||||
<VSCodeCheckbox
|
||||
id={param.id}
|
||||
checked={userParameters[param.id]}
|
||||
onChange={(e) =>
|
||||
handleParameterChange(param.id, (e.target as HTMLInputElement).checked)
|
||||
}></VSCodeCheckbox>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="flex gap-2 mt-4">
|
||||
<VSCodeButton onClick={handleSubmit} className="flex-1">
|
||||
Install
|
||||
</VSCodeButton>
|
||||
<VSCodeButton appearance="secondary" onClick={onClose} className="flex-1">
|
||||
Cancel
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default InstallSidebar
|
||||
333
webview-ui/src/components/marketplace/MarketplaceListView.tsx
Normal file
333
webview-ui/src/components/marketplace/MarketplaceListView.tsx
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
import { Input } from "@/components/ui/input"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"
|
||||
import { Clock, X, ChevronsUpDown, Rocket, Server, Package, Sparkles, ALargeSmall } from "lucide-react"
|
||||
import { MarketplaceItemCard } from "./components/MarketplaceItemCard"
|
||||
import { MarketplaceViewStateManager } from "./MarketplaceViewStateManager"
|
||||
import { useAppTranslation } from "@/i18n/TranslationContext"
|
||||
import { useStateManager } from "./useStateManager"
|
||||
|
||||
export interface MarketplaceListViewProps {
|
||||
stateManager: MarketplaceViewStateManager
|
||||
allTags: string[]
|
||||
filteredTags: string[]
|
||||
tagSearch: string
|
||||
setTagSearch: (value: string) => void
|
||||
isTagPopoverOpen: boolean
|
||||
setIsTagPopoverOpen: (value: boolean) => void
|
||||
}
|
||||
|
||||
export function MarketplaceListView({
|
||||
stateManager,
|
||||
allTags,
|
||||
filteredTags,
|
||||
tagSearch,
|
||||
setTagSearch,
|
||||
isTagPopoverOpen,
|
||||
setIsTagPopoverOpen,
|
||||
}: MarketplaceListViewProps) {
|
||||
const [state, manager] = useStateManager(stateManager)
|
||||
const { t } = useAppTranslation()
|
||||
const items = state.displayItems || []
|
||||
const isEmpty = items.length === 0
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={t("marketplace:filters.search.placeholder")}
|
||||
value={state.filters.search}
|
||||
onChange={(e) =>
|
||||
manager.transition({
|
||||
type: "UPDATE_FILTERS",
|
||||
payload: { filters: { search: e.target.value } },
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<div className="flex w-full gap-1 bg-vscode-panel-background rounded-md">
|
||||
<div className="flex-1 flex flex-col">
|
||||
<label htmlFor="type-filter" className="mb-1 font-medium text-sm">
|
||||
{t("marketplace:filters.type.label")}
|
||||
</label>
|
||||
<Select
|
||||
value={state.filters.type || "all"}
|
||||
onValueChange={(value) =>
|
||||
manager.transition({
|
||||
type: "UPDATE_FILTERS",
|
||||
payload: { filters: { type: value === "all" ? "" : value } },
|
||||
})
|
||||
}>
|
||||
<SelectTrigger id="type-filter" className="w-full">
|
||||
<SelectValue placeholder={t("marketplace:filters.type.all")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">
|
||||
<span className="flex items-center gap-2">
|
||||
<ALargeSmall className="h-4 w-4" />
|
||||
{t("marketplace:filters.type.all")}
|
||||
</span>
|
||||
</SelectItem>
|
||||
<SelectItem value="mode">
|
||||
<span className="flex items-center gap-2">
|
||||
<Rocket className="h-4 w-4" />
|
||||
{t("marketplace:filters.type.mode")}
|
||||
</span>
|
||||
</SelectItem>
|
||||
<SelectItem value="mcp server">
|
||||
<span className="flex items-center gap-2">
|
||||
<Server className="h-4 w-4" />
|
||||
{t("marketplace:filters.type.mcp server")}
|
||||
</span>
|
||||
</SelectItem>
|
||||
<SelectItem value="prompt">
|
||||
<span className="flex items-center gap-2">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
{t("marketplace:filters.type.prompt")}
|
||||
</span>
|
||||
</SelectItem>
|
||||
<SelectItem value="package">
|
||||
<span className="flex items-center gap-2">
|
||||
<Package className="h-4 w-4" />
|
||||
{t("marketplace:filters.type.package")}
|
||||
</span>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col">
|
||||
<label className="mb-1 font-medium text-sm">{t("marketplace:filters.sort.label")}</label>
|
||||
<div className="flex gap-1">
|
||||
<Select
|
||||
value={state.sortConfig.by}
|
||||
onValueChange={(value) =>
|
||||
manager.transition({
|
||||
type: "UPDATE_SORT",
|
||||
payload: { sortConfig: { by: value as any } },
|
||||
})
|
||||
}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder={t("marketplace:filters.sort.name")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="name">
|
||||
<span className="flex items-center gap-2">
|
||||
<ALargeSmall className="h-4 w-4" />
|
||||
{t("marketplace:filters.sort.name")}
|
||||
</span>
|
||||
</SelectItem>
|
||||
<SelectItem value="lastUpdated">
|
||||
<span className="flex items-center gap-2">
|
||||
<Clock className="h-4 w-4" />
|
||||
{t("marketplace:filters.sort.lastUpdated")}
|
||||
</span>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() =>
|
||||
manager.transition({
|
||||
type: "UPDATE_SORT",
|
||||
payload: {
|
||||
sortConfig: {
|
||||
order: state.sortConfig.order === "asc" ? "desc" : "asc",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
className="shadow-none bg-vscode-input-background px-2">
|
||||
{state.sortConfig.order === "asc" ? "↑" : "↓"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{allTags.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<label className="font-medium text-sm">{t("marketplace:filters.tags.label")}</label>
|
||||
<span className="text-xs text-vscode-foreground">({allTags.length})</span>
|
||||
</div>
|
||||
{state.filters.tags.length > 0 && (
|
||||
<Button
|
||||
className="shadow-none font-normal flex items-center gap-1 h-auto py-0.5 px-1.5 text-xs"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation() // Prevent popover from closing if it's open
|
||||
manager.transition({
|
||||
type: "UPDATE_FILTERS",
|
||||
payload: { filters: { tags: [] } },
|
||||
})
|
||||
}}>
|
||||
<span className="codicon codicon-close"></span>
|
||||
{t("marketplace:filters.tags.clear", {
|
||||
count: state.filters.tags.length,
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Popover open={isTagPopoverOpen} onOpenChange={setIsTagPopoverOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="combobox"
|
||||
role="combobox"
|
||||
aria-expanded={isTagPopoverOpen}
|
||||
className="w-full justify-between h-7">
|
||||
<span className="truncate">
|
||||
{state.filters.tags.length > 0
|
||||
? state.filters.tags
|
||||
.map((t: string) => t.charAt(0).toUpperCase() + t.slice(1))
|
||||
.join(", ")
|
||||
: "None"}
|
||||
</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0">
|
||||
<Command>
|
||||
<div className="relative">
|
||||
<CommandInput
|
||||
className="h-9 pr-8"
|
||||
placeholder={t("marketplace:filters.tags.placeholder")}
|
||||
value={tagSearch}
|
||||
onValueChange={setTagSearch}
|
||||
/>
|
||||
{tagSearch && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-1 top-1/2 transform -translate-y-1/2 h-7 w-7"
|
||||
onClick={() => setTagSearch("")}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<CommandList className="max-h-[200px] overflow-y-auto bg-vscode-dropdown-background divide-y divide-vscode-panel-border">
|
||||
<CommandEmpty className="p-2 text-sm text-vscode-descriptionForeground">
|
||||
{t("marketplace:filters.tags.noResults")}
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{filteredTags.map((tag: string) => (
|
||||
<CommandItem
|
||||
key={tag}
|
||||
value={tag}
|
||||
onSelect={() => {
|
||||
const isSelected = state.filters.tags.includes(tag)
|
||||
manager.transition({
|
||||
type: "UPDATE_FILTERS",
|
||||
payload: {
|
||||
filters: {
|
||||
tags: isSelected
|
||||
? state.filters.tags.filter(
|
||||
(t) => t !== tag,
|
||||
)
|
||||
: [...state.filters.tags, tag],
|
||||
},
|
||||
},
|
||||
})
|
||||
}}
|
||||
data-selected={state.filters.tags.includes(tag)}
|
||||
className="grid grid-cols-[1rem_1fr] gap-2 cursor-pointer text-sm capitalize"
|
||||
onMouseDown={(e) => e.preventDefault()}>
|
||||
{state.filters.tags.includes(tag) ? (
|
||||
<span className="codicon codicon-check" />
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
{tag}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{state.filters.tags.length > 0 && (
|
||||
<div className="text-xs text-vscode-descriptionForeground mt-2 flex items-center min-h-[16px]">
|
||||
<span className="codicon codicon-tag mr-1"></span>
|
||||
{t("marketplace:filters.tags.selected", {
|
||||
count: state.filters.tags.length,
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{state.isFetching && (
|
||||
<div className="flex flex-col items-center justify-center h-64 text-vscode-descriptionForeground animate-fade-in">
|
||||
<div className="animate-spin mb-4">
|
||||
<span className="codicon codicon-sync text-3xl"></span>
|
||||
</div>
|
||||
<p>{t("marketplace:items.refresh.refreshing")}</p>
|
||||
<p className="text-sm mt-2 animate-pulse">This may take a moment...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!state.isFetching && isEmpty && (
|
||||
<div className="flex flex-col items-center justify-center h-64 text-vscode-descriptionForeground animate-fade-in">
|
||||
<span className="codicon codicon-inbox text-4xl mb-4 opacity-70"></span>
|
||||
<p className="font-medium">{t("marketplace:items.empty.noItems")}</p>
|
||||
<p className="text-sm mt-2">Try adjusting your filters or search terms</p>
|
||||
<Button
|
||||
onClick={() =>
|
||||
manager.transition({
|
||||
type: "UPDATE_FILTERS",
|
||||
payload: { filters: { search: "", type: "", tags: [] } },
|
||||
})
|
||||
}
|
||||
className="mt-4 bg-vscode-button-secondaryBackground text-vscode-button-secondaryForeground hover:bg-vscode-button-secondaryHoverBackground transition-colors">
|
||||
<span className="codicon codicon-clear-all mr-2"></span>
|
||||
Clear all filters
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!state.isFetching && !isEmpty && (
|
||||
<div>
|
||||
<p className="text-vscode-descriptionForeground my-2 flex items-center">
|
||||
<span className="codicon codicon-list-filter mr-2"></span>
|
||||
{t("marketplace:items.count", { count: items.length })}
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-1 xl:grid-cols-2 gap-3 pb-3">
|
||||
{items.map((item) => (
|
||||
<MarketplaceItemCard
|
||||
key={`${item.repoUrl}-${item.id}`}
|
||||
item={item}
|
||||
filters={state.filters}
|
||||
installed={{
|
||||
project: state.installedMetadata.project[item.id],
|
||||
global: state.installedMetadata.global[item.id],
|
||||
}}
|
||||
setFilters={(filters) =>
|
||||
manager.transition({
|
||||
type: "UPDATE_FILTERS",
|
||||
payload: { filters },
|
||||
})
|
||||
}
|
||||
activeTab={state.activeTab}
|
||||
setActiveTab={(tab) =>
|
||||
manager.transition({
|
||||
type: "SET_ACTIVE_TAB",
|
||||
payload: { tab },
|
||||
})
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,237 @@
|
|||
import { MarketplaceSource } from "../../../../src/services/marketplace/types"
|
||||
import { MarketplaceViewStateManager } from "./MarketplaceViewStateManager"
|
||||
import { useAppTranslation } from "@/i18n/TranslationContext"
|
||||
import { useState, useCallback } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { useStateManager } from "./useStateManager"
|
||||
import { validateSource } from "@roo/shared/MarketplaceValidation"
|
||||
import { cn } from "@src/lib/utils"
|
||||
|
||||
export interface MarketplaceSourcesConfigProps {
|
||||
stateManager: MarketplaceViewStateManager
|
||||
}
|
||||
|
||||
export function MarketplaceSourcesConfig({ stateManager }: MarketplaceSourcesConfigProps) {
|
||||
const { t } = useAppTranslation()
|
||||
const [state, manager] = useStateManager(stateManager)
|
||||
const [newSourceUrl, setNewSourceUrl] = useState("")
|
||||
const [newSourceName, setNewSourceName] = useState("")
|
||||
const [error, setError] = useState("")
|
||||
|
||||
const handleAddSource = () => {
|
||||
const MAX_SOURCES = 10
|
||||
if (state.sources.length >= MAX_SOURCES) {
|
||||
setError(t("marketplace:sources.errors.maxSources", { max: MAX_SOURCES }))
|
||||
return
|
||||
}
|
||||
const sourceToValidate: MarketplaceSource = {
|
||||
url: newSourceUrl,
|
||||
name: newSourceName || undefined,
|
||||
enabled: true,
|
||||
}
|
||||
const validationErrors = validateSource(sourceToValidate, state.sources)
|
||||
if (validationErrors.length > 0) {
|
||||
const errorMessages: Record<string, string> = {
|
||||
"url:empty": "marketplace:sources.errors.emptyUrl",
|
||||
"url:nonvisible": "marketplace:sources.errors.nonVisibleChars",
|
||||
"url:invalid": "marketplace:sources.errors.invalidGitUrl",
|
||||
"url:duplicate": "marketplace:sources.errors.duplicateUrl",
|
||||
"name:length": "marketplace:sources.errors.nameTooLong",
|
||||
"name:nonvisible": "marketplace:sources.errors.nonVisibleCharsName",
|
||||
"name:duplicate": "marketplace:sources.errors.duplicateName",
|
||||
}
|
||||
const error = validationErrors[0]
|
||||
const errorKey = `${error.field}:${error.message.toLowerCase().split(" ")[0]}`
|
||||
setError(t(errorMessages[errorKey] || "marketplace:sources.errors.invalidGitUrl"))
|
||||
return
|
||||
}
|
||||
manager.transition({
|
||||
type: "UPDATE_SOURCES",
|
||||
payload: { sources: [...state.sources, sourceToValidate] },
|
||||
})
|
||||
setNewSourceUrl("")
|
||||
setNewSourceName("")
|
||||
setError("")
|
||||
}
|
||||
|
||||
const handleToggleSource = useCallback(
|
||||
(index: number) => {
|
||||
manager.transition({
|
||||
type: "UPDATE_SOURCES",
|
||||
payload: {
|
||||
sources: state.sources.map((source, i) =>
|
||||
i === index ? { ...source, enabled: !source.enabled } : source,
|
||||
),
|
||||
},
|
||||
})
|
||||
},
|
||||
[state.sources, manager],
|
||||
)
|
||||
|
||||
const handleRemoveSource = useCallback(
|
||||
(index: number) => {
|
||||
manager.transition({
|
||||
type: "UPDATE_SOURCES",
|
||||
payload: {
|
||||
sources: state.sources.filter((_, i) => i !== index),
|
||||
},
|
||||
})
|
||||
},
|
||||
[state.sources, manager],
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="mx-auto">
|
||||
<h4 className="text-xl font-semibold text-vscode-foreground my-0">{t("marketplace:sources.title")}</h4>
|
||||
<p className="text-vscode-descriptionForeground mt-2 mb-4">{t("marketplace:sources.description")}</p>
|
||||
|
||||
<div className="bg-vscode-panel-background">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="relative">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={t("marketplace:sources.add.namePlaceholder")}
|
||||
value={newSourceName}
|
||||
onChange={(e) => {
|
||||
setNewSourceName(e.target.value.slice(0, 20))
|
||||
setError("")
|
||||
}}
|
||||
maxLength={20}
|
||||
className="pl-10"
|
||||
/>
|
||||
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-vscode-descriptionForeground">
|
||||
<span className="codicon codicon-tag"></span>
|
||||
</span>
|
||||
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-vscode-descriptionForeground">
|
||||
{newSourceName.length}/20
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={t("marketplace:sources.add.urlPlaceholder")}
|
||||
value={newSourceUrl}
|
||||
onChange={(e) => {
|
||||
setNewSourceUrl(e.target.value)
|
||||
setError("")
|
||||
}}
|
||||
className="pl-10"
|
||||
/>
|
||||
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-vscode-descriptionForeground">
|
||||
<span className="codicon codicon-link"></span>
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-vscode-descriptionForeground m-0">
|
||||
{t("marketplace:sources.add.urlFormats")}
|
||||
</p>
|
||||
</div>
|
||||
{error && (
|
||||
<div className="flex flex-col items-center justify-center mb-0 mt-2">
|
||||
<p className="text-red-500 p-2 bg-red-100 dark:bg-red-900 dark:bg-opacity-20 rounded-md flex items-center w-full my-0">
|
||||
<span className="codicon codicon-error mr-2"></span>
|
||||
{error}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<Button onClick={handleAddSource} className="mt-2 w-full shadow-none border-none">
|
||||
<span className="codicon codicon-add"></span>
|
||||
{t("marketplace:sources.add.button")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pb-2 pt-4">
|
||||
<div className="flex items-center">
|
||||
<span className="codicon codicon-repo mr-2"></span>
|
||||
<span className="text-base font-medium text-vscode-foreground">
|
||||
{t("marketplace:sources.current.title")}
|
||||
</span>
|
||||
</div>
|
||||
<span className="bg-primary text-primary-foreground text-xs px-2 py-0.5 rounded-full">
|
||||
{state.sources.length} / 10
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{state.sources.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-64 text-vscode-descriptionForeground animate-fade-in border border-dashed border-vscode-panel-border rounded-lg bg-vscode-panel-background bg-opacity-80 text-center">
|
||||
<span className="codicon codicon-inbox text-5xl mb-4 opacity-70"></span>
|
||||
<p className="font-medium">{t("marketplace:sources.current.empty")}</p>
|
||||
<p className="text-sm mt-2">{t("marketplace:sources.current.emptyHint")}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-3 pb-3">
|
||||
{state.sources.map((source, index) => (
|
||||
<div
|
||||
key={source.url}
|
||||
className={cn(
|
||||
"group flex items-center justify-between p-2 border rounded-sm bg-vscode-panel-background",
|
||||
{
|
||||
"border-vscode-input-border": source.enabled,
|
||||
"border-dashed border-vscode-input-border opacity-75": !source.enabled,
|
||||
},
|
||||
)}
|
||||
style={{ animationDelay: `${index * 40}ms` }}>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="px-1">
|
||||
<Checkbox
|
||||
id={`source-${index}`}
|
||||
checked={source.enabled}
|
||||
onCheckedChange={() => handleToggleSource(index)}
|
||||
variant="description"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p
|
||||
className={cn(
|
||||
"text-vscode-foreground font-medium my-0",
|
||||
!source.enabled && "text-vscode-disabledForeground",
|
||||
)}>
|
||||
{source.name || source.url}
|
||||
</p>
|
||||
{source.name && (
|
||||
<p className="text-xs text-vscode-descriptionForeground my-0 flex items-center">
|
||||
{source.url}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() =>
|
||||
manager.transition({ type: "REFRESH_SOURCE", payload: { url: source.url } })
|
||||
}
|
||||
title={t("marketplace:sources.current.refresh")}
|
||||
className="text-vscode-foreground"
|
||||
disabled={state.refreshingUrls.includes(source.url)}>
|
||||
<span
|
||||
className={cn(
|
||||
"codicon",
|
||||
state.refreshingUrls.includes(source.url)
|
||||
? "codicon-sync codicon-modifier-spin"
|
||||
: "codicon-refresh",
|
||||
)}></span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleRemoveSource(index)}
|
||||
title={t("marketplace:sources.current.remove")}
|
||||
className="text-vscode-errorForeground hover:bg-vscode-errorForeground/10 hover:text-vscode-errorForeground">
|
||||
<span className="codicon codicon-trash"></span>
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="text-vscode-foreground">
|
||||
<span className="codicon codicon-link-external"></span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
194
webview-ui/src/components/marketplace/MarketplaceView.tsx
Normal file
194
webview-ui/src/components/marketplace/MarketplaceView.tsx
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
import { useState, useEffect, useMemo, useCallback } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Tab, TabContent, TabHeader } from "../common/Tab"
|
||||
import { MarketplaceItem } from "../../../../src/services/marketplace/types"
|
||||
import { MarketplaceViewStateManager } from "./MarketplaceViewStateManager"
|
||||
import { useStateManager } from "./useStateManager"
|
||||
import { useAppTranslation } from "@/i18n/TranslationContext"
|
||||
import InstallSidebar from "./InstallSidebar"
|
||||
import { useEvent } from "react-use"
|
||||
import { ExtensionMessage } from "@roo/shared/ExtensionMessage"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { RocketConfig } from "config-rocket"
|
||||
import { MarketplaceSourcesConfig } from "./MarketplaceSourcesConfigView"
|
||||
import { MarketplaceListView } from "./MarketplaceListView"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Package, RefreshCw, Server } from "lucide-react"
|
||||
import { TooltipProvider } from "@/components/ui/tooltip"
|
||||
|
||||
interface MarketplaceViewProps {
|
||||
onDone?: () => void
|
||||
stateManager: MarketplaceViewStateManager
|
||||
}
|
||||
export function MarketplaceView({ stateManager }: MarketplaceViewProps) {
|
||||
const { t } = useAppTranslation()
|
||||
const [state, manager] = useStateManager(stateManager)
|
||||
|
||||
const [tagSearch, setTagSearch] = useState("")
|
||||
const [isTagPopoverOpen, setIsTagPopoverOpen] = useState(false)
|
||||
const [showInstallSidebar, setShowInstallSidebar] = useState<
|
||||
| {
|
||||
item: MarketplaceItem
|
||||
config: RocketConfig
|
||||
}
|
||||
| false
|
||||
>(false)
|
||||
|
||||
const handleInstallSubmit = (item: MarketplaceItem, parameters: Record<string, any>) => {
|
||||
vscode.postMessage({
|
||||
type: "installMarketplaceItemWithParameters",
|
||||
payload: { item, parameters },
|
||||
})
|
||||
setShowInstallSidebar(false)
|
||||
}
|
||||
|
||||
const onMessage = useCallback(
|
||||
(e: MessageEvent) => {
|
||||
const message: ExtensionMessage = e.data
|
||||
if (message.type === "openMarketplaceInstallSidebarWithConfig") {
|
||||
setShowInstallSidebar({ item: message.payload.item, config: message.payload.config })
|
||||
}
|
||||
},
|
||||
[setShowInstallSidebar],
|
||||
)
|
||||
|
||||
useEvent("message", onMessage)
|
||||
|
||||
// Listen for panel visibility events to fetch data when panel becomes visible
|
||||
useEffect(() => {
|
||||
const handleVisibilityMessage = (event: MessageEvent) => {
|
||||
const message = event.data
|
||||
if (message.type === "webviewVisible" && message.visible === true) {
|
||||
// Fetch items when panel becomes visible and we're on browse tab
|
||||
if (state.activeTab === "browse" && !state.isFetching) {
|
||||
manager.transition({ type: "FETCH_ITEMS" })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("message", handleVisibilityMessage)
|
||||
return () => window.removeEventListener("message", handleVisibilityMessage)
|
||||
}, [manager, state.activeTab, state.isFetching])
|
||||
|
||||
// Fetch items on first mount or when returning to empty state
|
||||
useEffect(() => {
|
||||
if (!state.allItems.length && !state.isFetching) {
|
||||
manager.transition({ type: "FETCH_ITEMS" })
|
||||
}
|
||||
}, [manager, state.allItems.length, state.isFetching])
|
||||
|
||||
// Memoize all available tags
|
||||
const allTags = useMemo(
|
||||
() => Array.from(new Set(state.allItems.flatMap((item) => item.tags || []))).sort(),
|
||||
[state.allItems],
|
||||
)
|
||||
|
||||
// Memoize filtered tags
|
||||
const filteredTags = useMemo(
|
||||
() =>
|
||||
tagSearch ? allTags.filter((tag: string) => tag.toLowerCase().includes(tagSearch.toLowerCase())) : allTags,
|
||||
[allTags, tagSearch],
|
||||
)
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tab>
|
||||
<TabHeader className="flex flex-col sticky top-0 z-10 px-3 py-2">
|
||||
<div className="flex justify-between items-center px-2">
|
||||
<h3 className="font-bold m-0">{t("marketplace:title")}</h3>
|
||||
<Button
|
||||
className={cn("h-4 px-0", {
|
||||
flex: state.activeTab === "browse",
|
||||
hidden: state.activeTab === "sources",
|
||||
})}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => manager.transition({ type: "FETCH_ITEMS" })}>
|
||||
<RefreshCw className={cn("size-4", state.isFetching && "animate-spin")} />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="w-full mt-2">
|
||||
<div className="bg-vscode-input-background rounded-md flex relative py-1">
|
||||
<div
|
||||
className={cn(
|
||||
"absolute w-1/2 h-full top-0 rounded-sm bg-vscode-button-background transition-all duration-300 ease-in-out",
|
||||
{
|
||||
"left-0": state.activeTab === "browse",
|
||||
"left-1/2": state.activeTab === "sources",
|
||||
},
|
||||
)}
|
||||
/>
|
||||
<button
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-2 flex-1 text-sm font-medium rounded-sm transition-colors duration-300 relative z-10",
|
||||
state.activeTab === "browse"
|
||||
? "text-vscode-button-foreground"
|
||||
: "text-vscode-foreground hover:text-vscode-button-background",
|
||||
)}
|
||||
onClick={() =>
|
||||
manager.transition({ type: "SET_ACTIVE_TAB", payload: { tab: "browse" } })
|
||||
}>
|
||||
<Package className="h-4 w-4" />
|
||||
{t("marketplace:tabs.browse")}
|
||||
</button>
|
||||
<button
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-2 flex-1 text-sm font-medium rounded-sm transition-colors duration-300 relative z-10",
|
||||
state.activeTab === "sources"
|
||||
? "text-vscode-button-foreground"
|
||||
: "text-vscode-foreground hover:text-vscode-button-background",
|
||||
)}
|
||||
onClick={() =>
|
||||
manager.transition({ type: "SET_ACTIVE_TAB", payload: { tab: "sources" } })
|
||||
}>
|
||||
<Server className="h-4 w-4" />
|
||||
{t("marketplace:tabs.sources")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</TabHeader>
|
||||
|
||||
<TabContent className="p-3 pt-2 overflow-x-hidden">
|
||||
<div className="relative w-full h-full">
|
||||
<div
|
||||
className={cn("absolute w-full transition-all duration-300 ease-in-out", {
|
||||
"translate-x-0 opacity-100 z-10": state.activeTab === "browse",
|
||||
"translate-x-[-100%] opacity-0 z-0": state.activeTab === "sources",
|
||||
})}>
|
||||
<MarketplaceListView
|
||||
stateManager={stateManager}
|
||||
allTags={allTags}
|
||||
filteredTags={filteredTags}
|
||||
tagSearch={tagSearch}
|
||||
setTagSearch={setTagSearch}
|
||||
isTagPopoverOpen={isTagPopoverOpen}
|
||||
setIsTagPopoverOpen={setIsTagPopoverOpen}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn("absolute w-full transition-all duration-300 ease-in-out", {
|
||||
"translate-x-0 opacity-100 z-10": state.activeTab === "sources",
|
||||
"translate-x-[100%] opacity-0 z-0": state.activeTab === "browse",
|
||||
})}>
|
||||
<MarketplaceSourcesConfig stateManager={stateManager} />
|
||||
</div>
|
||||
</div>
|
||||
</TabContent>
|
||||
</Tab>
|
||||
|
||||
{showInstallSidebar && (
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex justify-end">
|
||||
<div className="animate-slide-in-right">
|
||||
<InstallSidebar
|
||||
onClose={() => setShowInstallSidebar(false)}
|
||||
onSubmit={handleInstallSubmit}
|
||||
item={showInstallSidebar.item}
|
||||
config={showInstallSidebar.config}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,613 @@
|
|||
/**
|
||||
* MarketplaceViewStateManager
|
||||
*
|
||||
* This class manages the state for the marketplace view in the Roo Code extensions interface.
|
||||
*
|
||||
* IMPORTANT: Fixed issue where the marketplace feature was causing the Roo Code extensions interface
|
||||
* to switch to the browse tab and redraw it every 30 seconds. The fix prevents unnecessary tab switching
|
||||
* and redraws by:
|
||||
* 1. Only updating the UI when necessary
|
||||
* 2. Preserving the current tab when handling timeouts
|
||||
* 3. Using minimal state updates to avoid resetting scroll position
|
||||
*/
|
||||
|
||||
import { MarketplaceItem, MarketplaceSource, MatchInfo } from "../../../../src/services/marketplace/types"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { WebviewMessage } from "../../../../src/shared/WebviewMessage"
|
||||
import { DEFAULT_MARKETPLACE_SOURCE } from "../../../../src/services/marketplace/constants"
|
||||
import { FullInstallatedMetadata } from "../../../../src/services/marketplace/InstalledMetadataManager"
|
||||
|
||||
export interface ViewState {
|
||||
allItems: MarketplaceItem[]
|
||||
displayItems?: MarketplaceItem[] // Items currently being displayed (filtered or all)
|
||||
isFetching: boolean
|
||||
activeTab: "browse" | "sources"
|
||||
refreshingUrls: string[]
|
||||
sources: MarketplaceSource[]
|
||||
installedMetadata: FullInstallatedMetadata
|
||||
filters: {
|
||||
type: string
|
||||
search: string
|
||||
tags: string[]
|
||||
}
|
||||
sortConfig: {
|
||||
by: "name" | "author" | "lastUpdated"
|
||||
order: "asc" | "desc"
|
||||
}
|
||||
}
|
||||
|
||||
// Define a default empty metadata structure
|
||||
const defaultInstalledMetadata: FullInstallatedMetadata = {
|
||||
project: {},
|
||||
global: {},
|
||||
}
|
||||
|
||||
type TransitionPayloads = {
|
||||
FETCH_ITEMS: undefined
|
||||
FETCH_COMPLETE: { items: MarketplaceItem[] }
|
||||
FETCH_ERROR: undefined
|
||||
SET_ACTIVE_TAB: { tab: ViewState["activeTab"] }
|
||||
UPDATE_FILTERS: { filters: Partial<ViewState["filters"]> }
|
||||
UPDATE_SORT: { sortConfig: Partial<ViewState["sortConfig"]> }
|
||||
REFRESH_SOURCE: { url: string }
|
||||
REFRESH_SOURCE_COMPLETE: { url: string }
|
||||
UPDATE_SOURCES: { sources: MarketplaceSource[] }
|
||||
}
|
||||
|
||||
export interface ViewStateTransition {
|
||||
type: keyof TransitionPayloads
|
||||
payload?: TransitionPayloads[keyof TransitionPayloads]
|
||||
}
|
||||
|
||||
export type StateChangeHandler = (state: ViewState) => void
|
||||
|
||||
export class MarketplaceViewStateManager {
|
||||
private state: ViewState = this.loadInitialState()
|
||||
|
||||
private loadInitialState(): ViewState {
|
||||
// Try to restore state from sessionStorage if available
|
||||
if (typeof sessionStorage !== "undefined") {
|
||||
const savedState = sessionStorage.getItem("marketplaceState")
|
||||
if (savedState) {
|
||||
try {
|
||||
return JSON.parse(savedState)
|
||||
} catch {
|
||||
return this.getDefaultState()
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.getDefaultState()
|
||||
}
|
||||
|
||||
private getDefaultState(): ViewState {
|
||||
return {
|
||||
allItems: [],
|
||||
displayItems: [] as MarketplaceItem[],
|
||||
isFetching: false,
|
||||
activeTab: "browse",
|
||||
refreshingUrls: [],
|
||||
sources: [DEFAULT_MARKETPLACE_SOURCE],
|
||||
installedMetadata: defaultInstalledMetadata,
|
||||
filters: {
|
||||
type: "",
|
||||
search: "",
|
||||
tags: [],
|
||||
},
|
||||
sortConfig: {
|
||||
by: "name",
|
||||
order: "asc",
|
||||
},
|
||||
}
|
||||
}
|
||||
// Removed auto-polling timeout
|
||||
private stateChangeHandlers: Set<StateChangeHandler> = new Set()
|
||||
|
||||
// Empty constructor is required for test initialization
|
||||
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
||||
constructor() {
|
||||
// Initialize is now handled by the loadInitialState call in the property initialization
|
||||
}
|
||||
|
||||
public initialize(): void {
|
||||
// Set initial state
|
||||
this.state = this.getDefaultState()
|
||||
|
||||
// Send initial sources to extension
|
||||
vscode.postMessage({
|
||||
type: "marketplaceSources",
|
||||
sources: [DEFAULT_MARKETPLACE_SOURCE],
|
||||
} as WebviewMessage)
|
||||
}
|
||||
|
||||
public onStateChange(handler: StateChangeHandler): () => void {
|
||||
this.stateChangeHandlers.add(handler)
|
||||
return () => this.stateChangeHandlers.delete(handler)
|
||||
}
|
||||
|
||||
public cleanup(): void {
|
||||
// Reset fetching state
|
||||
if (this.state.isFetching) {
|
||||
this.state.isFetching = false
|
||||
this.notifyStateChange()
|
||||
}
|
||||
|
||||
// Clear handlers but preserve state
|
||||
this.stateChangeHandlers.clear()
|
||||
}
|
||||
|
||||
public getState(): ViewState {
|
||||
// Only create new arrays if they exist and have items
|
||||
const allItems = this.state.allItems.length ? [...this.state.allItems] : []
|
||||
const displayItems = this.state.displayItems?.length ? [...this.state.displayItems] : this.state.displayItems
|
||||
const refreshingUrls = this.state.refreshingUrls.length ? [...this.state.refreshingUrls] : []
|
||||
const tags = this.state.filters.tags.length ? [...this.state.filters.tags] : []
|
||||
const sources = this.state.sources.length ? [...this.state.sources] : [DEFAULT_MARKETPLACE_SOURCE]
|
||||
const installedMetadata = this.state.installedMetadata
|
||||
|
||||
// Create minimal new state object
|
||||
return {
|
||||
...this.state,
|
||||
allItems,
|
||||
displayItems,
|
||||
refreshingUrls,
|
||||
sources,
|
||||
installedMetadata,
|
||||
filters: {
|
||||
...this.state.filters,
|
||||
tags,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify all registered handlers of a state change
|
||||
* @param preserveTab If true, ensures the active tab is not changed during notification
|
||||
*/
|
||||
private notifyStateChange(preserveTab: boolean = false): void {
|
||||
const newState = this.getState() // Use getState to ensure proper copying
|
||||
|
||||
if (preserveTab) {
|
||||
// When preserveTab is true, we're careful not to cause tab switching
|
||||
// This is used during timeout handling to prevent disrupting the user
|
||||
this.stateChangeHandlers.forEach((handler) => {
|
||||
// Store the current active tab
|
||||
const currentTab = newState.activeTab
|
||||
|
||||
// Create a state update that won't change the active tab
|
||||
const safeState = {
|
||||
...newState,
|
||||
// Don't change these properties to avoid UI disruption
|
||||
activeTab: currentTab,
|
||||
}
|
||||
handler(safeState)
|
||||
})
|
||||
} else {
|
||||
// Normal state change notification
|
||||
this.stateChangeHandlers.forEach((handler) => {
|
||||
handler(newState)
|
||||
})
|
||||
}
|
||||
|
||||
// Save state to sessionStorage if available
|
||||
if (typeof sessionStorage !== "undefined") {
|
||||
try {
|
||||
sessionStorage.setItem("marketplaceState", JSON.stringify(this.state))
|
||||
} catch (error) {
|
||||
console.warn("Failed to save marketplace state:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async transition(transition: ViewStateTransition): Promise<void> {
|
||||
switch (transition.type) {
|
||||
case "FETCH_ITEMS": {
|
||||
// Don't start a new fetch if one is in progress
|
||||
if (this.state.isFetching) {
|
||||
return
|
||||
}
|
||||
|
||||
// Send fetch request
|
||||
vscode.postMessage({
|
||||
type: "fetchMarketplaceItems",
|
||||
} as WebviewMessage)
|
||||
|
||||
// Store current items before updating state
|
||||
const currentItems = this.state.allItems.length ? [...this.state.allItems] : []
|
||||
|
||||
// Update state after sending request
|
||||
this.state = {
|
||||
...this.state,
|
||||
isFetching: true,
|
||||
allItems: currentItems,
|
||||
displayItems: currentItems,
|
||||
}
|
||||
this.notifyStateChange()
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case "FETCH_COMPLETE": {
|
||||
const { items } = transition.payload as TransitionPayloads["FETCH_COMPLETE"]
|
||||
// No timeout to clear anymore
|
||||
|
||||
// Sort incoming items
|
||||
const sortedItems = this.sortItems([...items])
|
||||
|
||||
// Compare with current state to avoid unnecessary updates
|
||||
const currentSortedItems = this.sortItems([...this.state.allItems])
|
||||
if (JSON.stringify(sortedItems) === JSON.stringify(currentSortedItems)) {
|
||||
// No changes: update only isFetching flag and send minimal update
|
||||
this.state.isFetching = false
|
||||
this.stateChangeHandlers.forEach((handler) => {
|
||||
handler({
|
||||
...this.getState(),
|
||||
isFetching: false,
|
||||
})
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
// Update allItems as source of truth
|
||||
this.state = {
|
||||
...this.state,
|
||||
allItems: sortedItems,
|
||||
displayItems: this.isFilterActive() ? this.filterItems(sortedItems) : sortedItems,
|
||||
isFetching: false,
|
||||
}
|
||||
|
||||
// Notify state change
|
||||
this.notifyStateChange()
|
||||
break
|
||||
}
|
||||
|
||||
case "FETCH_ERROR": {
|
||||
// Preserve current filters and sources
|
||||
const { filters, sources, activeTab } = this.state
|
||||
|
||||
// Reset state but preserve filters and sources
|
||||
this.state = {
|
||||
...this.getDefaultState(),
|
||||
filters,
|
||||
sources,
|
||||
activeTab,
|
||||
isFetching: false,
|
||||
}
|
||||
this.notifyStateChange()
|
||||
break
|
||||
}
|
||||
|
||||
case "SET_ACTIVE_TAB": {
|
||||
const { tab } = transition.payload as TransitionPayloads["SET_ACTIVE_TAB"]
|
||||
|
||||
// Update tab state
|
||||
this.state = {
|
||||
...this.state,
|
||||
activeTab: tab,
|
||||
}
|
||||
|
||||
// If switching to browse tab, trigger fetch
|
||||
if (tab === "browse") {
|
||||
this.state.isFetching = true
|
||||
|
||||
vscode.postMessage({
|
||||
type: "fetchMarketplaceItems",
|
||||
} as WebviewMessage)
|
||||
}
|
||||
|
||||
this.notifyStateChange()
|
||||
break
|
||||
}
|
||||
|
||||
case "UPDATE_FILTERS": {
|
||||
const { filters = {} } = (transition.payload as TransitionPayloads["UPDATE_FILTERS"]) || {}
|
||||
|
||||
// Create new filters object preserving existing values for undefined fields
|
||||
const updatedFilters = {
|
||||
type: filters.type !== undefined ? filters.type : this.state.filters.type,
|
||||
search: filters.search !== undefined ? filters.search : this.state.filters.search,
|
||||
tags: filters.tags !== undefined ? filters.tags : this.state.filters.tags,
|
||||
}
|
||||
|
||||
// Update state
|
||||
this.state = {
|
||||
...this.state,
|
||||
filters: updatedFilters,
|
||||
}
|
||||
|
||||
// Send filter message
|
||||
vscode.postMessage({
|
||||
type: "filterMarketplaceItems",
|
||||
filters: updatedFilters,
|
||||
} as WebviewMessage)
|
||||
|
||||
this.notifyStateChange()
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case "UPDATE_SORT": {
|
||||
const { sortConfig } = transition.payload as TransitionPayloads["UPDATE_SORT"]
|
||||
// Create new state with updated sort config
|
||||
this.state = {
|
||||
...this.state,
|
||||
sortConfig: {
|
||||
...this.state.sortConfig,
|
||||
...sortConfig,
|
||||
},
|
||||
}
|
||||
// Apply sorting to both allItems and displayItems
|
||||
// Sort items immutably
|
||||
// Create new sorted arrays
|
||||
const sortedAllItems = this.sortItems([...this.state.allItems])
|
||||
const sortedDisplayItems = this.state.displayItems?.length
|
||||
? this.sortItems([...this.state.displayItems])
|
||||
: this.state.displayItems
|
||||
|
||||
this.state = {
|
||||
...this.state,
|
||||
allItems: sortedAllItems,
|
||||
displayItems: sortedDisplayItems,
|
||||
}
|
||||
this.notifyStateChange()
|
||||
break
|
||||
}
|
||||
|
||||
case "REFRESH_SOURCE": {
|
||||
const { url } = transition.payload as TransitionPayloads["REFRESH_SOURCE"]
|
||||
if (!this.state.refreshingUrls.includes(url)) {
|
||||
this.state = {
|
||||
...this.state,
|
||||
refreshingUrls: [...this.state.refreshingUrls, url],
|
||||
}
|
||||
this.notifyStateChange()
|
||||
vscode.postMessage({
|
||||
type: "refreshMarketplaceSource",
|
||||
url,
|
||||
} as WebviewMessage)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "REFRESH_SOURCE_COMPLETE": {
|
||||
const { url } = transition.payload as TransitionPayloads["REFRESH_SOURCE_COMPLETE"]
|
||||
this.state = {
|
||||
...this.state,
|
||||
refreshingUrls: this.state.refreshingUrls.filter((existingUrl) => existingUrl !== url),
|
||||
}
|
||||
this.notifyStateChange()
|
||||
break
|
||||
}
|
||||
|
||||
case "UPDATE_SOURCES": {
|
||||
const { sources } = transition.payload as TransitionPayloads["UPDATE_SOURCES"]
|
||||
// If all sources are removed, add the default source
|
||||
const updatedSources = sources.length === 0 ? [DEFAULT_MARKETPLACE_SOURCE] : [...sources]
|
||||
|
||||
this.state = {
|
||||
...this.state,
|
||||
sources: updatedSources,
|
||||
isFetching: false, // Reset fetching state
|
||||
}
|
||||
|
||||
this.notifyStateChange()
|
||||
|
||||
// Send sources update to extension
|
||||
vscode.postMessage({
|
||||
type: "marketplaceSources",
|
||||
sources: updatedSources,
|
||||
} as WebviewMessage)
|
||||
|
||||
// If we're on the browse tab, trigger a fetch
|
||||
if (this.state.activeTab === "browse") {
|
||||
this.state.isFetching = true
|
||||
this.notifyStateChange()
|
||||
|
||||
vscode.postMessage({
|
||||
type: "fetchMarketplaceItems",
|
||||
} as WebviewMessage)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public isFilterActive(): boolean {
|
||||
return !!(this.state.filters.type || this.state.filters.search || this.state.filters.tags.length > 0)
|
||||
}
|
||||
|
||||
public filterItems(items: MarketplaceItem[]): MarketplaceItem[] {
|
||||
const { type, search, tags } = this.state.filters
|
||||
|
||||
return items
|
||||
.map((item) => {
|
||||
// Create a copy of the item to modify
|
||||
const itemCopy = { ...item }
|
||||
|
||||
// Check specific match conditions for the main item
|
||||
const typeMatch = !type || item.type === type
|
||||
const nameMatch = search ? item.name.toLowerCase().includes(search.toLowerCase()) : false
|
||||
const descriptionMatch = search
|
||||
? (item.description || "").toLowerCase().includes(search.toLowerCase())
|
||||
: false
|
||||
const tagMatch = tags.length > 0 ? item.tags?.some((tag) => tags.includes(tag)) : false
|
||||
|
||||
// Determine if the main item matches all filters
|
||||
const mainItemMatches =
|
||||
typeMatch && (!search || nameMatch || descriptionMatch) && (!tags.length || tagMatch)
|
||||
|
||||
// For packages, check and mark matching subcomponents
|
||||
if (item.type === "package" && item.items?.length) {
|
||||
itemCopy.items = item.items.map((subItem) => {
|
||||
// Check specific match conditions for subitem
|
||||
const subTypeMatch = !type || subItem.type === type
|
||||
const subNameMatch =
|
||||
search && subItem.metadata
|
||||
? subItem.metadata.name.toLowerCase().includes(search.toLowerCase())
|
||||
: false
|
||||
const subDescriptionMatch =
|
||||
search && subItem.metadata
|
||||
? subItem.metadata.description.toLowerCase().includes(search.toLowerCase())
|
||||
: false
|
||||
const subTagMatch =
|
||||
tags.length > 0 ? Boolean(subItem.metadata?.tags?.some((tag) => tags.includes(tag))) : false
|
||||
|
||||
const subItemMatches =
|
||||
subTypeMatch &&
|
||||
(!search || subNameMatch || subDescriptionMatch) &&
|
||||
(!tags.length || subTagMatch)
|
||||
|
||||
// Ensure all match properties are booleans
|
||||
const matchInfo: MatchInfo = {
|
||||
matched: Boolean(subItemMatches),
|
||||
matchReason: subItemMatches
|
||||
? {
|
||||
typeMatch: Boolean(subTypeMatch),
|
||||
nameMatch: Boolean(subNameMatch),
|
||||
descriptionMatch: Boolean(subDescriptionMatch),
|
||||
tagMatch: Boolean(subTagMatch),
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
|
||||
return {
|
||||
...subItem,
|
||||
matchInfo,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const hasMatchingSubcomponents = itemCopy.items?.some((subItem) => subItem.matchInfo?.matched)
|
||||
|
||||
// Set match info on the main item
|
||||
itemCopy.matchInfo = {
|
||||
matched: mainItemMatches || Boolean(hasMatchingSubcomponents),
|
||||
matchReason: {
|
||||
typeMatch,
|
||||
nameMatch,
|
||||
descriptionMatch,
|
||||
tagMatch,
|
||||
hasMatchingSubcomponents: Boolean(hasMatchingSubcomponents),
|
||||
},
|
||||
}
|
||||
|
||||
// Return the item if it matches or has matching subcomponents
|
||||
if (itemCopy.matchInfo.matched) {
|
||||
return itemCopy
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
.filter((item): item is MarketplaceItem => item !== null)
|
||||
}
|
||||
|
||||
private sortItems(items: MarketplaceItem[]): MarketplaceItem[] {
|
||||
const { by, order } = this.state.sortConfig
|
||||
const itemsCopy = [...items]
|
||||
|
||||
return itemsCopy.sort((a, b) => {
|
||||
const aValue = by === "lastUpdated" ? a[by] || "1970-01-01T00:00:00Z" : a[by] || ""
|
||||
const bValue = by === "lastUpdated" ? b[by] || "1970-01-01T00:00:00Z" : b[by] || ""
|
||||
|
||||
return order === "asc" ? aValue.localeCompare(bValue) : bValue.localeCompare(aValue)
|
||||
})
|
||||
}
|
||||
|
||||
public async handleMessage(message: any): Promise<void> {
|
||||
// Handle empty or invalid message
|
||||
if (!message || !message.type || message.type === "invalidType") {
|
||||
const { sources } = this.state
|
||||
this.state = {
|
||||
...this.getDefaultState(),
|
||||
sources: [...sources],
|
||||
}
|
||||
this.notifyStateChange()
|
||||
return
|
||||
}
|
||||
|
||||
// Handle state updates
|
||||
if (message.type === "state") {
|
||||
// Handle empty state
|
||||
if (!message.state) {
|
||||
const { sources } = this.state
|
||||
this.state = {
|
||||
...this.getDefaultState(),
|
||||
sources: [...sources],
|
||||
}
|
||||
this.notifyStateChange()
|
||||
return
|
||||
}
|
||||
|
||||
// Update sources if present
|
||||
const sources = message.state.marketplaceSources || message.state.sources
|
||||
if (sources) {
|
||||
this.state = {
|
||||
...this.state,
|
||||
sources: sources.length > 0 ? [...sources] : [DEFAULT_MARKETPLACE_SOURCE],
|
||||
}
|
||||
// Don't notify yet, combine with other state updates below
|
||||
}
|
||||
|
||||
// Update installedMetadata if present
|
||||
const installedMetadata = message.state.marketplaceInstalledMetadata
|
||||
if (installedMetadata) {
|
||||
this.state = {
|
||||
...this.state,
|
||||
installedMetadata,
|
||||
}
|
||||
// Don't notify yet
|
||||
}
|
||||
|
||||
// Handle state updates for marketplace items
|
||||
// The state.marketplaceItems come from ClineProvider, see the file src/core/webview/ClineProvider.ts
|
||||
const marketplaceItems = message.state.marketplaceItems
|
||||
if (marketplaceItems !== undefined) {
|
||||
const currentItems = this.state.allItems || []
|
||||
const hasNewItems = marketplaceItems.length > 0
|
||||
const hasCurrentItems = currentItems.length > 0
|
||||
const isOnBrowseTab = this.state.activeTab === "browse"
|
||||
|
||||
// Determine which items to use
|
||||
const itemsToUse = hasNewItems ? marketplaceItems : isOnBrowseTab && hasCurrentItems ? currentItems : []
|
||||
const sortedItems = this.sortItems([...itemsToUse])
|
||||
const newDisplayItems = this.isFilterActive() ? this.filterItems(sortedItems) : sortedItems
|
||||
|
||||
// Update state in a single operation
|
||||
this.state = {
|
||||
...this.state,
|
||||
isFetching: false,
|
||||
allItems: sortedItems,
|
||||
displayItems: newDisplayItems,
|
||||
}
|
||||
// Notification is handled below after all state parts are processed
|
||||
}
|
||||
|
||||
// Notify state change once after processing all parts (sources, metadata, items)
|
||||
// This prevents multiple redraws for a single 'state' message
|
||||
// Determine if notification should preserve tab based on item update logic
|
||||
const isOnBrowseTab = this.state.activeTab === "browse"
|
||||
const hasCurrentItems = (this.state.allItems || []).length > 0
|
||||
const preserveTab = !isOnBrowseTab && hasCurrentItems
|
||||
|
||||
this.notifyStateChange(preserveTab)
|
||||
}
|
||||
|
||||
// Handle repository refresh completion
|
||||
if (message.type === "repositoryRefreshComplete" && message.url) {
|
||||
void this.transition({
|
||||
type: "REFRESH_SOURCE_COMPLETE",
|
||||
payload: { url: message.url },
|
||||
})
|
||||
}
|
||||
|
||||
// Handle marketplace button clicks
|
||||
if (message.type === "marketplaceButtonClicked") {
|
||||
if (message.text) {
|
||||
// Error case
|
||||
void this.transition({ type: "FETCH_ERROR" })
|
||||
} else {
|
||||
// Refresh request
|
||||
void this.transition({ type: "FETCH_ITEMS" })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
import { fireEvent, screen } from "@testing-library/react"
|
||||
import InstallSidebar from "../InstallSidebar"
|
||||
import { MarketplaceItem } from "../../../../../src/services/marketplace/types"
|
||||
import { RocketConfig } from "config-rocket"
|
||||
import { renderWithProviders } from "@/test/test-utils"
|
||||
|
||||
// Mock VSCode components
|
||||
jest.mock("@vscode/webview-ui-toolkit/react", () => ({
|
||||
VSCodeButton: ({ children, onClick, appearance }: any) => (
|
||||
<button onClick={onClick} data-appearance={appearance}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
VSCodeTextField: ({ id, value, onChange }: any) => (
|
||||
<input
|
||||
type="text"
|
||||
id={id}
|
||||
value={value}
|
||||
onChange={(e) => onChange({ target: e.target })}
|
||||
data-testid={`text-${id}`}
|
||||
/>
|
||||
),
|
||||
VSCodeCheckbox: ({ id, checked, onChange }: any) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
id={id}
|
||||
checked={checked}
|
||||
onChange={(e) => onChange({ target: e.target })}
|
||||
data-testid={`checkbox-${id}`}
|
||||
/>
|
||||
),
|
||||
}))
|
||||
|
||||
describe("InstallSidebar", () => {
|
||||
const mockItem: MarketplaceItem = {
|
||||
id: "test-item",
|
||||
name: "Test Item",
|
||||
description: "Test Description",
|
||||
type: "package",
|
||||
url: "https://test.com",
|
||||
repoUrl: "https://github.com/test/repo",
|
||||
author: "Test Author",
|
||||
version: "1.0.0",
|
||||
}
|
||||
|
||||
const mockConfig: RocketConfig = {
|
||||
parameters: [
|
||||
{
|
||||
id: "testText",
|
||||
resolver: {
|
||||
operation: "prompt",
|
||||
type: "text",
|
||||
label: "Text Input",
|
||||
initial: "default text",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "testConfirm",
|
||||
resolver: {
|
||||
operation: "prompt",
|
||||
type: "confirm",
|
||||
label: "Confirm Input",
|
||||
initial: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
it("renders sidebar with item name", () => {
|
||||
renderWithProviders(
|
||||
<InstallSidebar item={mockItem} config={mockConfig} onClose={() => {}} onSubmit={() => {}} />,
|
||||
)
|
||||
|
||||
expect(screen.getByText(`Install ${mockItem.name}`)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders text input parameter", () => {
|
||||
renderWithProviders(
|
||||
<InstallSidebar item={mockItem} config={mockConfig} onClose={() => {}} onSubmit={() => {}} />,
|
||||
)
|
||||
|
||||
const textInput = screen.getByTestId("text-testText")
|
||||
expect(textInput).toBeInTheDocument()
|
||||
expect(textInput).toHaveValue("default text")
|
||||
})
|
||||
|
||||
it("renders checkbox parameter", () => {
|
||||
renderWithProviders(
|
||||
<InstallSidebar item={mockItem} config={mockConfig} onClose={() => {}} onSubmit={() => {}} />,
|
||||
)
|
||||
|
||||
const checkbox = screen.getByTestId("checkbox-testConfirm")
|
||||
expect(checkbox).toBeInTheDocument()
|
||||
expect(checkbox).toBeChecked()
|
||||
})
|
||||
|
||||
it("updates text parameter value", () => {
|
||||
const onSubmit = jest.fn()
|
||||
renderWithProviders(
|
||||
<InstallSidebar item={mockItem} config={mockConfig} onClose={() => {}} onSubmit={onSubmit} />,
|
||||
)
|
||||
|
||||
const textInput = screen.getByTestId("text-testText")
|
||||
fireEvent.change(textInput, { target: { value: "new value" } })
|
||||
|
||||
const installButton = screen.getByText("Install")
|
||||
fireEvent.click(installButton)
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith(mockItem, {
|
||||
testText: "new value",
|
||||
testConfirm: true,
|
||||
})
|
||||
})
|
||||
|
||||
it("updates checkbox parameter value", () => {
|
||||
const onSubmit = jest.fn()
|
||||
renderWithProviders(
|
||||
<InstallSidebar item={mockItem} config={mockConfig} onClose={() => {}} onSubmit={onSubmit} />,
|
||||
)
|
||||
|
||||
const checkbox = screen.getByTestId("checkbox-testConfirm")
|
||||
fireEvent.click(checkbox)
|
||||
|
||||
const installButton = screen.getByText("Install")
|
||||
fireEvent.click(installButton)
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith(mockItem, {
|
||||
testText: "default text",
|
||||
testConfirm: false,
|
||||
})
|
||||
})
|
||||
|
||||
it("calls onClose when clicking outside sidebar", () => {
|
||||
const onClose = jest.fn()
|
||||
renderWithProviders(
|
||||
<InstallSidebar item={mockItem} config={mockConfig} onClose={onClose} onSubmit={() => {}} />,
|
||||
)
|
||||
|
||||
// Click the overlay (parent div)
|
||||
const overlay = screen.getByText(`Install ${mockItem.name}`).parentElement?.parentElement
|
||||
if (overlay) {
|
||||
fireEvent.click(overlay)
|
||||
}
|
||||
|
||||
expect(onClose).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("calls onClose when clicking cancel button", () => {
|
||||
const onClose = jest.fn()
|
||||
renderWithProviders(
|
||||
<InstallSidebar item={mockItem} config={mockConfig} onClose={onClose} onSubmit={() => {}} />,
|
||||
)
|
||||
|
||||
const cancelButton = screen.getByText("Cancel")
|
||||
fireEvent.click(cancelButton)
|
||||
|
||||
expect(onClose).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
import { render, screen, fireEvent } from "@testing-library/react"
|
||||
import { MarketplaceListView } from "../MarketplaceListView"
|
||||
import { MarketplaceItem } from "../../../../../src/services/marketplace/types"
|
||||
import { ViewState } from "../MarketplaceViewStateManager"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
|
||||
// Mock translation hook
|
||||
jest.mock("@/i18n/TranslationContext", () => ({
|
||||
useAppTranslation: () => ({
|
||||
t: (key: string) => key, // Return the key as-is for easy testing
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock ResizeObserver
|
||||
class MockResizeObserver {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
}
|
||||
|
||||
global.ResizeObserver = MockResizeObserver
|
||||
|
||||
// Mock state manager with initial state
|
||||
const mockTransition = jest.fn()
|
||||
const mockState: ViewState = {
|
||||
allItems: [],
|
||||
displayItems: [],
|
||||
isFetching: false,
|
||||
activeTab: "browse",
|
||||
refreshingUrls: [],
|
||||
sources: [],
|
||||
installedMetadata: {
|
||||
project: {},
|
||||
global: {},
|
||||
},
|
||||
filters: {
|
||||
type: "",
|
||||
search: "",
|
||||
tags: [],
|
||||
},
|
||||
sortConfig: {
|
||||
by: "name",
|
||||
order: "asc",
|
||||
},
|
||||
}
|
||||
|
||||
// Mock useStateManager hook
|
||||
jest.mock("../useStateManager", () => ({
|
||||
useStateManager: () => [mockState, { transition: mockTransition }],
|
||||
}))
|
||||
|
||||
// Mock all lucide-react icons
|
||||
jest.mock("lucide-react", () => {
|
||||
return new Proxy(
|
||||
{},
|
||||
{
|
||||
get: function (obj, prop) {
|
||||
if (prop === "__esModule") {
|
||||
return true
|
||||
}
|
||||
return () => <div data-testid={`${String(prop)}-icon`}>{String(prop)}</div>
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
const defaultProps = {
|
||||
stateManager: {} as any,
|
||||
allTags: ["tag1", "tag2"],
|
||||
filteredTags: ["tag1", "tag2"],
|
||||
tagSearch: "",
|
||||
setTagSearch: jest.fn(),
|
||||
isTagPopoverOpen: false,
|
||||
setIsTagPopoverOpen: jest.fn(),
|
||||
}
|
||||
|
||||
describe("MarketplaceListView", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
mockState.filters.tags = []
|
||||
mockState.isFetching = false
|
||||
mockState.displayItems = []
|
||||
})
|
||||
|
||||
it("renders search input", () => {
|
||||
render(<MarketplaceListView {...defaultProps} />)
|
||||
|
||||
const searchInput = screen.getByPlaceholderText("marketplace:filters.search.placeholder")
|
||||
expect(searchInput).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders type filter", () => {
|
||||
render(<MarketplaceListView {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText("marketplace:filters.type.label")).toBeInTheDocument()
|
||||
expect(screen.getByText("marketplace:filters.type.all")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders sort options", () => {
|
||||
render(<MarketplaceListView {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText("marketplace:filters.sort.label")).toBeInTheDocument()
|
||||
expect(screen.getByText("marketplace:filters.sort.name")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders tags section when tags are available", () => {
|
||||
render(<MarketplaceListView {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText("marketplace:filters.tags.label")).toBeInTheDocument()
|
||||
expect(screen.getByText("(2)")).toBeInTheDocument() // Shows tag count
|
||||
})
|
||||
|
||||
it("shows loading state when fetching", () => {
|
||||
mockState.isFetching = true
|
||||
|
||||
render(<MarketplaceListView {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText("marketplace:items.refresh.refreshing")).toBeInTheDocument()
|
||||
expect(screen.getByText("This may take a moment...")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("shows empty state when no items and not fetching", () => {
|
||||
render(<MarketplaceListView {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText("marketplace:items.empty.noItems")).toBeInTheDocument()
|
||||
expect(screen.getByText("Try adjusting your filters or search terms")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("shows items count when items are available", () => {
|
||||
const mockItems: MarketplaceItem[] = [
|
||||
{
|
||||
id: "1",
|
||||
repoUrl: "test1",
|
||||
name: "Test 1",
|
||||
type: "mode",
|
||||
description: "Test description 1",
|
||||
url: "https://test1.com",
|
||||
version: "1.0.0",
|
||||
author: "Test Author 1",
|
||||
lastUpdated: "2024-01-01",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
repoUrl: "test2",
|
||||
name: "Test 2",
|
||||
type: "mode",
|
||||
description: "Test description 2",
|
||||
url: "https://test2.com",
|
||||
version: "1.0.0",
|
||||
author: "Test Author 2",
|
||||
lastUpdated: "2024-01-02",
|
||||
},
|
||||
]
|
||||
mockState.displayItems = mockItems
|
||||
|
||||
render(<MarketplaceListView {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText("marketplace:items.count")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("updates search filter when typing", () => {
|
||||
render(<MarketplaceListView {...defaultProps} />)
|
||||
|
||||
const searchInput = screen.getByPlaceholderText("marketplace:filters.search.placeholder")
|
||||
fireEvent.change(searchInput, { target: { value: "test" } })
|
||||
|
||||
expect(mockTransition).toHaveBeenCalledWith({
|
||||
type: "UPDATE_FILTERS",
|
||||
payload: { filters: { search: "test" } },
|
||||
})
|
||||
})
|
||||
|
||||
it("shows clear tags button when tags are selected", async () => {
|
||||
const user = userEvent.setup()
|
||||
mockState.filters.tags = ["tag1"]
|
||||
|
||||
render(<MarketplaceListView {...defaultProps} />)
|
||||
|
||||
const clearButton = screen.getByText("marketplace:filters.tags.clear")
|
||||
expect(clearButton).toBeInTheDocument()
|
||||
|
||||
await user.click(clearButton)
|
||||
expect(mockTransition).toHaveBeenCalledWith({
|
||||
type: "UPDATE_FILTERS",
|
||||
payload: { filters: { tags: [] } },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,230 @@
|
|||
import { render, fireEvent, screen } from "@testing-library/react"
|
||||
import { MarketplaceSourcesConfig } from "../MarketplaceSourcesConfigView"
|
||||
import { MarketplaceViewStateManager } from "../MarketplaceViewStateManager"
|
||||
|
||||
// Mock the translation hook
|
||||
jest.mock("@/i18n/TranslationContext", () => ({
|
||||
useAppTranslation: () => ({
|
||||
t: (key: string) => key, // Return the key as-is for testing
|
||||
}),
|
||||
}))
|
||||
|
||||
describe("MarketplaceSourcesConfig", () => {
|
||||
let stateManager: MarketplaceViewStateManager
|
||||
|
||||
beforeEach(() => {
|
||||
stateManager = new MarketplaceViewStateManager()
|
||||
// Reset state manager to have no sources
|
||||
stateManager.transition({
|
||||
type: "UPDATE_SOURCES",
|
||||
payload: { sources: [] },
|
||||
})
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("shows source count", () => {
|
||||
render(<MarketplaceSourcesConfig stateManager={stateManager} />)
|
||||
const countElement = screen.getByText((content) => content.includes("/ 10"))
|
||||
expect(countElement).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("adds a new source with URL only", async () => {
|
||||
render(<MarketplaceSourcesConfig stateManager={stateManager} />)
|
||||
|
||||
const urlInput = screen.getByPlaceholderText("marketplace:sources.add.urlPlaceholder")
|
||||
const testUrl = "https://github.com/test/repo-1"
|
||||
fireEvent.change(urlInput, { target: { value: testUrl } })
|
||||
|
||||
const addButton = screen.getByText("marketplace:sources.add.button")
|
||||
fireEvent.click(addButton)
|
||||
|
||||
const sources = stateManager.getState().sources
|
||||
const newSource = sources.find((s) => s.url === testUrl)
|
||||
expect(newSource).toEqual({
|
||||
url: testUrl,
|
||||
enabled: true,
|
||||
})
|
||||
})
|
||||
|
||||
it("adds a new source with URL and name", async () => {
|
||||
render(<MarketplaceSourcesConfig stateManager={stateManager} />)
|
||||
|
||||
const nameInput = screen.getByPlaceholderText("marketplace:sources.add.namePlaceholder")
|
||||
const urlInput = screen.getByPlaceholderText("marketplace:sources.add.urlPlaceholder")
|
||||
const testUrl = "https://github.com/test/repo-2"
|
||||
|
||||
fireEvent.change(nameInput, { target: { value: "Test Source" } })
|
||||
fireEvent.change(urlInput, { target: { value: testUrl } })
|
||||
|
||||
const addButton = screen.getByText("marketplace:sources.add.button")
|
||||
fireEvent.click(addButton)
|
||||
|
||||
const sources = stateManager.getState().sources
|
||||
const newSource = sources.find((s) => s.url === testUrl)
|
||||
expect(newSource).toEqual({
|
||||
url: testUrl,
|
||||
name: "Test Source",
|
||||
enabled: true,
|
||||
})
|
||||
})
|
||||
|
||||
it("shows error when URL is empty", () => {
|
||||
render(<MarketplaceSourcesConfig stateManager={stateManager} />)
|
||||
|
||||
const addButton = screen.getByText("marketplace:sources.add.button")
|
||||
fireEvent.click(addButton)
|
||||
|
||||
const errorElement = screen.getByText("marketplace:sources.errors.invalidGitUrl")
|
||||
expect(errorElement).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("shows error when max sources reached", () => {
|
||||
// Add max number of sources with unique URLs
|
||||
const maxSources = Array(10)
|
||||
.fill(null)
|
||||
.map((_, i) => ({
|
||||
url: `https://github.com/test/repo-${i}`,
|
||||
enabled: true,
|
||||
}))
|
||||
|
||||
stateManager.transition({
|
||||
type: "UPDATE_SOURCES",
|
||||
payload: { sources: maxSources },
|
||||
})
|
||||
|
||||
render(<MarketplaceSourcesConfig stateManager={stateManager} />)
|
||||
|
||||
const urlInput = screen.getByPlaceholderText("marketplace:sources.add.urlPlaceholder")
|
||||
fireEvent.change(urlInput, { target: { value: "https://github.com/test/new" } })
|
||||
|
||||
const addButton = screen.getByText("marketplace:sources.add.button")
|
||||
fireEvent.click(addButton)
|
||||
|
||||
const errorElement = screen.getByText("marketplace:sources.errors.maxSources")
|
||||
expect(errorElement).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("accepts multi-part corporate git URLs", async () => {
|
||||
render(<MarketplaceSourcesConfig stateManager={stateManager} />)
|
||||
|
||||
const urlInput = screen.getByPlaceholderText("marketplace:sources.add.urlPlaceholder")
|
||||
const gitUrl = "git@git.lab.company.com:team-core/project-name.git"
|
||||
fireEvent.change(urlInput, { target: { value: gitUrl } })
|
||||
|
||||
const addButton = screen.getByText("marketplace:sources.add.button")
|
||||
fireEvent.click(addButton)
|
||||
|
||||
const sources = stateManager.getState().sources
|
||||
const newSource = sources.find((s) => s.url === gitUrl)
|
||||
expect(newSource).toEqual({
|
||||
url: gitUrl,
|
||||
enabled: true,
|
||||
})
|
||||
})
|
||||
|
||||
it("toggles source enabled state", () => {
|
||||
const testUrl = "https://github.com/test/repo-3"
|
||||
stateManager.transition({
|
||||
type: "UPDATE_SOURCES",
|
||||
payload: {
|
||||
sources: [
|
||||
{
|
||||
url: testUrl,
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
render(<MarketplaceSourcesConfig stateManager={stateManager} />)
|
||||
|
||||
const checkbox = screen.getByRole("checkbox", { name: "" })
|
||||
fireEvent.click(checkbox)
|
||||
|
||||
const sources = stateManager.getState().sources
|
||||
const updatedSource = sources.find((s) => s.url === testUrl)
|
||||
expect(updatedSource?.enabled).toBe(false)
|
||||
})
|
||||
|
||||
it("removes a source", () => {
|
||||
const testUrl = "https://github.com/test/repo-4"
|
||||
stateManager.transition({
|
||||
type: "UPDATE_SOURCES",
|
||||
payload: {
|
||||
sources: [
|
||||
{
|
||||
url: testUrl,
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
render(<MarketplaceSourcesConfig stateManager={stateManager} />)
|
||||
|
||||
const removeButtons = screen.getAllByTitle("marketplace:sources.current.remove")
|
||||
fireEvent.click(removeButtons[0])
|
||||
|
||||
const sources = stateManager.getState().sources
|
||||
expect(sources.find((s) => s.url === testUrl)).toBeUndefined()
|
||||
})
|
||||
|
||||
it("refreshes a source", () => {
|
||||
const testUrl = "https://github.com/test/repo-5"
|
||||
stateManager.transition({
|
||||
type: "UPDATE_SOURCES",
|
||||
payload: {
|
||||
sources: [
|
||||
{
|
||||
url: testUrl,
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
render(<MarketplaceSourcesConfig stateManager={stateManager} />)
|
||||
|
||||
const refreshButtons = screen.getAllByTitle("marketplace:sources.current.refresh")
|
||||
fireEvent.click(refreshButtons[0])
|
||||
|
||||
expect(stateManager.getState().refreshingUrls).toContain(testUrl)
|
||||
})
|
||||
|
||||
it("limits source name to 20 characters", () => {
|
||||
render(<MarketplaceSourcesConfig stateManager={stateManager} />)
|
||||
|
||||
const nameInput = screen.getByPlaceholderText("marketplace:sources.add.namePlaceholder")
|
||||
const longName = "This is a very long source name that exceeds limit"
|
||||
fireEvent.change(nameInput, { target: { value: longName } })
|
||||
|
||||
// The component should truncate to 20 chars
|
||||
expect(nameInput).toHaveValue(longName.slice(0, 20))
|
||||
})
|
||||
|
||||
it("shows character count for source name", () => {
|
||||
render(<MarketplaceSourcesConfig stateManager={stateManager} />)
|
||||
|
||||
const nameInput = screen.getByPlaceholderText("marketplace:sources.add.namePlaceholder")
|
||||
fireEvent.change(nameInput, { target: { value: "Test Source" } })
|
||||
|
||||
expect(screen.getByText("11/20")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("clears inputs after adding source", () => {
|
||||
render(<MarketplaceSourcesConfig stateManager={stateManager} />)
|
||||
|
||||
const nameInput = screen.getByPlaceholderText("marketplace:sources.add.namePlaceholder")
|
||||
const urlInput = screen.getByPlaceholderText("marketplace:sources.add.urlPlaceholder")
|
||||
const testUrl = "https://github.com/test/repo-6"
|
||||
|
||||
fireEvent.change(nameInput, { target: { value: "Test Source" } })
|
||||
fireEvent.change(urlInput, { target: { value: testUrl } })
|
||||
|
||||
const addButton = screen.getByText("marketplace:sources.add.button")
|
||||
fireEvent.click(addButton)
|
||||
|
||||
expect(nameInput).toHaveValue("")
|
||||
expect(urlInput).toHaveValue("")
|
||||
})
|
||||
})
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,52 @@
|
|||
import React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@src/components/ui/accordion"
|
||||
|
||||
interface ExpandableSectionProps {
|
||||
title: string
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
defaultExpanded?: boolean
|
||||
badge?: string
|
||||
}
|
||||
|
||||
export const ExpandableSection: React.FC<ExpandableSectionProps> = ({
|
||||
title,
|
||||
children,
|
||||
className,
|
||||
defaultExpanded = false,
|
||||
badge,
|
||||
}) => {
|
||||
// Create a unique value for the accordion item
|
||||
const accordionValue = React.useMemo(() => `section-${title.replace(/\s+/g, "-").toLowerCase()}`, [title])
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
defaultValue={defaultExpanded ? accordionValue : undefined}
|
||||
className={cn("border-t-0", className)}>
|
||||
<AccordionItem value={accordionValue}>
|
||||
<AccordionTrigger
|
||||
className="py-2 text-sm hover:no-underline hover:cursor-pointer"
|
||||
aria-controls="details-content"
|
||||
id="details-button">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<span className="font-medium flex items-center">
|
||||
<span className="codicon codicon-list-unordered mr-1"></span>
|
||||
{title}
|
||||
</span>
|
||||
{badge && (
|
||||
<span className="mr-2 text-xs bg-vscode-badge-background text-vscode-badge-foreground px-1 py-0.5 rounded">
|
||||
{badge}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent id="details-content" className="pt-0" role="region" aria-labelledby="details-button">
|
||||
{children}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
import React, { useCallback, useMemo } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||
import { MoreVertical, ExternalLink, Download, Trash } from "lucide-react"
|
||||
import {
|
||||
InstallMarketplaceItemOptions,
|
||||
MarketplaceItem,
|
||||
RemoveInstalledMarketplaceItemOptions,
|
||||
} from "../../../../../src/services/marketplace/types"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { useAppTranslation } from "@/i18n/TranslationContext"
|
||||
import { isValidUrl } from "@roo/utils/url"
|
||||
import { ItemInstalledMetadata } from "@roo/services/marketplace/InstalledMetadataManager"
|
||||
|
||||
interface MarketplaceItemActionsMenuProps {
|
||||
item: MarketplaceItem
|
||||
installed: {
|
||||
project: ItemInstalledMetadata | undefined
|
||||
global: ItemInstalledMetadata | undefined
|
||||
}
|
||||
}
|
||||
|
||||
export const MarketplaceItemActionsMenu: React.FC<MarketplaceItemActionsMenuProps> = ({ item, installed }) => {
|
||||
const { t } = useAppTranslation()
|
||||
|
||||
const itemSourceUrl = useMemo(() => {
|
||||
if (item.sourceUrl && isValidUrl(item.sourceUrl)) {
|
||||
return item.sourceUrl
|
||||
}
|
||||
|
||||
let url = item.repoUrl
|
||||
if (item.defaultBranch) {
|
||||
url = `${url}/tree/${item.defaultBranch}`
|
||||
if (item.path) {
|
||||
const normalizedPath = item.path.replace(/\\/g, "/").replace(/^\/+/, "")
|
||||
url = `${url}/${normalizedPath}`
|
||||
}
|
||||
}
|
||||
return url
|
||||
}, [item.sourceUrl, item.repoUrl, item.defaultBranch, item.path])
|
||||
|
||||
const handleOpenSourceUrl = useCallback(() => {
|
||||
vscode.postMessage({
|
||||
type: "openExternal",
|
||||
url: itemSourceUrl,
|
||||
})
|
||||
}, [itemSourceUrl])
|
||||
|
||||
const handleInstall = (options?: InstallMarketplaceItemOptions) => {
|
||||
vscode.postMessage({
|
||||
type: "installMarketplaceItem",
|
||||
mpItem: item,
|
||||
mpInstallOptions: options,
|
||||
})
|
||||
}
|
||||
|
||||
const handleRemove = (options?: RemoveInstalledMarketplaceItemOptions) => {
|
||||
vscode.postMessage({
|
||||
type: "removeInstalledMarketplaceItem",
|
||||
mpItem: item,
|
||||
mpInstallOptions: options,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
className="px-2 w-4"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label={t("marketplace:items.card.actionsMenuLabel") || "Actions"}>
|
||||
<MoreVertical className="size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" side="bottom">
|
||||
{/* View Source / External Link Item */}
|
||||
<DropdownMenuItem aria-label={item.sourceName} onClick={handleOpenSourceUrl}>
|
||||
<ExternalLink className="mr-2 h-4 w-4" />
|
||||
<span>{t("marketplace:items.card.viewSource")}</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
{/* Remove (Project) */}
|
||||
{installed.project ? (
|
||||
<DropdownMenuItem onClick={() => handleRemove({ target: "project" })}>
|
||||
<Trash className="mr-2 h-4 w-4" />
|
||||
<span>{t("marketplace:items.card.removeProject")}</span>
|
||||
</DropdownMenuItem>
|
||||
) : (
|
||||
<DropdownMenuItem className="" onClick={() => handleInstall({ target: "project" })}>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
<span>{t("marketplace:items.card.installProject")}</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{/* Remove (Global) */}
|
||||
{installed.global ? (
|
||||
<DropdownMenuItem onClick={() => handleRemove({ target: "global" })}>
|
||||
<Trash className="mr-2 h-4 w-4" />
|
||||
<span>{t("marketplace:items.card.removeGlobal")}</span>
|
||||
</DropdownMenuItem>
|
||||
) : (
|
||||
<DropdownMenuItem onClick={() => handleInstall({ target: "global" })}>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
<span>{t("marketplace:items.card.installGlobal")}</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
import React, { useMemo } from "react"
|
||||
import { MarketplaceItem } from "@roo/services/marketplace/types" // Updated import path
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { groupItemsByType, GroupedItems } from "../utils/grouping"
|
||||
import { ExpandableSection } from "./ExpandableSection"
|
||||
import { TypeGroup } from "./TypeGroup"
|
||||
import { ViewState } from "../MarketplaceViewStateManager"
|
||||
import { useAppTranslation } from "@/i18n/TranslationContext"
|
||||
import { MarketplaceItemActionsMenu } from "./MarketplaceItemActionsMenu"
|
||||
import { isValidUrl } from "@roo/utils/url"
|
||||
import { ItemInstalledMetadata } from "@roo/services/marketplace/InstalledMetadataManager"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
import { Rocket, Server, Package, Sparkles, Download } from "lucide-react"
|
||||
|
||||
interface MarketplaceItemCardProps {
|
||||
item: MarketplaceItem
|
||||
installed: {
|
||||
project: ItemInstalledMetadata | undefined
|
||||
global: ItemInstalledMetadata | undefined
|
||||
}
|
||||
filters: ViewState["filters"]
|
||||
setFilters: (filters: Partial<ViewState["filters"]>) => void
|
||||
activeTab: ViewState["activeTab"]
|
||||
setActiveTab: (tab: ViewState["activeTab"]) => void
|
||||
}
|
||||
|
||||
const icons = {
|
||||
mode: <Rocket className="h-4 w-4" />,
|
||||
mcp: <Server className="h-4 w-4" />,
|
||||
package: <Package className="h-4 w-4" />,
|
||||
prompt: <Sparkles className="h-4 w-4" />,
|
||||
}
|
||||
|
||||
export const MarketplaceItemCard: React.FC<MarketplaceItemCardProps> = ({
|
||||
item,
|
||||
installed,
|
||||
filters,
|
||||
setFilters,
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
}) => {
|
||||
const { t } = useAppTranslation()
|
||||
|
||||
const typeLabel = useMemo(() => {
|
||||
const labels: Partial<Record<MarketplaceItem["type"], string>> = {
|
||||
mode: t("marketplace:filters.type.mode"),
|
||||
mcp: t("marketplace:filters.type.mcp server"),
|
||||
prompt: t("marketplace:filters.type.prompt"),
|
||||
package: t("marketplace:filters.type.package"),
|
||||
}
|
||||
return labels[item.type] ?? "N/A"
|
||||
}, [item.type, t])
|
||||
|
||||
const groupedItems = useMemo(() => {
|
||||
if (!item.items?.length) return null
|
||||
return groupItemsByType(item.items)
|
||||
}, [item.items]) as GroupedItems | null
|
||||
|
||||
const expandableSectionBadge = useMemo(() => {
|
||||
const matchCount = item.items?.filter((subItem) => subItem.matchInfo?.matched).length ?? 0
|
||||
return matchCount > 0 ? t("marketplace:items.components", { count: matchCount }) : undefined
|
||||
}, [item.items, t])
|
||||
|
||||
return (
|
||||
<div className="border border-vscode-panel-border rounded-sm p-3 bg-vscode-editor-background">
|
||||
<div className="mb-2 flex gap-2">
|
||||
{installed.project && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
className={cn(
|
||||
"px-2 py-1 text-xs rounded-sm flex items-center gap-1.5 w-fit",
|
||||
"bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground",
|
||||
)}
|
||||
aria-label="Installed in project">
|
||||
<Download className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
<span>Project</span>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>This package is installed in your current project workspace</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{installed.global && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
className={cn(
|
||||
"px-2 py-1 text-xs rounded-sm flex items-center gap-1.5 w-fit",
|
||||
"bg-vscode-badge-background text-vscode-badge-foreground",
|
||||
)}
|
||||
aria-label="Installed globally">
|
||||
<Download className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
<span>Global</span>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>This package is installed globally on your system</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-vscode-foreground mt-0 mb-1 leading-none">{item.name}</h3>
|
||||
<AuthorInfo item={item} />
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
"px-2 py-1 text-xs rounded-md flex gap-1 items-center bg-primary text-vscode-button-foreground",
|
||||
)}>
|
||||
{icons[item.type]} {typeLabel}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="my-2 text-vscode-foreground">{item.description}</p>
|
||||
|
||||
{item.tags && item.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 my-2">
|
||||
{item.tags.map((tag) => (
|
||||
<Button
|
||||
key={tag}
|
||||
size="sm"
|
||||
variant={filters.tags.includes(tag) ? "default" : "secondary"}
|
||||
className="shadow-none border-none rounded-sm capitalize text-xs px-2 h-5"
|
||||
onClick={() => {
|
||||
const newTags = filters.tags.includes(tag)
|
||||
? filters.tags.filter((t: string) => t !== tag)
|
||||
: [...filters.tags, tag]
|
||||
setFilters({ tags: newTags })
|
||||
|
||||
if (!filters.tags.includes(tag) && activeTab !== "browse") {
|
||||
setActiveTab("browse")
|
||||
}
|
||||
}}
|
||||
title={
|
||||
filters.tags.includes(tag)
|
||||
? t("marketplace:filters.tags.clear", { count: tag })
|
||||
: t("marketplace:filters.tags.clickToFilter")
|
||||
}>
|
||||
{tag}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-4 text-sm text-vscode-descriptionForeground">
|
||||
{item.version && (
|
||||
<span className="flex items-center">
|
||||
<span className="codicon codicon-tag mr-1"></span>
|
||||
{item.version}
|
||||
</span>
|
||||
)}
|
||||
{item.lastUpdated && (
|
||||
<span className="flex items-center">
|
||||
<span className="codicon codicon-calendar mr-1"></span>
|
||||
{new Date(item.lastUpdated).toLocaleDateString(undefined, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<MarketplaceItemActionsMenu item={item} installed={installed} />
|
||||
</div>
|
||||
|
||||
{item.type === "package" && (
|
||||
<ExpandableSection
|
||||
title={t("marketplace:items.components", { count: item.items?.length ?? 0 })}
|
||||
badge={expandableSectionBadge}
|
||||
defaultExpanded={item.items?.some((subItem) => subItem.matchInfo?.matched) ?? false}>
|
||||
<div className="space-y-4">
|
||||
{groupedItems &&
|
||||
Object.entries(groupedItems).map(([type, group]) => (
|
||||
<TypeGroup key={type} type={type} items={group.items} />
|
||||
))}
|
||||
</div>
|
||||
</ExpandableSection>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface AuthorInfoProps {
|
||||
item: MarketplaceItem
|
||||
}
|
||||
|
||||
const AuthorInfo: React.FC<AuthorInfoProps> = ({ item }) => {
|
||||
const { t } = useAppTranslation()
|
||||
|
||||
const handleOpenAuthorUrl = () => {
|
||||
if (item.authorUrl && isValidUrl(item.authorUrl)) {
|
||||
vscode.postMessage({ type: "openExternal", url: item.authorUrl })
|
||||
}
|
||||
}
|
||||
|
||||
if (item.author) {
|
||||
return (
|
||||
<p className="text-sm text-vscode-descriptionForeground my-0">
|
||||
{item.authorUrl && isValidUrl(item.authorUrl) ? (
|
||||
<Button
|
||||
variant="link"
|
||||
className="p-0 h-auto text-sm text-vscode-textLink hover:underline"
|
||||
onClick={handleOpenAuthorUrl}>
|
||||
{t("marketplace:items.card.by", { author: item.author })}
|
||||
</Button>
|
||||
) : (
|
||||
t("marketplace:items.card.by", { author: item.author })
|
||||
)}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
143
webview-ui/src/components/marketplace/components/TypeGroup.tsx
Normal file
143
webview-ui/src/components/marketplace/components/TypeGroup.tsx
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
import React, { useMemo } from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useAppTranslation } from "@/i18n/TranslationContext"
|
||||
import { Rocket, Server, Package, Sparkles } from "lucide-react"
|
||||
|
||||
interface TypeGroupProps {
|
||||
type: "mode" | "mcp" | "prompt" | "package" | (string & {})
|
||||
items: Array<{
|
||||
name: string
|
||||
description?: string
|
||||
metadata?: any
|
||||
path?: string
|
||||
matchInfo?: {
|
||||
matched: boolean
|
||||
matchReason?: Record<string, boolean>
|
||||
}
|
||||
}>
|
||||
className?: string
|
||||
}
|
||||
|
||||
const typeIcons = {
|
||||
mode: <Rocket className="size-3" />,
|
||||
mcp: <Server className="size-3" />,
|
||||
prompt: <Sparkles className="size-3" />,
|
||||
package: <Package className="size-3" />,
|
||||
}
|
||||
|
||||
export const TypeGroup: React.FC<TypeGroupProps> = ({ type, items, className }) => {
|
||||
const { t } = useAppTranslation()
|
||||
const typeLabel = useMemo(() => {
|
||||
switch (type) {
|
||||
case "mode":
|
||||
return t("marketplace:type-group.modes")
|
||||
case "mcp":
|
||||
return t("marketplace:type-group.mcps")
|
||||
case "prompt":
|
||||
return t("marketplace:type-group.prompts")
|
||||
case "package":
|
||||
return t("marketplace:type-group.packages")
|
||||
default:
|
||||
return t("marketplace:type-group.generic-type", {
|
||||
type: type.charAt(0).toUpperCase() + type.slice(1),
|
||||
})
|
||||
}
|
||||
}, [type, t])
|
||||
|
||||
// Get the appropriate icon for the type
|
||||
const typeIcon = typeIcons[type as keyof typeof typeIcons] || <Package className="size-3" />
|
||||
|
||||
// Determine if we should use horizontal layout (modes only for now) or card layout (for mcps)
|
||||
const isHorizontalLayout = type === "mode"
|
||||
|
||||
// Memoize the list items
|
||||
const listItems = useMemo(() => {
|
||||
if (!items?.length) return null
|
||||
|
||||
if (isHorizontalLayout) {
|
||||
// Horizontal layout for modes
|
||||
return (
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(140px,1fr))] mt-2 gap-1.5">
|
||||
{items.map((item, index) => {
|
||||
const cardClassName = cn(
|
||||
"flex items-center gap-2 py-1 px-2 rounded-md bg-vscode-input-background/50",
|
||||
"hover:border-vscode-focusBorder transition-colors",
|
||||
{
|
||||
"border-vscode-textLink": item.matchInfo?.matched,
|
||||
"border-vscode-panel-border": !item.matchInfo?.matched,
|
||||
},
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`${item.path || index}`}
|
||||
className={cardClassName}
|
||||
title={item.description || item.name}>
|
||||
<span
|
||||
className={cn("font-medium text-sm", {
|
||||
"text-vscode-textLink": item.matchInfo?.matched,
|
||||
"text-vscode-foreground": !item.matchInfo?.matched,
|
||||
})}>
|
||||
{item.name}
|
||||
</span>
|
||||
{item.matchInfo?.matched && (
|
||||
<span className="text-xs bg-vscode-badge-background text-vscode-badge-foreground px-1 py-0.5 rounded">
|
||||
{t("marketplace:type-group.match")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-3 mt-2">
|
||||
{items.map((item, index) => (
|
||||
<div key={`${item.path || index}`} className="bg-vscode-input-background/50 p-2 rounded-sm">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h5
|
||||
className={cn(
|
||||
"text-sm font-medium m-0",
|
||||
item.matchInfo?.matched ? "text-vscode-textLink" : "text-vscode-foreground",
|
||||
)}>
|
||||
{item.name}
|
||||
</h5>
|
||||
{item.matchInfo?.matched && (
|
||||
<span className="ml-auto text-xs bg-vscode-badge-background text-vscode-badge-foreground px-1 py-0.5 rounded">
|
||||
{t("marketplace:type-group.match")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{item.description && (
|
||||
<p className="text-sm text-vscode-descriptionForeground m-0 ml-4">{item.description}</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}, [items, t, isHorizontalLayout])
|
||||
|
||||
if (!items?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="flex items-center gap-2 bg-vscode-input-background p-1 rounded-sm">
|
||||
<div
|
||||
className={cn("p-1 rounded-sm", {
|
||||
"bg-chart-2 text-vscode-button-foreground": type === "mode",
|
||||
"bg-chart-5 text-vscode-button-foreground": type === "mcp",
|
||||
"bg-vscode-badge-background text-vscode-badge-foreground": type === "prompt",
|
||||
"bg-chart-4 text-vscode-button-foreground": type === "package",
|
||||
})}>
|
||||
{typeIcon}
|
||||
</div>
|
||||
<h4 className="text-sm font-medium text-vscode-foreground my-0">{typeLabel}</h4>
|
||||
</div>
|
||||
{listItems}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
import { render, screen } from "@testing-library/react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
import { ExpandableSection } from "../ExpandableSection"
|
||||
|
||||
// Mock ChevronDownIcon used in Accordion component
|
||||
jest.mock("lucide-react", () => ({
|
||||
ChevronDownIcon: () => <div data-testid="chevron-icon" />,
|
||||
}))
|
||||
|
||||
// Mock ResizeObserver
|
||||
class MockResizeObserver {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
}
|
||||
|
||||
global.ResizeObserver = MockResizeObserver
|
||||
|
||||
describe("ExpandableSection", () => {
|
||||
it("renders with basic props", () => {
|
||||
render(
|
||||
<ExpandableSection title="Test Section">
|
||||
<div>Test Content</div>
|
||||
</ExpandableSection>,
|
||||
)
|
||||
|
||||
expect(screen.getByText("Test Section")).toBeInTheDocument()
|
||||
// Content is hidden in accordion
|
||||
const content = screen.getByRole("region", { hidden: true })
|
||||
expect(content).toHaveAttribute("hidden")
|
||||
})
|
||||
|
||||
it("applies custom className", () => {
|
||||
render(
|
||||
<ExpandableSection title="Test Section" className="custom-class">
|
||||
<div>Test Content</div>
|
||||
</ExpandableSection>,
|
||||
)
|
||||
|
||||
const accordion = screen.getByTestId("chevron-icon").closest(".border-t-0")
|
||||
expect(accordion).toHaveClass("custom-class")
|
||||
})
|
||||
|
||||
it("renders badge when provided", () => {
|
||||
render(
|
||||
<ExpandableSection title="Test Section" badge="123">
|
||||
<div>Test Content</div>
|
||||
</ExpandableSection>,
|
||||
)
|
||||
|
||||
expect(screen.getByText("123")).toBeInTheDocument()
|
||||
expect(screen.getByText("123")).toHaveClass(
|
||||
"text-xs",
|
||||
"bg-vscode-badge-background",
|
||||
"text-vscode-badge-foreground",
|
||||
)
|
||||
})
|
||||
|
||||
it("expands and collapses on click", async () => {
|
||||
const user = userEvent.setup()
|
||||
render(
|
||||
<ExpandableSection title="Test Section">
|
||||
<div>Test Content</div>
|
||||
</ExpandableSection>,
|
||||
)
|
||||
|
||||
const trigger = screen.getByRole("button")
|
||||
const content = screen.getByRole("region", { hidden: true })
|
||||
|
||||
// Initially hidden
|
||||
expect(content).toHaveAttribute("hidden")
|
||||
|
||||
// Expand
|
||||
await user.click(trigger)
|
||||
expect(content).not.toHaveAttribute("hidden")
|
||||
|
||||
// Collapse
|
||||
await user.click(trigger)
|
||||
expect(content).toHaveAttribute("hidden")
|
||||
})
|
||||
|
||||
it("starts expanded when defaultExpanded is true", () => {
|
||||
render(
|
||||
<ExpandableSection title="Test Section" defaultExpanded={true}>
|
||||
<div>Test Content</div>
|
||||
</ExpandableSection>,
|
||||
)
|
||||
|
||||
const content = screen.getByRole("region")
|
||||
expect(content).not.toHaveAttribute("hidden")
|
||||
})
|
||||
|
||||
it("has correct accessibility attributes", () => {
|
||||
render(
|
||||
<ExpandableSection title="Test Section">
|
||||
<div>Test Content</div>
|
||||
</ExpandableSection>,
|
||||
)
|
||||
|
||||
const trigger = screen.getByRole("button")
|
||||
const content = screen.getByRole("region", { hidden: true })
|
||||
|
||||
expect(trigger).toHaveAttribute("id", "details-button")
|
||||
expect(trigger).toHaveAttribute("aria-controls", "details-content")
|
||||
expect(content).toHaveAttribute("id", "details-content")
|
||||
expect(content).toHaveAttribute("aria-labelledby", "details-button")
|
||||
})
|
||||
|
||||
it("renders list icon", () => {
|
||||
render(
|
||||
<ExpandableSection title="Test Section">
|
||||
<div>Test Content</div>
|
||||
</ExpandableSection>,
|
||||
)
|
||||
|
||||
const icon = screen.getByRole("button").querySelector(".codicon-list-unordered")
|
||||
expect(icon).toHaveClass("codicon", "codicon-list-unordered")
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
import { render, screen } from "@testing-library/react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
import { MarketplaceItemCard } from "../MarketplaceItemCard"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { MarketplaceItem } from "@roo/services/marketplace/types"
|
||||
import { TooltipProvider } from "@/components/ui/tooltip"
|
||||
import { AccordionTrigger } from "@/components/ui/accordion"
|
||||
|
||||
// Mock vscode API
|
||||
jest.mock("@/utils/vscode", () => ({
|
||||
vscode: {
|
||||
postMessage: jest.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock MarketplaceItemActionsMenu component
|
||||
jest.mock("../MarketplaceItemActionsMenu", () => ({
|
||||
MarketplaceItemActionsMenu: () => <div data-testid="actions-menu" />,
|
||||
}))
|
||||
|
||||
// Mock ChevronDownIcon for Accordion
|
||||
jest.mock("@/components/ui/accordion", () => {
|
||||
const actual = jest.requireActual("@/components/ui/accordion")
|
||||
return {
|
||||
...actual,
|
||||
AccordionTrigger: ({ children, ...props }: React.ComponentProps<typeof AccordionTrigger>) => (
|
||||
<button {...props}>
|
||||
{children}
|
||||
<span data-testid="chevron-icon" />
|
||||
</button>
|
||||
),
|
||||
}
|
||||
})
|
||||
|
||||
// Mock translation hook
|
||||
jest.mock("@/i18n/TranslationContext", () => ({
|
||||
useAppTranslation: () => ({
|
||||
t: (key: string, params?: any) => {
|
||||
if (key === "marketplace:items.card.by") {
|
||||
return `by ${params.author}`
|
||||
}
|
||||
const translations: Record<string, string> = {
|
||||
"marketplace:filters.type.mode": "Mode",
|
||||
"marketplace:filters.type.mcp server": "MCP Server",
|
||||
"marketplace:filters.type.prompt": "Prompt",
|
||||
"marketplace:filters.type.package": "Package",
|
||||
"marketplace:filters.tags.clear": "Remove filter",
|
||||
"marketplace:filters.tags.clickToFilter": "Add filter",
|
||||
"marketplace:items.components": "Components",
|
||||
}
|
||||
return translations[key] || key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock icons
|
||||
jest.mock("lucide-react", () => ({
|
||||
Rocket: () => <div data-testid="rocket-icon" />,
|
||||
Server: () => <div data-testid="server-icon" />,
|
||||
Package: () => <div data-testid="package-icon" />,
|
||||
Sparkles: () => <div data-testid="sparkles-icon" />,
|
||||
Download: () => <div data-testid="download-icon" />,
|
||||
}))
|
||||
|
||||
const renderWithProviders = (ui: React.ReactElement) => {
|
||||
return render(<TooltipProvider>{ui}</TooltipProvider>)
|
||||
}
|
||||
|
||||
describe("MarketplaceItemCard", () => {
|
||||
const defaultItem: MarketplaceItem = {
|
||||
id: "test-item",
|
||||
name: "Test Item",
|
||||
description: "Test Description",
|
||||
type: "mode",
|
||||
version: "1.0.0",
|
||||
author: "Test Author",
|
||||
authorUrl: "https://example.com",
|
||||
lastUpdated: "2024-01-01",
|
||||
tags: ["test", "example"],
|
||||
repoUrl: "https://github.com/test/repo",
|
||||
url: "https://example.com/item",
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
item: defaultItem,
|
||||
installed: {
|
||||
project: undefined,
|
||||
global: undefined,
|
||||
},
|
||||
filters: {
|
||||
type: "",
|
||||
search: "",
|
||||
tags: [],
|
||||
},
|
||||
setFilters: jest.fn(),
|
||||
activeTab: "browse" as const,
|
||||
setActiveTab: jest.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("renders basic item information", () => {
|
||||
renderWithProviders(<MarketplaceItemCard {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText("Test Item")).toBeInTheDocument()
|
||||
expect(screen.getByText("Test Description")).toBeInTheDocument()
|
||||
expect(screen.getByText("by Test Author")).toBeInTheDocument()
|
||||
expect(screen.getByText("1.0.0")).toBeInTheDocument()
|
||||
expect(screen.getByText("Jan 1, 2024")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders project installation badge", () => {
|
||||
renderWithProviders(
|
||||
<MarketplaceItemCard
|
||||
{...defaultProps}
|
||||
installed={{
|
||||
project: { version: "1.0.0" },
|
||||
global: undefined,
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText("Project")).toBeInTheDocument()
|
||||
expect(screen.getByLabelText("Installed in project")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders global installation badge", () => {
|
||||
renderWithProviders(
|
||||
<MarketplaceItemCard
|
||||
{...defaultProps}
|
||||
installed={{
|
||||
project: undefined,
|
||||
global: { version: "1.0.0" },
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText("Global")).toBeInTheDocument()
|
||||
expect(screen.getByLabelText("Installed globally")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders type with appropriate icon", () => {
|
||||
renderWithProviders(<MarketplaceItemCard {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText("Mode")).toBeInTheDocument()
|
||||
expect(screen.getByTestId("rocket-icon")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders tags and handles tag clicks", async () => {
|
||||
const user = userEvent.setup()
|
||||
const setFilters = jest.fn()
|
||||
const setActiveTab = jest.fn()
|
||||
|
||||
renderWithProviders(
|
||||
<MarketplaceItemCard {...defaultProps} setFilters={setFilters} setActiveTab={setActiveTab} />,
|
||||
)
|
||||
|
||||
const tagButton = screen.getByText("test")
|
||||
await user.click(tagButton)
|
||||
|
||||
expect(setFilters).toHaveBeenCalledWith({ tags: ["test"] })
|
||||
expect(setActiveTab).not.toHaveBeenCalled() // Already on browse tab
|
||||
})
|
||||
|
||||
it("handles author link click", async () => {
|
||||
const user = userEvent.setup()
|
||||
renderWithProviders(<MarketplaceItemCard {...defaultProps} />)
|
||||
|
||||
const authorLink = screen.getByText("by Test Author")
|
||||
await user.click(authorLink)
|
||||
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "openExternal",
|
||||
url: "https://example.com",
|
||||
})
|
||||
})
|
||||
|
||||
it("renders package components when available", () => {
|
||||
const packageItem: MarketplaceItem = {
|
||||
...defaultItem,
|
||||
type: "package",
|
||||
items: [
|
||||
{
|
||||
type: "mode",
|
||||
path: "test/path",
|
||||
matchInfo: { matched: true },
|
||||
metadata: {
|
||||
name: "Component 1",
|
||||
description: "Test Component",
|
||||
type: "mode",
|
||||
version: "1.0.0",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
renderWithProviders(<MarketplaceItemCard {...defaultProps} item={packageItem} />)
|
||||
|
||||
// Find the section title by its parent button
|
||||
const sectionTitle = screen.getByRole("button", { name: /Components/ })
|
||||
expect(sectionTitle).toBeInTheDocument()
|
||||
expect(screen.getByText("Component 1")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("does not render invalid author URLs", () => {
|
||||
const itemWithInvalidUrl: MarketplaceItem = {
|
||||
...defaultItem,
|
||||
authorUrl: "invalid-url",
|
||||
}
|
||||
|
||||
renderWithProviders(<MarketplaceItemCard {...defaultProps} item={itemWithInvalidUrl} />)
|
||||
|
||||
const authorText = screen.getByText("by Test Author")
|
||||
expect(authorText.tagName).not.toBe("BUTTON")
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
import { render, screen } from "@testing-library/react"
|
||||
import { TypeGroup } from "../TypeGroup"
|
||||
|
||||
// Mock translation hook
|
||||
jest.mock("@/i18n/TranslationContext", () => ({
|
||||
useAppTranslation: () => ({
|
||||
t: (key: string, params?: any) => {
|
||||
if (key === "marketplace:type-group.generic-type") {
|
||||
return params.type
|
||||
}
|
||||
const translations: Record<string, string> = {
|
||||
"marketplace:type-group.modes": "Modes",
|
||||
"marketplace:type-group.mcps": "MCPs",
|
||||
"marketplace:type-group.prompts": "Prompts",
|
||||
"marketplace:type-group.packages": "Packages",
|
||||
"marketplace:type-group.match": "Match",
|
||||
}
|
||||
return translations[key] || key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock icons
|
||||
jest.mock("lucide-react", () => ({
|
||||
Rocket: () => <div data-testid="rocket-icon" />,
|
||||
Server: () => <div data-testid="server-icon" />,
|
||||
Package: () => <div data-testid="package-icon" />,
|
||||
Sparkles: () => <div data-testid="sparkles-icon" />,
|
||||
}))
|
||||
|
||||
describe("TypeGroup", () => {
|
||||
const defaultItems = [
|
||||
{
|
||||
name: "Test Item",
|
||||
description: "Test Description",
|
||||
path: "test/path",
|
||||
},
|
||||
]
|
||||
|
||||
it("renders nothing when items array is empty", () => {
|
||||
const { container } = render(<TypeGroup type="mode" items={[]} />)
|
||||
expect(container).toBeEmptyDOMElement()
|
||||
})
|
||||
|
||||
it("renders mode type with horizontal layout", () => {
|
||||
render(<TypeGroup type="mode" items={defaultItems} />)
|
||||
|
||||
expect(screen.getByText("Modes")).toBeInTheDocument()
|
||||
expect(screen.getByTestId("rocket-icon")).toBeInTheDocument()
|
||||
|
||||
// Find the grid container
|
||||
const gridContainer = screen.getByText("Test Item").closest(".grid")
|
||||
expect(gridContainer).toHaveClass("grid-cols-[repeat(auto-fit,minmax(140px,1fr))]")
|
||||
})
|
||||
|
||||
it("renders mcp type with vertical layout", () => {
|
||||
render(<TypeGroup type="mcp" items={defaultItems} />)
|
||||
|
||||
expect(screen.getByText("MCPs")).toBeInTheDocument()
|
||||
expect(screen.getByTestId("server-icon")).toBeInTheDocument()
|
||||
|
||||
// Find the grid container
|
||||
const gridContainer = screen.getByText("Test Item").closest(".grid")
|
||||
expect(gridContainer).toHaveClass("grid-cols-1")
|
||||
})
|
||||
|
||||
it("renders prompt type correctly", () => {
|
||||
render(<TypeGroup type="prompt" items={defaultItems} />)
|
||||
|
||||
expect(screen.getByText("Prompts")).toBeInTheDocument()
|
||||
expect(screen.getByTestId("sparkles-icon")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders package type correctly", () => {
|
||||
render(<TypeGroup type="package" items={defaultItems} />)
|
||||
|
||||
expect(screen.getByText("Packages")).toBeInTheDocument()
|
||||
expect(screen.getByTestId("package-icon")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders custom type with generic label", () => {
|
||||
render(<TypeGroup type="custom" items={defaultItems} />)
|
||||
|
||||
expect(screen.getByText("Custom")).toBeInTheDocument()
|
||||
// Falls back to package icon
|
||||
expect(screen.getByTestId("package-icon")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders matched items with special styling", () => {
|
||||
const matchedItems = [
|
||||
{
|
||||
name: "Matched Item",
|
||||
description: "Test Description",
|
||||
path: "test/path",
|
||||
matchInfo: {
|
||||
matched: true,
|
||||
matchReason: { name: true },
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
render(<TypeGroup type="mode" items={matchedItems} />)
|
||||
|
||||
const matchedText = screen.getByText("Matched Item")
|
||||
expect(matchedText).toHaveClass("text-vscode-textLink")
|
||||
expect(screen.getByText("Match")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders description when provided", () => {
|
||||
render(<TypeGroup type="mcp" items={defaultItems} />)
|
||||
|
||||
expect(screen.getByText("Test Description")).toBeInTheDocument()
|
||||
expect(screen.getByText("Test Description")).toHaveClass("text-vscode-descriptionForeground")
|
||||
})
|
||||
|
||||
it("applies custom className", () => {
|
||||
render(<TypeGroup type="mode" items={defaultItems} className="custom-class" />)
|
||||
|
||||
const container = screen.getByText("Modes").closest(".custom-class")
|
||||
expect(container).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
44
webview-ui/src/components/marketplace/useStateManager.ts
Normal file
44
webview-ui/src/components/marketplace/useStateManager.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { useState, useEffect } from "react"
|
||||
import { MarketplaceViewStateManager, ViewState } from "./MarketplaceViewStateManager"
|
||||
|
||||
export function useStateManager(existingManager?: MarketplaceViewStateManager) {
|
||||
const [manager] = useState(() => existingManager || new MarketplaceViewStateManager())
|
||||
const [state, setState] = useState(() => manager.getState())
|
||||
|
||||
useEffect(() => {
|
||||
const handleStateChange = (newState: ViewState) => {
|
||||
setState((prevState) => {
|
||||
// Compare specific state properties that matter for rendering
|
||||
const hasChanged =
|
||||
prevState.isFetching !== newState.isFetching ||
|
||||
prevState.activeTab !== newState.activeTab ||
|
||||
JSON.stringify(prevState.allItems) !== JSON.stringify(newState.allItems) ||
|
||||
JSON.stringify(prevState.displayItems) !== JSON.stringify(newState.displayItems) ||
|
||||
JSON.stringify(prevState.filters) !== JSON.stringify(newState.filters) ||
|
||||
JSON.stringify(prevState.sources) !== JSON.stringify(newState.sources) ||
|
||||
JSON.stringify(prevState.refreshingUrls) !== JSON.stringify(newState.refreshingUrls) ||
|
||||
JSON.stringify(prevState.installedMetadata) !== JSON.stringify(newState.installedMetadata)
|
||||
|
||||
return hasChanged ? newState : prevState
|
||||
})
|
||||
}
|
||||
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
manager.handleMessage(event.data)
|
||||
}
|
||||
|
||||
window.addEventListener("message", handleMessage)
|
||||
const unsubscribe = manager.onStateChange(handleStateChange)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("message", handleMessage)
|
||||
unsubscribe()
|
||||
// Don't cleanup the manager if it was provided externally
|
||||
if (!existingManager) {
|
||||
manager.cleanup()
|
||||
}
|
||||
}
|
||||
}, [manager, existingManager])
|
||||
|
||||
return [state, manager] as const
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
import { groupItemsByType, formatItemText, getTotalItemCount, getUniqueTypes } from "../grouping"
|
||||
import { MarketplaceItem } from "../../../../../../src/services/marketplace/types"
|
||||
|
||||
describe("grouping utilities", () => {
|
||||
const mockItems = [
|
||||
{
|
||||
type: "mcp",
|
||||
path: "servers/test-server",
|
||||
metadata: {
|
||||
name: "Test Server",
|
||||
description: "A test server",
|
||||
version: "1.0.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "mode",
|
||||
path: "modes/test-mode",
|
||||
metadata: {
|
||||
name: "Test Mode",
|
||||
description: "A test mode",
|
||||
version: "2.0.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "mcp",
|
||||
path: "servers/another-server",
|
||||
metadata: {
|
||||
name: "Another Server",
|
||||
description: "Another test server",
|
||||
version: "1.1.0",
|
||||
},
|
||||
},
|
||||
] as MarketplaceItem["items"]
|
||||
|
||||
describe("groupItemsByType", () => {
|
||||
it("should group items by type correctly", () => {
|
||||
const result = groupItemsByType(mockItems)
|
||||
|
||||
expect(Object.keys(result)).toHaveLength(2)
|
||||
expect(result["mcp"].items).toHaveLength(2)
|
||||
expect(result["mode"].items).toHaveLength(1)
|
||||
|
||||
expect(result["mcp"].items[0].name).toBe("Test Server")
|
||||
expect(result["mode"].items[0].name).toBe("Test Mode")
|
||||
})
|
||||
|
||||
it("should handle empty items array", () => {
|
||||
expect(groupItemsByType([])).toEqual({})
|
||||
expect(groupItemsByType(undefined)).toEqual({})
|
||||
})
|
||||
|
||||
it("should handle items with missing metadata", () => {
|
||||
const itemsWithMissingData = [
|
||||
{
|
||||
type: "mcp",
|
||||
path: "test/path",
|
||||
},
|
||||
] as MarketplaceItem["items"]
|
||||
|
||||
const result = groupItemsByType(itemsWithMissingData)
|
||||
expect(result["mcp"].items[0].name).toBe("Unnamed item")
|
||||
})
|
||||
|
||||
it("should preserve item order within groups", () => {
|
||||
const result = groupItemsByType(mockItems)
|
||||
const servers = result["mcp"].items
|
||||
|
||||
expect(servers[0].name).toBe("Test Server")
|
||||
expect(servers[1].name).toBe("Another Server")
|
||||
})
|
||||
|
||||
it("should skip items without type", () => {
|
||||
const itemsWithoutType = [
|
||||
{
|
||||
path: "test/path",
|
||||
metadata: { name: "Test" },
|
||||
},
|
||||
] as MarketplaceItem["items"]
|
||||
|
||||
const result = groupItemsByType(itemsWithoutType)
|
||||
expect(Object.keys(result)).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("formatItemText", () => {
|
||||
it("should format item with name and description", () => {
|
||||
const item = { name: "Test", description: "Description" }
|
||||
expect(formatItemText(item)).toBe("Test - Description")
|
||||
})
|
||||
|
||||
it("should handle items without description", () => {
|
||||
const item = { name: "Test" }
|
||||
expect(formatItemText(item)).toBe("Test")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getTotalItemCount", () => {
|
||||
it("should count total items across all groups", () => {
|
||||
const groups = groupItemsByType(mockItems)
|
||||
expect(getTotalItemCount(groups)).toBe(3)
|
||||
})
|
||||
|
||||
it("should handle empty groups", () => {
|
||||
expect(getTotalItemCount({})).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getUniqueTypes", () => {
|
||||
it("should return sorted array of unique types", () => {
|
||||
const groups = groupItemsByType(mockItems)
|
||||
const types = getUniqueTypes(groups)
|
||||
|
||||
expect(types).toEqual(["mcp", "mode"])
|
||||
})
|
||||
|
||||
it("should handle empty groups", () => {
|
||||
expect(getUniqueTypes({})).toEqual([])
|
||||
})
|
||||
})
|
||||
})
|
||||
90
webview-ui/src/components/marketplace/utils/grouping.ts
Normal file
90
webview-ui/src/components/marketplace/utils/grouping.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import { MarketplaceItem } from "../../../../../src/services/marketplace/types"
|
||||
|
||||
export interface GroupedItems {
|
||||
[type: string]: {
|
||||
type: string
|
||||
items: Array<{
|
||||
name: string
|
||||
description?: string
|
||||
metadata?: any
|
||||
path?: string
|
||||
matchInfo?: {
|
||||
matched: boolean
|
||||
matchReason?: Record<string, boolean>
|
||||
}
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups package items by their type
|
||||
* @param items Array of items to group
|
||||
* @returns Object with items grouped by type
|
||||
*/
|
||||
export function groupItemsByType(items: MarketplaceItem["items"] = []): GroupedItems {
|
||||
if (!items?.length) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const groups: GroupedItems = {}
|
||||
|
||||
for (const item of items) {
|
||||
if (!item.type) continue
|
||||
|
||||
if (!groups[item.type]) {
|
||||
groups[item.type] = {
|
||||
type: item.type,
|
||||
items: [],
|
||||
}
|
||||
}
|
||||
|
||||
groups[item.type].items.push({
|
||||
name: item.metadata?.name || "Unnamed item",
|
||||
description: item.metadata?.description,
|
||||
metadata: item.metadata,
|
||||
path: item.path,
|
||||
matchInfo: item.matchInfo,
|
||||
})
|
||||
}
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a formatted string representation of an item
|
||||
* @param item The item to format
|
||||
* @returns Formatted string with name and description
|
||||
*/
|
||||
export function formatItemText(item: { name: string; description?: string }): string {
|
||||
if (!item.description) {
|
||||
return item.name
|
||||
}
|
||||
|
||||
const maxLength = 100
|
||||
const result =
|
||||
item.name +
|
||||
" - " +
|
||||
(item.description.length > maxLength ? item.description.substring(0, maxLength) + "..." : item.description)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total number of items across all groups
|
||||
* @param groups Grouped items object
|
||||
* @returns Total number of items
|
||||
*/
|
||||
export function getTotalItemCount(groups: GroupedItems): number {
|
||||
return Object.values(groups).reduce((total, group) => total + group.items.length, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an array of unique types from the grouped items
|
||||
* @param groups Grouped items object
|
||||
* @returns Array of type strings
|
||||
*/
|
||||
export function getUniqueTypes(groups: GroupedItems): string[] {
|
||||
const types = Object.keys(groups)
|
||||
types.sort()
|
||||
return types
|
||||
}
|
||||
49
webview-ui/src/components/ui/accordion.tsx
Normal file
49
webview-ui/src/components/ui/accordion.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import * as React from "react"
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||
import { ChevronDownIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Accordion({ ...props }: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
||||
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
|
||||
}
|
||||
|
||||
function AccordionItem({ className, ...props }: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||
return (
|
||||
<AccordionPrimitive.Item
|
||||
data-slot="accordion-item"
|
||||
className={cn("border-b last:border-b-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AccordionTrigger({ className, children, ...props }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
||||
return (
|
||||
<AccordionPrimitive.Header className="flex my-0">
|
||||
<AccordionPrimitive.Trigger
|
||||
data-slot="accordion-trigger"
|
||||
className={cn(
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||
className,
|
||||
)}
|
||||
{...props}>
|
||||
{children}
|
||||
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
)
|
||||
}
|
||||
|
||||
function AccordionContent({ className, children, ...props }: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||
return (
|
||||
<AccordionPrimitive.Content
|
||||
data-slot="accordion-content"
|
||||
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||
{...props}>
|
||||
<div className={cn("pt-0", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
)
|
||||
}
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
|
|
@ -10,6 +10,8 @@ import { Mode, CustomModePrompts, defaultModeSlug, defaultPrompts, ModeConfig }
|
|||
import { CustomSupportPrompts } from "@roo/shared/support-prompt"
|
||||
import { experimentDefault, ExperimentId } from "@roo/shared/experiments"
|
||||
import { TelemetrySetting } from "@roo/shared/TelemetrySetting"
|
||||
import { DEFAULT_MARKETPLACE_SOURCE } from "@roo/services/marketplace/constants"
|
||||
import { MarketplaceSource } from "@roo/services/marketplace/types"
|
||||
|
||||
import { vscode } from "@src/utils/vscode"
|
||||
import { convertTextMateToHljs } from "@src/utils/textMateToHljs"
|
||||
|
|
@ -98,6 +100,7 @@ export interface ExtensionStateContextType extends ExtensionState {
|
|||
setHistoryPreviewCollapsed: (value: boolean) => void
|
||||
autoCondenseContextPercent: number
|
||||
setAutoCondenseContextPercent: (value: number) => void
|
||||
setMarketplaceSources: (value: MarketplaceSource[]) => void
|
||||
}
|
||||
|
||||
export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
|
||||
|
|
@ -171,6 +174,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
showRooIgnoredFiles: true, // Default to showing .rooignore'd files with lock symbol (current behavior).
|
||||
renderContext: "sidebar",
|
||||
maxReadFileLine: 500, // Default max read file line limit
|
||||
marketplaceSources: [DEFAULT_MARKETPLACE_SOURCE],
|
||||
pinnedApiConfigs: {}, // Empty object for pinned API configs
|
||||
terminalZshOhMy: false, // Default Oh My Zsh integration setting
|
||||
terminalZshP10k: false, // Default Powerlevel10k integration setting
|
||||
|
|
@ -356,6 +360,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
setState((prevState) => ({ ...prevState, historyPreviewCollapsed: value })), // Implement the setter
|
||||
setAutoCondenseContextPercent: (value) =>
|
||||
setState((prevState) => ({ ...prevState, autoCondenseContextPercent: value })),
|
||||
setMarketplaceSources: (value) => setState((prevState) => ({ ...prevState, marketplaceSources: value })),
|
||||
}
|
||||
|
||||
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
|
||||
|
|
|
|||
91
webview-ui/src/i18n/locales/ca/marketplace.json
Normal file
91
webview-ui/src/i18n/locales/ca/marketplace.json
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
{
|
||||
"title": "Marketplace",
|
||||
"tabs": {
|
||||
"browse": "Explorar",
|
||||
"sources": "Fonts"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "Cercar elements del marketplace..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Filtrar per tipus:",
|
||||
"all": "Tots els tipus",
|
||||
"mode": "Mode",
|
||||
"mcp": "Servidor MCP",
|
||||
"prompt": "Prompt",
|
||||
"package": "Paquet"
|
||||
},
|
||||
"sort": {
|
||||
"label": "Ordenar per:",
|
||||
"name": "Nom",
|
||||
"author": "Autor",
|
||||
"lastUpdated": "Última actualització"
|
||||
},
|
||||
"tags": {
|
||||
"label": "Filtrar per etiquetes:",
|
||||
"available": "{{count}} disponibles",
|
||||
"clear": "Netejar etiquetes ({{count}})",
|
||||
"placeholder": "Escriu per cercar i seleccionar etiquetes...",
|
||||
"noResults": "No s'han trobat etiquetes coincidents",
|
||||
"selected": "Mostrant elements amb qualsevol de les etiquetes seleccionades ({{count}} seleccionades)",
|
||||
"clickToFilter": "Fes clic a les etiquetes per filtrar elements"
|
||||
}
|
||||
},
|
||||
"type-group": {
|
||||
"match": "Coincidència",
|
||||
"modes": "Modes",
|
||||
"mcps": "Servidors MCP",
|
||||
"prompts": "Prompts",
|
||||
"packages": "Paquets",
|
||||
"generic-type": "{{type}}"
|
||||
},
|
||||
"items": {
|
||||
"empty": {
|
||||
"noItems": "No s'han trobat elements al marketplace",
|
||||
"withFilters": "Prova d'ajustar els filtres",
|
||||
"noSources": "Prova d'afegir una font a la pestanya Fonts"
|
||||
},
|
||||
"count": "S'han trobat {{count}} elements",
|
||||
"components": "{{count}} components",
|
||||
"refresh": {
|
||||
"button": "Actualitzar",
|
||||
"refreshing": "Actualitzant..."
|
||||
},
|
||||
"card": {
|
||||
"by": "per {{author}}",
|
||||
"from": "de {{source}}",
|
||||
"viewSource": "Veure",
|
||||
"viewOnSource": "Veure a {{source}}"
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
"title": "Configurar fonts del Marketplace",
|
||||
"description": "Afegeix repositoris Git que continguin elements del marketplace. Aquests repositoris es descarregaran quan s'explori el marketplace.",
|
||||
"add": {
|
||||
"title": "Afegir nova font",
|
||||
"urlPlaceholder": "URL del repositori Git (p. ex., https://github.com/username/repo)",
|
||||
"urlFormats": "Formats admesos: HTTPS (https://github.com/username/repo), SSH (git@github.com:username/repo.git), o protocol Git (git://github.com/username/repo.git)",
|
||||
"namePlaceholder": "Nom a mostrar (màx. 20 caràcters)",
|
||||
"button": "Afegir font"
|
||||
},
|
||||
"current": {
|
||||
"title": "Fonts actuals",
|
||||
"count": "{{current}}/{{max}} màx.",
|
||||
"empty": "No hi ha fonts configurades. Afegeix una font per començar.",
|
||||
"refresh": "Actualitzar aquesta font",
|
||||
"remove": "Eliminar font"
|
||||
},
|
||||
"errors": {
|
||||
"emptyUrl": "L'URL no pot estar buida",
|
||||
"invalidUrl": "Format d'URL no vàlid",
|
||||
"nonVisibleChars": "L'URL conté caràcters no visibles diferents d'espais",
|
||||
"invalidGitUrl": "L'URL ha de ser una URL vàlida de repositori Git (p. ex., https://github.com/username/repo)",
|
||||
"duplicateUrl": "Aquesta URL ja és a la llista (coincidència insensible a majúscules i espais)",
|
||||
"nameTooLong": "El nom ha de tenir 20 caràcters o menys",
|
||||
"nonVisibleCharsName": "El nom conté caràcters no visibles diferents d'espais",
|
||||
"duplicateName": "Aquest nom ja està en ús (coincidència insensible a majúscules i espais)",
|
||||
"maxSources": "Màxim de {{max}} fonts permeses"
|
||||
}
|
||||
}
|
||||
}
|
||||
91
webview-ui/src/i18n/locales/de/marketplace.json
Normal file
91
webview-ui/src/i18n/locales/de/marketplace.json
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
{
|
||||
"title": "Marketplace",
|
||||
"tabs": {
|
||||
"browse": "Durchsuchen",
|
||||
"sources": "Quellen"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "Marketplace-Einträge durchsuchen..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Nach Typ filtern:",
|
||||
"all": "Alle Typen",
|
||||
"mode": "Modus",
|
||||
"mcp": "MCP-Server",
|
||||
"prompt": "Prompt",
|
||||
"package": "Paket"
|
||||
},
|
||||
"sort": {
|
||||
"label": "Sortieren nach:",
|
||||
"name": "Name",
|
||||
"author": "Autor",
|
||||
"lastUpdated": "Zuletzt aktualisiert"
|
||||
},
|
||||
"tags": {
|
||||
"label": "Nach Tags filtern:",
|
||||
"available": "{{count}} verfügbar",
|
||||
"clear": "Tags löschen ({{count}})",
|
||||
"placeholder": "Tippen Sie, um Tags zu suchen und auszuwählen...",
|
||||
"noResults": "Keine passenden Tags gefunden",
|
||||
"selected": "Zeige Einträge mit beliebigen ausgewählten Tags ({{count}} ausgewählt)",
|
||||
"clickToFilter": "Klicken Sie auf Tags zum Filtern"
|
||||
}
|
||||
},
|
||||
"type-group": {
|
||||
"match": "Treffer",
|
||||
"modes": "Modi",
|
||||
"mcps": "MCP-Server",
|
||||
"prompts": "Prompts",
|
||||
"packages": "Pakete",
|
||||
"generic-type": "{{type}}"
|
||||
},
|
||||
"items": {
|
||||
"empty": {
|
||||
"noItems": "Keine Marketplace-Einträge gefunden",
|
||||
"withFilters": "Versuchen Sie, die Filter anzupassen",
|
||||
"noSources": "Versuchen Sie, eine Quelle im Quellen-Tab hinzuzufügen"
|
||||
},
|
||||
"count": "{{count}} Einträge gefunden",
|
||||
"components": "{{count}} Komponenten",
|
||||
"refresh": {
|
||||
"button": "Aktualisieren",
|
||||
"refreshing": "Aktualisiere..."
|
||||
},
|
||||
"card": {
|
||||
"by": "von {{author}}",
|
||||
"from": "von {{source}}",
|
||||
"viewSource": "Ansehen",
|
||||
"viewOnSource": "Auf {{source}} ansehen"
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
"title": "Marketplace-Quellen konfigurieren",
|
||||
"description": "Fügen Sie Git-Repositories hinzu, die Marketplace-Einträge enthalten. Diese Repositories werden beim Durchsuchen des Marketplaces abgerufen.",
|
||||
"add": {
|
||||
"title": "Neue Quelle hinzufügen",
|
||||
"urlPlaceholder": "Git-Repository-URL (z.B. https://github.com/username/repo)",
|
||||
"urlFormats": "Unterstützte Formate: HTTPS (https://github.com/username/repo), SSH (git@github.com:username/repo.git) oder Git-Protokoll (git://github.com/username/repo.git)",
|
||||
"namePlaceholder": "Anzeigename (max. 20 Zeichen)",
|
||||
"button": "Quelle hinzufügen"
|
||||
},
|
||||
"current": {
|
||||
"title": "Aktuelle Quellen",
|
||||
"count": "{{current}}/{{max}} max.",
|
||||
"empty": "Keine Quellen konfiguriert. Fügen Sie eine Quelle hinzu, um zu beginnen.",
|
||||
"refresh": "Diese Quelle aktualisieren",
|
||||
"remove": "Quelle entfernen"
|
||||
},
|
||||
"errors": {
|
||||
"emptyUrl": "URL darf nicht leer sein",
|
||||
"invalidUrl": "Ungültiges URL-Format",
|
||||
"nonVisibleChars": "URL enthält unsichtbare Zeichen außer Leerzeichen",
|
||||
"invalidGitUrl": "URL muss eine gültige Git-Repository-URL sein (z.B. https://github.com/username/repo)",
|
||||
"duplicateUrl": "Diese URL ist bereits in der Liste (Groß-/Kleinschreibung und Leerzeichen werden ignoriert)",
|
||||
"nameTooLong": "Name darf maximal 20 Zeichen lang sein",
|
||||
"nonVisibleCharsName": "Name enthält unsichtbare Zeichen außer Leerzeichen",
|
||||
"duplicateName": "Dieser Name wird bereits verwendet (Groß-/Kleinschreibung und Leerzeichen werden ignoriert)",
|
||||
"maxSources": "Maximal {{max}} Quellen erlaubt"
|
||||
}
|
||||
}
|
||||
}
|
||||
94
webview-ui/src/i18n/locales/en/marketplace.json
Normal file
94
webview-ui/src/i18n/locales/en/marketplace.json
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
{
|
||||
"title": "Marketplace",
|
||||
"tabs": {
|
||||
"browse": "Browse",
|
||||
"sources": "Sources"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "Search marketplace items..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Filter by type:",
|
||||
"all": "All types",
|
||||
"mode": "Mode",
|
||||
"mcp server": "MCP Server",
|
||||
"prompt": "Prompt",
|
||||
"package": "Package"
|
||||
},
|
||||
"sort": {
|
||||
"label": "Sort by:",
|
||||
"name": "Name",
|
||||
"author": "Author",
|
||||
"lastUpdated": "Last Updated"
|
||||
},
|
||||
"tags": {
|
||||
"label": "Filter by tags:",
|
||||
"available": "{{count}} available",
|
||||
"clear": "Clear tags ({{count}})",
|
||||
"placeholder": "Type to search and select tags...",
|
||||
"noResults": "No matching tags found",
|
||||
"selected": "Showing items with any of the selected tags ({{count}} selected)",
|
||||
"clickToFilter": "Click tags to filter items"
|
||||
}
|
||||
},
|
||||
"type-group": {
|
||||
"match": "Match",
|
||||
"modes": "Modes",
|
||||
"mcps": "MCP Servers",
|
||||
"prompts": "Prompts",
|
||||
"packages": "Packages",
|
||||
"generic-type": "{{type}}"
|
||||
},
|
||||
"items": {
|
||||
"empty": {
|
||||
"noItems": "No marketplace items found",
|
||||
"withFilters": "Try adjusting your filters",
|
||||
"noSources": "Try adding a source in the Sources tab"
|
||||
},
|
||||
"count": "{{count}} items found",
|
||||
"components": "{{count}} components",
|
||||
"refresh": {
|
||||
"button": "Refresh",
|
||||
"refreshing": "Refreshing..."
|
||||
},
|
||||
"card": {
|
||||
"by": "by {{author}}",
|
||||
"from": "from {{source}}",
|
||||
"installProject": "Install (Project)",
|
||||
"installGlobal": "Install (Global)",
|
||||
"removeProject": "Remove (Project)",
|
||||
"removeGlobal": "Remove (Global)",
|
||||
"viewSource": "View",
|
||||
"viewOnSource": "View on {{source}}"
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
"title": "Configure Marketplace Sources",
|
||||
"description": "Add Git repositories that contain marketplace items. These repositories will be fetched when browsing the marketplace.",
|
||||
"add": {
|
||||
"title": "Add New Source",
|
||||
"urlPlaceholder": "Git repository URL (e.g., https://github.com/username/repo)",
|
||||
"urlFormats": "Supported formats: HTTPS (https://github.com/username/repo), SSH (git@github.com:username/repo.git), or Git protocol (git://github.com/username/repo.git)",
|
||||
"namePlaceholder": "Display name (max 20 chars)",
|
||||
"button": "Add Source"
|
||||
},
|
||||
"current": {
|
||||
"title": "Current Sources",
|
||||
"empty": "No sources configured. Add a source to get started.",
|
||||
"refresh": "Refresh this source",
|
||||
"remove": "Remove source"
|
||||
},
|
||||
"errors": {
|
||||
"emptyUrl": "URL cannot be empty",
|
||||
"invalidUrl": "Invalid URL format",
|
||||
"nonVisibleChars": "URL contains non-visible characters other than spaces",
|
||||
"invalidGitUrl": "URL must be a valid Git repository URL (e.g., https://github.com/username/repo)",
|
||||
"duplicateUrl": "This URL is already in the list (case and whitespace insensitive match)",
|
||||
"nameTooLong": "Name must be 20 characters or less",
|
||||
"nonVisibleCharsName": "Name contains non-visible characters other than spaces",
|
||||
"duplicateName": "This name is already in use (case and whitespace insensitive match)",
|
||||
"maxSources": "Maximum of {{max}} sources allowed"
|
||||
}
|
||||
}
|
||||
}
|
||||
91
webview-ui/src/i18n/locales/es/marketplace.json
Normal file
91
webview-ui/src/i18n/locales/es/marketplace.json
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
{
|
||||
"title": "Marketplace",
|
||||
"tabs": {
|
||||
"browse": "Explorar",
|
||||
"sources": "Fuentes"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "Buscar elementos del marketplace..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Filtrar por tipo:",
|
||||
"all": "Todos los tipos",
|
||||
"mode": "Modo",
|
||||
"mcp": "Servidor MCP",
|
||||
"prompt": "Prompt",
|
||||
"package": "Paquete"
|
||||
},
|
||||
"sort": {
|
||||
"label": "Ordenar por:",
|
||||
"name": "Nombre",
|
||||
"author": "Autor",
|
||||
"lastUpdated": "Última actualización"
|
||||
},
|
||||
"tags": {
|
||||
"label": "Filtrar por etiquetas:",
|
||||
"available": "{{count}} disponibles",
|
||||
"clear": "Limpiar etiquetas ({{count}})",
|
||||
"placeholder": "Escriba para buscar y seleccionar etiquetas...",
|
||||
"noResults": "No se encontraron etiquetas coincidentes",
|
||||
"selected": "Mostrando elementos con cualquiera de las etiquetas seleccionadas ({{count}} seleccionadas)",
|
||||
"clickToFilter": "Haga clic en las etiquetas para filtrar elementos"
|
||||
}
|
||||
},
|
||||
"type-group": {
|
||||
"match": "Coincidencia",
|
||||
"modes": "Modos",
|
||||
"mcps": "Servidores MCP",
|
||||
"prompts": "Prompts",
|
||||
"packages": "Paquetes",
|
||||
"generic-type": "{{type}}"
|
||||
},
|
||||
"items": {
|
||||
"empty": {
|
||||
"noItems": "No se encontraron elementos en el marketplace",
|
||||
"withFilters": "Intente ajustar los filtros",
|
||||
"noSources": "Intente agregar una fuente en la pestaña Fuentes"
|
||||
},
|
||||
"count": "{{count}} elementos encontrados",
|
||||
"components": "{{count}} componentes",
|
||||
"refresh": {
|
||||
"button": "Actualizar",
|
||||
"refreshing": "Actualizando..."
|
||||
},
|
||||
"card": {
|
||||
"by": "por {{author}}",
|
||||
"from": "de {{source}}",
|
||||
"viewSource": "Ver",
|
||||
"viewOnSource": "Ver en {{source}}"
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
"title": "Configurar fuentes del Marketplace",
|
||||
"description": "Agregue repositorios Git que contengan elementos del marketplace. Estos repositorios se descargarán al explorar el marketplace.",
|
||||
"add": {
|
||||
"title": "Agregar nueva fuente",
|
||||
"urlPlaceholder": "URL del repositorio Git (ej., https://github.com/username/repo)",
|
||||
"urlFormats": "Formatos admitidos: HTTPS (https://github.com/username/repo), SSH (git@github.com:username/repo.git) o protocolo Git (git://github.com/username/repo.git)",
|
||||
"namePlaceholder": "Nombre para mostrar (máx. 20 caracteres)",
|
||||
"button": "Agregar fuente"
|
||||
},
|
||||
"current": {
|
||||
"title": "Fuentes actuales",
|
||||
"count": "{{current}}/{{max}} máx.",
|
||||
"empty": "No hay fuentes configuradas. Agregue una fuente para comenzar.",
|
||||
"refresh": "Actualizar esta fuente",
|
||||
"remove": "Eliminar fuente"
|
||||
},
|
||||
"errors": {
|
||||
"emptyUrl": "La URL no puede estar vacía",
|
||||
"invalidUrl": "Formato de URL no válido",
|
||||
"nonVisibleChars": "La URL contiene caracteres no visibles distintos de espacios",
|
||||
"invalidGitUrl": "La URL debe ser una URL válida de repositorio Git (ej., https://github.com/username/repo)",
|
||||
"duplicateUrl": "Esta URL ya está en la lista (coincidencia insensible a mayúsculas y espacios)",
|
||||
"nameTooLong": "El nombre debe tener 20 caracteres o menos",
|
||||
"nonVisibleCharsName": "El nombre contiene caracteres no visibles distintos de espacios",
|
||||
"duplicateName": "Este nombre ya está en uso (coincidencia insensible a mayúsculas y espacios)",
|
||||
"maxSources": "Máximo de {{max}} fuentes permitidas"
|
||||
}
|
||||
}
|
||||
}
|
||||
91
webview-ui/src/i18n/locales/fr/marketplace.json
Normal file
91
webview-ui/src/i18n/locales/fr/marketplace.json
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
{
|
||||
"title": "Marketplace",
|
||||
"tabs": {
|
||||
"browse": "Parcourir",
|
||||
"sources": "Sources"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "Rechercher des éléments du marketplace..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Filtrer par type :",
|
||||
"all": "Tous les types",
|
||||
"mode": "Mode",
|
||||
"mcp": "Serveur MCP",
|
||||
"prompt": "Prompt",
|
||||
"package": "Paquet"
|
||||
},
|
||||
"sort": {
|
||||
"label": "Trier par :",
|
||||
"name": "Nom",
|
||||
"author": "Auteur",
|
||||
"lastUpdated": "Dernière mise à jour"
|
||||
},
|
||||
"tags": {
|
||||
"label": "Filtrer par tags :",
|
||||
"available": "{{count}} disponibles",
|
||||
"clear": "Effacer les tags ({{count}})",
|
||||
"placeholder": "Tapez pour rechercher et sélectionner des tags...",
|
||||
"noResults": "Aucun tag correspondant trouvé",
|
||||
"selected": "Affichage des éléments avec l'un des tags sélectionnés ({{count}} sélectionnés)",
|
||||
"clickToFilter": "Cliquez sur les tags pour filtrer les éléments"
|
||||
}
|
||||
},
|
||||
"type-group": {
|
||||
"match": "Correspondance",
|
||||
"modes": "Modes",
|
||||
"mcps": "Serveurs MCP",
|
||||
"prompts": "Prompts",
|
||||
"packages": "Paquets",
|
||||
"generic-type": "{{type}}"
|
||||
},
|
||||
"items": {
|
||||
"empty": {
|
||||
"noItems": "Aucun élément trouvé dans le marketplace",
|
||||
"withFilters": "Essayez d'ajuster les filtres",
|
||||
"noSources": "Essayez d'ajouter une source dans l'onglet Sources"
|
||||
},
|
||||
"count": "{{count}} éléments trouvés",
|
||||
"components": "{{count}} composants",
|
||||
"refresh": {
|
||||
"button": "Actualiser",
|
||||
"refreshing": "Actualisation..."
|
||||
},
|
||||
"card": {
|
||||
"by": "par {{author}}",
|
||||
"from": "de {{source}}",
|
||||
"viewSource": "Voir",
|
||||
"viewOnSource": "Voir sur {{source}}"
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
"title": "Configurer les sources du Marketplace",
|
||||
"description": "Ajoutez des dépôts Git contenant des éléments du marketplace. Ces dépôts seront récupérés lors de la navigation dans le marketplace.",
|
||||
"add": {
|
||||
"title": "Ajouter une nouvelle source",
|
||||
"urlPlaceholder": "URL du dépôt Git (ex., https://github.com/username/repo)",
|
||||
"urlFormats": "Formats pris en charge : HTTPS (https://github.com/username/repo), SSH (git@github.com:username/repo.git) ou protocole Git (git://github.com/username/repo.git)",
|
||||
"namePlaceholder": "Nom d'affichage (max. 20 caractères)",
|
||||
"button": "Ajouter la source"
|
||||
},
|
||||
"current": {
|
||||
"title": "Sources actuelles",
|
||||
"count": "{{current}}/{{max}} max.",
|
||||
"empty": "Aucune source configurée. Ajoutez une source pour commencer.",
|
||||
"refresh": "Actualiser cette source",
|
||||
"remove": "Supprimer la source"
|
||||
},
|
||||
"errors": {
|
||||
"emptyUrl": "L'URL ne peut pas être vide",
|
||||
"invalidUrl": "Format d'URL invalide",
|
||||
"nonVisibleChars": "L'URL contient des caractères non visibles autres que des espaces",
|
||||
"invalidGitUrl": "L'URL doit être une URL de dépôt Git valide (ex., https://github.com/username/repo)",
|
||||
"duplicateUrl": "Cette URL est déjà dans la liste (correspondance insensible à la casse et aux espaces)",
|
||||
"nameTooLong": "Le nom doit faire 20 caractères ou moins",
|
||||
"nonVisibleCharsName": "Le nom contient des caractères non visibles autres que des espaces",
|
||||
"duplicateName": "Ce nom est déjà utilisé (correspondance insensible à la casse et aux espaces)",
|
||||
"maxSources": "Maximum de {{max}} sources autorisées"
|
||||
}
|
||||
}
|
||||
}
|
||||
91
webview-ui/src/i18n/locales/hi/marketplace.json
Normal file
91
webview-ui/src/i18n/locales/hi/marketplace.json
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
{
|
||||
"title": "मार्केटप्लेस",
|
||||
"tabs": {
|
||||
"browse": "ब्राउज़ करें",
|
||||
"sources": "स्रोत"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "मार्केटप्लेस आइटम खोजें..."
|
||||
},
|
||||
"type": {
|
||||
"label": "प्रकार से फ़िल्टर करें:",
|
||||
"all": "सभी प्रकार",
|
||||
"mode": "मोड",
|
||||
"mcp": "एमसीपी सर्वर",
|
||||
"prompt": "प्रॉम्प्ट",
|
||||
"package": "पैकेज"
|
||||
},
|
||||
"sort": {
|
||||
"label": "इसके अनुसार क्रमबद्ध करें:",
|
||||
"name": "नाम",
|
||||
"author": "लेखक",
|
||||
"lastUpdated": "अंतिम अपडेट"
|
||||
},
|
||||
"tags": {
|
||||
"label": "टैग से फ़िल्टर करें:",
|
||||
"available": "{{count}} उपलब्ध",
|
||||
"clear": "टैग साफ़ करें ({{count}})",
|
||||
"placeholder": "टैग खोजने और चुनने के लिए टाइप करें...",
|
||||
"noResults": "कोई मिलान टैग नहीं मिला",
|
||||
"selected": "चयनित टैग वाले आइटम दिखा रहे हैं ({{count}} चयनित)",
|
||||
"clickToFilter": "आइटम फ़िल्टर करने के लिए टैग पर क्लिक करें"
|
||||
}
|
||||
},
|
||||
"type-group": {
|
||||
"match": "मिलान",
|
||||
"modes": "मोड्स",
|
||||
"mcps": "एमसीपी सर्वर",
|
||||
"prompts": "प्रॉम्प्ट्स",
|
||||
"packages": "पैकेज",
|
||||
"generic-type": "{{type}}"
|
||||
},
|
||||
"items": {
|
||||
"empty": {
|
||||
"noItems": "कोई मार्केटप्लेस आइटम नहीं मिला",
|
||||
"withFilters": "फ़िल्टर समायोजित करने का प्रयास करें",
|
||||
"noSources": "स्रोत टैब में एक स्रोत जोड़ने का प्रयास करें"
|
||||
},
|
||||
"count": "{{count}} आइटम मिले",
|
||||
"components": "{{count}} कंपोनेंट",
|
||||
"refresh": {
|
||||
"button": "रीफ्रेश करें",
|
||||
"refreshing": "रीफ्रेश हो रहा है..."
|
||||
},
|
||||
"card": {
|
||||
"by": "{{author}} द्वारा",
|
||||
"from": "{{source}} से",
|
||||
"viewSource": "देखें",
|
||||
"viewOnSource": "{{source}} पर देखें"
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
"title": "मार्केटप्लेस स्रोत कॉन्फ़िगर करें",
|
||||
"description": "मार्केटप्लेस आइटम वाले Git रिपॉजिटरी जोड़ें। मार्केटप्लेस ब्राउज़ करते समय इन रिपॉजिटरी को फ़ेच किया जाएगा।",
|
||||
"add": {
|
||||
"title": "नया स्रोत जोड़ें",
|
||||
"urlPlaceholder": "Git रिपॉजिटरी URL (उदा., https://github.com/username/repo)",
|
||||
"urlFormats": "समर्थित प्रारूप: HTTPS (https://github.com/username/repo), SSH (git@github.com:username/repo.git), या Git प्रोटोकॉल (git://github.com/username/repo.git)",
|
||||
"namePlaceholder": "प्रदर्शन नाम (अधिकतम 20 अक्षर)",
|
||||
"button": "स्रोत जोड़ें"
|
||||
},
|
||||
"current": {
|
||||
"title": "वर्तमान स्रोत",
|
||||
"count": "{{current}}/{{max}} अधिकतम",
|
||||
"empty": "कोई स्रोत कॉन्फ़िगर नहीं किया गया। शुरू करने के लिए एक स्रोत जोड़ें।",
|
||||
"refresh": "इस स्रोत को रीफ्रेश करें",
|
||||
"remove": "स्रोत हटाएं"
|
||||
},
|
||||
"errors": {
|
||||
"emptyUrl": "URL खाली नहीं हो सकता",
|
||||
"invalidUrl": "अमान्य URL प्रारूप",
|
||||
"nonVisibleChars": "URL में स्पेस के अलावा अदृश्य वर्ण हैं",
|
||||
"invalidGitUrl": "URL एक वैध Git रिपॉजिटरी URL होना चाहिए (उदा., https://github.com/username/repo)",
|
||||
"duplicateUrl": "यह URL पहले से सूची में है (केस और व्हाइटस्पेस असंवेदनशील मिलान)",
|
||||
"nameTooLong": "नाम 20 अक्षरों या उससे कम का होना चाहिए",
|
||||
"nonVisibleCharsName": "नाम में स्पेस के अलावा अदृश्य वर्ण हैं",
|
||||
"duplicateName": "यह नाम पहले से उपयोग में है (केस और व्हाइटस्पेस असंवेदनशील मिलान)",
|
||||
"maxSources": "अधिकतम {{max}} स्रोत की अनुमति है"
|
||||
}
|
||||
}
|
||||
}
|
||||
91
webview-ui/src/i18n/locales/it/marketplace.json
Normal file
91
webview-ui/src/i18n/locales/it/marketplace.json
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
{
|
||||
"title": "Marketplace",
|
||||
"tabs": {
|
||||
"browse": "Sfoglia",
|
||||
"sources": "Fonti"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "Cerca elementi del marketplace..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Filtra per tipo:",
|
||||
"all": "Tutti i tipi",
|
||||
"mode": "Modalità",
|
||||
"mcp": "Server MCP",
|
||||
"prompt": "Prompt",
|
||||
"package": "Pacchetto"
|
||||
},
|
||||
"sort": {
|
||||
"label": "Ordina per:",
|
||||
"name": "Nome",
|
||||
"author": "Autore",
|
||||
"lastUpdated": "Ultimo aggiornamento"
|
||||
},
|
||||
"tags": {
|
||||
"label": "Filtra per tag:",
|
||||
"available": "{{count}} disponibili",
|
||||
"clear": "Cancella tag ({{count}})",
|
||||
"placeholder": "Digita per cercare e selezionare i tag...",
|
||||
"noResults": "Nessun tag corrispondente trovato",
|
||||
"selected": "Mostrando elementi con uno qualsiasi dei tag selezionati ({{count}} selezionati)",
|
||||
"clickToFilter": "Clicca sui tag per filtrare gli elementi"
|
||||
}
|
||||
},
|
||||
"type-group": {
|
||||
"match": "Corrispondenza",
|
||||
"modes": "Modalità",
|
||||
"mcps": "Server MCP",
|
||||
"prompts": "Prompt",
|
||||
"packages": "Pacchetti",
|
||||
"generic-type": "{{type}}"
|
||||
},
|
||||
"items": {
|
||||
"empty": {
|
||||
"noItems": "Nessun elemento trovato nel marketplace",
|
||||
"withFilters": "Prova a modificare i filtri",
|
||||
"noSources": "Prova ad aggiungere una fonte nella scheda Fonti"
|
||||
},
|
||||
"count": "{{count}} elementi trovati",
|
||||
"components": "{{count}} componenti",
|
||||
"refresh": {
|
||||
"button": "Aggiorna",
|
||||
"refreshing": "Aggiornamento in corso..."
|
||||
},
|
||||
"card": {
|
||||
"by": "di {{author}}",
|
||||
"from": "da {{source}}",
|
||||
"viewSource": "Visualizza",
|
||||
"viewOnSource": "Visualizza su {{source}}"
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
"title": "Configura fonti del Marketplace",
|
||||
"description": "Aggiungi repository Git che contengono elementi del marketplace. Questi repository verranno recuperati durante la navigazione nel marketplace.",
|
||||
"add": {
|
||||
"title": "Aggiungi nuova fonte",
|
||||
"urlPlaceholder": "URL del repository Git (es., https://github.com/username/repo)",
|
||||
"urlFormats": "Formati supportati: HTTPS (https://github.com/username/repo), SSH (git@github.com:username/repo.git) o protocollo Git (git://github.com/username/repo.git)",
|
||||
"namePlaceholder": "Nome visualizzato (max 20 caratteri)",
|
||||
"button": "Aggiungi fonte"
|
||||
},
|
||||
"current": {
|
||||
"title": "Fonti attuali",
|
||||
"count": "{{current}}/{{max}} max",
|
||||
"empty": "Nessuna fonte configurata. Aggiungi una fonte per iniziare.",
|
||||
"refresh": "Aggiorna questa fonte",
|
||||
"remove": "Rimuovi fonte"
|
||||
},
|
||||
"errors": {
|
||||
"emptyUrl": "L'URL non può essere vuoto",
|
||||
"invalidUrl": "Formato URL non valido",
|
||||
"nonVisibleChars": "L'URL contiene caratteri non visibili diversi dagli spazi",
|
||||
"invalidGitUrl": "L'URL deve essere un URL valido di un repository Git (es., https://github.com/username/repo)",
|
||||
"duplicateUrl": "Questo URL è già presente nell'elenco (corrispondenza insensibile a maiuscole/minuscole e spazi)",
|
||||
"nameTooLong": "Il nome deve essere di 20 caratteri o meno",
|
||||
"nonVisibleCharsName": "Il nome contiene caratteri non visibili diversi dagli spazi",
|
||||
"duplicateName": "Questo nome è già in uso (corrispondenza insensibile a maiuscole/minuscole e spazi)",
|
||||
"maxSources": "Massimo {{max}} fonti consentite"
|
||||
}
|
||||
}
|
||||
}
|
||||
91
webview-ui/src/i18n/locales/ja/marketplace.json
Normal file
91
webview-ui/src/i18n/locales/ja/marketplace.json
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
{
|
||||
"title": "マーケットプレイス",
|
||||
"tabs": {
|
||||
"browse": "ブラウズ",
|
||||
"sources": "ソース"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "マーケットプレイスのアイテムを検索..."
|
||||
},
|
||||
"type": {
|
||||
"label": "タイプでフィルター:",
|
||||
"all": "すべてのタイプ",
|
||||
"mode": "モード",
|
||||
"mcp": "MCPサーバー",
|
||||
"prompt": "プロンプト",
|
||||
"package": "パッケージ"
|
||||
},
|
||||
"sort": {
|
||||
"label": "並び替え:",
|
||||
"name": "名前",
|
||||
"author": "作者",
|
||||
"lastUpdated": "最終更新"
|
||||
},
|
||||
"tags": {
|
||||
"label": "タグでフィルター:",
|
||||
"available": "{{count}}個利用可能",
|
||||
"clear": "タグをクリア ({{count}})",
|
||||
"placeholder": "タグを検索して選択...",
|
||||
"noResults": "一致するタグが見つかりません",
|
||||
"selected": "選択したタグのいずれかを含むアイテムを表示中 ({{count}}個選択)",
|
||||
"clickToFilter": "タグをクリックしてアイテムをフィルター"
|
||||
}
|
||||
},
|
||||
"type-group": {
|
||||
"match": "一致",
|
||||
"modes": "モード",
|
||||
"mcps": "MCPサーバー",
|
||||
"prompts": "プロンプト",
|
||||
"packages": "パッケージ",
|
||||
"generic-type": "{{type}}"
|
||||
},
|
||||
"items": {
|
||||
"empty": {
|
||||
"noItems": "マーケットプレイスのアイテムが見つかりません",
|
||||
"withFilters": "フィルターを調整してみてください",
|
||||
"noSources": "ソースタブでソースを追加してみてください"
|
||||
},
|
||||
"count": "{{count}}個のアイテムが見つかりました",
|
||||
"components": "{{count}}個のコンポーネント",
|
||||
"refresh": {
|
||||
"button": "更新",
|
||||
"refreshing": "更新中..."
|
||||
},
|
||||
"card": {
|
||||
"by": "作者: {{author}}",
|
||||
"from": "ソース: {{source}}",
|
||||
"viewSource": "表示",
|
||||
"viewOnSource": "{{source}}で表示"
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
"title": "マーケットプレイスのソースを設定",
|
||||
"description": "マーケットプレイスのアイテムを含むGitリポジトリを追加します。これらのリポジトリはマーケットプレイスの閲覧時に取得されます。",
|
||||
"add": {
|
||||
"title": "新しいソースを追加",
|
||||
"urlPlaceholder": "GitリポジトリのURL (例: https://github.com/username/repo)",
|
||||
"urlFormats": "サポートされている形式: HTTPS (https://github.com/username/repo)、SSH (git@github.com:username/repo.git)、またはGitプロトコル (git://github.com/username/repo.git)",
|
||||
"namePlaceholder": "表示名 (最大20文字)",
|
||||
"button": "ソースを追加"
|
||||
},
|
||||
"current": {
|
||||
"title": "現在のソース",
|
||||
"count": "{{current}}/{{max}}個まで",
|
||||
"empty": "ソースが設定されていません。ソースを追加して始めましょう。",
|
||||
"refresh": "このソースを更新",
|
||||
"remove": "ソースを削除"
|
||||
},
|
||||
"errors": {
|
||||
"emptyUrl": "URLを入力してください",
|
||||
"invalidUrl": "無効なURL形式です",
|
||||
"nonVisibleChars": "URLにスペース以外の不可視文字が含まれています",
|
||||
"invalidGitUrl": "URLは有効なGitリポジトリのURLである必要があります (例: https://github.com/username/repo)",
|
||||
"duplicateUrl": "このURLは既にリストに存在します (大文字小文字とスペースを区別しない一致)",
|
||||
"nameTooLong": "名前は20文字以内である必要があります",
|
||||
"nonVisibleCharsName": "名前にスペース以外の不可視文字が含まれています",
|
||||
"duplicateName": "この名前は既に使用されています (大文字小文字とスペースを区別しない一致)",
|
||||
"maxSources": "最大{{max}}個のソースまで追加できます"
|
||||
}
|
||||
}
|
||||
}
|
||||
91
webview-ui/src/i18n/locales/ko/marketplace.json
Normal file
91
webview-ui/src/i18n/locales/ko/marketplace.json
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
{
|
||||
"title": "마켓플레이스",
|
||||
"tabs": {
|
||||
"browse": "찾아보기",
|
||||
"sources": "소스"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "마켓플레이스 항목 검색..."
|
||||
},
|
||||
"type": {
|
||||
"label": "유형별 필터:",
|
||||
"all": "모든 유형",
|
||||
"mode": "모드",
|
||||
"mcp": "MCP 서버",
|
||||
"prompt": "프롬프트",
|
||||
"package": "패키지"
|
||||
},
|
||||
"sort": {
|
||||
"label": "정렬 기준:",
|
||||
"name": "이름",
|
||||
"author": "작성자",
|
||||
"lastUpdated": "최근 업데이트"
|
||||
},
|
||||
"tags": {
|
||||
"label": "태그별 필터:",
|
||||
"available": "{{count}}개 사용 가능",
|
||||
"clear": "태그 지우기 ({{count}})",
|
||||
"placeholder": "태그 검색 및 선택...",
|
||||
"noResults": "일치하는 태그가 없습니다",
|
||||
"selected": "선택한 태그가 있는 항목 표시 중 ({{count}}개 선택됨)",
|
||||
"clickToFilter": "태그를 클릭하여 항목 필터링"
|
||||
}
|
||||
},
|
||||
"type-group": {
|
||||
"match": "일치",
|
||||
"modes": "모드",
|
||||
"mcps": "MCP 서버",
|
||||
"prompts": "프롬프트",
|
||||
"packages": "패키지",
|
||||
"generic-type": "{{type}}"
|
||||
},
|
||||
"items": {
|
||||
"empty": {
|
||||
"noItems": "마켓플레이스 항목을 찾을 수 없습니다",
|
||||
"withFilters": "필터를 조정해 보세요",
|
||||
"noSources": "소스 탭에서 소스를 추가해 보세요"
|
||||
},
|
||||
"count": "{{count}}개 항목 찾음",
|
||||
"components": "{{count}}개 구성 요소",
|
||||
"refresh": {
|
||||
"button": "새로 고침",
|
||||
"refreshing": "새로 고치는 중..."
|
||||
},
|
||||
"card": {
|
||||
"by": "작성자: {{author}}",
|
||||
"from": "출처: {{source}}",
|
||||
"viewSource": "보기",
|
||||
"viewOnSource": "{{source}}에서 보기"
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
"title": "마켓플레이스 소스 구성",
|
||||
"description": "마켓플레이스 항목이 포함된 Git 저장소를 추가합니다. 마켓플레이스를 탐색할 때 이러한 저장소를 가져옵니다.",
|
||||
"add": {
|
||||
"title": "새 소스 추가",
|
||||
"urlPlaceholder": "Git 저장소 URL (예: https://github.com/username/repo)",
|
||||
"urlFormats": "지원되는 형식: HTTPS (https://github.com/username/repo), SSH (git@github.com:username/repo.git) 또는 Git 프로토콜 (git://github.com/username/repo.git)",
|
||||
"namePlaceholder": "표시 이름 (최대 20자)",
|
||||
"button": "소스 추가"
|
||||
},
|
||||
"current": {
|
||||
"title": "현재 소스",
|
||||
"count": "{{current}}/{{max}} 최대",
|
||||
"empty": "구성된 소스가 없습니다. 소스를 추가하여 시작하세요.",
|
||||
"refresh": "이 소스 새로 고침",
|
||||
"remove": "소스 제거"
|
||||
},
|
||||
"errors": {
|
||||
"emptyUrl": "URL을 입력해야 합니다",
|
||||
"invalidUrl": "잘못된 URL 형식",
|
||||
"nonVisibleChars": "URL에 공백 이외의 보이지 않는 문자가 포함되어 있습니다",
|
||||
"invalidGitUrl": "URL은 유효한 Git 저장소 URL이어야 합니다 (예: https://github.com/username/repo)",
|
||||
"duplicateUrl": "이 URL은 이미 목록에 있습니다 (대소문자 및 공백 구분 없이 일치)",
|
||||
"nameTooLong": "이름은 20자 이하여야 합니다",
|
||||
"nonVisibleCharsName": "이름에 공백 이외의 보이지 않는 문자가 포함되어 있습니다",
|
||||
"duplicateName": "이 이름은 이미 사용 중입니다 (대소문자 및 공백 구분 없이 일치)",
|
||||
"maxSources": "최대 {{max}}개의 소스만 허용됩니다"
|
||||
}
|
||||
}
|
||||
}
|
||||
91
webview-ui/src/i18n/locales/pl/marketplace.json
Normal file
91
webview-ui/src/i18n/locales/pl/marketplace.json
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
{
|
||||
"title": "Marketplace",
|
||||
"tabs": {
|
||||
"browse": "Przeglądaj",
|
||||
"sources": "Źródła"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "Szukaj elementów marketplace..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Filtruj według typu:",
|
||||
"all": "Wszystkie typy",
|
||||
"mode": "Tryb",
|
||||
"mcp": "Serwer MCP",
|
||||
"prompt": "Prompt",
|
||||
"package": "Pakiet"
|
||||
},
|
||||
"sort": {
|
||||
"label": "Sortuj według:",
|
||||
"name": "Nazwa",
|
||||
"author": "Autor",
|
||||
"lastUpdated": "Ostatnia aktualizacja"
|
||||
},
|
||||
"tags": {
|
||||
"label": "Filtruj według tagów:",
|
||||
"available": "{{count}} dostępnych",
|
||||
"clear": "Wyczyść tagi ({{count}})",
|
||||
"placeholder": "Wpisz, aby wyszukać i wybrać tagi...",
|
||||
"noResults": "Nie znaleziono pasujących tagów",
|
||||
"selected": "Pokazywanie elementów z wybranymi tagami (wybrano {{count}})",
|
||||
"clickToFilter": "Kliknij tagi, aby filtrować elementy"
|
||||
}
|
||||
},
|
||||
"type-group": {
|
||||
"match": "Dopasowanie",
|
||||
"modes": "Tryby",
|
||||
"mcps": "Serwery MCP",
|
||||
"prompts": "Prompty",
|
||||
"packages": "Pakiety",
|
||||
"generic-type": "{{type}}"
|
||||
},
|
||||
"items": {
|
||||
"empty": {
|
||||
"noItems": "Nie znaleziono elementów marketplace",
|
||||
"withFilters": "Spróbuj dostosować filtry",
|
||||
"noSources": "Spróbuj dodać źródło w zakładce Źródła"
|
||||
},
|
||||
"count": "Znaleziono {{count}} elementów",
|
||||
"components": "{{count}} komponentów",
|
||||
"refresh": {
|
||||
"button": "Odśwież",
|
||||
"refreshing": "Odświeżanie..."
|
||||
},
|
||||
"card": {
|
||||
"by": "autor: {{author}}",
|
||||
"from": "z {{source}}",
|
||||
"viewSource": "Zobacz",
|
||||
"viewOnSource": "Zobacz na {{source}}"
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
"title": "Konfiguruj źródła Marketplace",
|
||||
"description": "Dodaj repozytoria Git zawierające elementy marketplace. Te repozytoria będą pobierane podczas przeglądania marketplace.",
|
||||
"add": {
|
||||
"title": "Dodaj nowe źródło",
|
||||
"urlPlaceholder": "URL repozytorium Git (np. https://github.com/username/repo)",
|
||||
"urlFormats": "Obsługiwane formaty: HTTPS (https://github.com/username/repo), SSH (git@github.com:username/repo.git) lub protokół Git (git://github.com/username/repo.git)",
|
||||
"namePlaceholder": "Nazwa wyświetlana (maks. 20 znaków)",
|
||||
"button": "Dodaj źródło"
|
||||
},
|
||||
"current": {
|
||||
"title": "Obecne źródła",
|
||||
"count": "{{current}}/{{max}} maks.",
|
||||
"empty": "Brak skonfigurowanych źródeł. Dodaj źródło, aby rozpocząć.",
|
||||
"refresh": "Odśwież to źródło",
|
||||
"remove": "Usuń źródło"
|
||||
},
|
||||
"errors": {
|
||||
"emptyUrl": "URL nie może być pusty",
|
||||
"invalidUrl": "Nieprawidłowy format URL",
|
||||
"nonVisibleChars": "URL zawiera niewidoczne znaki inne niż spacje",
|
||||
"invalidGitUrl": "URL musi być prawidłowym URL-em repozytorium Git (np. https://github.com/username/repo)",
|
||||
"duplicateUrl": "Ten URL już znajduje się na liście (dopasowanie niewrażliwe na wielkość liter i spacje)",
|
||||
"nameTooLong": "Nazwa musi mieć 20 znaków lub mniej",
|
||||
"nonVisibleCharsName": "Nazwa zawiera niewidoczne znaki inne niż spacje",
|
||||
"duplicateName": "Ta nazwa jest już używana (dopasowanie niewrażliwe na wielkość liter i spacje)",
|
||||
"maxSources": "Maksymalnie dozwolone {{max}} źródeł"
|
||||
}
|
||||
}
|
||||
}
|
||||
91
webview-ui/src/i18n/locales/pt-BR/marketplace.json
Normal file
91
webview-ui/src/i18n/locales/pt-BR/marketplace.json
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
{
|
||||
"title": "Marketplace",
|
||||
"tabs": {
|
||||
"browse": "Explorar",
|
||||
"sources": "Fontes"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "Pesquisar itens do marketplace..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Filtrar por tipo:",
|
||||
"all": "Todos os tipos",
|
||||
"mode": "Modo",
|
||||
"mcp": "Servidor MCP",
|
||||
"prompt": "Prompt",
|
||||
"package": "Pacote"
|
||||
},
|
||||
"sort": {
|
||||
"label": "Ordenar por:",
|
||||
"name": "Nome",
|
||||
"author": "Autor",
|
||||
"lastUpdated": "Última atualização"
|
||||
},
|
||||
"tags": {
|
||||
"label": "Filtrar por tags:",
|
||||
"available": "{{count}} disponíveis",
|
||||
"clear": "Limpar tags ({{count}})",
|
||||
"placeholder": "Digite para pesquisar e selecionar tags...",
|
||||
"noResults": "Nenhuma tag correspondente encontrada",
|
||||
"selected": "Mostrando itens com qualquer uma das tags selecionadas ({{count}} selecionadas)",
|
||||
"clickToFilter": "Clique nas tags para filtrar itens"
|
||||
}
|
||||
},
|
||||
"type-group": {
|
||||
"match": "Correspondência",
|
||||
"modes": "Modos",
|
||||
"mcps": "Servidores MCP",
|
||||
"prompts": "Prompts",
|
||||
"packages": "Pacotes",
|
||||
"generic-type": "{{type}}"
|
||||
},
|
||||
"items": {
|
||||
"empty": {
|
||||
"noItems": "Nenhum item encontrado no marketplace",
|
||||
"withFilters": "Tente ajustar os filtros",
|
||||
"noSources": "Tente adicionar uma fonte na aba Fontes"
|
||||
},
|
||||
"count": "{{count}} itens encontrados",
|
||||
"components": "{{count}} componentes",
|
||||
"refresh": {
|
||||
"button": "Atualizar",
|
||||
"refreshing": "Atualizando..."
|
||||
},
|
||||
"card": {
|
||||
"by": "por {{author}}",
|
||||
"from": "de {{source}}",
|
||||
"viewSource": "Ver",
|
||||
"viewOnSource": "Ver no {{source}}"
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
"title": "Configurar Fontes do Marketplace",
|
||||
"description": "Adicione repositórios Git que contenham itens do marketplace. Estes repositórios serão buscados ao navegar pelo marketplace.",
|
||||
"add": {
|
||||
"title": "Adicionar Nova Fonte",
|
||||
"urlPlaceholder": "URL do repositório Git (ex., https://github.com/username/repo)",
|
||||
"urlFormats": "Formatos suportados: HTTPS (https://github.com/username/repo), SSH (git@github.com:username/repo.git) ou protocolo Git (git://github.com/username/repo.git)",
|
||||
"namePlaceholder": "Nome de exibição (máx. 20 caracteres)",
|
||||
"button": "Adicionar Fonte"
|
||||
},
|
||||
"current": {
|
||||
"title": "Fontes Atuais",
|
||||
"count": "{{current}}/{{max}} máx.",
|
||||
"empty": "Nenhuma fonte configurada. Adicione uma fonte para começar.",
|
||||
"refresh": "Atualizar esta fonte",
|
||||
"remove": "Remover fonte"
|
||||
},
|
||||
"errors": {
|
||||
"emptyUrl": "A URL não pode estar vazia",
|
||||
"invalidUrl": "Formato de URL inválido",
|
||||
"nonVisibleChars": "A URL contém caracteres não visíveis além de espaços",
|
||||
"invalidGitUrl": "A URL deve ser uma URL válida de repositório Git (ex., https://github.com/username/repo)",
|
||||
"duplicateUrl": "Esta URL já está na lista (correspondência insensível a maiúsculas/minúsculas e espaços)",
|
||||
"nameTooLong": "O nome deve ter 20 caracteres ou menos",
|
||||
"nonVisibleCharsName": "O nome contém caracteres não visíveis além de espaços",
|
||||
"duplicateName": "Este nome já está em uso (correspondência insensível a maiúsculas/minúsculas e espaços)",
|
||||
"maxSources": "Máximo de {{max}} fontes permitidas"
|
||||
}
|
||||
}
|
||||
}
|
||||
91
webview-ui/src/i18n/locales/tr/marketplace.json
Normal file
91
webview-ui/src/i18n/locales/tr/marketplace.json
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
{
|
||||
"title": "Marketplace",
|
||||
"tabs": {
|
||||
"browse": "Göz At",
|
||||
"sources": "Kaynaklar"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "Marketplace öğelerini ara..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Türe göre filtrele:",
|
||||
"all": "Tüm türler",
|
||||
"mode": "Mod",
|
||||
"mcp": "MCP Sunucusu",
|
||||
"prompt": "Prompt",
|
||||
"package": "Paket"
|
||||
},
|
||||
"sort": {
|
||||
"label": "Sırala:",
|
||||
"name": "İsim",
|
||||
"author": "Yazar",
|
||||
"lastUpdated": "Son güncelleme"
|
||||
},
|
||||
"tags": {
|
||||
"label": "Etiketlere göre filtrele:",
|
||||
"available": "{{count}} mevcut",
|
||||
"clear": "Etiketleri temizle ({{count}})",
|
||||
"placeholder": "Etiket aramak ve seçmek için yazın...",
|
||||
"noResults": "Eşleşen etiket bulunamadı",
|
||||
"selected": "Seçili etiketlerden herhangi birine sahip öğeler gösteriliyor ({{count}} seçili)",
|
||||
"clickToFilter": "Öğeleri filtrelemek için etiketlere tıklayın"
|
||||
}
|
||||
},
|
||||
"type-group": {
|
||||
"match": "Eşleşme",
|
||||
"modes": "Modlar",
|
||||
"mcps": "MCP Sunucuları",
|
||||
"prompts": "Promptlar",
|
||||
"packages": "Paketler",
|
||||
"generic-type": "{{type}}"
|
||||
},
|
||||
"items": {
|
||||
"empty": {
|
||||
"noItems": "Marketplace öğesi bulunamadı",
|
||||
"withFilters": "Filtreleri ayarlamayı deneyin",
|
||||
"noSources": "Kaynaklar sekmesinde bir kaynak eklemeyi deneyin"
|
||||
},
|
||||
"count": "{{count}} öğe bulundu",
|
||||
"components": "{{count}} bileşen",
|
||||
"refresh": {
|
||||
"button": "Yenile",
|
||||
"refreshing": "Yenileniyor..."
|
||||
},
|
||||
"card": {
|
||||
"by": "yazar: {{author}}",
|
||||
"from": "kaynak: {{source}}",
|
||||
"viewSource": "Görüntüle",
|
||||
"viewOnSource": "{{source}} üzerinde görüntüle"
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
"title": "Marketplace Kaynaklarını Yapılandır",
|
||||
"description": "Marketplace öğeleri içeren Git depolarını ekleyin. Bu depolar marketplace'de gezinirken getirilecektir.",
|
||||
"add": {
|
||||
"title": "Yeni Kaynak Ekle",
|
||||
"urlPlaceholder": "Git deposu URL'si (örn., https://github.com/username/repo)",
|
||||
"urlFormats": "Desteklenen formatlar: HTTPS (https://github.com/username/repo), SSH (git@github.com:username/repo.git) veya Git protokolü (git://github.com/username/repo.git)",
|
||||
"namePlaceholder": "Görünen ad (maks. 20 karakter)",
|
||||
"button": "Kaynak Ekle"
|
||||
},
|
||||
"current": {
|
||||
"title": "Mevcut Kaynaklar",
|
||||
"count": "{{current}}/{{max}} maks.",
|
||||
"empty": "Yapılandırılmış kaynak yok. Başlamak için bir kaynak ekleyin.",
|
||||
"refresh": "Bu kaynağı yenile",
|
||||
"remove": "Kaynağı kaldır"
|
||||
},
|
||||
"errors": {
|
||||
"emptyUrl": "URL boş olamaz",
|
||||
"invalidUrl": "Geçersiz URL formatı",
|
||||
"nonVisibleChars": "URL boşluk dışında görünmeyen karakterler içeriyor",
|
||||
"invalidGitUrl": "URL geçerli bir Git deposu URL'si olmalıdır (örn., https://github.com/username/repo)",
|
||||
"duplicateUrl": "Bu URL zaten listede (büyük/küçük harf ve boşluk duyarsız eşleşme)",
|
||||
"nameTooLong": "İsim 20 karakter veya daha az olmalıdır",
|
||||
"nonVisibleCharsName": "İsim boşluk dışında görünmeyen karakterler içeriyor",
|
||||
"duplicateName": "Bu isim zaten kullanımda (büyük/küçük harf ve boşluk duyarsız eşleşme)",
|
||||
"maxSources": "Maksimum {{max}} kaynak izin veriliyor"
|
||||
}
|
||||
}
|
||||
}
|
||||
91
webview-ui/src/i18n/locales/vi/marketplace.json
Normal file
91
webview-ui/src/i18n/locales/vi/marketplace.json
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
{
|
||||
"title": "Marketplace",
|
||||
"tabs": {
|
||||
"browse": "Duyệt",
|
||||
"sources": "Nguồn"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "Tìm kiếm các mục marketplace..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Lọc theo loại:",
|
||||
"all": "Tất cả các loại",
|
||||
"mode": "Chế độ",
|
||||
"mcp": "Máy chủ MCP",
|
||||
"prompt": "Prompt",
|
||||
"package": "Gói"
|
||||
},
|
||||
"sort": {
|
||||
"label": "Sắp xếp theo:",
|
||||
"name": "Tên",
|
||||
"author": "Tác giả",
|
||||
"lastUpdated": "Cập nhật lần cuối"
|
||||
},
|
||||
"tags": {
|
||||
"label": "Lọc theo thẻ:",
|
||||
"available": "{{count}} có sẵn",
|
||||
"clear": "Xóa thẻ ({{count}})",
|
||||
"placeholder": "Gõ để tìm kiếm và chọn thẻ...",
|
||||
"noResults": "Không tìm thấy thẻ phù hợp",
|
||||
"selected": "Hiển thị các mục có bất kỳ thẻ đã chọn nào (đã chọn {{count}})",
|
||||
"clickToFilter": "Nhấp vào thẻ để lọc các mục"
|
||||
}
|
||||
},
|
||||
"type-group": {
|
||||
"match": "Khớp",
|
||||
"modes": "Chế độ",
|
||||
"mcps": "Máy chủ MCP",
|
||||
"prompts": "Prompt",
|
||||
"packages": "Gói",
|
||||
"generic-type": "{{type}}"
|
||||
},
|
||||
"items": {
|
||||
"empty": {
|
||||
"noItems": "Không tìm thấy mục marketplace nào",
|
||||
"withFilters": "Thử điều chỉnh bộ lọc",
|
||||
"noSources": "Thử thêm nguồn trong tab Nguồn"
|
||||
},
|
||||
"count": "Tìm thấy {{count}} mục",
|
||||
"components": "{{count}} thành phần",
|
||||
"refresh": {
|
||||
"button": "Làm mới",
|
||||
"refreshing": "Đang làm mới..."
|
||||
},
|
||||
"card": {
|
||||
"by": "bởi {{author}}",
|
||||
"from": "từ {{source}}",
|
||||
"viewSource": "Xem",
|
||||
"viewOnSource": "Xem trên {{source}}"
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
"title": "Cấu hình Nguồn Marketplace",
|
||||
"description": "Thêm kho Git chứa các mục marketplace. Các kho này sẽ được tải khi duyệt marketplace.",
|
||||
"add": {
|
||||
"title": "Thêm Nguồn Mới",
|
||||
"urlPlaceholder": "URL kho Git (ví dụ: https://github.com/username/repo)",
|
||||
"urlFormats": "Định dạng được hỗ trợ: HTTPS (https://github.com/username/repo), SSH (git@github.com:username/repo.git) hoặc giao thức Git (git://github.com/username/repo.git)",
|
||||
"namePlaceholder": "Tên hiển thị (tối đa 20 ký tự)",
|
||||
"button": "Thêm Nguồn"
|
||||
},
|
||||
"current": {
|
||||
"title": "Nguồn Hiện Tại",
|
||||
"count": "{{current}}/{{max}} tối đa",
|
||||
"empty": "Chưa có nguồn nào được cấu hình. Thêm một nguồn để bắt đầu.",
|
||||
"refresh": "Làm mới nguồn này",
|
||||
"remove": "Xóa nguồn"
|
||||
},
|
||||
"errors": {
|
||||
"emptyUrl": "URL không được để trống",
|
||||
"invalidUrl": "Định dạng URL không hợp lệ",
|
||||
"nonVisibleChars": "URL chứa ký tự không nhìn thấy ngoài dấu cách",
|
||||
"invalidGitUrl": "URL phải là URL kho Git hợp lệ (ví dụ: https://github.com/username/repo)",
|
||||
"duplicateUrl": "URL này đã có trong danh sách (khớp không phân biệt chữ hoa/thường và dấu cách)",
|
||||
"nameTooLong": "Tên phải có 20 ký tự trở xuống",
|
||||
"nonVisibleCharsName": "Tên chứa ký tự không nhìn thấy ngoài dấu cách",
|
||||
"duplicateName": "Tên này đã được sử dụng (khớp không phân biệt chữ hoa/thường và dấu cách)",
|
||||
"maxSources": "Cho phép tối đa {{max}} nguồn"
|
||||
}
|
||||
}
|
||||
}
|
||||
91
webview-ui/src/i18n/locales/zh-CN/marketplace.json
Normal file
91
webview-ui/src/i18n/locales/zh-CN/marketplace.json
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
{
|
||||
"title": "市场",
|
||||
"tabs": {
|
||||
"browse": "浏览",
|
||||
"sources": "来源"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "搜索市场项目..."
|
||||
},
|
||||
"type": {
|
||||
"label": "按类型筛选:",
|
||||
"all": "所有类型",
|
||||
"mode": "模式",
|
||||
"mcp": "MCP 服务器",
|
||||
"prompt": "提示",
|
||||
"package": "包"
|
||||
},
|
||||
"sort": {
|
||||
"label": "排序方式:",
|
||||
"name": "名称",
|
||||
"author": "作者",
|
||||
"lastUpdated": "最近更新"
|
||||
},
|
||||
"tags": {
|
||||
"label": "按标签筛选:",
|
||||
"available": "{{count}} 个可用",
|
||||
"clear": "清除标签 ({{count}})",
|
||||
"placeholder": "输入以搜索和选择标签...",
|
||||
"noResults": "未找到匹配的标签",
|
||||
"selected": "显示具有任何已选标签的项目(已选 {{count}} 个)",
|
||||
"clickToFilter": "点击标签以筛选项目"
|
||||
}
|
||||
},
|
||||
"type-group": {
|
||||
"match": "匹配",
|
||||
"modes": "模式",
|
||||
"mcps": "MCP 服务器",
|
||||
"prompts": "提示",
|
||||
"packages": "包",
|
||||
"generic-type": "{{type}}"
|
||||
},
|
||||
"items": {
|
||||
"empty": {
|
||||
"noItems": "未找到市场项目",
|
||||
"withFilters": "尝试调整筛选条件",
|
||||
"noSources": "尝试在来源标签页中添加来源"
|
||||
},
|
||||
"count": "找到 {{count}} 个项目",
|
||||
"components": "{{count}} 个组件",
|
||||
"refresh": {
|
||||
"button": "刷新",
|
||||
"refreshing": "正在刷新..."
|
||||
},
|
||||
"card": {
|
||||
"by": "作者:{{author}}",
|
||||
"from": "来自:{{source}}",
|
||||
"viewSource": "查看",
|
||||
"viewOnSource": "在 {{source}} 上查看"
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
"title": "配置市场来源",
|
||||
"description": "添加包含市场项目的 Git 仓库。浏览市场时将获取这些仓库。",
|
||||
"add": {
|
||||
"title": "添加新来源",
|
||||
"urlPlaceholder": "Git 仓库 URL(例如:https://github.com/username/repo)",
|
||||
"urlFormats": "支持的格式:HTTPS (https://github.com/username/repo)、SSH (git@github.com:username/repo.git) 或 Git 协议 (git://github.com/username/repo.git)",
|
||||
"namePlaceholder": "显示名称(最多 20 个字符)",
|
||||
"button": "添加来源"
|
||||
},
|
||||
"current": {
|
||||
"title": "当前来源",
|
||||
"count": "{{current}}/{{max}} 个上限",
|
||||
"empty": "未配置来源。添加一个来源以开始。",
|
||||
"refresh": "刷新此来源",
|
||||
"remove": "移除来源"
|
||||
},
|
||||
"errors": {
|
||||
"emptyUrl": "URL 不能为空",
|
||||
"invalidUrl": "无效的 URL 格式",
|
||||
"nonVisibleChars": "URL 包含空格以外的不可见字符",
|
||||
"invalidGitUrl": "URL 必须是有效的 Git 仓库 URL(例如:https://github.com/username/repo)",
|
||||
"duplicateUrl": "此 URL 已在列表中(不区分大小写和空格的匹配)",
|
||||
"nameTooLong": "名称必须不超过 20 个字符",
|
||||
"nonVisibleCharsName": "名称包含空格以外的不可见字符",
|
||||
"duplicateName": "此名称已被使用(不区分大小写和空格的匹配)",
|
||||
"maxSources": "最多允许 {{max}} 个来源"
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue