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:
NamesMT 2025-05-21 15:57:19 +00:00 committed by hannesrudolph
parent 43fa77ca94
commit 5c49366c49
104 changed files with 16719 additions and 17 deletions

View file

@ -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

View 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.

View 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)

View 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)

View 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)

View 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)

View 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)

File diff suppressed because it is too large Load diff

View 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)

View 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)

View 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)

View 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)

View 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)

View 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)

View 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)

View file

@ -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()

View file

@ -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()

View file

@ -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

View 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
}
}

View file

@ -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}`)
}
}
}

View file

@ -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

View file

@ -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

View 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"
}
}

View 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"
}
}

View 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' : ''}}"
}
}

View 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"
}
}

View 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"
}
}

View 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}} आइटम"
}
}

View 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"
}
}

View 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}}個のアイテム"
}
}

View 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}}개 항목"
}
}

View 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"
}
}

View 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"
}
}

View 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}} элементов"
}
}

View 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"
}
}

View 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"
}
}

View 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}} 项"
}
}

View 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}} 個項目"
}
}

View file

@ -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"
}
]
},

View file

@ -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,
}

View 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)
}
}

View 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}`)
}
}
}

View 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())
}

View 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"
}
}

View 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"))
})
})
})

View file

@ -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)
})
})

View 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()
})
})

View file

@ -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")
})
})
})

View file

@ -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")
})
})

View 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()
})
})
})

View 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")
})
})
})

View 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,
}

View file

@ -0,0 +1,4 @@
export * from "./GitFetcher"
export * from "./MarketplaceManager"
export * from "./types"
export * from "../../shared/MarketplaceValidation"

View 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(),
})

View 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"
}

View 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()
}

View file

@ -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 }

View 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)
}

View file

@ -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

View 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)
})
})

View 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
View file

@ -0,0 +1,8 @@
export const isValidUrl = (urlString: string): boolean => {
try {
new URL(urlString)
return true
} catch (e) {
return false
}
}

View file

@ -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",

View file

@ -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"}

View file

@ -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")

View 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

View 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>
)}
</>
)
}

View file

@ -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>
)
}

View 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>
)
}

View file

@ -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" })
}
}
}
}

View file

@ -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()
})
})

View file

@ -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: [] } },
})
})
})

View file

@ -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("")
})
})

View file

@ -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>
)
}

View file

@ -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>
)
}

View file

@ -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
}

View 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>
)
}

View file

@ -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")
})
})

View file

@ -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")
})
})

View file

@ -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()
})
})

View 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
}

View file

@ -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([])
})
})
})

View 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
}

View 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 }

View file

@ -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>

View 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"
}
}
}

View 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"
}
}
}

View 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"
}
}
}

View 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"
}
}
}

View 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"
}
}
}

View 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}} स्रोत की अनुमति है"
}
}
}

View 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"
}
}
}

View 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}}個のソースまで追加できます"
}
}
}

View 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}}개의 소스만 허용됩니다"
}
}
}

View 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ł"
}
}
}

View 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"
}
}
}

View 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"
}
}
}

View 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"
}
}
}

View 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