mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Remove the MCP marketplace (#12326)
* Remove the MCP marketplace * Remove unused URL utility
This commit is contained in:
parent
ff16c9c297
commit
22d845cecb
171 changed files with 31 additions and 10091 deletions
|
|
@ -11,7 +11,6 @@ Roo Code respects your privacy and is committed to transparency about how we han
|
|||
- **Prompts & AI Requests**: When you use AI-powered features, your prompts and relevant project context are sent to your chosen AI model provider (e.g., OpenAI, Anthropic, OpenRouter) to generate responses. We do not store or process this data. These AI providers have their own privacy policies and may store data per their terms of service. If you choose Roo Code Cloud as the provider (proxy mode), prompts may transit Roo Code servers only to forward them to the upstream model and are not stored.
|
||||
- **API Keys & Credentials**: If you enter an API key (e.g., to connect an AI model), it is stored locally on your device and never sent to us or any third party, except the provider you have chosen.
|
||||
- **Telemetry (Usage Data)**: We collect anonymous feature usage and error data to help us improve Roo Code. This telemetry is powered by PostHog and includes your VS Code machine ID, feature usage patterns, and exception reports. This telemetry does **not** collect personally identifiable information, your code, or AI prompts. You can opt out of this telemetry at any time through the settings.
|
||||
- **Marketplace Requests**: When you browse or search the Marketplace for Model Configuration Profiles (MCPs) or Custom Modes, Roo Code makes a secure API call to Roo Code's backend servers to retrieve listing information. These requests send only the query parameters (e.g., extension version, search term) necessary to fulfill the request and do not include your code, prompts, or personally identifiable information.
|
||||
|
||||
### **How We Use Your Data (If Collected)**
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export const features: Feature[] = [
|
|||
icon: Users2,
|
||||
title: "Specialized modes",
|
||||
description:
|
||||
"Planning, Architecture, Debugging and beyond: Roo's modes stay on-task and deliver. They even know when to hand off work to other modes. Create your own or download from the marketplace.",
|
||||
"Planning, Architecture, Debugging and beyond: Roo's modes stay on-task and deliver. They even know when to hand off work to other modes. Create your own modes for your workflow.",
|
||||
},
|
||||
{
|
||||
icon: ReplaceAll,
|
||||
|
|
|
|||
|
|
@ -94,9 +94,6 @@ describe("organizationSettingsSchema with features", () => {
|
|||
},
|
||||
},
|
||||
features: {},
|
||||
hiddenMcps: ["test-mcp"],
|
||||
hideMarketplaceMcps: true,
|
||||
mcps: [],
|
||||
providerProfiles: {},
|
||||
}
|
||||
const result = organizationSettingsSchema.safeParse(input)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import { RooCodeEventName } from "./events.js"
|
|||
import { TaskStatus, taskMetadataSchema } from "./task.js"
|
||||
import { globalSettingsSchema } from "./global-settings.js"
|
||||
import { providerSettingsWithIdSchema } from "./provider-settings.js"
|
||||
import { mcpMarketplaceItemSchema } from "./marketplace.js"
|
||||
import { clineMessageSchema, queuedMessageSchema, tokenUsageSchema } from "./message.js"
|
||||
|
||||
const extensionAppPropertiesSchema = z.object({
|
||||
|
|
@ -170,9 +169,6 @@ export const organizationSettingsSchema = z.object({
|
|||
defaultSettings: organizationDefaultSettingsSchema,
|
||||
allowList: organizationAllowListSchema,
|
||||
features: organizationFeaturesSchema.optional(),
|
||||
hiddenMcps: z.array(z.string()).optional(),
|
||||
hideMarketplaceMcps: z.boolean().optional(),
|
||||
mcps: z.array(mcpMarketplaceItemSchema).optional(),
|
||||
providerProfiles: z.record(z.string(), providerSettingsWithIdSchema).optional(),
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ export * from "./global-settings.js"
|
|||
export * from "./history.js"
|
||||
export * from "./image-generation.js"
|
||||
export * from "./ipc.js"
|
||||
export * from "./marketplace.js"
|
||||
export * from "./mcp.js"
|
||||
export * from "./message.js"
|
||||
export * from "./mode.js"
|
||||
|
|
|
|||
|
|
@ -1,93 +0,0 @@
|
|||
import { z } from "zod"
|
||||
|
||||
/**
|
||||
* Schema for MCP parameter definitions
|
||||
*/
|
||||
export const mcpParameterSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
key: z.string().min(1),
|
||||
placeholder: z.string().optional(),
|
||||
optional: z.boolean().optional().default(false),
|
||||
})
|
||||
|
||||
export type McpParameter = z.infer<typeof mcpParameterSchema>
|
||||
|
||||
/**
|
||||
* Schema for MCP installation method with name
|
||||
*/
|
||||
export const mcpInstallationMethodSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
content: z.string().min(1),
|
||||
parameters: z.array(mcpParameterSchema).optional(),
|
||||
prerequisites: z.array(z.string()).optional(),
|
||||
})
|
||||
|
||||
export type McpInstallationMethod = z.infer<typeof mcpInstallationMethodSchema>
|
||||
|
||||
/**
|
||||
* Component type validation
|
||||
*/
|
||||
export const marketplaceItemTypeSchema = z.enum(["mode", "mcp"] as const)
|
||||
|
||||
export type MarketplaceItemType = z.infer<typeof marketplaceItemTypeSchema>
|
||||
|
||||
/**
|
||||
* Base schema for common marketplace item fields
|
||||
*/
|
||||
const baseMarketplaceItemSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
name: z.string().min(1, "Name is required"),
|
||||
description: z.string(),
|
||||
author: z.string().optional(),
|
||||
authorUrl: z.string().url("Author URL must be a valid URL").optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
prerequisites: z.array(z.string()).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Type-specific schemas for YAML parsing (without type field, added programmatically)
|
||||
*/
|
||||
export const modeMarketplaceItemSchema = baseMarketplaceItemSchema.extend({
|
||||
content: z.string().min(1), // YAML content for modes
|
||||
})
|
||||
|
||||
export type ModeMarketplaceItem = z.infer<typeof modeMarketplaceItemSchema>
|
||||
|
||||
export const mcpMarketplaceItemSchema = baseMarketplaceItemSchema.extend({
|
||||
url: z.string().url(), // Required url field
|
||||
content: z.union([z.string().min(1), z.array(mcpInstallationMethodSchema)]), // Single config or array of methods
|
||||
parameters: z.array(mcpParameterSchema).optional(),
|
||||
})
|
||||
|
||||
export type McpMarketplaceItem = z.infer<typeof mcpMarketplaceItemSchema>
|
||||
|
||||
/**
|
||||
* Unified marketplace item schema using discriminated union
|
||||
*/
|
||||
export const marketplaceItemSchema = z.discriminatedUnion("type", [
|
||||
// Mode marketplace item
|
||||
modeMarketplaceItemSchema.extend({
|
||||
type: z.literal("mode"),
|
||||
}),
|
||||
// MCP marketplace item
|
||||
mcpMarketplaceItemSchema.extend({
|
||||
type: z.literal("mcp"),
|
||||
}),
|
||||
])
|
||||
|
||||
export type MarketplaceItem = z.infer<typeof marketplaceItemSchema>
|
||||
|
||||
/**
|
||||
* Installation options for marketplace items
|
||||
*/
|
||||
export const installMarketplaceItemOptionsSchema = z.object({
|
||||
target: z.enum(["global", "project"]).optional().default("project"),
|
||||
parameters: z.record(z.string(), z.any()).optional(),
|
||||
})
|
||||
|
||||
export type InstallMarketplaceItemOptions = z.infer<typeof installMarketplaceItemOptionsSchema>
|
||||
|
||||
export interface MarketplaceInstalledMetadata {
|
||||
project: Record<string, { type: string }>
|
||||
global: Record<string, { type: string }>
|
||||
}
|
||||
|
|
@ -6,12 +6,6 @@ import type { HistoryItem } from "./history.js"
|
|||
import type { ModeConfig, PromptComponent } from "./mode.js"
|
||||
import type { Experiments } from "./experiment.js"
|
||||
import type { ClineMessage, QueuedMessage } from "./message.js"
|
||||
import {
|
||||
type MarketplaceItem,
|
||||
type MarketplaceInstalledMetadata,
|
||||
type InstallMarketplaceItemOptions,
|
||||
marketplaceItemSchema,
|
||||
} from "./marketplace.js"
|
||||
import type { TodoItem } from "./todo.js"
|
||||
import type { CloudUserInfo, CloudOrganizationMembership, OrganizationAllowList, ShareVisibility } from "./cloud.js"
|
||||
import type { SerializedCustomToolDefinition } from "./custom-tool.js"
|
||||
|
|
@ -75,9 +69,6 @@ export interface ExtensionMessage {
|
|||
| "indexingStatusUpdate"
|
||||
| "indexCleared"
|
||||
| "codebaseIndexConfig"
|
||||
| "marketplaceInstallResult"
|
||||
| "marketplaceRemoveResult"
|
||||
| "marketplaceData"
|
||||
| "shareTaskSuccess"
|
||||
| "codeIndexSettingsSaved"
|
||||
| "codeIndexSecretStatus"
|
||||
|
|
@ -115,7 +106,6 @@ export interface ExtensionMessage {
|
|||
| "chatButtonClicked"
|
||||
| "settingsButtonClicked"
|
||||
| "historyButtonClicked"
|
||||
| "marketplaceButtonClicked"
|
||||
| "cloudButtonClicked"
|
||||
| "didBecomeVisible"
|
||||
| "focusInput"
|
||||
|
|
@ -159,13 +149,9 @@ export interface ExtensionMessage {
|
|||
setting?: string
|
||||
value?: any // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
hasContent?: boolean
|
||||
items?: MarketplaceItem[]
|
||||
userInfo?: CloudUserInfo
|
||||
organizationAllowList?: OrganizationAllowList
|
||||
tab?: string
|
||||
marketplaceItems?: MarketplaceItem[]
|
||||
organizationMcps?: MarketplaceItem[]
|
||||
marketplaceInstalledMetadata?: MarketplaceInstalledMetadata
|
||||
errors?: string[]
|
||||
visibility?: ShareVisibility
|
||||
rulesFolderPath?: string
|
||||
|
|
@ -355,9 +341,6 @@ export type ExtensionState = Pick<
|
|||
|
||||
autoCondenseContext: boolean
|
||||
autoCondenseContextPercent: number
|
||||
marketplaceItems?: MarketplaceItem[]
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
marketplaceInstalledMetadata?: { project: Record<string, any>; global: Record<string, any> }
|
||||
profileThresholds: Record<string, number>
|
||||
hasOpenedModeSelector: boolean
|
||||
openRouterImageApiKey?: string
|
||||
|
|
@ -512,14 +495,6 @@ export interface WebviewMessage {
|
|||
| "setAutoEnableDefault"
|
||||
| "focusPanelRequest"
|
||||
| "openExternal"
|
||||
| "filterMarketplaceItems"
|
||||
| "marketplaceButtonClicked"
|
||||
| "installMarketplaceItem"
|
||||
| "installMarketplaceItemWithParameters"
|
||||
| "cancelMarketplaceInstall"
|
||||
| "removeInstalledMarketplaceItem"
|
||||
| "marketplaceInstallResult"
|
||||
| "fetchMarketplaceData"
|
||||
| "switchTab"
|
||||
| "shareTaskSuccess"
|
||||
| "exportMode"
|
||||
|
|
@ -576,7 +551,7 @@ export interface WebviewMessage {
|
|||
text?: string
|
||||
taskId?: string
|
||||
editedMessageContent?: string
|
||||
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"
|
||||
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "cloud"
|
||||
disabled?: boolean
|
||||
context?: string
|
||||
dataUri?: string
|
||||
|
|
@ -628,8 +603,6 @@ export interface WebviewMessage {
|
|||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
settings?: any
|
||||
url?: string // For openExternal
|
||||
mpItem?: MarketplaceItem
|
||||
mpInstallOptions?: InstallMarketplaceItemOptions
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
config?: Record<string, any> // Add config to the payload
|
||||
visibility?: ShareVisibility // For share visibility
|
||||
|
|
@ -715,21 +688,11 @@ export interface IndexClearedPayload {
|
|||
error?: string
|
||||
}
|
||||
|
||||
export const installMarketplaceItemWithParametersPayloadSchema = z.object({
|
||||
item: marketplaceItemSchema,
|
||||
parameters: z.record(z.string(), z.any()),
|
||||
})
|
||||
|
||||
export type InstallMarketplaceItemWithParametersPayload = z.infer<
|
||||
typeof installMarketplaceItemWithParametersPayloadSchema
|
||||
>
|
||||
|
||||
export type WebViewMessagePayload =
|
||||
| CheckpointDiffPayload
|
||||
| CheckpointRestorePayload
|
||||
| IndexingStatusPayload
|
||||
| IndexClearedPayload
|
||||
| InstallMarketplaceItemWithParametersPayload
|
||||
| UpdateTodoListPayload
|
||||
| EditQueuedMessagePayload
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ export const commandIds = [
|
|||
|
||||
"plusButtonClicked",
|
||||
"historyButtonClicked",
|
||||
"marketplaceButtonClicked",
|
||||
"popoutButtonClicked",
|
||||
"cloudButtonClicked",
|
||||
"settingsButtonClicked",
|
||||
|
|
|
|||
|
|
@ -117,11 +117,6 @@ 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" })
|
||||
},
|
||||
newTask: handleNewTask,
|
||||
setCustomStoragePath: async () => {
|
||||
const { promptForCustomStoragePath } = await import("../utils/storage")
|
||||
|
|
|
|||
|
|
@ -508,7 +508,7 @@ export class CustomModesManager {
|
|||
await this.onUpdate()
|
||||
}
|
||||
|
||||
public async deleteCustomMode(slug: string, fromMarketplace = false): Promise<void> {
|
||||
public async deleteCustomMode(slug: string): Promise<void> {
|
||||
try {
|
||||
const settingsPath = await this.getCustomModesFilePath()
|
||||
const roomodesPath = await this.getWorkspaceRoomodes()
|
||||
|
|
@ -540,7 +540,7 @@ export class CustomModesManager {
|
|||
|
||||
// Delete associated rules folder
|
||||
if (modeToDelete) {
|
||||
await this.deleteRulesFolder(slug, modeToDelete, fromMarketplace)
|
||||
await this.deleteRulesFolder(slug, modeToDelete)
|
||||
}
|
||||
|
||||
// Clear cache when modes are deleted
|
||||
|
|
@ -558,7 +558,7 @@ export class CustomModesManager {
|
|||
* @param slug - The mode slug
|
||||
* @param mode - The mode configuration to determine the scope
|
||||
*/
|
||||
private async deleteRulesFolder(slug: string, mode: ModeConfig, fromMarketplace = false): Promise<void> {
|
||||
private async deleteRulesFolder(slug: string, mode: ModeConfig): Promise<void> {
|
||||
try {
|
||||
// Determine the scope based on source (project or global)
|
||||
const scope = mode.source || "global"
|
||||
|
|
@ -587,10 +587,9 @@ export class CustomModesManager {
|
|||
} catch (error) {
|
||||
logger.error(`Failed to delete rules folder for mode ${slug}: ${error}`)
|
||||
// Notify the user about the failure
|
||||
const messageKey = fromMarketplace
|
||||
? "common:marketplace.mode.rulesCleanupFailed"
|
||||
: "common:customModes.errors.rulesCleanupFailed"
|
||||
vscode.window.showWarningMessage(t(messageKey, { rulesFolderPath }))
|
||||
vscode.window.showWarningMessage(
|
||||
t("common:customModes.errors.rulesCleanupFailed", { rulesFolderPath }),
|
||||
)
|
||||
// Continue even if folder deletion fails
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ import {
|
|||
type ToolUsage,
|
||||
type ExtensionMessage,
|
||||
type ExtensionState,
|
||||
type MarketplaceInstalledMetadata,
|
||||
RooCodeEventName,
|
||||
requestyDefaultModelId,
|
||||
openRouterDefaultModelId,
|
||||
|
|
@ -62,7 +61,6 @@ 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 { CodeIndexManager } from "../../services/code-index/manager"
|
||||
import type { IndexProgressUpdate } from "../../services/code-index/interfaces/manager"
|
||||
|
|
@ -133,7 +131,6 @@ export class ClineProvider
|
|||
private _workspaceTracker?: WorkspaceTracker // workSpaceTracker read-only for access outside this class
|
||||
protected mcpHub?: McpHub // Change from private to protected
|
||||
protected skillsManager?: SkillsManager
|
||||
private marketplaceManager: MarketplaceManager
|
||||
private taskCreationCallback: (task: Task) => void
|
||||
private taskEventListeners: WeakMap<Task, Array<() => void>> = new WeakMap()
|
||||
private currentWorkspacePath: string | undefined
|
||||
|
|
@ -212,8 +209,6 @@ export class ClineProvider
|
|||
this.log(`Failed to initialize Skills Manager: ${error}`)
|
||||
})
|
||||
|
||||
this.marketplaceManager = new MarketplaceManager(this.context, this.customModesManager)
|
||||
|
||||
// Forward <most> task events to the provider.
|
||||
// We do something fairly similar for the IPC-based API.
|
||||
this.taskCreationCallback = (instance: Task) => {
|
||||
|
|
@ -690,7 +685,6 @@ export class ClineProvider
|
|||
this.mcpHub = undefined
|
||||
await this.skillsManager?.dispose()
|
||||
this.skillsManager = undefined
|
||||
this.marketplaceManager?.cleanup()
|
||||
this.customModesManager?.dispose()
|
||||
this.taskHistoryStore.dispose()
|
||||
this.flushGlobalStateWriteThrough()
|
||||
|
|
@ -1356,8 +1350,7 @@ export class ClineProvider
|
|||
* @param webview A reference to the extension webview
|
||||
*/
|
||||
private setWebviewMessageListener(webview: vscode.Webview) {
|
||||
const onReceiveMessage = async (message: WebviewMessage) =>
|
||||
webviewMessageHandler(this, message, this.marketplaceManager)
|
||||
const onReceiveMessage = async (message: WebviewMessage) => webviewMessageHandler(this, message)
|
||||
|
||||
const messageDisposable = webview.onDidReceiveMessage(onReceiveMessage)
|
||||
this.webviewDisposables.push(messageDisposable)
|
||||
|
|
@ -1980,51 +1973,6 @@ export class ClineProvider
|
|||
this.postMessageToWebview({ type: "state", state: rest })
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches marketplace data on demand to avoid blocking main state updates
|
||||
*/
|
||||
async fetchMarketplaceData() {
|
||||
try {
|
||||
const [marketplaceResult, marketplaceInstalledMetadata] = await Promise.all([
|
||||
this.marketplaceManager.getMarketplaceItems().catch((error) => {
|
||||
console.error("Failed to fetch marketplace items:", error)
|
||||
return { organizationMcps: [], marketplaceItems: [], errors: [error.message] }
|
||||
}),
|
||||
this.marketplaceManager.getInstallationMetadata().catch((error) => {
|
||||
console.error("Failed to fetch installation metadata:", error)
|
||||
return { project: {}, global: {} } as MarketplaceInstalledMetadata
|
||||
}),
|
||||
])
|
||||
|
||||
// Send marketplace data separately
|
||||
this.postMessageToWebview({
|
||||
type: "marketplaceData",
|
||||
organizationMcps: marketplaceResult.organizationMcps || [],
|
||||
marketplaceItems: marketplaceResult.marketplaceItems || [],
|
||||
marketplaceInstalledMetadata: marketplaceInstalledMetadata || { project: {}, global: {} },
|
||||
errors: marketplaceResult.errors,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch marketplace data:", error)
|
||||
|
||||
// Send empty data on error to prevent UI from hanging
|
||||
this.postMessageToWebview({
|
||||
type: "marketplaceData",
|
||||
organizationMcps: [],
|
||||
marketplaceItems: [],
|
||||
marketplaceInstalledMetadata: { project: {}, global: {} },
|
||||
errors: [error instanceof Error ? error.message : String(error)],
|
||||
})
|
||||
|
||||
// Show user-friendly error notification for network issues
|
||||
if (error instanceof Error && error.message.includes("timeout")) {
|
||||
vscode.window.showWarningMessage(
|
||||
"Marketplace data could not be loaded due to network restrictions. Core functionality remains available.",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges allowed commands from global state and workspace configuration
|
||||
* with proper validation and deduplication
|
||||
|
|
|
|||
|
|
@ -52,7 +52,6 @@ vi.mock("../../../services/mcp/McpServerManager", () => ({
|
|||
unregisterProvider: vi.fn(),
|
||||
},
|
||||
}))
|
||||
vi.mock("../../../services/marketplace")
|
||||
vi.mock("../../../integrations/workspace/WorkspaceTracker")
|
||||
vi.mock("../../config/ProviderSettingsManager")
|
||||
vi.mock("../../config/CustomModesManager")
|
||||
|
|
|
|||
|
|
@ -71,7 +71,6 @@ import { getCommand } from "../../utils/commands"
|
|||
|
||||
const ALLOWED_VSCODE_SETTINGS = new Set(["terminal.integrated.inheritEnv"])
|
||||
|
||||
import { MarketplaceManager, MarketplaceItemType } from "../../services/marketplace"
|
||||
import { setPendingTodoList } from "../tools/UpdateTodoListTool"
|
||||
import {
|
||||
handleListWorktrees,
|
||||
|
|
@ -86,11 +85,7 @@ import {
|
|||
handleCheckoutBranch,
|
||||
} from "./worktree"
|
||||
|
||||
export const webviewMessageHandler = async (
|
||||
provider: ClineProvider,
|
||||
message: WebviewMessage,
|
||||
marketplaceManager?: MarketplaceManager,
|
||||
) => {
|
||||
export const webviewMessageHandler = async (provider: ClineProvider, message: WebviewMessage) => {
|
||||
// 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]) =>
|
||||
|
|
@ -2799,126 +2794,6 @@ export const webviewMessageHandler = async (
|
|||
await vscode.commands.executeCommand(getCommand("focusPanel"))
|
||||
break
|
||||
}
|
||||
case "filterMarketplaceItems": {
|
||||
if (marketplaceManager && message.filters) {
|
||||
try {
|
||||
await 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")
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "fetchMarketplaceData": {
|
||||
// Fetch marketplace data on demand
|
||||
await provider.fetchMarketplaceData()
|
||||
break
|
||||
}
|
||||
|
||||
case "installMarketplaceItem": {
|
||||
if (marketplaceManager && message.mpItem && message.mpInstallOptions) {
|
||||
try {
|
||||
const configFilePath = await marketplaceManager.installMarketplaceItem(
|
||||
message.mpItem,
|
||||
message.mpInstallOptions,
|
||||
)
|
||||
await provider.postStateToWebview()
|
||||
console.log(`Marketplace item installed and config file opened: ${configFilePath}`)
|
||||
|
||||
// Send success message to webview
|
||||
provider.postMessageToWebview({
|
||||
type: "marketplaceInstallResult",
|
||||
success: true,
|
||||
slug: message.mpItem.id,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(`Error installing marketplace item: ${error}`)
|
||||
// Send error message to webview
|
||||
provider.postMessageToWebview({
|
||||
type: "marketplaceInstallResult",
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
slug: message.mpItem.id,
|
||||
})
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "removeInstalledMarketplaceItem": {
|
||||
if (marketplaceManager && message.mpItem && message.mpInstallOptions) {
|
||||
try {
|
||||
await marketplaceManager.removeInstalledMarketplaceItem(message.mpItem, message.mpInstallOptions)
|
||||
await provider.postStateToWebview()
|
||||
|
||||
// Send success message to webview
|
||||
provider.postMessageToWebview({
|
||||
type: "marketplaceRemoveResult",
|
||||
success: true,
|
||||
slug: message.mpItem.id,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(`Error removing marketplace item: ${error}`)
|
||||
|
||||
// Show error message to user
|
||||
vscode.window.showErrorMessage(
|
||||
`Failed to remove marketplace item: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
|
||||
// Send error message to webview
|
||||
provider.postMessageToWebview({
|
||||
type: "marketplaceRemoveResult",
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
slug: message.mpItem.id,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// MarketplaceManager not available or missing required parameters
|
||||
const errorMessage = !marketplaceManager
|
||||
? "Marketplace manager is not available"
|
||||
: "Missing required parameters for marketplace item removal"
|
||||
console.error(errorMessage)
|
||||
|
||||
vscode.window.showErrorMessage(errorMessage)
|
||||
|
||||
if (message.mpItem?.id) {
|
||||
provider.postMessageToWebview({
|
||||
type: "marketplaceRemoveResult",
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
slug: message.mpItem.id,
|
||||
})
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "installMarketplaceItemWithParameters": {
|
||||
if (marketplaceManager && message.payload && "item" in message.payload && "parameters" in message.payload) {
|
||||
try {
|
||||
const configFilePath = await marketplaceManager.installMarketplaceItem(message.payload.item, {
|
||||
parameters: message.payload.parameters,
|
||||
})
|
||||
await provider.postStateToWebview()
|
||||
console.log(`Marketplace item with parameters installed and config file opened: ${configFilePath}`)
|
||||
} catch (error) {
|
||||
console.error(`Error installing marketplace item with parameters: ${error}`)
|
||||
vscode.window.showErrorMessage(
|
||||
`Failed to install marketplace item: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "switchTab": {
|
||||
if (message.tab) {
|
||||
await provider.postMessageToWebview({
|
||||
|
|
@ -3590,14 +3465,11 @@ export const webviewMessageHandler = async (
|
|||
// "vsCodeSetting" |
|
||||
// "indexingStatusUpdate" |
|
||||
// "indexCleared" |
|
||||
// "marketplaceInstallResult" |
|
||||
// "shareTaskSuccess" |
|
||||
// "playSound" |
|
||||
// "draggedImages" |
|
||||
// "setApiConfigPassword" |
|
||||
// "setopenAiCustomModelInfo" |
|
||||
// "marketplaceButtonClicked" |
|
||||
// "cancelMarketplaceInstall" |
|
||||
// "imageGenerationSettings"
|
||||
break
|
||||
}
|
||||
|
|
|
|||
5
src/i18n/locales/ca/common.json
generated
5
src/i18n/locales/ca/common.json
generated
|
|
@ -215,11 +215,6 @@
|
|||
"global": "global"
|
||||
}
|
||||
},
|
||||
"marketplace": {
|
||||
"mode": {
|
||||
"rulesCleanupFailed": "El mode s'ha eliminat correctament, però no s'ha pogut eliminar la carpeta de regles a {{rulesFolderPath}}. És possible que l'hagis d'eliminar manualment."
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"deleteMode": {
|
||||
"title": "Suprimeix el mode personalitzat",
|
||||
|
|
|
|||
69
src/i18n/locales/ca/marketplace.json
generated
69
src/i18n/locales/ca/marketplace.json
generated
|
|
@ -1,69 +0,0 @@
|
|||
{
|
||||
"type-group": {
|
||||
"modes": "Modes",
|
||||
"mcps": "Servidors MCP",
|
||||
"match": "coincidència"
|
||||
},
|
||||
"item-card": {
|
||||
"type-mode": "Mode",
|
||||
"type-mcp": "Servidor MCP",
|
||||
"type-other": "Altre",
|
||||
"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",
|
||||
"view": "Veure",
|
||||
"source": "Font"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "Cercar al marketplace..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Tipus",
|
||||
"all": "Tots els tipus",
|
||||
"mode": "Mode",
|
||||
"mcpServer": "Servidor MCP"
|
||||
},
|
||||
"sort": {
|
||||
"label": "Ordenar per",
|
||||
"name": "Nom",
|
||||
"lastUpdated": "Última actualització"
|
||||
},
|
||||
"tags": {
|
||||
"label": "Etiquetes",
|
||||
"clear": "Netejar etiquetes",
|
||||
"placeholder": "Cercar etiquetes...",
|
||||
"noResults": "No s'han trobat etiquetes.",
|
||||
"selected": "Mostrant elements amb qualsevol de les etiquetes seleccionades"
|
||||
},
|
||||
"installed": {
|
||||
"label": "Filtra per estat",
|
||||
"all": "Tots els articles",
|
||||
"installed": "Instal·lats",
|
||||
"notInstalled": "No instal·lats"
|
||||
},
|
||||
"title": "Marketplace"
|
||||
},
|
||||
"done": "Fet",
|
||||
"tabs": {
|
||||
"installed": "Instal·lat",
|
||||
"browse": "Navegar",
|
||||
"settings": "Configuració"
|
||||
},
|
||||
"items": {
|
||||
"empty": {
|
||||
"noItems": "No s'han trobat elements del marketplace.",
|
||||
"emptyHint": "Prova d'ajustar els filtres o termes de cerca"
|
||||
}
|
||||
},
|
||||
"installation": {
|
||||
"installing": "Instal·lant element: \"{{itemName}}\"",
|
||||
"installSuccess": "\"{{itemName}}\" instal·lat correctament",
|
||||
"installError": "Error en instal·lar \"{{itemName}}\": {{errorMessage}}",
|
||||
"removing": "Eliminant element: \"{{itemName}}\"",
|
||||
"removeSuccess": "\"{{itemName}}\" eliminat correctament",
|
||||
"removeError": "Error en eliminar \"{{itemName}}\": {{errorMessage}}"
|
||||
}
|
||||
}
|
||||
5
src/i18n/locales/de/common.json
generated
5
src/i18n/locales/de/common.json
generated
|
|
@ -215,11 +215,6 @@
|
|||
"global": "global"
|
||||
}
|
||||
},
|
||||
"marketplace": {
|
||||
"mode": {
|
||||
"rulesCleanupFailed": "Der Modus wurde erfolgreich entfernt, aber der Regelordner unter {{rulesFolderPath}} konnte nicht gelöscht werden. Möglicherweise musst du ihn manuell löschen."
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"deleteMode": {
|
||||
"title": "Benutzerdefinierten Modus löschen",
|
||||
|
|
|
|||
69
src/i18n/locales/de/marketplace.json
generated
69
src/i18n/locales/de/marketplace.json
generated
|
|
@ -1,69 +0,0 @@
|
|||
{
|
||||
"type-group": {
|
||||
"modes": "Modi",
|
||||
"mcps": "MCP-Server",
|
||||
"match": "Übereinstimmung"
|
||||
},
|
||||
"item-card": {
|
||||
"type-mode": "Modus",
|
||||
"type-mcp": "MCP-Server",
|
||||
"type-other": "Andere",
|
||||
"by-author": "von {{author}}",
|
||||
"authors-profile": "Autorenprofil",
|
||||
"remove-tag-filter": "Tag-Filter entfernen: {{tag}}",
|
||||
"filter-by-tag": "Nach Tag filtern: {{tag}}",
|
||||
"component-details": "Komponentendetails",
|
||||
"view": "Anzeigen",
|
||||
"source": "Quelle"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "Marketplace durchsuchen..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Typ",
|
||||
"all": "Alle Typen",
|
||||
"mode": "Modus",
|
||||
"mcpServer": "MCP-Server"
|
||||
},
|
||||
"sort": {
|
||||
"label": "Sortieren nach",
|
||||
"name": "Name",
|
||||
"lastUpdated": "Zuletzt aktualisiert"
|
||||
},
|
||||
"tags": {
|
||||
"label": "Tags",
|
||||
"clear": "Tags löschen",
|
||||
"placeholder": "Tags suchen...",
|
||||
"noResults": "Keine Tags gefunden.",
|
||||
"selected": "Zeige Elemente mit einem der ausgewählten Tags"
|
||||
},
|
||||
"installed": {
|
||||
"label": "Nach Status filtern",
|
||||
"all": "Alle Artikel",
|
||||
"installed": "Installierte",
|
||||
"notInstalled": "Nicht installiert"
|
||||
},
|
||||
"title": "Marketplace"
|
||||
},
|
||||
"done": "Fertig",
|
||||
"tabs": {
|
||||
"installed": "Installiert",
|
||||
"browse": "Durchsuchen",
|
||||
"settings": "Einstellungen"
|
||||
},
|
||||
"items": {
|
||||
"empty": {
|
||||
"noItems": "Keine Marketplace-Elemente gefunden.",
|
||||
"emptyHint": "Versuche deine Filter oder Suchbegriffe anzupassen"
|
||||
}
|
||||
},
|
||||
"installation": {
|
||||
"installing": "Element wird installiert: \"{{itemName}}\"",
|
||||
"installSuccess": "\"{{itemName}}\" erfolgreich installiert",
|
||||
"installError": "Installation von \"{{itemName}}\" fehlgeschlagen: {{errorMessage}}",
|
||||
"removing": "Element wird entfernt: \"{{itemName}}\"",
|
||||
"removeSuccess": "\"{{itemName}}\" erfolgreich entfernt",
|
||||
"removeError": "Entfernung von \"{{itemName}}\" fehlgeschlagen: {{errorMessage}}"
|
||||
}
|
||||
}
|
||||
|
|
@ -212,11 +212,6 @@
|
|||
"global": "global"
|
||||
}
|
||||
},
|
||||
"marketplace": {
|
||||
"mode": {
|
||||
"rulesCleanupFailed": "Mode removed successfully, but failed to delete rules folder at {{rulesFolderPath}}. You may need to delete it manually."
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"deleteMode": {
|
||||
"title": "Delete Custom Mode",
|
||||
|
|
|
|||
|
|
@ -1,69 +0,0 @@
|
|||
{
|
||||
"type-group": {
|
||||
"modes": "Modes",
|
||||
"mcps": "MCP Servers",
|
||||
"match": "match"
|
||||
},
|
||||
"item-card": {
|
||||
"type-mode": "Mode",
|
||||
"type-mcp": "MCP Server",
|
||||
"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",
|
||||
"view": "View",
|
||||
"source": "Source"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "Search marketplace..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Type",
|
||||
"all": "All Types",
|
||||
"mode": "Mode",
|
||||
"mcpServer": "MCP Server"
|
||||
},
|
||||
"sort": {
|
||||
"label": "Sort By",
|
||||
"name": "Name",
|
||||
"lastUpdated": "Last Updated"
|
||||
},
|
||||
"tags": {
|
||||
"label": "Tags",
|
||||
"clear": "Clear tags",
|
||||
"placeholder": "Search tags...",
|
||||
"noResults": "No tags found.",
|
||||
"selected": "Showing items with any of the selected tags"
|
||||
},
|
||||
"installed": {
|
||||
"label": "Filter by status",
|
||||
"all": "All Items",
|
||||
"installed": "Installed",
|
||||
"notInstalled": "Not Installed"
|
||||
},
|
||||
"title": "Marketplace"
|
||||
},
|
||||
"done": "Done",
|
||||
"tabs": {
|
||||
"installed": "Installed",
|
||||
"browse": "Browse",
|
||||
"settings": "Settings"
|
||||
},
|
||||
"items": {
|
||||
"empty": {
|
||||
"noItems": "No marketplace items found.",
|
||||
"emptyHint": "Try adjusting your filters or search terms"
|
||||
}
|
||||
},
|
||||
"installation": {
|
||||
"installing": "Installing item: \"{{itemName}}\"",
|
||||
"installSuccess": "\"{{itemName}}\" installed successfully",
|
||||
"installError": "Failed to install \"{{itemName}}\": {{errorMessage}}",
|
||||
"removing": "Removing item: \"{{itemName}}\"",
|
||||
"removeSuccess": "\"{{itemName}}\" removed successfully",
|
||||
"removeError": "Failed to remove \"{{itemName}}\": {{errorMessage}}"
|
||||
}
|
||||
}
|
||||
5
src/i18n/locales/es/common.json
generated
5
src/i18n/locales/es/common.json
generated
|
|
@ -215,11 +215,6 @@
|
|||
"global": "global"
|
||||
}
|
||||
},
|
||||
"marketplace": {
|
||||
"mode": {
|
||||
"rulesCleanupFailed": "El modo se eliminó correctamente, pero no se pudo eliminar la carpeta de reglas en {{rulesFolderPath}}. Es posible que debas eliminarla manually."
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"deleteMode": {
|
||||
"title": "Eliminar modo personalizado",
|
||||
|
|
|
|||
69
src/i18n/locales/es/marketplace.json
generated
69
src/i18n/locales/es/marketplace.json
generated
|
|
@ -1,69 +0,0 @@
|
|||
{
|
||||
"type-group": {
|
||||
"modes": "Modos",
|
||||
"mcps": "Servidores MCP",
|
||||
"match": "coincidencia"
|
||||
},
|
||||
"item-card": {
|
||||
"type-mode": "Modo",
|
||||
"type-mcp": "Servidor MCP",
|
||||
"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",
|
||||
"view": "Ver",
|
||||
"source": "Fuente"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "Buscar en marketplace..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Tipo",
|
||||
"all": "Todos los tipos",
|
||||
"mode": "Modo",
|
||||
"mcpServer": "Servidor MCP"
|
||||
},
|
||||
"sort": {
|
||||
"label": "Ordenar por",
|
||||
"name": "Nombre",
|
||||
"lastUpdated": "Última actualización"
|
||||
},
|
||||
"tags": {
|
||||
"label": "Etiquetas",
|
||||
"clear": "Limpiar etiquetas",
|
||||
"placeholder": "Buscar etiquetas...",
|
||||
"noResults": "No se encontraron etiquetas.",
|
||||
"selected": "Mostrando elementos con cualquiera de las etiquetas seleccionadas"
|
||||
},
|
||||
"installed": {
|
||||
"label": "Filtrar por estado",
|
||||
"all": "Todos los artículos",
|
||||
"installed": "Instalados",
|
||||
"notInstalled": "No instalados"
|
||||
},
|
||||
"title": "Marketplace"
|
||||
},
|
||||
"done": "Hecho",
|
||||
"tabs": {
|
||||
"installed": "Instalado",
|
||||
"browse": "Explorar",
|
||||
"settings": "Configuración"
|
||||
},
|
||||
"items": {
|
||||
"empty": {
|
||||
"noItems": "No se encontraron elementos del marketplace.",
|
||||
"emptyHint": "Intenta ajustar tus filtros o términos de búsqueda"
|
||||
}
|
||||
},
|
||||
"installation": {
|
||||
"installing": "Instalando elemento: \"{{itemName}}\"",
|
||||
"installSuccess": "\"{{itemName}}\" instalado correctamente",
|
||||
"installError": "Error al instalar \"{{itemName}}\": {{errorMessage}}",
|
||||
"removing": "Eliminando elemento: \"{{itemName}}\"",
|
||||
"removeSuccess": "\"{{itemName}}\" eliminado correctamente",
|
||||
"removeError": "Error al eliminar \"{{itemName}}\": {{errorMessage}}"
|
||||
}
|
||||
}
|
||||
5
src/i18n/locales/fr/common.json
generated
5
src/i18n/locales/fr/common.json
generated
|
|
@ -215,11 +215,6 @@
|
|||
"global": "global"
|
||||
}
|
||||
},
|
||||
"marketplace": {
|
||||
"mode": {
|
||||
"rulesCleanupFailed": "Le mode a été supprimé avec succès, mais la suppression du dossier de règles à l'adresse {{rulesFolderPath}} a échoué. Vous devrez peut-être le supprimer manuellement."
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"deleteMode": {
|
||||
"title": "Supprimer le mode personnalisé",
|
||||
|
|
|
|||
69
src/i18n/locales/fr/marketplace.json
generated
69
src/i18n/locales/fr/marketplace.json
generated
|
|
@ -1,69 +0,0 @@
|
|||
{
|
||||
"type-group": {
|
||||
"modes": "Modes",
|
||||
"mcps": "Serveurs MCP",
|
||||
"match": "correspondance"
|
||||
},
|
||||
"item-card": {
|
||||
"type-mode": "Mode",
|
||||
"type-mcp": "Serveur MCP",
|
||||
"type-other": "Autre",
|
||||
"by-author": "par {{author}}",
|
||||
"authors-profile": "Profil de l'auteur",
|
||||
"remove-tag-filter": "Supprimer le filtre d'étiquette : {{tag}}",
|
||||
"filter-by-tag": "Filtrer par étiquette : {{tag}}",
|
||||
"component-details": "Détails du composant",
|
||||
"view": "Voir",
|
||||
"source": "Source"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "Rechercher dans le marketplace..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Type",
|
||||
"all": "Tous les types",
|
||||
"mode": "Mode",
|
||||
"mcpServer": "Serveur MCP"
|
||||
},
|
||||
"sort": {
|
||||
"label": "Trier par",
|
||||
"name": "Nom",
|
||||
"lastUpdated": "Dernière mise à jour"
|
||||
},
|
||||
"tags": {
|
||||
"label": "Étiquettes",
|
||||
"clear": "Effacer les étiquettes",
|
||||
"placeholder": "Rechercher des étiquettes...",
|
||||
"noResults": "Aucune étiquette trouvée.",
|
||||
"selected": "Affichage des éléments avec l'une des étiquettes sélectionnées"
|
||||
},
|
||||
"installed": {
|
||||
"label": "Filtrer par statut",
|
||||
"all": "Tous les articles",
|
||||
"installed": "Installés",
|
||||
"notInstalled": "Non installés"
|
||||
},
|
||||
"title": "Marketplace"
|
||||
},
|
||||
"done": "Terminé",
|
||||
"tabs": {
|
||||
"installed": "Installé",
|
||||
"browse": "Parcourir",
|
||||
"settings": "Paramètres"
|
||||
},
|
||||
"items": {
|
||||
"empty": {
|
||||
"noItems": "Aucun élément du marketplace trouvé.",
|
||||
"emptyHint": "Essayez d'ajuster vos filtres ou termes de recherche"
|
||||
}
|
||||
},
|
||||
"installation": {
|
||||
"installing": "Installation de l'élément : \"{{itemName}}\"",
|
||||
"installSuccess": "\"{{itemName}}\" installé avec succès",
|
||||
"installError": "Échec de l'installation de \"{{itemName}}\" : {{errorMessage}}",
|
||||
"removing": "Suppression de l'élément : \"{{itemName}}\"",
|
||||
"removeSuccess": "\"{{itemName}}\" supprimé avec succès",
|
||||
"removeError": "Échec de la suppression de \"{{itemName}}\" : {{errorMessage}}"
|
||||
}
|
||||
}
|
||||
5
src/i18n/locales/hi/common.json
generated
5
src/i18n/locales/hi/common.json
generated
|
|
@ -215,11 +215,6 @@
|
|||
"global": "वैश्विक"
|
||||
}
|
||||
},
|
||||
"marketplace": {
|
||||
"mode": {
|
||||
"rulesCleanupFailed": "मोड सफलतापूर्वक हटा दिया गया, लेकिन {{rulesFolderPath}} पर नियम फ़ोल्डर को हटाने में विफल रहा। आपको इसे मैन्युअल रूप से हटाना पड़ सकता है।"
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"deleteMode": {
|
||||
"title": "कस्टम मोड हटाएं",
|
||||
|
|
|
|||
69
src/i18n/locales/hi/marketplace.json
generated
69
src/i18n/locales/hi/marketplace.json
generated
|
|
@ -1,69 +0,0 @@
|
|||
{
|
||||
"type-group": {
|
||||
"modes": "मोड्स",
|
||||
"mcps": "MCP सर्वर",
|
||||
"match": "मैच"
|
||||
},
|
||||
"item-card": {
|
||||
"type-mode": "मोड",
|
||||
"type-mcp": "MCP सर्वर",
|
||||
"type-other": "अन्य",
|
||||
"by-author": "{{author}} द्वारा",
|
||||
"authors-profile": "लेखक की प्रोफ़ाइल",
|
||||
"remove-tag-filter": "टैग फ़िल्टर हटाएं: {{tag}}",
|
||||
"filter-by-tag": "टैग द्वारा फ़िल्टर करें: {{tag}}",
|
||||
"component-details": "कंपोनेंट विवरण",
|
||||
"view": "देखें",
|
||||
"source": "स्रोत"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "मार्केटप्लेस खोजें..."
|
||||
},
|
||||
"type": {
|
||||
"label": "प्रकार",
|
||||
"all": "सभी प्रकार",
|
||||
"mode": "मोड",
|
||||
"mcpServer": "MCP सर्वर"
|
||||
},
|
||||
"sort": {
|
||||
"label": "इसके द्वारा क्रमबद्ध करें",
|
||||
"name": "नाम",
|
||||
"lastUpdated": "अंतिम अपडेट"
|
||||
},
|
||||
"tags": {
|
||||
"label": "टैग्स",
|
||||
"clear": "टैग्स साफ़ करें",
|
||||
"placeholder": "टैग्स खोजें...",
|
||||
"noResults": "कोई टैग नहीं मिले।",
|
||||
"selected": "चयनित टैग्स में से किसी भी के साथ आइटम दिखा रहे हैं"
|
||||
},
|
||||
"installed": {
|
||||
"label": "स्थिति के अनुसार फ़िल्टर करें",
|
||||
"all": "सभी आइटम",
|
||||
"installed": "स्थापित",
|
||||
"notInstalled": "स्थापित नहीं"
|
||||
},
|
||||
"title": "मार्केटप्लेस"
|
||||
},
|
||||
"done": "हो गया",
|
||||
"tabs": {
|
||||
"installed": "इंस्टॉल किया गया",
|
||||
"browse": "ब्राउज़ करें",
|
||||
"settings": "सेटिंग्स"
|
||||
},
|
||||
"items": {
|
||||
"empty": {
|
||||
"noItems": "कोई मार्केटप्लेस आइटम नहीं मिले।",
|
||||
"emptyHint": "अपने फ़िल्टर या खोज शब्दों को समायोजित करने का प्रयास करें"
|
||||
}
|
||||
},
|
||||
"installation": {
|
||||
"installing": "आइटम इंस्टॉल कर रहे हैं: \"{{itemName}}\"",
|
||||
"installSuccess": "\"{{itemName}}\" सफलतापूर्वक इंस्टॉल हुआ",
|
||||
"installError": "\"{{itemName}}\" इंस्टॉल करने में विफल: {{errorMessage}}",
|
||||
"removing": "आइटम हटा रहे हैं: \"{{itemName}}\"",
|
||||
"removeSuccess": "\"{{itemName}}\" सफलतापूर्वक हटाया गया",
|
||||
"removeError": "\"{{itemName}}\" हटाने में विफल: {{errorMessage}}"
|
||||
}
|
||||
}
|
||||
5
src/i18n/locales/id/common.json
generated
5
src/i18n/locales/id/common.json
generated
|
|
@ -215,11 +215,6 @@
|
|||
"global": "global"
|
||||
}
|
||||
},
|
||||
"marketplace": {
|
||||
"mode": {
|
||||
"rulesCleanupFailed": "Mode berhasil dihapus, tetapi gagal menghapus folder aturan di {{rulesFolderPath}}. Kamu mungkin perlu menghapusnya secara manual."
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"deleteMode": {
|
||||
"title": "Hapus Mode Kustom",
|
||||
|
|
|
|||
69
src/i18n/locales/id/marketplace.json
generated
69
src/i18n/locales/id/marketplace.json
generated
|
|
@ -1,69 +0,0 @@
|
|||
{
|
||||
"type-group": {
|
||||
"modes": "Mode",
|
||||
"mcps": "Server MCP",
|
||||
"match": "cocok"
|
||||
},
|
||||
"item-card": {
|
||||
"type-mode": "Mode",
|
||||
"type-mcp": "Server MCP",
|
||||
"type-other": "Lainnya",
|
||||
"by-author": "oleh {{author}}",
|
||||
"authors-profile": "Profil Penulis",
|
||||
"remove-tag-filter": "Hapus filter tag: {{tag}}",
|
||||
"filter-by-tag": "Filter berdasarkan tag: {{tag}}",
|
||||
"component-details": "Detail Komponen",
|
||||
"view": "Lihat",
|
||||
"source": "Sumber"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "Cari marketplace..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Tipe",
|
||||
"all": "Semua Tipe",
|
||||
"mode": "Mode",
|
||||
"mcpServer": "Server MCP"
|
||||
},
|
||||
"sort": {
|
||||
"label": "Urutkan Berdasarkan",
|
||||
"name": "Nama",
|
||||
"lastUpdated": "Terakhir Diperbarui"
|
||||
},
|
||||
"tags": {
|
||||
"label": "Tag",
|
||||
"clear": "Hapus tag",
|
||||
"placeholder": "Cari tag...",
|
||||
"noResults": "Tidak ada tag ditemukan.",
|
||||
"selected": "Menampilkan item dengan salah satu tag yang dipilih"
|
||||
},
|
||||
"installed": {
|
||||
"label": "Filter berdasarkan status",
|
||||
"all": "Semua Item",
|
||||
"installed": "Terpasang",
|
||||
"notInstalled": "Tidak Terpasang"
|
||||
},
|
||||
"title": "Marketplace"
|
||||
},
|
||||
"done": "Selesai",
|
||||
"tabs": {
|
||||
"installed": "Terinstal",
|
||||
"browse": "Jelajahi",
|
||||
"settings": "Pengaturan"
|
||||
},
|
||||
"items": {
|
||||
"empty": {
|
||||
"noItems": "Tidak ada item marketplace ditemukan.",
|
||||
"emptyHint": "Coba sesuaikan filter atau kata kunci pencarian kamu"
|
||||
}
|
||||
},
|
||||
"installation": {
|
||||
"installing": "Menginstal item: \"{{itemName}}\"",
|
||||
"installSuccess": "\"{{itemName}}\" berhasil diinstal",
|
||||
"installError": "Gagal menginstal \"{{itemName}}\": {{errorMessage}}",
|
||||
"removing": "Menghapus item: \"{{itemName}}\"",
|
||||
"removeSuccess": "\"{{itemName}}\" berhasil dihapus",
|
||||
"removeError": "Gagal menghapus \"{{itemName}}\": {{errorMessage}}"
|
||||
}
|
||||
}
|
||||
5
src/i18n/locales/it/common.json
generated
5
src/i18n/locales/it/common.json
generated
|
|
@ -215,11 +215,6 @@
|
|||
"global": "globale"
|
||||
}
|
||||
},
|
||||
"marketplace": {
|
||||
"mode": {
|
||||
"rulesCleanupFailed": "La modalità è stata rimossa con successo, ma non è stato possibile eliminare la cartella delle regole in {{rulesFolderPath}}. Potrebbe essere necessario eliminarla manualmente."
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"deleteMode": {
|
||||
"title": "Elimina Modalità Personalizzata",
|
||||
|
|
|
|||
69
src/i18n/locales/it/marketplace.json
generated
69
src/i18n/locales/it/marketplace.json
generated
|
|
@ -1,69 +0,0 @@
|
|||
{
|
||||
"type-group": {
|
||||
"modes": "Modalità",
|
||||
"mcps": "Server MCP",
|
||||
"match": "corrispondenza"
|
||||
},
|
||||
"item-card": {
|
||||
"type-mode": "Modalità",
|
||||
"type-mcp": "Server MCP",
|
||||
"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",
|
||||
"view": "Visualizza",
|
||||
"source": "Sorgente"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "Cerca nel marketplace..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Tipo",
|
||||
"all": "Tutti i tipi",
|
||||
"mode": "Modalità",
|
||||
"mcpServer": "Server MCP"
|
||||
},
|
||||
"sort": {
|
||||
"label": "Ordina per",
|
||||
"name": "Nome",
|
||||
"lastUpdated": "Ultimo aggiornamento"
|
||||
},
|
||||
"tags": {
|
||||
"label": "Tag",
|
||||
"clear": "Cancella tag",
|
||||
"placeholder": "Cerca tag...",
|
||||
"noResults": "Nessun tag trovato.",
|
||||
"selected": "Mostrando elementi con uno qualsiasi dei tag selezionati"
|
||||
},
|
||||
"installed": {
|
||||
"label": "Filtra per stato",
|
||||
"all": "Tutti gli articoli",
|
||||
"installed": "Installati",
|
||||
"notInstalled": "Non installati"
|
||||
},
|
||||
"title": "Marketplace"
|
||||
},
|
||||
"done": "Fatto",
|
||||
"tabs": {
|
||||
"installed": "Installato",
|
||||
"browse": "Sfoglia",
|
||||
"settings": "Impostazioni"
|
||||
},
|
||||
"items": {
|
||||
"empty": {
|
||||
"noItems": "Nessun elemento del marketplace trovato.",
|
||||
"emptyHint": "Prova ad aggiustare i tuoi filtri o termini di ricerca"
|
||||
}
|
||||
},
|
||||
"installation": {
|
||||
"installing": "Installazione elemento: \"{{itemName}}\"",
|
||||
"installSuccess": "\"{{itemName}}\" installato con successo",
|
||||
"installError": "Installazione di \"{{itemName}}\" fallita: {{errorMessage}}",
|
||||
"removing": "Rimozione elemento: \"{{itemName}}\"",
|
||||
"removeSuccess": "\"{{itemName}}\" rimosso con successo",
|
||||
"removeError": "Rimozione di \"{{itemName}}\" fallita: {{errorMessage}}"
|
||||
}
|
||||
}
|
||||
5
src/i18n/locales/ja/common.json
generated
5
src/i18n/locales/ja/common.json
generated
|
|
@ -215,11 +215,6 @@
|
|||
"global": "グローバル"
|
||||
}
|
||||
},
|
||||
"marketplace": {
|
||||
"mode": {
|
||||
"rulesCleanupFailed": "モードは正常に削除されましたが、{{rulesFolderPath}} にあるルールフォルダの削除に失敗しました。手動で削除する必要がある場合があります。"
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"deleteMode": {
|
||||
"title": "カスタムモードの削除",
|
||||
|
|
|
|||
69
src/i18n/locales/ja/marketplace.json
generated
69
src/i18n/locales/ja/marketplace.json
generated
|
|
@ -1,69 +0,0 @@
|
|||
{
|
||||
"type-group": {
|
||||
"modes": "モード",
|
||||
"mcps": "MCPサーバー",
|
||||
"match": "マッチ"
|
||||
},
|
||||
"item-card": {
|
||||
"type-mode": "モード",
|
||||
"type-mcp": "MCPサーバー",
|
||||
"type-other": "その他",
|
||||
"by-author": "{{author}}による",
|
||||
"authors-profile": "作者のプロフィール",
|
||||
"remove-tag-filter": "タグフィルターを削除: {{tag}}",
|
||||
"filter-by-tag": "タグでフィルター: {{tag}}",
|
||||
"component-details": "コンポーネントの詳細",
|
||||
"view": "表示",
|
||||
"source": "ソース"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "マーケットプレイスを検索..."
|
||||
},
|
||||
"type": {
|
||||
"label": "タイプ",
|
||||
"all": "すべてのタイプ",
|
||||
"mode": "モード",
|
||||
"mcpServer": "MCPサーバー"
|
||||
},
|
||||
"sort": {
|
||||
"label": "並び替え",
|
||||
"name": "名前",
|
||||
"lastUpdated": "最終更新"
|
||||
},
|
||||
"tags": {
|
||||
"label": "タグ",
|
||||
"clear": "タグをクリア",
|
||||
"placeholder": "タグを検索...",
|
||||
"noResults": "タグが見つかりません。",
|
||||
"selected": "選択されたタグのいずれかを持つアイテムを表示"
|
||||
},
|
||||
"installed": {
|
||||
"label": "ステータスで絞り込む",
|
||||
"all": "すべてのアイテム",
|
||||
"installed": "インストール済み",
|
||||
"notInstalled": "未インストール"
|
||||
},
|
||||
"title": "マーケットプレイス"
|
||||
},
|
||||
"done": "完了",
|
||||
"tabs": {
|
||||
"installed": "インストール済み",
|
||||
"browse": "参照",
|
||||
"settings": "設定"
|
||||
},
|
||||
"items": {
|
||||
"empty": {
|
||||
"noItems": "マーケットプレイスのアイテムが見つかりません。",
|
||||
"emptyHint": "フィルターや検索用語を調整してみてください"
|
||||
}
|
||||
},
|
||||
"installation": {
|
||||
"installing": "アイテムをインストール中: \"{{itemName}}\"",
|
||||
"installSuccess": "\"{{itemName}}\"のインストールが完了しました",
|
||||
"installError": "\"{{itemName}}\"のインストールに失敗しました: {{errorMessage}}",
|
||||
"removing": "アイテムを削除中: \"{{itemName}}\"",
|
||||
"removeSuccess": "\"{{itemName}}\"の削除が完了しました",
|
||||
"removeError": "\"{{itemName}}\"の削除に失敗しました: {{errorMessage}}"
|
||||
}
|
||||
}
|
||||
5
src/i18n/locales/ko/common.json
generated
5
src/i18n/locales/ko/common.json
generated
|
|
@ -215,11 +215,6 @@
|
|||
"global": "글로벌"
|
||||
}
|
||||
},
|
||||
"marketplace": {
|
||||
"mode": {
|
||||
"rulesCleanupFailed": "모드가 성공적으로 제거되었지만 {{rulesFolderPath}}의 규칙 폴더를 삭제하지 못했습니다. 수동으로 삭제해야 할 수도 있습니다."
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"deleteMode": {
|
||||
"title": "사용자 정의 모드 삭제",
|
||||
|
|
|
|||
69
src/i18n/locales/ko/marketplace.json
generated
69
src/i18n/locales/ko/marketplace.json
generated
|
|
@ -1,69 +0,0 @@
|
|||
{
|
||||
"type-group": {
|
||||
"modes": "모드",
|
||||
"mcps": "MCP 서버",
|
||||
"match": "일치"
|
||||
},
|
||||
"item-card": {
|
||||
"type-mode": "모드",
|
||||
"type-mcp": "MCP 서버",
|
||||
"type-other": "기타",
|
||||
"by-author": "{{author}} 작성",
|
||||
"authors-profile": "작성자 프로필",
|
||||
"remove-tag-filter": "태그 필터 제거: {{tag}}",
|
||||
"filter-by-tag": "태그로 필터링: {{tag}}",
|
||||
"component-details": "컴포넌트 세부사항",
|
||||
"view": "보기",
|
||||
"source": "소스"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "마켓플레이스 검색..."
|
||||
},
|
||||
"type": {
|
||||
"label": "유형",
|
||||
"all": "모든 유형",
|
||||
"mode": "모드",
|
||||
"mcpServer": "MCP 서버"
|
||||
},
|
||||
"sort": {
|
||||
"label": "정렬 기준",
|
||||
"name": "이름",
|
||||
"lastUpdated": "마지막 업데이트"
|
||||
},
|
||||
"tags": {
|
||||
"label": "태그",
|
||||
"clear": "태그 지우기",
|
||||
"placeholder": "태그 검색...",
|
||||
"noResults": "태그를 찾을 수 없습니다.",
|
||||
"selected": "선택된 태그 중 하나를 가진 항목 표시"
|
||||
},
|
||||
"installed": {
|
||||
"label": "상태별로 필터링",
|
||||
"all": "모든 항목",
|
||||
"installed": "설치됨",
|
||||
"notInstalled": "설치되지 않음"
|
||||
},
|
||||
"title": "마켓플레이스"
|
||||
},
|
||||
"done": "완료",
|
||||
"tabs": {
|
||||
"installed": "설치됨",
|
||||
"browse": "찾아보기",
|
||||
"settings": "설정"
|
||||
},
|
||||
"items": {
|
||||
"empty": {
|
||||
"noItems": "마켓플레이스 항목을 찾을 수 없습니다.",
|
||||
"emptyHint": "필터나 검색어를 조정해 보세요"
|
||||
}
|
||||
},
|
||||
"installation": {
|
||||
"installing": "항목 설치 중: \"{{itemName}}\"",
|
||||
"installSuccess": "\"{{itemName}}\" 설치 완료",
|
||||
"installError": "\"{{itemName}}\" 설치 실패: {{errorMessage}}",
|
||||
"removing": "항목 제거 중: \"{{itemName}}\"",
|
||||
"removeSuccess": "\"{{itemName}}\" 제거 완료",
|
||||
"removeError": "\"{{itemName}}\" 제거 실패: {{errorMessage}}"
|
||||
}
|
||||
}
|
||||
5
src/i18n/locales/nl/common.json
generated
5
src/i18n/locales/nl/common.json
generated
|
|
@ -215,11 +215,6 @@
|
|||
"global": "globaal"
|
||||
}
|
||||
},
|
||||
"marketplace": {
|
||||
"mode": {
|
||||
"rulesCleanupFailed": "Modus succesvol verwijderd, maar het verwijderen van de regelsmap op {{rulesFolderPath}} is mislukt. Je moet deze mogelijk handmatig verwijderen."
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"deleteMode": {
|
||||
"title": "Aangepaste modus verwijderen",
|
||||
|
|
|
|||
69
src/i18n/locales/nl/marketplace.json
generated
69
src/i18n/locales/nl/marketplace.json
generated
|
|
@ -1,69 +0,0 @@
|
|||
{
|
||||
"type-group": {
|
||||
"modes": "Modi",
|
||||
"mcps": "MCP Servers",
|
||||
"match": "overeenkomst"
|
||||
},
|
||||
"item-card": {
|
||||
"type-mode": "Modus",
|
||||
"type-mcp": "MCP Server",
|
||||
"type-other": "Andere",
|
||||
"by-author": "door {{author}}",
|
||||
"authors-profile": "Auteursprofiel",
|
||||
"remove-tag-filter": "Tag filter verwijderen: {{tag}}",
|
||||
"filter-by-tag": "Filteren op tag: {{tag}}",
|
||||
"component-details": "Component details",
|
||||
"view": "Bekijken",
|
||||
"source": "Bron"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "Marketplace doorzoeken..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Type",
|
||||
"all": "Alle types",
|
||||
"mode": "Modus",
|
||||
"mcpServer": "MCP Server"
|
||||
},
|
||||
"sort": {
|
||||
"label": "Sorteren op",
|
||||
"name": "Naam",
|
||||
"lastUpdated": "Laatst bijgewerkt"
|
||||
},
|
||||
"tags": {
|
||||
"label": "Tags",
|
||||
"clear": "Tags wissen",
|
||||
"placeholder": "Tags zoeken...",
|
||||
"noResults": "Geen tags gevonden.",
|
||||
"selected": "Items tonen met een van de geselecteerde tags"
|
||||
},
|
||||
"installed": {
|
||||
"label": "Filteren op status",
|
||||
"all": "Alle items",
|
||||
"installed": "Geïnstalleerd",
|
||||
"notInstalled": "Niet geïnstalleerd"
|
||||
},
|
||||
"title": "Marketplace"
|
||||
},
|
||||
"done": "Klaar",
|
||||
"tabs": {
|
||||
"installed": "Geïnstalleerd",
|
||||
"browse": "Bladeren",
|
||||
"settings": "Instellingen"
|
||||
},
|
||||
"items": {
|
||||
"empty": {
|
||||
"noItems": "Geen marketplace items gevonden.",
|
||||
"emptyHint": "Probeer je filters of zoektermen aan te passen"
|
||||
}
|
||||
},
|
||||
"installation": {
|
||||
"installing": "Item installeren: \"{{itemName}}\"",
|
||||
"installSuccess": "\"{{itemName}}\" succesvol geïnstalleerd",
|
||||
"installError": "Installatie van \"{{itemName}}\" mislukt: {{errorMessage}}",
|
||||
"removing": "Item verwijderen: \"{{itemName}}\"",
|
||||
"removeSuccess": "\"{{itemName}}\" succesvol verwijderd",
|
||||
"removeError": "Verwijdering van \"{{itemName}}\" mislukt: {{errorMessage}}"
|
||||
}
|
||||
}
|
||||
5
src/i18n/locales/pl/common.json
generated
5
src/i18n/locales/pl/common.json
generated
|
|
@ -215,11 +215,6 @@
|
|||
"global": "globalny"
|
||||
}
|
||||
},
|
||||
"marketplace": {
|
||||
"mode": {
|
||||
"rulesCleanupFailed": "Tryb został pomyślnie usunięty, ale nie udało się usunąć folderu reguł w {{rulesFolderPath}}. Może być konieczne ręczne usunięcie."
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"deleteMode": {
|
||||
"title": "Usuń tryb niestandardowy",
|
||||
|
|
|
|||
69
src/i18n/locales/pl/marketplace.json
generated
69
src/i18n/locales/pl/marketplace.json
generated
|
|
@ -1,69 +0,0 @@
|
|||
{
|
||||
"type-group": {
|
||||
"modes": "Tryby",
|
||||
"mcps": "Serwery MCP",
|
||||
"match": "dopasowanie"
|
||||
},
|
||||
"item-card": {
|
||||
"type-mode": "Tryb",
|
||||
"type-mcp": "Serwer MCP",
|
||||
"type-other": "Inne",
|
||||
"by-author": "przez {{author}}",
|
||||
"authors-profile": "Profil autora",
|
||||
"remove-tag-filter": "Usuń filtr tagu: {{tag}}",
|
||||
"filter-by-tag": "Filtruj według tagu: {{tag}}",
|
||||
"component-details": "Szczegóły komponentu",
|
||||
"view": "Zobacz",
|
||||
"source": "Źródło"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "Przeszukaj marketplace..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Typ",
|
||||
"all": "Wszystkie typy",
|
||||
"mode": "Tryb",
|
||||
"mcpServer": "Serwer MCP"
|
||||
},
|
||||
"sort": {
|
||||
"label": "Sortuj według",
|
||||
"name": "Nazwa",
|
||||
"lastUpdated": "Ostatnia aktualizacja"
|
||||
},
|
||||
"tags": {
|
||||
"label": "Tagi",
|
||||
"clear": "Wyczyść tagi",
|
||||
"placeholder": "Szukaj tagów...",
|
||||
"noResults": "Nie znaleziono tagów.",
|
||||
"selected": "Pokazywanie elementów z dowolnym z wybranych tagów"
|
||||
},
|
||||
"installed": {
|
||||
"label": "Filtruj według statusu",
|
||||
"all": "Wszystkie elementy",
|
||||
"installed": "Zainstalowane",
|
||||
"notInstalled": "Niezainstalowane"
|
||||
},
|
||||
"title": "Marketplace"
|
||||
},
|
||||
"done": "Gotowe",
|
||||
"tabs": {
|
||||
"installed": "Zainstalowane",
|
||||
"browse": "Przeglądaj",
|
||||
"settings": "Ustawienia"
|
||||
},
|
||||
"items": {
|
||||
"empty": {
|
||||
"noItems": "Nie znaleziono elementów marketplace.",
|
||||
"emptyHint": "Spróbuj dostosować filtry lub terminy wyszukiwania"
|
||||
}
|
||||
},
|
||||
"installation": {
|
||||
"installing": "Instalowanie elementu: \"{{itemName}}\"",
|
||||
"installSuccess": "\"{{itemName}}\" zainstalowano pomyślnie",
|
||||
"installError": "Instalacja \"{{itemName}}\" nie powiodła się: {{errorMessage}}",
|
||||
"removing": "Usuwanie elementu: \"{{itemName}}\"",
|
||||
"removeSuccess": "\"{{itemName}}\" usunięto pomyślnie",
|
||||
"removeError": "Usunięcie \"{{itemName}}\" nie powiodło się: {{errorMessage}}"
|
||||
}
|
||||
}
|
||||
5
src/i18n/locales/pt-BR/common.json
generated
5
src/i18n/locales/pt-BR/common.json
generated
|
|
@ -215,11 +215,6 @@
|
|||
"global": "global"
|
||||
}
|
||||
},
|
||||
"marketplace": {
|
||||
"mode": {
|
||||
"rulesCleanupFailed": "O modo foi removido com sucesso, mas falhou ao excluir a pasta de regras em {{rulesFolderPath}}. Você pode precisar excluí-la manualmente."
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"deleteMode": {
|
||||
"title": "Excluir Modo Personalizado",
|
||||
|
|
|
|||
69
src/i18n/locales/pt-BR/marketplace.json
generated
69
src/i18n/locales/pt-BR/marketplace.json
generated
|
|
@ -1,69 +0,0 @@
|
|||
{
|
||||
"type-group": {
|
||||
"modes": "Modos",
|
||||
"mcps": "Servidores MCP",
|
||||
"match": "correspondência"
|
||||
},
|
||||
"item-card": {
|
||||
"type-mode": "Modo",
|
||||
"type-mcp": "Servidor MCP",
|
||||
"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",
|
||||
"view": "Visualizar",
|
||||
"source": "Fonte"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "Pesquisar marketplace..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Tipo",
|
||||
"all": "Todos os tipos",
|
||||
"mode": "Modo",
|
||||
"mcpServer": "Servidor MCP"
|
||||
},
|
||||
"sort": {
|
||||
"label": "Ordenar por",
|
||||
"name": "Nome",
|
||||
"lastUpdated": "Última atualização"
|
||||
},
|
||||
"tags": {
|
||||
"label": "Tags",
|
||||
"clear": "Limpar tags",
|
||||
"placeholder": "Pesquisar tags...",
|
||||
"noResults": "Nenhuma tag encontrada.",
|
||||
"selected": "Mostrando itens com qualquer uma das tags selecionadas"
|
||||
},
|
||||
"installed": {
|
||||
"label": "Filtrar por status",
|
||||
"all": "Todos os itens",
|
||||
"installed": "Instalados",
|
||||
"notInstalled": "Não instalados"
|
||||
},
|
||||
"title": "Marketplace"
|
||||
},
|
||||
"done": "Concluído",
|
||||
"tabs": {
|
||||
"installed": "Instalado",
|
||||
"browse": "Navegar",
|
||||
"settings": "Configurações"
|
||||
},
|
||||
"items": {
|
||||
"empty": {
|
||||
"noItems": "Nenhum item do marketplace encontrado.",
|
||||
"emptyHint": "Tente ajustar seus filtros ou termos de pesquisa"
|
||||
}
|
||||
},
|
||||
"installation": {
|
||||
"installing": "Instalando item: \"{{itemName}}\"",
|
||||
"installSuccess": "\"{{itemName}}\" instalado com sucesso",
|
||||
"installError": "Falha ao instalar \"{{itemName}}\": {{errorMessage}}",
|
||||
"removing": "Removendo item: \"{{itemName}}\"",
|
||||
"removeSuccess": "\"{{itemName}}\" removido com sucesso",
|
||||
"removeError": "Falha ao remover \"{{itemName}}\": {{errorMessage}}"
|
||||
}
|
||||
}
|
||||
5
src/i18n/locales/ru/common.json
generated
5
src/i18n/locales/ru/common.json
generated
|
|
@ -215,11 +215,6 @@
|
|||
"global": "глобальный"
|
||||
}
|
||||
},
|
||||
"marketplace": {
|
||||
"mode": {
|
||||
"rulesCleanupFailed": "Режим успешно удален, но не удалось удалить папку правил в {{rulesFolderPath}}. Возможно, вам придется удалить ее вручную."
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"deleteMode": {
|
||||
"title": "Удалить пользовательский режим",
|
||||
|
|
|
|||
69
src/i18n/locales/ru/marketplace.json
generated
69
src/i18n/locales/ru/marketplace.json
generated
|
|
@ -1,69 +0,0 @@
|
|||
{
|
||||
"type-group": {
|
||||
"modes": "Режимы",
|
||||
"mcps": "MCP серверы",
|
||||
"match": "совпадение"
|
||||
},
|
||||
"item-card": {
|
||||
"type-mode": "Режим",
|
||||
"type-mcp": "MCP сервер",
|
||||
"type-other": "Другое",
|
||||
"by-author": "от {{author}}",
|
||||
"authors-profile": "Профиль автора",
|
||||
"remove-tag-filter": "Удалить фильтр тега: {{tag}}",
|
||||
"filter-by-tag": "Фильтровать по тегу: {{tag}}",
|
||||
"component-details": "Детали компонента",
|
||||
"view": "Просмотр",
|
||||
"source": "Источник"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "Поиск в marketplace..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Тип",
|
||||
"all": "Все типы",
|
||||
"mode": "Режим",
|
||||
"mcpServer": "MCP сервер"
|
||||
},
|
||||
"sort": {
|
||||
"label": "Сортировать по",
|
||||
"name": "Имя",
|
||||
"lastUpdated": "Последнее обновление"
|
||||
},
|
||||
"tags": {
|
||||
"label": "Теги",
|
||||
"clear": "Очистить теги",
|
||||
"placeholder": "Поиск тегов...",
|
||||
"noResults": "Теги не найдены.",
|
||||
"selected": "Показ элементов с любым из выбранных тегов"
|
||||
},
|
||||
"installed": {
|
||||
"label": "Фильтр по статусу",
|
||||
"all": "Все элементы",
|
||||
"installed": "Установленные",
|
||||
"notInstalled": "Не установленные"
|
||||
},
|
||||
"title": "Marketplace"
|
||||
},
|
||||
"done": "Готово",
|
||||
"tabs": {
|
||||
"installed": "Установлено",
|
||||
"browse": "Обзор",
|
||||
"settings": "Настройки"
|
||||
},
|
||||
"items": {
|
||||
"empty": {
|
||||
"noItems": "Элементы marketplace не найдены.",
|
||||
"emptyHint": "Попробуйте настроить фильтры или поисковые термины"
|
||||
}
|
||||
},
|
||||
"installation": {
|
||||
"installing": "Установка элемента: \"{{itemName}}\"",
|
||||
"installSuccess": "\"{{itemName}}\" успешно установлен",
|
||||
"installError": "Не удалось установить \"{{itemName}}\": {{errorMessage}}",
|
||||
"removing": "Удаление элемента: \"{{itemName}}\"",
|
||||
"removeSuccess": "\"{{itemName}}\" успешно удален",
|
||||
"removeError": "Не удалось удалить \"{{itemName}}\": {{errorMessage}}"
|
||||
}
|
||||
}
|
||||
5
src/i18n/locales/tr/common.json
generated
5
src/i18n/locales/tr/common.json
generated
|
|
@ -215,11 +215,6 @@
|
|||
"global": "küresel"
|
||||
}
|
||||
},
|
||||
"marketplace": {
|
||||
"mode": {
|
||||
"rulesCleanupFailed": "Mod başarıyla kaldırıldı, ancak {{rulesFolderPath}} konumundaki kurallar klasörü silinemedi. Manuel olarak silmeniz gerekebilir."
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"deleteMode": {
|
||||
"title": "Özel Modu Sil",
|
||||
|
|
|
|||
69
src/i18n/locales/tr/marketplace.json
generated
69
src/i18n/locales/tr/marketplace.json
generated
|
|
@ -1,69 +0,0 @@
|
|||
{
|
||||
"type-group": {
|
||||
"modes": "Modlar",
|
||||
"mcps": "MCP Sunucuları",
|
||||
"match": "eşleşme"
|
||||
},
|
||||
"item-card": {
|
||||
"type-mode": "Mod",
|
||||
"type-mcp": "MCP Sunucusu",
|
||||
"type-other": "Diğer",
|
||||
"by-author": "{{author}} tarafından",
|
||||
"authors-profile": "Yazarın Profili",
|
||||
"remove-tag-filter": "Etiket filtresini kaldır: {{tag}}",
|
||||
"filter-by-tag": "Etikete göre filtrele: {{tag}}",
|
||||
"component-details": "Bileşen Detayları",
|
||||
"view": "Görüntüle",
|
||||
"source": "Kaynak"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "Marketplace'te ara..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Tür",
|
||||
"all": "Tüm Türler",
|
||||
"mode": "Mod",
|
||||
"mcpServer": "MCP Sunucusu"
|
||||
},
|
||||
"sort": {
|
||||
"label": "Sırala",
|
||||
"name": "İsim",
|
||||
"lastUpdated": "Son Güncelleme"
|
||||
},
|
||||
"tags": {
|
||||
"label": "Etiketler",
|
||||
"clear": "Etiketleri temizle",
|
||||
"placeholder": "Etiket ara...",
|
||||
"noResults": "Etiket bulunamadı.",
|
||||
"selected": "Seçilen etiketlerden herhangi birine sahip öğeleri göster"
|
||||
},
|
||||
"installed": {
|
||||
"label": "Duruma göre filtrele",
|
||||
"all": "Tüm Öğeler",
|
||||
"installed": "Yüklü",
|
||||
"notInstalled": "Yüklü Değil"
|
||||
},
|
||||
"title": "Marketplace"
|
||||
},
|
||||
"done": "Tamam",
|
||||
"tabs": {
|
||||
"installed": "Yüklü",
|
||||
"browse": "Gözat",
|
||||
"settings": "Ayarlar"
|
||||
},
|
||||
"items": {
|
||||
"empty": {
|
||||
"noItems": "Marketplace öğesi bulunamadı.",
|
||||
"emptyHint": "Filtrelerinizi veya arama terimlerinizi ayarlamayı deneyin"
|
||||
}
|
||||
},
|
||||
"installation": {
|
||||
"installing": "Öğe yükleniyor: \"{{itemName}}\"",
|
||||
"installSuccess": "\"{{itemName}}\" başarıyla yüklendi",
|
||||
"installError": "\"{{itemName}}\" yüklenemedi: {{errorMessage}}",
|
||||
"removing": "Öğe kaldırılıyor: \"{{itemName}}\"",
|
||||
"removeSuccess": "\"{{itemName}}\" başarıyla kaldırıldı",
|
||||
"removeError": "\"{{itemName}}\" kaldırılamadı: {{errorMessage}}"
|
||||
}
|
||||
}
|
||||
5
src/i18n/locales/vi/common.json
generated
5
src/i18n/locales/vi/common.json
generated
|
|
@ -215,11 +215,6 @@
|
|||
"global": "toàn cầu"
|
||||
}
|
||||
},
|
||||
"marketplace": {
|
||||
"mode": {
|
||||
"rulesCleanupFailed": "Đã xóa chế độ thành công, nhưng không thể xóa thư mục quy tắc tại {{rulesFolderPath}}. Bạn có thể cần xóa thủ công."
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"deleteMode": {
|
||||
"title": "Xóa chế độ tùy chỉnh",
|
||||
|
|
|
|||
69
src/i18n/locales/vi/marketplace.json
generated
69
src/i18n/locales/vi/marketplace.json
generated
|
|
@ -1,69 +0,0 @@
|
|||
{
|
||||
"type-group": {
|
||||
"modes": "Chế độ",
|
||||
"mcps": "Máy chủ MCP",
|
||||
"match": "khớp"
|
||||
},
|
||||
"item-card": {
|
||||
"type-mode": "Chế độ",
|
||||
"type-mcp": "Máy chủ MCP",
|
||||
"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",
|
||||
"view": "Xem",
|
||||
"source": "Nguồn"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "Tìm kiếm marketplace..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Loại",
|
||||
"all": "Tất cả loại",
|
||||
"mode": "Chế độ",
|
||||
"mcpServer": "Máy chủ MCP"
|
||||
},
|
||||
"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 thẻ",
|
||||
"placeholder": "Tìm thẻ...",
|
||||
"noResults": "Không tìm thấy thẻ nào.",
|
||||
"selected": "Hiển thị các mục có bất kỳ thẻ nào được chọn"
|
||||
},
|
||||
"installed": {
|
||||
"label": "Lọc theo trạng thái",
|
||||
"all": "Tất cả các mục",
|
||||
"installed": "Đã cài đặt",
|
||||
"notInstalled": "Chưa cài đặt"
|
||||
},
|
||||
"title": "Marketplace"
|
||||
},
|
||||
"done": "Hoàn thành",
|
||||
"tabs": {
|
||||
"installed": "Đã cài đặt",
|
||||
"browse": "Duyệt",
|
||||
"settings": "Cài đặt"
|
||||
},
|
||||
"items": {
|
||||
"empty": {
|
||||
"noItems": "Không tìm thấy mục marketplace nào.",
|
||||
"emptyHint": "Thử điều chỉnh bộ lọc hoặc từ khóa tìm kiếm"
|
||||
}
|
||||
},
|
||||
"installation": {
|
||||
"installing": "Đang cài đặt mục: \"{{itemName}}\"",
|
||||
"installSuccess": "\"{{itemName}}\" đã được cài đặt thành công",
|
||||
"installError": "Cài đặt \"{{itemName}}\" thất bại: {{errorMessage}}",
|
||||
"removing": "Đang xóa mục: \"{{itemName}}\"",
|
||||
"removeSuccess": "\"{{itemName}}\" đã được xóa thành công",
|
||||
"removeError": "Xóa \"{{itemName}}\" thất bại: {{errorMessage}}"
|
||||
}
|
||||
}
|
||||
5
src/i18n/locales/zh-CN/common.json
generated
5
src/i18n/locales/zh-CN/common.json
generated
|
|
@ -220,11 +220,6 @@
|
|||
"global": "全局"
|
||||
}
|
||||
},
|
||||
"marketplace": {
|
||||
"mode": {
|
||||
"rulesCleanupFailed": "模式已成功移除,但无法删除位于 {{rulesFolderPath}} 的规则文件夹。您可能需要手动删除。"
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"deleteMode": {
|
||||
"title": "删除自定义模式",
|
||||
|
|
|
|||
69
src/i18n/locales/zh-CN/marketplace.json
generated
69
src/i18n/locales/zh-CN/marketplace.json
generated
|
|
@ -1,69 +0,0 @@
|
|||
{
|
||||
"type-group": {
|
||||
"modes": "模式",
|
||||
"mcps": "MCP 服务",
|
||||
"match": "匹配"
|
||||
},
|
||||
"item-card": {
|
||||
"type-mode": "模式",
|
||||
"type-mcp": "MCP 服务",
|
||||
"type-other": "其他",
|
||||
"by-author": "作者:{{author}}",
|
||||
"authors-profile": "作者资料",
|
||||
"remove-tag-filter": "移除标签过滤器:{{tag}}",
|
||||
"filter-by-tag": "按标签过滤:{{tag}}",
|
||||
"component-details": "组件详情",
|
||||
"view": "查看",
|
||||
"source": "来源"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "搜索 Marketplace..."
|
||||
},
|
||||
"type": {
|
||||
"label": "类型",
|
||||
"all": "所有类型",
|
||||
"mode": "模式",
|
||||
"mcpServer": "MCP 服务"
|
||||
},
|
||||
"sort": {
|
||||
"label": "排序方式",
|
||||
"name": "名称",
|
||||
"lastUpdated": "最后更新"
|
||||
},
|
||||
"tags": {
|
||||
"label": "标签",
|
||||
"clear": "清除标签",
|
||||
"placeholder": "搜索标签...",
|
||||
"noResults": "未找到标签。",
|
||||
"selected": "显示包含任一选中标签的项目"
|
||||
},
|
||||
"installed": {
|
||||
"label": "按状态筛选",
|
||||
"all": "所有项目",
|
||||
"installed": "已安装",
|
||||
"notInstalled": "未安装"
|
||||
},
|
||||
"title": "Marketplace"
|
||||
},
|
||||
"done": "完成",
|
||||
"tabs": {
|
||||
"installed": "已安装",
|
||||
"browse": "浏览",
|
||||
"settings": "设置"
|
||||
},
|
||||
"items": {
|
||||
"empty": {
|
||||
"noItems": "未找到 Marketplace 项目。",
|
||||
"emptyHint": "尝试调整过滤器或搜索条件"
|
||||
}
|
||||
},
|
||||
"installation": {
|
||||
"installing": "正在安装项目:\"{{itemName}}\"",
|
||||
"installSuccess": "\"{{itemName}}\" 安装成功",
|
||||
"installError": "\"{{itemName}}\" 安装失败:{{errorMessage}}",
|
||||
"removing": "正在移除项目:\"{{itemName}}\"",
|
||||
"removeSuccess": "\"{{itemName}}\" 移除成功",
|
||||
"removeError": "\"{{itemName}}\" 移除失败:{{errorMessage}}"
|
||||
}
|
||||
}
|
||||
5
src/i18n/locales/zh-TW/common.json
generated
5
src/i18n/locales/zh-TW/common.json
generated
|
|
@ -215,11 +215,6 @@
|
|||
"global": "全域"
|
||||
}
|
||||
},
|
||||
"marketplace": {
|
||||
"mode": {
|
||||
"rulesCleanupFailed": "模式已成功移除,但無法刪除位於 {{rulesFolderPath}} 的規則資料夾。您可能需要手動刪除。"
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"deleteMode": {
|
||||
"title": "刪除自訂模式",
|
||||
|
|
|
|||
69
src/i18n/locales/zh-TW/marketplace.json
generated
69
src/i18n/locales/zh-TW/marketplace.json
generated
|
|
@ -1,69 +0,0 @@
|
|||
{
|
||||
"type-group": {
|
||||
"modes": "模式",
|
||||
"mcps": "MCP 伺服器",
|
||||
"match": "符合"
|
||||
},
|
||||
"item-card": {
|
||||
"type-mode": "模式",
|
||||
"type-mcp": "MCP 伺服器",
|
||||
"type-other": "其他",
|
||||
"by-author": "作者:{{author}}",
|
||||
"authors-profile": "作者檔案",
|
||||
"remove-tag-filter": "移除標籤篩選器:{{tag}}",
|
||||
"filter-by-tag": "依標籤篩選:{{tag}}",
|
||||
"component-details": "元件詳情",
|
||||
"view": "檢視",
|
||||
"source": "來源"
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "搜尋 Marketplace..."
|
||||
},
|
||||
"type": {
|
||||
"label": "類型",
|
||||
"all": "所有類型",
|
||||
"mode": "模式",
|
||||
"mcpServer": "MCP 伺服器"
|
||||
},
|
||||
"sort": {
|
||||
"label": "排序方式",
|
||||
"name": "名稱",
|
||||
"lastUpdated": "最後更新"
|
||||
},
|
||||
"tags": {
|
||||
"label": "標籤",
|
||||
"clear": "清除標籤",
|
||||
"placeholder": "搜尋標籤...",
|
||||
"noResults": "找不到標籤。",
|
||||
"selected": "顯示包含任一選取標籤的項目"
|
||||
},
|
||||
"installed": {
|
||||
"label": "按狀態篩選",
|
||||
"all": "所有項目",
|
||||
"installed": "已安裝",
|
||||
"notInstalled": "未安裝"
|
||||
},
|
||||
"title": "Marketplace"
|
||||
},
|
||||
"done": "完成",
|
||||
"tabs": {
|
||||
"installed": "已安裝",
|
||||
"browse": "瀏覽",
|
||||
"settings": "設定"
|
||||
},
|
||||
"items": {
|
||||
"empty": {
|
||||
"noItems": "找不到 Marketplace 項目。",
|
||||
"emptyHint": "嘗試調整篩選器或搜尋條件"
|
||||
}
|
||||
},
|
||||
"installation": {
|
||||
"installing": "正在安裝項目:「{{itemName}}」",
|
||||
"installSuccess": "「{{itemName}}」安裝成功",
|
||||
"installError": "「{{itemName}}」安裝失敗:{{errorMessage}}",
|
||||
"removing": "正在移除項目:「{{itemName}}」",
|
||||
"removeSuccess": "「{{itemName}}」移除成功",
|
||||
"removeError": "「{{itemName}}」移除失敗:{{errorMessage}}"
|
||||
}
|
||||
}
|
||||
|
|
@ -80,11 +80,6 @@
|
|||
"title": "%command.history.title%",
|
||||
"icon": "$(history)"
|
||||
},
|
||||
{
|
||||
"command": "roo-cline.marketplaceButtonClicked",
|
||||
"title": "%command.marketplace.title%",
|
||||
"icon": "$(extensions)"
|
||||
},
|
||||
{
|
||||
"command": "roo-cline.popoutButtonClicked",
|
||||
"title": "%command.openInEditor.title%",
|
||||
|
|
@ -228,11 +223,6 @@
|
|||
"group": "navigation@3",
|
||||
"when": "view == roo-cline.SidebarProvider"
|
||||
},
|
||||
{
|
||||
"command": "roo-cline.marketplaceButtonClicked",
|
||||
"group": "navigation@4",
|
||||
"when": "view == roo-cline.SidebarProvider"
|
||||
},
|
||||
{
|
||||
"command": "roo-cline.historyButtonClicked",
|
||||
"group": "overflow@1",
|
||||
|
|
@ -260,11 +250,6 @@
|
|||
"group": "navigation@3",
|
||||
"when": "activeWebviewPanelId == roo-cline.TabPanelProvider"
|
||||
},
|
||||
{
|
||||
"command": "roo-cline.marketplaceButtonClicked",
|
||||
"group": "navigation@4",
|
||||
"when": "activeWebviewPanelId == roo-cline.TabPanelProvider"
|
||||
},
|
||||
{
|
||||
"command": "roo-cline.historyButtonClicked",
|
||||
"group": "overflow@1",
|
||||
|
|
|
|||
1
src/package.nls.ca.json
generated
1
src/package.nls.ca.json
generated
|
|
@ -20,7 +20,6 @@
|
|||
"views.terminalMenu.label": "Roo Code",
|
||||
"views.sidebar.name": "Roo Code",
|
||||
"command.history.title": "Historial de Tasques",
|
||||
"command.marketplace.title": "Mercat",
|
||||
"command.openInEditor.title": "Obrir a l'Editor",
|
||||
"command.cloud.title": "Cloud",
|
||||
"command.settings.title": "Configuració",
|
||||
|
|
|
|||
1
src/package.nls.de.json
generated
1
src/package.nls.de.json
generated
|
|
@ -20,7 +20,6 @@
|
|||
"views.terminalMenu.label": "Roo Code",
|
||||
"views.sidebar.name": "Roo Code",
|
||||
"command.history.title": "Aufgabenverlauf",
|
||||
"command.marketplace.title": "Marktplatz",
|
||||
"command.openInEditor.title": "Im Editor Öffnen",
|
||||
"command.cloud.title": "Cloud",
|
||||
"command.settings.title": "Einstellungen",
|
||||
|
|
|
|||
1
src/package.nls.es.json
generated
1
src/package.nls.es.json
generated
|
|
@ -20,7 +20,6 @@
|
|||
"views.terminalMenu.label": "Roo Code",
|
||||
"views.sidebar.name": "Roo Code",
|
||||
"command.history.title": "Historial de Tareas",
|
||||
"command.marketplace.title": "Mercado",
|
||||
"command.openInEditor.title": "Abrir en Editor",
|
||||
"command.cloud.title": "Cloud",
|
||||
"command.settings.title": "Configuración",
|
||||
|
|
|
|||
1
src/package.nls.fr.json
generated
1
src/package.nls.fr.json
generated
|
|
@ -20,7 +20,6 @@
|
|||
"views.terminalMenu.label": "Roo Code",
|
||||
"views.sidebar.name": "Roo Code",
|
||||
"command.history.title": "Historique des Tâches",
|
||||
"command.marketplace.title": "Marché",
|
||||
"command.openInEditor.title": "Ouvrir dans l'Éditeur",
|
||||
"command.cloud.title": "Cloud",
|
||||
"command.settings.title": "Paramètres",
|
||||
|
|
|
|||
1
src/package.nls.hi.json
generated
1
src/package.nls.hi.json
generated
|
|
@ -20,7 +20,6 @@
|
|||
"views.terminalMenu.label": "Roo Code",
|
||||
"views.sidebar.name": "Roo Code",
|
||||
"command.history.title": "कार्य इतिहास",
|
||||
"command.marketplace.title": "मार्केटप्लेस",
|
||||
"command.openInEditor.title": "एडिटर में खोलें",
|
||||
"command.cloud.title": "Cloud",
|
||||
"command.settings.title": "सेटिंग्स",
|
||||
|
|
|
|||
1
src/package.nls.id.json
generated
1
src/package.nls.id.json
generated
|
|
@ -7,7 +7,6 @@
|
|||
"views.sidebar.name": "Roo Code",
|
||||
"command.newTask.title": "Tugas Baru",
|
||||
"command.history.title": "Riwayat Tugas",
|
||||
"command.marketplace.title": "Marketplace",
|
||||
"command.openInEditor.title": "Buka di Editor",
|
||||
"command.cloud.title": "Cloud",
|
||||
"command.settings.title": "Pengaturan",
|
||||
|
|
|
|||
1
src/package.nls.it.json
generated
1
src/package.nls.it.json
generated
|
|
@ -20,7 +20,6 @@
|
|||
"views.terminalMenu.label": "Roo Code",
|
||||
"views.sidebar.name": "Roo Code",
|
||||
"command.history.title": "Cronologia Attività",
|
||||
"command.marketplace.title": "Marketplace",
|
||||
"command.openInEditor.title": "Apri nell'Editor",
|
||||
"command.cloud.title": "Cloud",
|
||||
"command.settings.title": "Impostazioni",
|
||||
|
|
|
|||
1
src/package.nls.ja.json
generated
1
src/package.nls.ja.json
generated
|
|
@ -7,7 +7,6 @@
|
|||
"views.sidebar.name": "Roo Code",
|
||||
"command.newTask.title": "新しいタスク",
|
||||
"command.history.title": "タスク履歴",
|
||||
"command.marketplace.title": "マーケットプレイス",
|
||||
"command.openInEditor.title": "エディタで開く",
|
||||
"command.cloud.title": "Cloud",
|
||||
"command.settings.title": "設定",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
"views.sidebar.name": "Roo Code",
|
||||
"command.newTask.title": "New Task",
|
||||
"command.history.title": "Task History",
|
||||
"command.marketplace.title": "Marketplace",
|
||||
"command.openInEditor.title": "Open in Editor",
|
||||
"command.cloud.title": "Cloud",
|
||||
"command.settings.title": "Settings",
|
||||
|
|
|
|||
1
src/package.nls.ko.json
generated
1
src/package.nls.ko.json
generated
|
|
@ -20,7 +20,6 @@
|
|||
"views.terminalMenu.label": "Roo Code",
|
||||
"views.sidebar.name": "Roo Code",
|
||||
"command.history.title": "작업 기록",
|
||||
"command.marketplace.title": "마켓플레이스",
|
||||
"command.openInEditor.title": "에디터에서 열기",
|
||||
"command.cloud.title": "Cloud",
|
||||
"command.settings.title": "설정",
|
||||
|
|
|
|||
1
src/package.nls.nl.json
generated
1
src/package.nls.nl.json
generated
|
|
@ -7,7 +7,6 @@
|
|||
"views.sidebar.name": "Roo Code",
|
||||
"command.newTask.title": "Nieuwe Taak",
|
||||
"command.history.title": "Taakgeschiedenis",
|
||||
"command.marketplace.title": "Marktplaats",
|
||||
"command.openInEditor.title": "Openen in Editor",
|
||||
"command.cloud.title": "Cloud",
|
||||
"command.settings.title": "Instellingen",
|
||||
|
|
|
|||
1
src/package.nls.pl.json
generated
1
src/package.nls.pl.json
generated
|
|
@ -20,7 +20,6 @@
|
|||
"views.terminalMenu.label": "Roo Code",
|
||||
"views.sidebar.name": "Roo Code",
|
||||
"command.history.title": "Historia Zadań",
|
||||
"command.marketplace.title": "Marketplace",
|
||||
"command.openInEditor.title": "Otwórz w Edytorze",
|
||||
"command.cloud.title": "Cloud",
|
||||
"command.settings.title": "Ustawienia",
|
||||
|
|
|
|||
1
src/package.nls.pt-BR.json
generated
1
src/package.nls.pt-BR.json
generated
|
|
@ -20,7 +20,6 @@
|
|||
"views.terminalMenu.label": "Roo Code",
|
||||
"views.sidebar.name": "Roo Code",
|
||||
"command.history.title": "Histórico de Tarefas",
|
||||
"command.marketplace.title": "Marketplace",
|
||||
"command.openInEditor.title": "Abrir no Editor",
|
||||
"command.cloud.title": "Cloud",
|
||||
"command.settings.title": "Configurações",
|
||||
|
|
|
|||
1
src/package.nls.ru.json
generated
1
src/package.nls.ru.json
generated
|
|
@ -7,7 +7,6 @@
|
|||
"views.sidebar.name": "Roo Code",
|
||||
"command.newTask.title": "Новая задача",
|
||||
"command.history.title": "История задач",
|
||||
"command.marketplace.title": "Маркетплейс",
|
||||
"command.openInEditor.title": "Открыть в редакторе",
|
||||
"command.cloud.title": "Cloud",
|
||||
"command.settings.title": "Настройки",
|
||||
|
|
|
|||
1
src/package.nls.tr.json
generated
1
src/package.nls.tr.json
generated
|
|
@ -20,7 +20,6 @@
|
|||
"views.terminalMenu.label": "Roo Code",
|
||||
"views.sidebar.name": "Roo Code",
|
||||
"command.history.title": "Görev Geçmişi",
|
||||
"command.marketplace.title": "Marketplace",
|
||||
"command.openInEditor.title": "Düzenleyicide Aç",
|
||||
"command.cloud.title": "Cloud",
|
||||
"command.settings.title": "Ayarlar",
|
||||
|
|
|
|||
1
src/package.nls.vi.json
generated
1
src/package.nls.vi.json
generated
|
|
@ -20,7 +20,6 @@
|
|||
"views.terminalMenu.label": "Roo Code",
|
||||
"views.sidebar.name": "Roo Code",
|
||||
"command.history.title": "Lịch Sử Tác Vụ",
|
||||
"command.marketplace.title": "Marketplace",
|
||||
"command.openInEditor.title": "Mở trong Trình Soạn Thảo",
|
||||
"command.cloud.title": "Cloud",
|
||||
"command.settings.title": "Cài Đặt",
|
||||
|
|
|
|||
1
src/package.nls.zh-CN.json
generated
1
src/package.nls.zh-CN.json
generated
|
|
@ -20,7 +20,6 @@
|
|||
"views.terminalMenu.label": "Roo Code",
|
||||
"views.sidebar.name": "Roo Code",
|
||||
"command.history.title": "任务历史记录",
|
||||
"command.marketplace.title": "应用市场",
|
||||
"command.openInEditor.title": "在编辑器中打开",
|
||||
"command.cloud.title": "Cloud",
|
||||
"command.settings.title": "设置",
|
||||
|
|
|
|||
1
src/package.nls.zh-TW.json
generated
1
src/package.nls.zh-TW.json
generated
|
|
@ -20,7 +20,6 @@
|
|||
"views.terminalMenu.label": "Roo Code",
|
||||
"views.sidebar.name": "Roo Code",
|
||||
"command.history.title": "工作歷史記錄",
|
||||
"command.marketplace.title": "應用市場",
|
||||
"command.openInEditor.title": "在編輯器中開啟",
|
||||
"command.cloud.title": "Cloud",
|
||||
"command.settings.title": "設定",
|
||||
|
|
|
|||
|
|
@ -1,311 +0,0 @@
|
|||
import * as fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
|
||||
import * as vscode from "vscode"
|
||||
import * as yaml from "yaml"
|
||||
|
||||
import type { OrganizationSettings, MarketplaceItem, MarketplaceItemType, McpMarketplaceItem } from "@roo-code/types"
|
||||
import { CloudService } from "@roo-code/cloud"
|
||||
|
||||
import { GlobalFileNames } from "../../shared/globalFileNames"
|
||||
import { ensureSettingsDirectoryExists } from "../../utils/globalContext"
|
||||
import { t } from "../../i18n"
|
||||
import type { CustomModesManager } from "../../core/config/CustomModesManager"
|
||||
|
||||
import { RemoteConfigLoader } from "./RemoteConfigLoader"
|
||||
import { SimpleInstaller } from "./SimpleInstaller"
|
||||
|
||||
export interface MarketplaceItemsResponse {
|
||||
organizationMcps: MarketplaceItem[]
|
||||
marketplaceItems: MarketplaceItem[]
|
||||
errors?: string[]
|
||||
}
|
||||
|
||||
export class MarketplaceManager {
|
||||
private configLoader: RemoteConfigLoader
|
||||
private installer: SimpleInstaller
|
||||
|
||||
constructor(
|
||||
private readonly context: vscode.ExtensionContext,
|
||||
private readonly customModesManager?: CustomModesManager,
|
||||
) {
|
||||
this.configLoader = new RemoteConfigLoader()
|
||||
this.installer = new SimpleInstaller(context, customModesManager)
|
||||
}
|
||||
|
||||
async getMarketplaceItems(): Promise<MarketplaceItemsResponse> {
|
||||
try {
|
||||
const errors: string[] = []
|
||||
|
||||
let orgSettings: OrganizationSettings | undefined
|
||||
|
||||
try {
|
||||
if (CloudService.hasInstance() && CloudService.instance.isAuthenticated()) {
|
||||
orgSettings = CloudService.instance.getOrganizationSettings()
|
||||
}
|
||||
} catch (orgError) {
|
||||
console.warn("Failed to load organization settings:", orgError)
|
||||
const orgErrorMessage = orgError instanceof Error ? orgError.message : String(orgError)
|
||||
errors.push(`Organization settings: ${orgErrorMessage}`)
|
||||
}
|
||||
|
||||
const allMarketplaceItems = await this.configLoader.loadAllItems(orgSettings?.hideMarketplaceMcps)
|
||||
let organizationMcps: MarketplaceItem[] = []
|
||||
let marketplaceItems = allMarketplaceItems
|
||||
|
||||
if (orgSettings) {
|
||||
if (orgSettings.mcps && orgSettings.mcps.length > 0) {
|
||||
organizationMcps = orgSettings.mcps.map(
|
||||
(mcp: McpMarketplaceItem): MarketplaceItem => ({
|
||||
...mcp,
|
||||
type: "mcp" as const,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
if (orgSettings.hiddenMcps && orgSettings.hiddenMcps.length > 0) {
|
||||
const hiddenMcpIds = new Set(orgSettings.hiddenMcps)
|
||||
marketplaceItems = allMarketplaceItems.filter(
|
||||
(item) => item.type !== "mcp" || !hiddenMcpIds.has(item.id),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
organizationMcps,
|
||||
marketplaceItems,
|
||||
errors: errors.length > 0 ? errors : undefined,
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
console.error("Failed to load marketplace items:", error)
|
||||
|
||||
return {
|
||||
organizationMcps: [],
|
||||
marketplaceItems: [],
|
||||
errors: [errorMessage],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getCurrentItems(): Promise<MarketplaceItem[]> {
|
||||
const result = await this.getMarketplaceItems()
|
||||
return [...result.organizationMcps, ...result.marketplaceItems]
|
||||
}
|
||||
|
||||
filterItems(
|
||||
items: MarketplaceItem[],
|
||||
filters: { type?: MarketplaceItemType; search?: string; tags?: string[] },
|
||||
): MarketplaceItem[] {
|
||||
return items.filter((item) => {
|
||||
// Type filter
|
||||
if (filters.type && item.type !== filters.type) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Search filter
|
||||
if (filters.search) {
|
||||
const searchTerm = filters.search.toLowerCase()
|
||||
const searchableText = `${item.name} ${item.description}`.toLowerCase()
|
||||
if (!searchableText.includes(searchTerm)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Tags filter
|
||||
if (filters.tags?.length) {
|
||||
if (!item.tags?.some((tag) => filters.tags!.includes(tag))) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
async updateWithFilteredItems(filters: {
|
||||
type?: MarketplaceItemType
|
||||
search?: string
|
||||
tags?: string[]
|
||||
}): Promise<MarketplaceItem[]> {
|
||||
const allItems = await this.getCurrentItems()
|
||||
|
||||
if (!filters.type && !filters.search && (!filters.tags || filters.tags.length === 0)) {
|
||||
return allItems
|
||||
}
|
||||
|
||||
return this.filterItems(allItems, filters)
|
||||
}
|
||||
|
||||
async installMarketplaceItem(
|
||||
item: MarketplaceItem,
|
||||
options?: { target?: "global" | "project"; parameters?: Record<string, any> },
|
||||
): Promise<string> {
|
||||
const { target = "project", parameters } = options || {}
|
||||
|
||||
vscode.window.showInformationMessage(t("marketplace:installation.installing", { itemName: item.name }))
|
||||
|
||||
try {
|
||||
const result = await this.installer.installItem(item, { target, parameters })
|
||||
vscode.window.showInformationMessage(t("marketplace:installation.installSuccess", { itemName: item.name }))
|
||||
|
||||
// Open the config file that was modified, optionally at the specific line
|
||||
const document = await vscode.workspace.openTextDocument(result.filePath)
|
||||
const options: vscode.TextDocumentShowOptions = {}
|
||||
|
||||
if (result.line !== undefined) {
|
||||
// Position cursor at the line where content was added
|
||||
options.selection = new vscode.Range(result.line - 1, 0, result.line - 1, 0)
|
||||
}
|
||||
|
||||
await vscode.window.showTextDocument(document, options)
|
||||
|
||||
return result.filePath
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
vscode.window.showErrorMessage(
|
||||
t("marketplace:installation.installError", { itemName: item.name, errorMessage }),
|
||||
)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async removeInstalledMarketplaceItem(
|
||||
item: MarketplaceItem,
|
||||
options?: { target?: "global" | "project" },
|
||||
): Promise<void> {
|
||||
const { target = "project" } = options || {}
|
||||
|
||||
vscode.window.showInformationMessage(t("marketplace:installation.removing", { itemName: item.name }))
|
||||
|
||||
try {
|
||||
await this.installer.removeItem(item, { target })
|
||||
vscode.window.showInformationMessage(t("marketplace:installation.removeSuccess", { itemName: item.name }))
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
vscode.window.showErrorMessage(
|
||||
t("marketplace:installation.removeError", { itemName: item.name, errorMessage }),
|
||||
)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async cleanup(): Promise<void> {
|
||||
// Clear API cache if needed
|
||||
this.configLoader.clearCache()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get installation metadata by checking config files for installed items
|
||||
*/
|
||||
async getInstallationMetadata(): Promise<{
|
||||
project: Record<string, { type: string }>
|
||||
global: Record<string, { type: string }>
|
||||
}> {
|
||||
const metadata = {
|
||||
project: {} as Record<string, { type: string }>,
|
||||
global: {} as Record<string, { type: string }>,
|
||||
}
|
||||
|
||||
// Check project-level installations
|
||||
await this.checkProjectInstallations(metadata.project)
|
||||
|
||||
// Check global-level installations
|
||||
await this.checkGlobalInstallations(metadata.global)
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for project-level installed items
|
||||
*/
|
||||
private async checkProjectInstallations(metadata: Record<string, { type: string }>): Promise<void> {
|
||||
try {
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0]
|
||||
if (!workspaceFolder) {
|
||||
return // No workspace, no project installations
|
||||
}
|
||||
|
||||
// Check modes in .roomodes
|
||||
const projectModesPath = path.join(workspaceFolder.uri.fsPath, ".roomodes")
|
||||
try {
|
||||
const content = await fs.readFile(projectModesPath, "utf-8")
|
||||
const data = yaml.parse(content)
|
||||
if (data?.customModes && Array.isArray(data.customModes)) {
|
||||
for (const mode of data.customModes) {
|
||||
if (mode.slug) {
|
||||
metadata[mode.slug] = {
|
||||
type: "mode",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// File doesn't exist or can't be read, skip
|
||||
}
|
||||
|
||||
// Check MCPs in .roo/mcp.json
|
||||
const projectMcpPath = path.join(workspaceFolder.uri.fsPath, ".roo", "mcp.json")
|
||||
try {
|
||||
const content = await fs.readFile(projectMcpPath, "utf-8")
|
||||
const data = JSON.parse(content)
|
||||
if (data?.mcpServers && typeof data.mcpServers === "object") {
|
||||
for (const serverName of Object.keys(data.mcpServers)) {
|
||||
metadata[serverName] = {
|
||||
type: "mcp",
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// File doesn't exist or can't be read, skip
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error checking project installations:", error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for global-level installed items
|
||||
*/
|
||||
private async checkGlobalInstallations(metadata: Record<string, { type: string }>): Promise<void> {
|
||||
try {
|
||||
const globalSettingsPath = await ensureSettingsDirectoryExists(this.context)
|
||||
|
||||
// Check global modes
|
||||
const globalModesPath = path.join(globalSettingsPath, GlobalFileNames.customModes)
|
||||
try {
|
||||
const content = await fs.readFile(globalModesPath, "utf-8")
|
||||
const data = yaml.parse(content)
|
||||
if (data?.customModes && Array.isArray(data.customModes)) {
|
||||
for (const mode of data.customModes) {
|
||||
if (mode.slug) {
|
||||
metadata[mode.slug] = {
|
||||
type: "mode",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// File doesn't exist or can't be read, skip
|
||||
}
|
||||
|
||||
// Check global MCPs
|
||||
const globalMcpPath = path.join(globalSettingsPath, GlobalFileNames.mcpSettings)
|
||||
try {
|
||||
const content = await fs.readFile(globalMcpPath, "utf-8")
|
||||
const data = JSON.parse(content)
|
||||
if (data?.mcpServers && typeof data.mcpServers === "object") {
|
||||
for (const serverName of Object.keys(data.mcpServers)) {
|
||||
metadata[serverName] = {
|
||||
type: "mcp",
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// File doesn't exist or can't be read, skip
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error checking global installations:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
import axios from "axios"
|
||||
import * as yaml from "yaml"
|
||||
import { z } from "zod"
|
||||
|
||||
import {
|
||||
type MarketplaceItem,
|
||||
type MarketplaceItemType,
|
||||
modeMarketplaceItemSchema,
|
||||
mcpMarketplaceItemSchema,
|
||||
} from "@roo-code/types"
|
||||
import { getRooCodeApiUrl } from "@roo-code/cloud"
|
||||
|
||||
const modeMarketplaceResponse = z.object({
|
||||
items: z.array(modeMarketplaceItemSchema),
|
||||
})
|
||||
|
||||
const mcpMarketplaceResponse = z.object({
|
||||
items: z.array(mcpMarketplaceItemSchema),
|
||||
})
|
||||
|
||||
export class RemoteConfigLoader {
|
||||
private apiBaseUrl: string
|
||||
private cache: Map<string, { data: MarketplaceItem[]; timestamp: number }> = new Map()
|
||||
private cacheDuration = 5 * 60 * 1000 // 5 minutes
|
||||
|
||||
constructor() {
|
||||
this.apiBaseUrl = getRooCodeApiUrl()
|
||||
}
|
||||
|
||||
async loadAllItems(hideMarketplaceMcps = false): Promise<MarketplaceItem[]> {
|
||||
const items: MarketplaceItem[] = []
|
||||
|
||||
const modesPromise = this.fetchModes()
|
||||
const mcpsPromise = hideMarketplaceMcps ? Promise.resolve([]) : this.fetchMcps()
|
||||
|
||||
const [modes, mcps] = await Promise.all([modesPromise, mcpsPromise])
|
||||
|
||||
items.push(...modes, ...mcps)
|
||||
return items
|
||||
}
|
||||
|
||||
private async fetchModes(): Promise<MarketplaceItem[]> {
|
||||
const cacheKey = "modes"
|
||||
const cached = this.getFromCache(cacheKey)
|
||||
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
|
||||
const data = await this.fetchWithRetry<string>(`${this.apiBaseUrl}/api/marketplace/modes`)
|
||||
|
||||
const yamlData = yaml.parse(data)
|
||||
const validated = modeMarketplaceResponse.parse(yamlData)
|
||||
|
||||
const items: MarketplaceItem[] = validated.items.map((item) => ({
|
||||
type: "mode" as const,
|
||||
...item,
|
||||
}))
|
||||
|
||||
this.setCache(cacheKey, items)
|
||||
return items
|
||||
}
|
||||
|
||||
private async fetchMcps(): Promise<MarketplaceItem[]> {
|
||||
const cacheKey = "mcps"
|
||||
const cached = this.getFromCache(cacheKey)
|
||||
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
|
||||
const data = await this.fetchWithRetry<string>(`${this.apiBaseUrl}/api/marketplace/mcps`)
|
||||
|
||||
const yamlData = yaml.parse(data)
|
||||
const validated = mcpMarketplaceResponse.parse(yamlData)
|
||||
|
||||
const items: MarketplaceItem[] = validated.items.map((item) => ({
|
||||
type: "mcp" as const,
|
||||
...item,
|
||||
}))
|
||||
|
||||
this.setCache(cacheKey, items)
|
||||
return items
|
||||
}
|
||||
|
||||
private async fetchWithRetry<T>(url: string, maxRetries = 3): Promise<T> {
|
||||
let lastError: Error
|
||||
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
const response = await axios.get(url, {
|
||||
timeout: 10000, // 10 second timeout
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
return response.data as T
|
||||
} catch (error) {
|
||||
lastError = error as Error
|
||||
if (i < maxRetries - 1) {
|
||||
// Exponential backoff: 1s, 2s, 4s
|
||||
const delay = Math.pow(2, i) * 1000
|
||||
await new Promise((resolve) => setTimeout(resolve, delay))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError!
|
||||
}
|
||||
|
||||
async getItem(id: string, type: MarketplaceItemType): Promise<MarketplaceItem | null> {
|
||||
const items = await this.loadAllItems()
|
||||
return items.find((item) => item.id === id && item.type === type) || null
|
||||
}
|
||||
|
||||
private getFromCache(key: string): MarketplaceItem[] | null {
|
||||
const cached = this.cache.get(key)
|
||||
if (!cached) return null
|
||||
|
||||
const now = Date.now()
|
||||
if (now - cached.timestamp > this.cacheDuration) {
|
||||
this.cache.delete(key)
|
||||
return null
|
||||
}
|
||||
|
||||
return cached.data
|
||||
}
|
||||
|
||||
private setCache(key: string, data: MarketplaceItem[]): void {
|
||||
this.cache.set(key, {
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
}
|
||||
|
||||
clearCache(): void {
|
||||
this.cache.clear()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,384 +0,0 @@
|
|||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
import * as fs from "fs/promises"
|
||||
import * as yaml from "yaml"
|
||||
import type { MarketplaceItem, MarketplaceItemType, InstallMarketplaceItemOptions, McpParameter } from "@roo-code/types"
|
||||
import { GlobalFileNames } from "../../shared/globalFileNames"
|
||||
import { ensureSettingsDirectoryExists } from "../../utils/globalContext"
|
||||
import type { CustomModesManager } from "../../core/config/CustomModesManager"
|
||||
|
||||
export interface InstallOptions extends InstallMarketplaceItemOptions {
|
||||
target: "project" | "global"
|
||||
selectedIndex?: number // Which installation method to use (for array content)
|
||||
}
|
||||
|
||||
export class SimpleInstaller {
|
||||
constructor(
|
||||
private readonly context: vscode.ExtensionContext,
|
||||
private readonly customModesManager?: CustomModesManager,
|
||||
) {}
|
||||
|
||||
async installItem(item: MarketplaceItem, options: InstallOptions): Promise<{ filePath: string; line?: number }> {
|
||||
const { target } = options
|
||||
|
||||
switch (item.type) {
|
||||
case "mode":
|
||||
return await this.installMode(item, target)
|
||||
case "mcp":
|
||||
return await this.installMcp(item, target, options)
|
||||
default:
|
||||
throw new Error(`Unsupported item type: ${(item as any).type}`)
|
||||
}
|
||||
}
|
||||
|
||||
private async installMode(
|
||||
item: MarketplaceItem,
|
||||
target: "project" | "global",
|
||||
): Promise<{ filePath: string; line?: number }> {
|
||||
if (!item.content) {
|
||||
throw new Error("Mode item missing content")
|
||||
}
|
||||
|
||||
// Modes should always have string content, not array
|
||||
if (Array.isArray(item.content)) {
|
||||
throw new Error("Mode content should not be an array")
|
||||
}
|
||||
|
||||
// If CustomModesManager is available, use importModeWithRules
|
||||
if (this.customModesManager) {
|
||||
// Transform marketplace content to import format (wrap in customModes array)
|
||||
const importData = {
|
||||
customModes: [yaml.parse(item.content)],
|
||||
}
|
||||
const importYaml = yaml.stringify(importData)
|
||||
|
||||
// Call customModesManager.importModeWithRules
|
||||
const result = await this.customModesManager.importModeWithRules(importYaml, target)
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || "Failed to import mode")
|
||||
}
|
||||
|
||||
// Return the file path and line number for VS Code to open
|
||||
const filePath = await this.getModeFilePath(target)
|
||||
|
||||
// Try to find the line number where the mode was added
|
||||
let line: number | undefined
|
||||
try {
|
||||
const fileContent = await fs.readFile(filePath, "utf-8")
|
||||
const lines = fileContent.split("\n")
|
||||
const modeData = yaml.parse(item.content)
|
||||
|
||||
// Find the line containing the slug of the added mode
|
||||
if (modeData?.slug) {
|
||||
const slugLineIndex = lines.findIndex(
|
||||
(l) => l.includes(`slug: ${modeData.slug}`) || l.includes(`slug: "${modeData.slug}"`),
|
||||
)
|
||||
if (slugLineIndex >= 0) {
|
||||
line = slugLineIndex + 1 // Convert to 1-based line number
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// If we can't find the line number, that's okay
|
||||
}
|
||||
|
||||
return { filePath, line }
|
||||
}
|
||||
|
||||
// Fallback to original implementation if CustomModesManager is not available
|
||||
const filePath = await this.getModeFilePath(target)
|
||||
const modeData = yaml.parse(item.content)
|
||||
|
||||
// Read existing file or create new structure
|
||||
let existingData: any = { customModes: [] }
|
||||
try {
|
||||
const existing = await fs.readFile(filePath, "utf-8")
|
||||
const parsed = yaml.parse(existing)
|
||||
// Ensure we have a valid object with customModes array
|
||||
existingData = parsed && typeof parsed === "object" ? parsed : { customModes: [] }
|
||||
} catch (error: any) {
|
||||
if (error.code === "ENOENT") {
|
||||
// File doesn't exist, use default structure - this is fine
|
||||
existingData = { customModes: [] }
|
||||
} else if (error.name === "YAMLParseError" || error.message?.includes("YAML")) {
|
||||
// YAML parsing error - don't overwrite the file!
|
||||
const fileName = target === "project" ? ".roomodes" : "custom-modes.yaml"
|
||||
throw new Error(
|
||||
`Cannot install mode: The ${fileName} file contains invalid YAML. ` +
|
||||
`Please fix the syntax errors in the file before installing new modes.`,
|
||||
)
|
||||
} else {
|
||||
// Other unexpected errors - re-throw
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure customModes array exists
|
||||
if (!existingData.customModes) {
|
||||
existingData.customModes = []
|
||||
}
|
||||
|
||||
// The content is now a single mode object directly
|
||||
if (!modeData.slug) {
|
||||
throw new Error("Invalid mode content: mode missing slug")
|
||||
}
|
||||
|
||||
// Remove existing mode with same slug if it exists
|
||||
existingData.customModes = existingData.customModes.filter((mode: any) => mode.slug !== modeData.slug)
|
||||
|
||||
// Add the new mode
|
||||
existingData.customModes.push(modeData)
|
||||
const addedModeIndex = existingData.customModes.length - 1
|
||||
|
||||
// Write back to file
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
||||
const yamlContent = yaml.stringify(existingData, { lineWidth: 0 })
|
||||
await fs.writeFile(filePath, yamlContent, "utf-8")
|
||||
|
||||
// Calculate approximate line number where the new mode was added
|
||||
let line: number | undefined
|
||||
if (addedModeIndex >= 0) {
|
||||
const lines = yamlContent.split("\n")
|
||||
// Find the line containing the slug of the added mode
|
||||
const addedMode = existingData.customModes[addedModeIndex]
|
||||
if (addedMode?.slug) {
|
||||
const slugLineIndex = lines.findIndex(
|
||||
(l) => l.includes(`slug: ${addedMode.slug}`) || l.includes(`slug: "${addedMode.slug}"`),
|
||||
)
|
||||
if (slugLineIndex >= 0) {
|
||||
line = slugLineIndex + 1 // Convert to 1-based line number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { filePath, line }
|
||||
}
|
||||
|
||||
private async installMcp(
|
||||
item: MarketplaceItem,
|
||||
target: "project" | "global",
|
||||
options?: InstallOptions,
|
||||
): Promise<{ filePath: string; line?: number }> {
|
||||
if (!item.content) {
|
||||
throw new Error("MCP item missing content")
|
||||
}
|
||||
|
||||
// Get the content to use
|
||||
let contentToUse: string
|
||||
if (Array.isArray(item.content)) {
|
||||
// Array of McpInstallationMethod objects
|
||||
const index = options?.selectedIndex ?? 0
|
||||
const method = item.content[index] || item.content[0]
|
||||
contentToUse = method.content
|
||||
} else {
|
||||
contentToUse = item.content
|
||||
}
|
||||
|
||||
// Get method-specific parameters if using array content
|
||||
let methodParameters: McpParameter[] = []
|
||||
if (Array.isArray(item.content)) {
|
||||
const index = options?.selectedIndex ?? 0
|
||||
const method = item.content[index] || item.content[0]
|
||||
methodParameters = method.parameters || []
|
||||
}
|
||||
|
||||
// Merge parameters (method-specific override global)
|
||||
const itemParameters = item.type === "mcp" ? item.parameters || [] : []
|
||||
const allParameters = [...itemParameters, ...methodParameters]
|
||||
const uniqueParameters = Array.from(new Map(allParameters.map((p) => [p.key, p])).values())
|
||||
|
||||
// Replace parameters if provided
|
||||
if (options?.parameters && uniqueParameters.length > 0) {
|
||||
for (const param of uniqueParameters) {
|
||||
const value = options.parameters[param.key]
|
||||
if (value !== undefined) {
|
||||
contentToUse = contentToUse.replace(new RegExp(`{{${param.key}}}`, "g"), String(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle _selectedIndex from parameters if provided
|
||||
if (options?.parameters?._selectedIndex !== undefined && Array.isArray(item.content)) {
|
||||
const index = options.parameters._selectedIndex
|
||||
if (index >= 0 && index < item.content.length) {
|
||||
// Array of McpInstallationMethod objects
|
||||
const method = item.content[index]
|
||||
contentToUse = method.content
|
||||
methodParameters = method.parameters || []
|
||||
|
||||
// Re-merge parameters with the newly selected method
|
||||
const itemParametersForNewMethod = item.type === "mcp" ? item.parameters || [] : []
|
||||
const allParametersForNewMethod = [...itemParametersForNewMethod, ...methodParameters]
|
||||
const uniqueParametersForNewMethod = Array.from(
|
||||
new Map(allParametersForNewMethod.map((p) => [p.key, p])).values(),
|
||||
)
|
||||
|
||||
// Re-apply parameter replacements to the newly selected content
|
||||
for (const param of uniqueParametersForNewMethod) {
|
||||
const value = options.parameters[param.key]
|
||||
if (value !== undefined) {
|
||||
contentToUse = contentToUse.replace(new RegExp(`{{${param.key}}}`, "g"), String(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const filePath = await this.getMcpFilePath(target)
|
||||
const mcpData = JSON.parse(contentToUse)
|
||||
|
||||
// Read existing file or create new structure
|
||||
let existingData: any = { mcpServers: {} }
|
||||
try {
|
||||
const existing = await fs.readFile(filePath, "utf-8")
|
||||
existingData = JSON.parse(existing) || { mcpServers: {} }
|
||||
} catch (error: any) {
|
||||
if (error.code === "ENOENT") {
|
||||
// File doesn't exist, use default structure
|
||||
existingData = { mcpServers: {} }
|
||||
} else if (error instanceof SyntaxError) {
|
||||
// JSON parsing error - don't overwrite the file!
|
||||
const fileName = target === "project" ? ".roo/mcp.json" : "mcp-settings.json"
|
||||
throw new Error(
|
||||
`Cannot install MCP server: The ${fileName} file contains invalid JSON. ` +
|
||||
`Please fix the syntax errors in the file before installing new servers.`,
|
||||
)
|
||||
} else {
|
||||
// Other unexpected errors - re-throw
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure mcpServers object exists
|
||||
if (!existingData.mcpServers) {
|
||||
existingData.mcpServers = {}
|
||||
}
|
||||
|
||||
// Use the item id as the server name
|
||||
const serverName = item.id
|
||||
|
||||
// Add or update the single server
|
||||
existingData.mcpServers[serverName] = mcpData
|
||||
|
||||
// Write back to file
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
||||
const jsonContent = JSON.stringify(existingData, null, 2)
|
||||
await fs.writeFile(filePath, jsonContent, "utf-8")
|
||||
|
||||
// Calculate approximate line number where the new server was added
|
||||
let line: number | undefined
|
||||
if (serverName) {
|
||||
const lines = jsonContent.split("\n")
|
||||
// Find the line containing the server name
|
||||
const serverLineIndex = lines.findIndex((l) => l.includes(`"${serverName}"`))
|
||||
if (serverLineIndex >= 0) {
|
||||
line = serverLineIndex + 1 // Convert to 1-based line number
|
||||
}
|
||||
}
|
||||
|
||||
return { filePath, line }
|
||||
}
|
||||
|
||||
async removeItem(item: MarketplaceItem, options: InstallOptions): Promise<void> {
|
||||
const { target } = options
|
||||
|
||||
switch (item.type) {
|
||||
case "mode":
|
||||
await this.removeMode(item, target)
|
||||
break
|
||||
case "mcp":
|
||||
await this.removeMcp(item, target)
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported item type: ${(item as any).type}`)
|
||||
}
|
||||
}
|
||||
|
||||
private async removeMode(item: MarketplaceItem, target: "project" | "global"): Promise<void> {
|
||||
if (!this.customModesManager) {
|
||||
throw new Error("CustomModesManager is not available")
|
||||
}
|
||||
|
||||
// Parse the item content to get the slug
|
||||
let content: string
|
||||
if (Array.isArray(item.content)) {
|
||||
// Array of McpInstallationMethod objects - use first method
|
||||
content = item.content[0].content
|
||||
} else {
|
||||
content = item.content || ""
|
||||
}
|
||||
|
||||
let modeSlug: string
|
||||
try {
|
||||
const modeData = yaml.parse(content)
|
||||
modeSlug = modeData.slug
|
||||
} catch (error) {
|
||||
throw new Error("Invalid mode content: unable to parse YAML")
|
||||
}
|
||||
|
||||
if (!modeSlug) {
|
||||
throw new Error("Mode missing slug identifier")
|
||||
}
|
||||
|
||||
// Get the current modes to determine the source
|
||||
const modes = await this.customModesManager.getCustomModes()
|
||||
const mode = modes.find((m) => m.slug === modeSlug)
|
||||
|
||||
// Use CustomModesManager to delete the mode configuration
|
||||
// This also handles rules folder deletion
|
||||
await this.customModesManager.deleteCustomMode(modeSlug, true)
|
||||
}
|
||||
|
||||
private async removeMcp(item: MarketplaceItem, target: "project" | "global"): Promise<void> {
|
||||
const filePath = await this.getMcpFilePath(target)
|
||||
|
||||
try {
|
||||
const existing = await fs.readFile(filePath, "utf-8")
|
||||
const existingData = JSON.parse(existing)
|
||||
|
||||
if (existingData?.mcpServers) {
|
||||
// Parse the item content to get server names
|
||||
let content: string
|
||||
if (Array.isArray(item.content)) {
|
||||
// Array of McpInstallationMethod objects - use first method
|
||||
content = item.content[0].content
|
||||
} else {
|
||||
content = item.content
|
||||
}
|
||||
|
||||
const serverName = item.id
|
||||
delete existingData.mcpServers[serverName]
|
||||
|
||||
// Always write back the file, even if empty
|
||||
await fs.writeFile(filePath, JSON.stringify(existingData, null, 2), "utf-8")
|
||||
}
|
||||
} catch (error) {
|
||||
// File doesn't exist or other error, nothing to remove
|
||||
}
|
||||
}
|
||||
|
||||
private async getModeFilePath(target: "project" | "global"): Promise<string> {
|
||||
if (target === "project") {
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0]
|
||||
if (!workspaceFolder) {
|
||||
throw new Error("No workspace folder found")
|
||||
}
|
||||
return path.join(workspaceFolder.uri.fsPath, ".roomodes")
|
||||
} else {
|
||||
const globalSettingsPath = await ensureSettingsDirectoryExists(this.context)
|
||||
return path.join(globalSettingsPath, GlobalFileNames.customModes)
|
||||
}
|
||||
}
|
||||
|
||||
private async getMcpFilePath(target: "project" | "global"): Promise<string> {
|
||||
if (target === "project") {
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0]
|
||||
if (!workspaceFolder) {
|
||||
throw new Error("No workspace folder found")
|
||||
}
|
||||
return path.join(workspaceFolder.uri.fsPath, ".roo", "mcp.json")
|
||||
} else {
|
||||
const globalSettingsPath = await ensureSettingsDirectoryExists(this.context)
|
||||
return path.join(globalSettingsPath, GlobalFileNames.mcpSettings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,388 +0,0 @@
|
|||
// npx vitest services/marketplace/__tests__/MarketplaceManager.spec.ts
|
||||
|
||||
import type { MarketplaceItem } from "@roo-code/types"
|
||||
|
||||
import { MarketplaceManager } from "../MarketplaceManager"
|
||||
|
||||
// Mock CloudService
|
||||
vi.mock("@roo-code/cloud", () => ({
|
||||
getRooCodeApiUrl: () => "https://test.api.com",
|
||||
CloudService: {
|
||||
hasInstance: vi.fn(),
|
||||
instance: {
|
||||
isAuthenticated: vi.fn(),
|
||||
getOrganizationSettings: vi.fn(),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock axios
|
||||
vi.mock("axios")
|
||||
|
||||
// Mock vscode first
|
||||
vi.mock("vscode", () => ({
|
||||
workspace: {
|
||||
workspaceFolders: [
|
||||
{
|
||||
uri: { fsPath: "/test/workspace" },
|
||||
name: "test",
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
openTextDocument: vi.fn(),
|
||||
},
|
||||
window: {
|
||||
showInformationMessage: vi.fn(),
|
||||
showErrorMessage: vi.fn(),
|
||||
showTextDocument: vi.fn(),
|
||||
},
|
||||
Range: vi.fn().mockImplementation((startLine, startChar, endLine, endChar) => ({
|
||||
start: { line: startLine, character: startChar },
|
||||
end: { line: endLine, character: endChar },
|
||||
})),
|
||||
}))
|
||||
|
||||
const mockContext = {
|
||||
subscriptions: [],
|
||||
workspaceState: {
|
||||
get: vi.fn(),
|
||||
update: vi.fn(),
|
||||
},
|
||||
globalState: {
|
||||
get: vi.fn(),
|
||||
update: vi.fn(),
|
||||
},
|
||||
extensionUri: { fsPath: "/test/extension" },
|
||||
} as any
|
||||
|
||||
// Mock fs
|
||||
vi.mock("fs/promises", () => ({
|
||||
readFile: vi.fn(),
|
||||
access: vi.fn(),
|
||||
writeFile: vi.fn(),
|
||||
mkdir: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock yaml
|
||||
vi.mock("yaml", () => ({
|
||||
parse: vi.fn(),
|
||||
stringify: vi.fn(),
|
||||
}))
|
||||
|
||||
describe("MarketplaceManager", () => {
|
||||
let manager: MarketplaceManager
|
||||
|
||||
beforeEach(() => {
|
||||
manager = new MarketplaceManager(mockContext)
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("filterItems", () => {
|
||||
it("should filter items by search term", () => {
|
||||
const items: MarketplaceItem[] = [
|
||||
{
|
||||
id: "test-mode",
|
||||
name: "Test Mode",
|
||||
description: "A test mode for testing",
|
||||
type: "mode",
|
||||
content: "# Test Mode\nThis is a test mode.",
|
||||
},
|
||||
{
|
||||
id: "other-mode",
|
||||
name: "Other Mode",
|
||||
description: "Another mode",
|
||||
type: "mode",
|
||||
content: "# Other Mode\nThis is another mode.",
|
||||
},
|
||||
]
|
||||
|
||||
const filtered = manager.filterItems(items, { search: "test" })
|
||||
|
||||
expect(filtered).toHaveLength(1)
|
||||
expect(filtered[0].name).toBe("Test Mode")
|
||||
})
|
||||
|
||||
it("should filter items by type", () => {
|
||||
const items: MarketplaceItem[] = [
|
||||
{
|
||||
id: "test-mode",
|
||||
name: "Test Mode",
|
||||
description: "A test mode",
|
||||
type: "mode",
|
||||
content: "# Test Mode",
|
||||
},
|
||||
{
|
||||
id: "test-mcp",
|
||||
name: "Test MCP",
|
||||
description: "A test MCP",
|
||||
type: "mcp",
|
||||
url: "https://example.com/test-mcp",
|
||||
content: '{"command": "node", "args": ["server.js"]}',
|
||||
},
|
||||
]
|
||||
|
||||
const filtered = manager.filterItems(items, { type: "mode" })
|
||||
|
||||
expect(filtered).toHaveLength(1)
|
||||
expect(filtered[0].type).toBe("mode")
|
||||
})
|
||||
|
||||
it("should return empty array when no items match", () => {
|
||||
const items: MarketplaceItem[] = [
|
||||
{
|
||||
id: "test-mode",
|
||||
name: "Test Mode",
|
||||
description: "A test mode",
|
||||
type: "mode",
|
||||
content: "# Test Mode",
|
||||
},
|
||||
]
|
||||
|
||||
const filtered = manager.filterItems(items, { search: "nonexistent" })
|
||||
|
||||
expect(filtered).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getMarketplaceItems", () => {
|
||||
it("should return items from API", async () => {
|
||||
// Mock the config loader to return test data
|
||||
const mockItems: MarketplaceItem[] = [
|
||||
{
|
||||
id: "test-mode",
|
||||
name: "Test Mode",
|
||||
description: "A test mode",
|
||||
type: "mode",
|
||||
content: "# Test Mode",
|
||||
},
|
||||
]
|
||||
|
||||
// Mock the loadAllItems method
|
||||
vi.spyOn(manager["configLoader"], "loadAllItems").mockResolvedValue(mockItems)
|
||||
|
||||
const result = await manager.getMarketplaceItems()
|
||||
|
||||
expect(result.marketplaceItems).toHaveLength(1)
|
||||
expect(result.marketplaceItems[0].name).toBe("Test Mode")
|
||||
expect(result.organizationMcps).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should handle API errors gracefully", async () => {
|
||||
// Mock the config loader to throw an error
|
||||
vi.spyOn(manager["configLoader"], "loadAllItems").mockRejectedValue(new Error("API request failed"))
|
||||
|
||||
const result = await manager.getMarketplaceItems()
|
||||
|
||||
expect(result.marketplaceItems).toHaveLength(0)
|
||||
expect(result.organizationMcps).toHaveLength(0)
|
||||
expect(result.errors).toEqual(["API request failed"])
|
||||
})
|
||||
|
||||
it("should return organization MCPs when available", async () => {
|
||||
const { CloudService } = await import("@roo-code/cloud")
|
||||
|
||||
// Mock CloudService to return organization settings
|
||||
vi.mocked(CloudService.hasInstance).mockReturnValue(true)
|
||||
vi.mocked(CloudService.instance.isAuthenticated).mockReturnValue(true)
|
||||
vi.mocked(CloudService.instance.getOrganizationSettings).mockReturnValue({
|
||||
version: 1,
|
||||
mcps: [
|
||||
{
|
||||
id: "org-mcp-1",
|
||||
name: "Organization MCP",
|
||||
description: "An organization MCP",
|
||||
url: "https://example.com/org-mcp",
|
||||
content: '{"command": "node", "args": ["org-server.js"]}',
|
||||
},
|
||||
],
|
||||
hiddenMcps: [],
|
||||
allowList: { allowAll: true, providers: {} },
|
||||
defaultSettings: {},
|
||||
})
|
||||
|
||||
// Mock the config loader to return test data
|
||||
const mockItems: MarketplaceItem[] = [
|
||||
{
|
||||
id: "test-mcp",
|
||||
name: "Test MCP",
|
||||
description: "A test MCP",
|
||||
type: "mcp",
|
||||
url: "https://example.com/test-mcp",
|
||||
content: '{"command": "node", "args": ["server.js"]}',
|
||||
},
|
||||
]
|
||||
|
||||
vi.spyOn(manager["configLoader"], "loadAllItems").mockResolvedValue(mockItems)
|
||||
|
||||
const result = await manager.getMarketplaceItems()
|
||||
|
||||
expect(result.organizationMcps).toHaveLength(1)
|
||||
expect(result.organizationMcps[0].name).toBe("Organization MCP")
|
||||
expect(result.marketplaceItems).toHaveLength(1)
|
||||
expect(result.marketplaceItems[0].name).toBe("Test MCP")
|
||||
})
|
||||
|
||||
it("should filter out hidden MCPs from marketplace results", async () => {
|
||||
const { CloudService } = await import("@roo-code/cloud")
|
||||
|
||||
// Mock CloudService to return organization settings with hidden MCPs
|
||||
vi.mocked(CloudService.hasInstance).mockReturnValue(true)
|
||||
vi.mocked(CloudService.instance.isAuthenticated).mockReturnValue(true)
|
||||
vi.mocked(CloudService.instance.getOrganizationSettings).mockReturnValue({
|
||||
version: 1,
|
||||
mcps: [],
|
||||
hiddenMcps: ["hidden-mcp"],
|
||||
allowList: { allowAll: true, providers: {} },
|
||||
defaultSettings: {},
|
||||
})
|
||||
|
||||
// Mock the config loader to return test data including a hidden MCP
|
||||
const mockItems: MarketplaceItem[] = [
|
||||
{
|
||||
id: "visible-mcp",
|
||||
name: "Visible MCP",
|
||||
description: "A visible MCP",
|
||||
type: "mcp",
|
||||
url: "https://example.com/visible-mcp",
|
||||
content: '{"command": "node", "args": ["visible.js"]}',
|
||||
},
|
||||
{
|
||||
id: "hidden-mcp",
|
||||
name: "Hidden MCP",
|
||||
description: "A hidden MCP",
|
||||
type: "mcp",
|
||||
url: "https://example.com/hidden-mcp",
|
||||
content: '{"command": "node", "args": ["hidden.js"]}',
|
||||
},
|
||||
]
|
||||
|
||||
vi.spyOn(manager["configLoader"], "loadAllItems").mockResolvedValue(mockItems)
|
||||
|
||||
const result = await manager.getMarketplaceItems()
|
||||
|
||||
expect(result.marketplaceItems).toHaveLength(1)
|
||||
expect(result.marketplaceItems[0].name).toBe("Visible MCP")
|
||||
expect(result.organizationMcps).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should handle CloudService not being available", async () => {
|
||||
const { CloudService } = await import("@roo-code/cloud")
|
||||
|
||||
// Mock CloudService to not be available
|
||||
vi.mocked(CloudService.hasInstance).mockReturnValue(false)
|
||||
|
||||
// Mock the config loader to return test data
|
||||
const mockItems: MarketplaceItem[] = [
|
||||
{
|
||||
id: "test-mcp",
|
||||
name: "Test MCP",
|
||||
description: "A test MCP",
|
||||
type: "mcp",
|
||||
url: "https://example.com/test-mcp",
|
||||
content: '{"command": "node", "args": ["server.js"]}',
|
||||
},
|
||||
]
|
||||
|
||||
vi.spyOn(manager["configLoader"], "loadAllItems").mockResolvedValue(mockItems)
|
||||
|
||||
const result = await manager.getMarketplaceItems()
|
||||
|
||||
expect(result.organizationMcps).toHaveLength(0)
|
||||
expect(result.marketplaceItems).toHaveLength(1)
|
||||
expect(result.marketplaceItems[0].name).toBe("Test MCP")
|
||||
})
|
||||
})
|
||||
|
||||
describe("installMarketplaceItem", () => {
|
||||
it("should install a mode item", async () => {
|
||||
const item: MarketplaceItem = {
|
||||
id: "test-mode",
|
||||
name: "Test Mode",
|
||||
description: "A test mode",
|
||||
type: "mode",
|
||||
content: "# Test Mode\nThis is a test mode.",
|
||||
}
|
||||
|
||||
// Mock the installer
|
||||
vi.spyOn(manager["installer"], "installItem").mockResolvedValue({
|
||||
filePath: "/test/path/.roomodes",
|
||||
line: 5,
|
||||
})
|
||||
|
||||
const result = await manager.installMarketplaceItem(item)
|
||||
|
||||
expect(manager["installer"].installItem).toHaveBeenCalledWith(item, { target: "project" })
|
||||
expect(result).toBe("/test/path/.roomodes")
|
||||
})
|
||||
|
||||
it("should install an MCP item", async () => {
|
||||
const item: MarketplaceItem = {
|
||||
id: "test-mcp",
|
||||
name: "Test MCP",
|
||||
description: "A test MCP",
|
||||
type: "mcp",
|
||||
url: "https://example.com/test-mcp",
|
||||
content: '{"command": "node", "args": ["server.js"]}',
|
||||
}
|
||||
|
||||
// Mock the installer
|
||||
vi.spyOn(manager["installer"], "installItem").mockResolvedValue({
|
||||
filePath: "/test/path/.roo/mcp.json",
|
||||
line: 3,
|
||||
})
|
||||
|
||||
const result = await manager.installMarketplaceItem(item)
|
||||
|
||||
expect(manager["installer"].installItem).toHaveBeenCalledWith(item, { target: "project" })
|
||||
expect(result).toBe("/test/path/.roo/mcp.json")
|
||||
})
|
||||
})
|
||||
|
||||
describe("removeInstalledMarketplaceItem", () => {
|
||||
it("should remove a mode item", async () => {
|
||||
const item: MarketplaceItem = {
|
||||
id: "test-mode",
|
||||
name: "Test Mode",
|
||||
description: "A test mode",
|
||||
type: "mode",
|
||||
content: "# Test Mode",
|
||||
}
|
||||
|
||||
// Mock the installer
|
||||
vi.spyOn(manager["installer"], "removeItem").mockResolvedValue()
|
||||
|
||||
await manager.removeInstalledMarketplaceItem(item)
|
||||
|
||||
expect(manager["installer"].removeItem).toHaveBeenCalledWith(item, { target: "project" })
|
||||
})
|
||||
|
||||
it("should remove an MCP item", async () => {
|
||||
const item: MarketplaceItem = {
|
||||
id: "test-mcp",
|
||||
name: "Test MCP",
|
||||
description: "A test MCP",
|
||||
type: "mcp",
|
||||
url: "https://example.com/test-mcp",
|
||||
content: '{"command": "node", "args": ["server.js"]}',
|
||||
}
|
||||
|
||||
// Mock the installer
|
||||
vi.spyOn(manager["installer"], "removeItem").mockResolvedValue()
|
||||
|
||||
await manager.removeInstalledMarketplaceItem(item)
|
||||
|
||||
expect(manager["installer"].removeItem).toHaveBeenCalledWith(item, { target: "project" })
|
||||
})
|
||||
})
|
||||
|
||||
describe("cleanup", () => {
|
||||
it("should clear API cache", async () => {
|
||||
// Mock the clearCache method
|
||||
vi.spyOn(manager["configLoader"], "clearCache")
|
||||
|
||||
await manager.cleanup()
|
||||
|
||||
expect(manager["configLoader"].clearCache).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,335 +0,0 @@
|
|||
// npx vitest services/marketplace/__tests__/RemoteConfigLoader.spec.ts
|
||||
|
||||
import axios from "axios"
|
||||
import { RemoteConfigLoader } from "../RemoteConfigLoader"
|
||||
import type { MarketplaceItemType } from "@roo-code/types"
|
||||
|
||||
// Mock axios
|
||||
vi.mock("axios")
|
||||
const mockedAxios = axios as any
|
||||
|
||||
// Mock the cloud config
|
||||
vi.mock("@roo-code/cloud", () => ({
|
||||
getRooCodeApiUrl: () => "https://test.api.com",
|
||||
}))
|
||||
|
||||
describe("RemoteConfigLoader", () => {
|
||||
let loader: RemoteConfigLoader
|
||||
|
||||
beforeEach(() => {
|
||||
loader = new RemoteConfigLoader()
|
||||
vi.clearAllMocks()
|
||||
// Clear any existing cache
|
||||
loader.clearCache()
|
||||
})
|
||||
|
||||
describe("loadAllItems", () => {
|
||||
it("should fetch and combine modes and MCPs from API", async () => {
|
||||
const mockModesYaml = `items:
|
||||
- id: "test-mode"
|
||||
name: "Test Mode"
|
||||
description: "A test mode"
|
||||
content: "customModes:\\n - slug: test\\n name: Test"`
|
||||
|
||||
const mockMcpsYaml = `items:
|
||||
- id: "test-mcp"
|
||||
name: "Test MCP"
|
||||
description: "A test MCP"
|
||||
url: "https://github.com/test/test-mcp"
|
||||
content: '{"command": "test"}'`
|
||||
|
||||
mockedAxios.get.mockImplementation((url: string) => {
|
||||
if (url.includes("/modes")) {
|
||||
return Promise.resolve({ data: mockModesYaml })
|
||||
}
|
||||
if (url.includes("/mcps")) {
|
||||
return Promise.resolve({ data: mockMcpsYaml })
|
||||
}
|
||||
return Promise.reject(new Error("Unknown URL"))
|
||||
})
|
||||
|
||||
const items = await loader.loadAllItems()
|
||||
|
||||
expect(mockedAxios.get).toHaveBeenCalledTimes(2)
|
||||
expect(mockedAxios.get).toHaveBeenCalledWith(
|
||||
"https://test.api.com/api/marketplace/modes",
|
||||
expect.objectContaining({
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}),
|
||||
)
|
||||
expect(mockedAxios.get).toHaveBeenCalledWith(
|
||||
"https://test.api.com/api/marketplace/mcps",
|
||||
expect.objectContaining({
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
expect(items).toHaveLength(2)
|
||||
expect(items[0]).toEqual({
|
||||
type: "mode",
|
||||
id: "test-mode",
|
||||
name: "Test Mode",
|
||||
description: "A test mode",
|
||||
content: "customModes:\n - slug: test\n name: Test",
|
||||
})
|
||||
expect(items[1]).toEqual({
|
||||
type: "mcp",
|
||||
id: "test-mcp",
|
||||
name: "Test MCP",
|
||||
description: "A test MCP",
|
||||
url: "https://github.com/test/test-mcp",
|
||||
content: '{"command": "test"}',
|
||||
})
|
||||
})
|
||||
|
||||
it("should use cache on subsequent calls", async () => {
|
||||
const mockModesYaml = `items:
|
||||
- id: "test-mode"
|
||||
name: "Test Mode"
|
||||
description: "A test mode"
|
||||
content: "test content"`
|
||||
|
||||
const mockMcpsYaml = `items:
|
||||
- id: "test-mcp"
|
||||
name: "Test MCP"
|
||||
description: "A test MCP"
|
||||
url: "https://github.com/test/test-mcp"
|
||||
content: "test content"`
|
||||
|
||||
mockedAxios.get.mockImplementation((url: string) => {
|
||||
if (url.includes("/modes")) {
|
||||
return Promise.resolve({ data: mockModesYaml })
|
||||
}
|
||||
if (url.includes("/mcps")) {
|
||||
return Promise.resolve({ data: mockMcpsYaml })
|
||||
}
|
||||
return Promise.reject(new Error("Unknown URL"))
|
||||
})
|
||||
|
||||
// First call - should hit API
|
||||
const items1 = await loader.loadAllItems()
|
||||
expect(mockedAxios.get).toHaveBeenCalledTimes(2)
|
||||
|
||||
// Second call - should use cache
|
||||
const items2 = await loader.loadAllItems()
|
||||
expect(mockedAxios.get).toHaveBeenCalledTimes(2) // Still 2, not 4
|
||||
|
||||
expect(items1).toEqual(items2)
|
||||
})
|
||||
|
||||
it("should retry on network failures", async () => {
|
||||
const mockModesYaml = `items:
|
||||
- id: "test-mode"
|
||||
name: "Test Mode"
|
||||
description: "A test mode"
|
||||
content: "test content"`
|
||||
|
||||
const mockMcpsYaml = `items: []`
|
||||
|
||||
// Mock modes endpoint to fail twice then succeed
|
||||
let modesCallCount = 0
|
||||
mockedAxios.get.mockImplementation((url: string) => {
|
||||
if (url.includes("/modes")) {
|
||||
modesCallCount++
|
||||
if (modesCallCount <= 2) {
|
||||
return Promise.reject(new Error("Network error"))
|
||||
}
|
||||
return Promise.resolve({ data: mockModesYaml })
|
||||
}
|
||||
if (url.includes("/mcps")) {
|
||||
return Promise.resolve({ data: mockMcpsYaml })
|
||||
}
|
||||
return Promise.reject(new Error("Unknown URL"))
|
||||
})
|
||||
|
||||
const items = await loader.loadAllItems()
|
||||
|
||||
// Should have retried modes endpoint 3 times (2 failures + 1 success)
|
||||
expect(modesCallCount).toBe(3)
|
||||
expect(items).toHaveLength(1)
|
||||
expect(items[0].type).toBe("mode")
|
||||
})
|
||||
|
||||
it("should throw error after max retries", async () => {
|
||||
mockedAxios.get.mockRejectedValue(new Error("Persistent network error"))
|
||||
|
||||
await expect(loader.loadAllItems()).rejects.toThrow("Persistent network error")
|
||||
|
||||
// Both endpoints will be called with retries since Promise.all starts both promises
|
||||
// Each endpoint retries 3 times, but due to Promise.all behavior, one might fail faster
|
||||
expect(mockedAxios.get).toHaveBeenCalledWith(
|
||||
expect.stringContaining("/api/marketplace/"),
|
||||
expect.any(Object),
|
||||
)
|
||||
// Verify we got at least some retry attempts (should be at least 2 calls)
|
||||
expect(mockedAxios.get.mock.calls.length).toBeGreaterThanOrEqual(2)
|
||||
})
|
||||
|
||||
it("should handle invalid data gracefully", async () => {
|
||||
const invalidModesYaml = `items:
|
||||
- id: "invalid-mode"
|
||||
# Missing required fields like name and description`
|
||||
|
||||
const validMcpsYaml = `items:
|
||||
- id: "valid-mcp"
|
||||
name: "Valid MCP"
|
||||
description: "A valid MCP"
|
||||
url: "https://github.com/test/test-mcp"
|
||||
content: "test content"`
|
||||
|
||||
mockedAxios.get.mockImplementation((url: string) => {
|
||||
if (url.includes("/modes")) {
|
||||
return Promise.resolve({ data: invalidModesYaml })
|
||||
}
|
||||
if (url.includes("/mcps")) {
|
||||
return Promise.resolve({ data: validMcpsYaml })
|
||||
}
|
||||
return Promise.reject(new Error("Unknown URL"))
|
||||
})
|
||||
|
||||
// Should throw validation error for invalid modes
|
||||
await expect(loader.loadAllItems()).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("getItem", () => {
|
||||
it("should find specific item by id and type", async () => {
|
||||
const mockModesYaml = `items:
|
||||
- id: "target-mode"
|
||||
name: "Target Mode"
|
||||
description: "The mode we want"
|
||||
content: "test content"`
|
||||
|
||||
const mockMcpsYaml = `items:
|
||||
- id: "target-mcp"
|
||||
name: "Target MCP"
|
||||
description: "The MCP we want"
|
||||
url: "https://github.com/test/test-mcp"
|
||||
content: "test content"`
|
||||
|
||||
mockedAxios.get.mockImplementation((url: string) => {
|
||||
if (url.includes("/modes")) {
|
||||
return Promise.resolve({ data: mockModesYaml })
|
||||
}
|
||||
if (url.includes("/mcps")) {
|
||||
return Promise.resolve({ data: mockMcpsYaml })
|
||||
}
|
||||
return Promise.reject(new Error("Unknown URL"))
|
||||
})
|
||||
|
||||
const modeItem = await loader.getItem("target-mode", "mode" as MarketplaceItemType)
|
||||
const mcpItem = await loader.getItem("target-mcp", "mcp" as MarketplaceItemType)
|
||||
const notFound = await loader.getItem("nonexistent", "mode" as MarketplaceItemType)
|
||||
|
||||
expect(modeItem).toEqual({
|
||||
type: "mode",
|
||||
id: "target-mode",
|
||||
name: "Target Mode",
|
||||
description: "The mode we want",
|
||||
content: "test content",
|
||||
})
|
||||
|
||||
expect(mcpItem).toEqual({
|
||||
type: "mcp",
|
||||
id: "target-mcp",
|
||||
name: "Target MCP",
|
||||
description: "The MCP we want",
|
||||
url: "https://github.com/test/test-mcp",
|
||||
content: "test content",
|
||||
})
|
||||
|
||||
expect(notFound).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe("clearCache", () => {
|
||||
it("should clear cache and force fresh API calls", async () => {
|
||||
const mockModesYaml = `items:
|
||||
- id: "test-mode"
|
||||
name: "Test Mode"
|
||||
description: "A test mode"
|
||||
content: "test content"`
|
||||
|
||||
const mockMcpsYaml = `items: []`
|
||||
|
||||
mockedAxios.get.mockImplementation((url: string) => {
|
||||
if (url.includes("/modes")) {
|
||||
return Promise.resolve({ data: mockModesYaml })
|
||||
}
|
||||
if (url.includes("/mcps")) {
|
||||
return Promise.resolve({ data: mockMcpsYaml })
|
||||
}
|
||||
return Promise.reject(new Error("Unknown URL"))
|
||||
})
|
||||
|
||||
// First call
|
||||
await loader.loadAllItems()
|
||||
expect(mockedAxios.get).toHaveBeenCalledTimes(2)
|
||||
|
||||
// Second call - should use cache
|
||||
await loader.loadAllItems()
|
||||
expect(mockedAxios.get).toHaveBeenCalledTimes(2)
|
||||
|
||||
// Clear cache
|
||||
loader.clearCache()
|
||||
|
||||
// Third call - should hit API again
|
||||
await loader.loadAllItems()
|
||||
expect(mockedAxios.get).toHaveBeenCalledTimes(4)
|
||||
})
|
||||
})
|
||||
|
||||
describe("cache expiration", () => {
|
||||
it("should expire cache after 5 minutes", async () => {
|
||||
const mockModesYaml = `items:
|
||||
- id: "test-mode"
|
||||
name: "Test Mode"
|
||||
description: "A test mode"
|
||||
content: "test content"`
|
||||
|
||||
const mockMcpsYaml = `items: []`
|
||||
|
||||
mockedAxios.get.mockImplementation((url: string) => {
|
||||
if (url.includes("/modes")) {
|
||||
return Promise.resolve({ data: mockModesYaml })
|
||||
}
|
||||
if (url.includes("/mcps")) {
|
||||
return Promise.resolve({ data: mockMcpsYaml })
|
||||
}
|
||||
return Promise.reject(new Error("Unknown URL"))
|
||||
})
|
||||
|
||||
// Mock Date.now to control time
|
||||
const originalDateNow = Date.now
|
||||
let currentTime = 1000000
|
||||
|
||||
Date.now = vi.fn(() => currentTime)
|
||||
|
||||
// First call
|
||||
await loader.loadAllItems()
|
||||
expect(mockedAxios.get).toHaveBeenCalledTimes(2)
|
||||
|
||||
// Second call immediately - should use cache
|
||||
await loader.loadAllItems()
|
||||
expect(mockedAxios.get).toHaveBeenCalledTimes(2)
|
||||
|
||||
// Advance time by 6 minutes (360,000 ms)
|
||||
currentTime += 6 * 60 * 1000
|
||||
|
||||
// Third call - cache should be expired
|
||||
await loader.loadAllItems()
|
||||
expect(mockedAxios.get).toHaveBeenCalledTimes(4)
|
||||
|
||||
// Restore original Date.now
|
||||
Date.now = originalDateNow
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,349 +0,0 @@
|
|||
// npx vitest services/marketplace/__tests__/SimpleInstaller.spec.ts
|
||||
|
||||
import { SimpleInstaller } from "../SimpleInstaller"
|
||||
import * as fs from "fs/promises"
|
||||
import * as yaml from "yaml"
|
||||
import * as vscode from "vscode"
|
||||
import * as os from "os"
|
||||
import type { MarketplaceItem } from "@roo-code/types"
|
||||
import type { CustomModesManager } from "../../../core/config/CustomModesManager"
|
||||
import * as path from "path"
|
||||
import { fileExistsAtPath } from "../../../utils/fs"
|
||||
|
||||
vi.mock("fs/promises", () => ({
|
||||
readFile: vi.fn(),
|
||||
writeFile: vi.fn(),
|
||||
mkdir: vi.fn(),
|
||||
rm: vi.fn(),
|
||||
}))
|
||||
vi.mock("os")
|
||||
vi.mock("vscode", () => ({
|
||||
workspace: {
|
||||
workspaceFolders: [
|
||||
{
|
||||
uri: { fsPath: "/test/workspace" },
|
||||
name: "test",
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
}))
|
||||
vi.mock("../../../utils/globalContext")
|
||||
vi.mock("../../../utils/fs")
|
||||
|
||||
const mockFs = vi.mocked(fs)
|
||||
|
||||
describe("SimpleInstaller", () => {
|
||||
let installer: SimpleInstaller
|
||||
let mockContext: vscode.ExtensionContext
|
||||
let mockCustomModesManager: CustomModesManager
|
||||
|
||||
beforeEach(() => {
|
||||
mockContext = {} as vscode.ExtensionContext
|
||||
mockCustomModesManager = {
|
||||
deleteCustomMode: vi.fn().mockResolvedValue(undefined),
|
||||
importModeWithRules: vi.fn().mockResolvedValue({ success: true }),
|
||||
getCustomModes: vi.fn().mockResolvedValue([]),
|
||||
} as any
|
||||
installer = new SimpleInstaller(mockContext, mockCustomModesManager)
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Mock mkdir to always succeed
|
||||
mockFs.mkdir.mockResolvedValue(undefined as any)
|
||||
// Mock rm to always succeed
|
||||
mockFs.rm.mockResolvedValue(undefined as any)
|
||||
// Mock os.homedir
|
||||
vi.mocked(os.homedir).mockReturnValue("/home/user")
|
||||
// Mock fileExistsAtPath to return false by default
|
||||
vi.mocked(fileExistsAtPath).mockResolvedValue(false)
|
||||
})
|
||||
|
||||
describe("installMode", () => {
|
||||
const mockModeItem: MarketplaceItem = {
|
||||
id: "test-mode",
|
||||
name: "Test Mode",
|
||||
description: "A test mode for testing",
|
||||
type: "mode",
|
||||
content: yaml.stringify({
|
||||
slug: "test",
|
||||
name: "Test Mode",
|
||||
roleDefinition: "Test role",
|
||||
groups: ["read"],
|
||||
}),
|
||||
}
|
||||
|
||||
it("should install mode using CustomModesManager", async () => {
|
||||
// Mock file not found error for getModeFilePath
|
||||
const notFoundError = new Error("File not found") as any
|
||||
notFoundError.code = "ENOENT"
|
||||
mockFs.readFile.mockRejectedValueOnce(notFoundError)
|
||||
|
||||
const result = await installer.installItem(mockModeItem, { target: "project" })
|
||||
|
||||
expect(result.filePath).toBe(path.join("/test/workspace", ".roomodes"))
|
||||
expect(mockCustomModesManager.importModeWithRules).toHaveBeenCalled()
|
||||
|
||||
// Verify the import was called with correct YAML structure
|
||||
const importCall = (mockCustomModesManager.importModeWithRules as any).mock.calls[0]
|
||||
const importedYaml = importCall[0]
|
||||
const importedData = yaml.parse(importedYaml)
|
||||
expect(importedData.customModes).toHaveLength(1)
|
||||
expect(importedData.customModes[0].slug).toBe("test")
|
||||
})
|
||||
|
||||
it("should handle import failure from CustomModesManager", async () => {
|
||||
mockCustomModesManager.importModeWithRules = vi.fn().mockResolvedValue({
|
||||
success: false,
|
||||
error: "Import failed",
|
||||
})
|
||||
|
||||
await expect(installer.installItem(mockModeItem, { target: "project" })).rejects.toThrow("Import failed")
|
||||
})
|
||||
|
||||
it("should throw error for array content in mode", async () => {
|
||||
const arrayContentMode: MarketplaceItem = {
|
||||
...mockModeItem,
|
||||
content: ["content1", "content2"] as any,
|
||||
}
|
||||
|
||||
await expect(installer.installItem(arrayContentMode, { target: "project" })).rejects.toThrow(
|
||||
"Mode content should not be an array",
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw error for missing content", async () => {
|
||||
const noContentMode: MarketplaceItem = {
|
||||
...mockModeItem,
|
||||
content: undefined as any,
|
||||
}
|
||||
|
||||
await expect(installer.installItem(noContentMode, { target: "project" })).rejects.toThrow(
|
||||
"Mode item missing content",
|
||||
)
|
||||
})
|
||||
|
||||
it("should work without CustomModesManager (fallback)", async () => {
|
||||
const installerWithoutManager = new SimpleInstaller(mockContext)
|
||||
|
||||
// Mock file not found
|
||||
const notFoundError = new Error("File not found") as any
|
||||
notFoundError.code = "ENOENT"
|
||||
mockFs.readFile.mockRejectedValueOnce(notFoundError)
|
||||
mockFs.writeFile.mockResolvedValueOnce(undefined as any)
|
||||
|
||||
const result = await installerWithoutManager.installItem(mockModeItem, { target: "project" })
|
||||
|
||||
expect(result.filePath).toBe(path.join("/test/workspace", ".roomodes"))
|
||||
expect(mockFs.writeFile).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("installMcp", () => {
|
||||
const mockMcpItem: MarketplaceItem = {
|
||||
id: "test-mcp",
|
||||
name: "Test MCP",
|
||||
description: "A test MCP server for testing",
|
||||
type: "mcp",
|
||||
url: "https://example.com/mcp",
|
||||
content: JSON.stringify({
|
||||
command: "test-server",
|
||||
args: ["--test"],
|
||||
}),
|
||||
}
|
||||
|
||||
it("should install MCP when mcp.json file does not exist", async () => {
|
||||
const notFoundError = new Error("File not found") as any
|
||||
notFoundError.code = "ENOENT"
|
||||
mockFs.readFile.mockRejectedValueOnce(notFoundError)
|
||||
mockFs.writeFile.mockResolvedValueOnce(undefined as any)
|
||||
|
||||
const result = await installer.installItem(mockMcpItem, { target: "project" })
|
||||
|
||||
expect(result.filePath).toBe(path.join("/test/workspace", ".roo", "mcp.json"))
|
||||
expect(mockFs.writeFile).toHaveBeenCalled()
|
||||
|
||||
// Verify the written content contains the new server
|
||||
const writtenContent = mockFs.writeFile.mock.calls[0][1] as string
|
||||
const writtenData = JSON.parse(writtenContent)
|
||||
expect(writtenData.mcpServers["test-mcp"]).toBeDefined()
|
||||
})
|
||||
|
||||
it("should throw error when mcp.json contains invalid JSON", async () => {
|
||||
const invalidJson = '{ "mcpServers": { invalid json'
|
||||
|
||||
mockFs.readFile.mockResolvedValueOnce(invalidJson)
|
||||
|
||||
await expect(installer.installItem(mockMcpItem, { target: "project" })).rejects.toThrow(
|
||||
"Cannot install MCP server: The .roo/mcp.json file contains invalid JSON",
|
||||
)
|
||||
|
||||
// Should NOT write to file
|
||||
expect(mockFs.writeFile).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should install MCP when mcp.json contains valid JSON", async () => {
|
||||
const existingContent = JSON.stringify({
|
||||
mcpServers: {
|
||||
"existing-server": { command: "existing", args: [] },
|
||||
},
|
||||
})
|
||||
|
||||
mockFs.readFile.mockResolvedValueOnce(existingContent)
|
||||
mockFs.writeFile.mockResolvedValueOnce(undefined as any)
|
||||
|
||||
await installer.installItem(mockMcpItem, { target: "project" })
|
||||
|
||||
const writtenContent = mockFs.writeFile.mock.calls[0][1] as string
|
||||
const writtenData = JSON.parse(writtenContent)
|
||||
|
||||
// Should contain both existing and new server
|
||||
expect(Object.keys(writtenData.mcpServers)).toHaveLength(2)
|
||||
expect(writtenData.mcpServers["existing-server"]).toBeDefined()
|
||||
expect(writtenData.mcpServers["test-mcp"]).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("removeMode", () => {
|
||||
const mockModeItem: MarketplaceItem = {
|
||||
id: "test-mode",
|
||||
name: "Test Mode",
|
||||
description: "A test mode for testing",
|
||||
type: "mode",
|
||||
content: yaml.stringify({
|
||||
slug: "test",
|
||||
name: "Test Mode",
|
||||
roleDefinition: "Test role",
|
||||
groups: ["read"],
|
||||
}),
|
||||
}
|
||||
|
||||
it("should use CustomModesManager to delete mode and clean up rules folder", async () => {
|
||||
// Mock that the mode exists with project source
|
||||
vi.mocked(mockCustomModesManager.getCustomModes).mockResolvedValueOnce([
|
||||
{ slug: "test", name: "Test Mode", source: "project" } as any,
|
||||
])
|
||||
|
||||
await installer.removeItem(mockModeItem, { target: "project" })
|
||||
|
||||
// Should call deleteCustomMode with fromMarketplace flag set to true
|
||||
expect(mockCustomModesManager.deleteCustomMode).toHaveBeenCalledWith("test", true)
|
||||
// The rules folder deletion is now handled by CustomModesManager, not SimpleInstaller
|
||||
expect(fileExistsAtPath).not.toHaveBeenCalled()
|
||||
expect(mockFs.rm).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should handle global mode removal with rules cleanup", async () => {
|
||||
// Mock that the mode exists with global source
|
||||
vi.mocked(mockCustomModesManager.getCustomModes).mockResolvedValueOnce([
|
||||
{ slug: "test", name: "Test Mode", source: "global" } as any,
|
||||
])
|
||||
|
||||
await installer.removeItem(mockModeItem, { target: "global" })
|
||||
|
||||
// Should call deleteCustomMode with fromMarketplace flag set to true
|
||||
expect(mockCustomModesManager.deleteCustomMode).toHaveBeenCalledWith("test", true)
|
||||
// The rules folder deletion is now handled by CustomModesManager, not SimpleInstaller
|
||||
expect(fileExistsAtPath).not.toHaveBeenCalled()
|
||||
expect(mockFs.rm).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should handle case when rules folder does not exist", async () => {
|
||||
// Mock that the mode exists
|
||||
vi.mocked(mockCustomModesManager.getCustomModes).mockResolvedValueOnce([
|
||||
{ slug: "test", name: "Test Mode", source: "project" } as any,
|
||||
])
|
||||
|
||||
await installer.removeItem(mockModeItem, { target: "project" })
|
||||
|
||||
// Should call deleteCustomMode with fromMarketplace flag set to true
|
||||
expect(mockCustomModesManager.deleteCustomMode).toHaveBeenCalledWith("test", true)
|
||||
// The rules folder deletion is now handled by CustomModesManager, not SimpleInstaller
|
||||
expect(fileExistsAtPath).not.toHaveBeenCalled()
|
||||
expect(mockFs.rm).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should throw error if deleteCustomMode fails", async () => {
|
||||
// Mock that the mode exists
|
||||
vi.mocked(mockCustomModesManager.getCustomModes).mockResolvedValueOnce([
|
||||
{ slug: "test", name: "Test Mode", source: "project" } as any,
|
||||
])
|
||||
// Mock that deleteCustomMode fails
|
||||
mockCustomModesManager.deleteCustomMode = vi.fn().mockRejectedValueOnce(new Error("Permission denied"))
|
||||
|
||||
// Should throw the error from deleteCustomMode
|
||||
await expect(installer.removeItem(mockModeItem, { target: "project" })).rejects.toThrow("Permission denied")
|
||||
|
||||
expect(mockCustomModesManager.deleteCustomMode).toHaveBeenCalledWith("test", true)
|
||||
})
|
||||
|
||||
it("should handle mode not found in custom modes list", async () => {
|
||||
// Mock that the mode doesn't exist in the list
|
||||
vi.mocked(mockCustomModesManager.getCustomModes).mockResolvedValueOnce([])
|
||||
|
||||
await installer.removeItem(mockModeItem, { target: "project" })
|
||||
|
||||
expect(mockCustomModesManager.deleteCustomMode).toHaveBeenCalledWith("test", true)
|
||||
// Should not attempt to delete rules folder
|
||||
expect(fileExistsAtPath).not.toHaveBeenCalled()
|
||||
expect(mockFs.rm).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should throw error when mode content is invalid YAML", async () => {
|
||||
const invalidModeItem: MarketplaceItem = {
|
||||
...mockModeItem,
|
||||
content: "invalid: yaml: content: {",
|
||||
}
|
||||
|
||||
await expect(installer.removeItem(invalidModeItem, { target: "project" })).rejects.toThrow(
|
||||
"Invalid mode content: unable to parse YAML",
|
||||
)
|
||||
|
||||
expect(mockCustomModesManager.deleteCustomMode).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should throw error when mode has no slug", async () => {
|
||||
const noSlugModeItem: MarketplaceItem = {
|
||||
...mockModeItem,
|
||||
content: yaml.stringify({
|
||||
name: "Test Mode",
|
||||
roleDefinition: "Test role",
|
||||
groups: ["read"],
|
||||
}),
|
||||
}
|
||||
|
||||
await expect(installer.removeItem(noSlugModeItem, { target: "project" })).rejects.toThrow(
|
||||
"Mode missing slug identifier",
|
||||
)
|
||||
|
||||
expect(mockCustomModesManager.deleteCustomMode).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should handle array content format", async () => {
|
||||
const arrayContentItem: MarketplaceItem = {
|
||||
...mockModeItem,
|
||||
content: [
|
||||
{
|
||||
content: yaml.stringify({
|
||||
slug: "test-array",
|
||||
name: "Test Array Mode",
|
||||
roleDefinition: "Test role",
|
||||
groups: ["read"],
|
||||
}),
|
||||
},
|
||||
] as any,
|
||||
}
|
||||
|
||||
await installer.removeItem(arrayContentItem, { target: "project" })
|
||||
|
||||
expect(mockCustomModesManager.deleteCustomMode).toHaveBeenCalledWith("test-array", true)
|
||||
})
|
||||
|
||||
it("should throw error when CustomModesManager is not available", async () => {
|
||||
const installerWithoutManager = new SimpleInstaller(mockContext)
|
||||
|
||||
await expect(installerWithoutManager.removeItem(mockModeItem, { target: "project" })).rejects.toThrow(
|
||||
"CustomModesManager is not available",
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
// npx vitest services/marketplace/__tests__/marketplace-setting-check.spec.ts
|
||||
|
||||
import { webviewMessageHandler } from "../../../core/webview/webviewMessageHandler"
|
||||
|
||||
// Mock the provider and marketplace manager
|
||||
const mockProvider = {
|
||||
getState: vi.fn(),
|
||||
postStateToWebview: vi.fn(),
|
||||
postMessageToWebview: vi.fn(),
|
||||
} as any
|
||||
|
||||
const mockMarketplaceManager = {
|
||||
updateWithFilteredItems: vi.fn(),
|
||||
} as any
|
||||
|
||||
describe("Marketplace General Availability", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should allow marketplace API calls (marketplace is generally available)", async () => {
|
||||
// Mock state without marketplace experiment (since it's now generally available)
|
||||
mockProvider.getState.mockResolvedValue({
|
||||
experiments: {},
|
||||
})
|
||||
|
||||
const message = {
|
||||
type: "filterMarketplaceItems" as const,
|
||||
filters: { type: "mcp", search: "", tags: [] },
|
||||
}
|
||||
|
||||
await webviewMessageHandler(mockProvider, message, mockMarketplaceManager)
|
||||
|
||||
// Should call marketplace manager methods since marketplace is generally available
|
||||
expect(mockMarketplaceManager.updateWithFilteredItems).toHaveBeenCalledWith({
|
||||
type: "mcp",
|
||||
search: "",
|
||||
tags: [],
|
||||
})
|
||||
expect(mockProvider.postStateToWebview).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should allow marketplace installation (marketplace is generally available)", async () => {
|
||||
// Mock state without marketplace experiment (since it's now generally available)
|
||||
mockProvider.getState.mockResolvedValue({
|
||||
experiments: {},
|
||||
})
|
||||
|
||||
const mockInstallMarketplaceItem = vi.fn().mockResolvedValue(undefined)
|
||||
const mockMarketplaceManagerWithInstall = {
|
||||
installMarketplaceItem: mockInstallMarketplaceItem,
|
||||
}
|
||||
|
||||
const message = {
|
||||
type: "installMarketplaceItem" as const,
|
||||
mpItem: {
|
||||
id: "test-item",
|
||||
name: "Test Item",
|
||||
type: "mcp" as const,
|
||||
description: "Test description",
|
||||
content: "test content",
|
||||
url: "https://example.com/test-mcp",
|
||||
},
|
||||
mpInstallOptions: { target: "project" as const },
|
||||
}
|
||||
|
||||
await webviewMessageHandler(mockProvider, message, mockMarketplaceManagerWithInstall as any)
|
||||
|
||||
// Should call install method since marketplace is generally available
|
||||
expect(mockInstallMarketplaceItem).toHaveBeenCalledWith(message.mpItem, message.mpInstallOptions)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,230 +0,0 @@
|
|||
import type { McpInstallationMethod } from "@roo-code/types"
|
||||
import { mcpInstallationMethodSchema, mcpMarketplaceItemSchema } from "@roo-code/types"
|
||||
|
||||
describe("Nested Parameters", () => {
|
||||
describe("McpInstallationMethod Schema", () => {
|
||||
it("should validate installation method without parameters", () => {
|
||||
const method = {
|
||||
name: "Docker Installation",
|
||||
content: '{"command": "docker", "args": ["run", "image"]}',
|
||||
}
|
||||
|
||||
const result = mcpInstallationMethodSchema.parse(method)
|
||||
expect(result.parameters).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should validate installation method with parameters", () => {
|
||||
const method = {
|
||||
name: "Docker Installation",
|
||||
content: '{"command": "docker", "args": ["run", "-p", "{{port}}:8080", "{{image}}"]}',
|
||||
parameters: [
|
||||
{
|
||||
name: "Port",
|
||||
key: "port",
|
||||
placeholder: "8080",
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
name: "Docker Image",
|
||||
key: "image",
|
||||
placeholder: "latest",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const result = mcpInstallationMethodSchema.parse(method)
|
||||
expect(result.parameters).toHaveLength(2)
|
||||
expect(result.parameters![0].key).toBe("port")
|
||||
expect(result.parameters![0].optional).toBe(true)
|
||||
expect(result.parameters![1].key).toBe("image")
|
||||
expect(result.parameters![1].optional).toBe(false)
|
||||
})
|
||||
|
||||
it("should validate installation method with empty parameters array", () => {
|
||||
const method = {
|
||||
name: "Simple Installation",
|
||||
content: '{"command": "npm", "args": ["start"]}',
|
||||
parameters: [],
|
||||
}
|
||||
|
||||
const result = mcpInstallationMethodSchema.parse(method)
|
||||
expect(result.parameters).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe("McpMarketplaceItem with Nested Parameters", () => {
|
||||
it("should validate MCP item with global and method-specific parameters", () => {
|
||||
const item = {
|
||||
id: "multi-method-mcp",
|
||||
name: "Multi-Method MCP",
|
||||
description: "MCP with multiple installation methods",
|
||||
url: "https://github.com/example/mcp",
|
||||
parameters: [
|
||||
{
|
||||
name: "API Key",
|
||||
key: "api_key",
|
||||
placeholder: "Enter your API key",
|
||||
},
|
||||
],
|
||||
content: [
|
||||
{
|
||||
name: "Docker Installation",
|
||||
content: '{"command": "docker", "args": ["-e", "API_KEY={{api_key}}", "-p", "{{port}}:8080"]}',
|
||||
parameters: [
|
||||
{
|
||||
name: "Port",
|
||||
key: "port",
|
||||
placeholder: "8080",
|
||||
optional: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "NPM Installation",
|
||||
content: '{"command": "npx", "args": ["package@{{version}}", "--api-key", "{{api_key}}"]}',
|
||||
parameters: [
|
||||
{
|
||||
name: "Package Version",
|
||||
key: "version",
|
||||
placeholder: "latest",
|
||||
optional: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const result = mcpMarketplaceItemSchema.parse(item)
|
||||
expect(result.parameters).toHaveLength(1)
|
||||
expect(result.parameters![0].key).toBe("api_key")
|
||||
|
||||
expect(Array.isArray(result.content)).toBe(true)
|
||||
const methods = result.content as McpInstallationMethod[]
|
||||
expect(methods).toHaveLength(2)
|
||||
|
||||
expect(methods[0].parameters).toHaveLength(1)
|
||||
expect(methods[0].parameters![0].key).toBe("port")
|
||||
|
||||
expect(methods[1].parameters).toHaveLength(1)
|
||||
expect(methods[1].parameters![0].key).toBe("version")
|
||||
})
|
||||
|
||||
it("should validate MCP item with only global parameters", () => {
|
||||
const item = {
|
||||
id: "global-only-mcp",
|
||||
name: "Global Only MCP",
|
||||
description: "MCP with only global parameters",
|
||||
url: "https://github.com/example/mcp",
|
||||
parameters: [
|
||||
{
|
||||
name: "API Key",
|
||||
key: "api_key",
|
||||
placeholder: "Enter your API key",
|
||||
},
|
||||
],
|
||||
content: [
|
||||
{
|
||||
name: "Installation",
|
||||
content: '{"command": "npm", "args": ["--api-key", "{{api_key}}"]}',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const result = mcpMarketplaceItemSchema.parse(item)
|
||||
expect(result.parameters).toHaveLength(1)
|
||||
|
||||
const methods = result.content as McpInstallationMethod[]
|
||||
expect(methods[0].parameters).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should validate MCP item with only method-specific parameters", () => {
|
||||
const item = {
|
||||
id: "method-only-mcp",
|
||||
name: "Method Only MCP",
|
||||
description: "MCP with only method-specific parameters",
|
||||
url: "https://github.com/example/mcp",
|
||||
content: [
|
||||
{
|
||||
name: "Docker Installation",
|
||||
content: '{"command": "docker", "args": ["-p", "{{port}}:8080"]}',
|
||||
parameters: [
|
||||
{
|
||||
name: "Port",
|
||||
key: "port",
|
||||
placeholder: "8080",
|
||||
optional: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const result = mcpMarketplaceItemSchema.parse(item)
|
||||
expect(result.parameters).toBeUndefined()
|
||||
|
||||
const methods = result.content as McpInstallationMethod[]
|
||||
expect(methods[0].parameters).toHaveLength(1)
|
||||
expect(methods[0].parameters![0].key).toBe("port")
|
||||
})
|
||||
|
||||
it("should validate MCP item with no parameters at all", () => {
|
||||
const item = {
|
||||
id: "no-params-mcp",
|
||||
name: "No Parameters MCP",
|
||||
description: "MCP with no parameters",
|
||||
url: "https://github.com/example/mcp",
|
||||
content: [
|
||||
{
|
||||
name: "Simple Installation",
|
||||
content: '{"command": "npm", "args": ["start"]}',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const result = mcpMarketplaceItemSchema.parse(item)
|
||||
expect(result.parameters).toBeUndefined()
|
||||
|
||||
const methods = result.content as McpInstallationMethod[]
|
||||
expect(methods[0].parameters).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Parameter Key Conflicts", () => {
|
||||
it("should allow same parameter key in global and method-specific parameters", () => {
|
||||
const item = {
|
||||
id: "conflict-mcp",
|
||||
name: "Conflict MCP",
|
||||
description: "MCP with parameter key conflicts",
|
||||
url: "https://github.com/example/mcp",
|
||||
parameters: [
|
||||
{
|
||||
name: "Global Version",
|
||||
key: "version",
|
||||
placeholder: "1.0.0",
|
||||
},
|
||||
],
|
||||
content: [
|
||||
{
|
||||
name: "Method Installation",
|
||||
content: '{"command": "npm", "args": ["package@{{version}}"]}',
|
||||
parameters: [
|
||||
{
|
||||
name: "Method Version",
|
||||
key: "version",
|
||||
placeholder: "latest",
|
||||
optional: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
// This should validate successfully - the conflict resolution happens at runtime
|
||||
const result = mcpMarketplaceItemSchema.parse(item)
|
||||
expect(result.parameters![0].key).toBe("version")
|
||||
|
||||
const methods = result.content as McpInstallationMethod[]
|
||||
expect(methods[0].parameters![0].key).toBe("version")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
import { mcpParameterSchema } from "@roo-code/types"
|
||||
|
||||
describe("Optional Parameters", () => {
|
||||
describe("McpParameter Schema", () => {
|
||||
it("should validate parameter with optional field set to true", () => {
|
||||
const param = {
|
||||
name: "Test Parameter",
|
||||
key: "test_key",
|
||||
placeholder: "Enter value",
|
||||
optional: true,
|
||||
}
|
||||
|
||||
const result = mcpParameterSchema.parse(param)
|
||||
expect(result.optional).toBe(true)
|
||||
})
|
||||
|
||||
it("should validate parameter with optional field set to false", () => {
|
||||
const param = {
|
||||
name: "Test Parameter",
|
||||
key: "test_key",
|
||||
placeholder: "Enter value",
|
||||
optional: false,
|
||||
}
|
||||
|
||||
const result = mcpParameterSchema.parse(param)
|
||||
expect(result.optional).toBe(false)
|
||||
})
|
||||
|
||||
it("should default optional to false when not provided", () => {
|
||||
const param = {
|
||||
name: "Test Parameter",
|
||||
key: "test_key",
|
||||
placeholder: "Enter value",
|
||||
}
|
||||
|
||||
const result = mcpParameterSchema.parse(param)
|
||||
expect(result.optional).toBe(false)
|
||||
})
|
||||
|
||||
it("should validate parameter without placeholder", () => {
|
||||
const param = {
|
||||
name: "Test Parameter",
|
||||
key: "test_key",
|
||||
optional: true,
|
||||
}
|
||||
|
||||
const result = mcpParameterSchema.parse(param)
|
||||
expect(result.optional).toBe(true)
|
||||
expect(result.placeholder).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should require name and key fields", () => {
|
||||
expect(() => {
|
||||
mcpParameterSchema.parse({
|
||||
key: "test_key",
|
||||
optional: true,
|
||||
})
|
||||
}).toThrow()
|
||||
|
||||
expect(() => {
|
||||
mcpParameterSchema.parse({
|
||||
name: "Test Parameter",
|
||||
optional: true,
|
||||
})
|
||||
}).toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
export * from "./SimpleInstaller"
|
||||
export * from "./MarketplaceManager"
|
||||
export type { MarketplaceItemType } from "@roo-code/types"
|
||||
|
|
@ -1,12 +1,10 @@
|
|||
import React, { useCallback, useEffect, useRef, useState, useMemo } from "react"
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react"
|
||||
import { useEvent } from "react-use"
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
||||
|
||||
import { type ExtensionMessage } from "@roo-code/types"
|
||||
|
||||
import TranslationProvider from "./i18n/TranslationContext"
|
||||
import { MarketplaceViewStateManager } from "./components/marketplace/MarketplaceViewStateManager"
|
||||
|
||||
import { vscode } from "./utils/vscode"
|
||||
import { initializeSourceMaps, exposeSourceMapsForDebugging } from "./utils/sourceMapInitializer"
|
||||
import { ExtensionStateContextProvider, useExtensionState } from "./context/ExtensionStateContext"
|
||||
|
|
@ -14,7 +12,6 @@ import ChatView, { ChatViewRef } from "./components/chat/ChatView"
|
|||
import HistoryView from "./components/history/HistoryView"
|
||||
import SettingsView, { SettingsViewRef } from "./components/settings/SettingsView"
|
||||
import WelcomeView from "./components/welcome/WelcomeViewProvider"
|
||||
import { MarketplaceView } from "./components/marketplace/MarketplaceView"
|
||||
import { CheckpointRestoreDialog } from "./components/chat/CheckpointRestoreDialog"
|
||||
import { DeleteMessageDialog, EditMessageDialog } from "./components/chat/MessageModificationConfirmationDialog"
|
||||
import ErrorBoundary from "./components/ErrorBoundary"
|
||||
|
|
@ -23,7 +20,7 @@ import { useAddNonInteractiveClickListener } from "./components/ui/hooks/useNonI
|
|||
import { TooltipProvider } from "./components/ui/tooltip"
|
||||
import { STANDARD_TOOLTIP_DELAY } from "./components/ui/standard-tooltip"
|
||||
|
||||
type Tab = "settings" | "history" | "chat" | "marketplace" | "cloud"
|
||||
type Tab = "settings" | "history" | "chat" | "cloud"
|
||||
|
||||
interface DeleteMessageDialogState {
|
||||
isOpen: boolean
|
||||
|
|
@ -47,7 +44,6 @@ const tabsByMessageAction: Partial<Record<NonNullable<ExtensionMessage["action"]
|
|||
chatButtonClicked: "chat",
|
||||
settingsButtonClicked: "settings",
|
||||
historyButtonClicked: "history",
|
||||
marketplaceButtonClicked: "marketplace",
|
||||
cloudButtonClicked: "cloud",
|
||||
}
|
||||
|
||||
|
|
@ -63,9 +59,6 @@ const App = () => {
|
|||
renderContext,
|
||||
} = useExtensionState()
|
||||
|
||||
// Create a persistent state manager
|
||||
const marketplaceStateManager = useMemo(() => new MarketplaceViewStateManager(), [])
|
||||
|
||||
const [showAnnouncement, setShowAnnouncement] = useState(false)
|
||||
const [tab, setTab] = useState<Tab>("chat")
|
||||
|
||||
|
|
@ -88,7 +81,6 @@ const App = () => {
|
|||
|
||||
const switchTab = useCallback((newTab: Tab) => {
|
||||
setCurrentSection(undefined)
|
||||
setCurrentMarketplaceTab(undefined)
|
||||
|
||||
if (settingsRef.current?.checkUnsaveChanges) {
|
||||
settingsRef.current.checkUnsaveChanges(() => setTab(newTab))
|
||||
|
|
@ -98,7 +90,6 @@ const App = () => {
|
|||
}, [])
|
||||
|
||||
const [currentSection, setCurrentSection] = useState<string | undefined>(undefined)
|
||||
const [currentMarketplaceTab, setCurrentMarketplaceTab] = useState<string | undefined>(undefined)
|
||||
|
||||
const onMessage = useCallback(
|
||||
(e: MessageEvent) => {
|
||||
|
|
@ -112,17 +103,14 @@ const App = () => {
|
|||
// Extract targetSection from values if provided
|
||||
const targetSection = message.values?.section as string | undefined
|
||||
setCurrentSection(targetSection)
|
||||
setCurrentMarketplaceTab(undefined)
|
||||
} else {
|
||||
// Handle other actions using the mapping
|
||||
const newTab = tabsByMessageAction[message.action]
|
||||
const section = message.values?.section as string | undefined
|
||||
const marketplaceTab = message.values?.marketplaceTab as string | undefined
|
||||
|
||||
if (newTab) {
|
||||
switchTab(newTab)
|
||||
setCurrentSection(section)
|
||||
setCurrentMarketplaceTab(marketplaceTab)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -201,13 +189,6 @@ const App = () => {
|
|||
{tab === "settings" && (
|
||||
<SettingsView ref={settingsRef} onDone={() => setTab("chat")} targetSection={currentSection} />
|
||||
)}
|
||||
{tab === "marketplace" && (
|
||||
<MarketplaceView
|
||||
stateManager={marketplaceStateManager}
|
||||
onDone={() => switchTab("chat")}
|
||||
targetTab={currentMarketplaceTab as "mcp" | "mode" | undefined}
|
||||
/>
|
||||
)}
|
||||
{tab === "cloud" && (
|
||||
<CloudView
|
||||
userInfo={cloudUserInfo}
|
||||
|
|
|
|||
|
|
@ -54,16 +54,6 @@ vi.mock("@src/components/modes/ModesView", () => ({
|
|||
},
|
||||
}))
|
||||
|
||||
vi.mock("@src/components/marketplace/MarketplaceView", () => ({
|
||||
MarketplaceView: function MarketplaceView({ onDone }: { onDone: () => void }) {
|
||||
return (
|
||||
<div data-testid="marketplace-view" onClick={onDone}>
|
||||
Marketplace View
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("@src/components/cloud/CloudView", () => ({
|
||||
CloudView: function CloudView() {
|
||||
return <div data-testid="cloud-view">Cloud View</div>
|
||||
|
|
@ -239,36 +229,4 @@ describe("App", () => {
|
|||
expect(chatView.getAttribute("data-hidden")).toBe("false")
|
||||
expect(screen.queryByTestId(`${view}-view`)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("switches to marketplace view when receiving marketplaceButtonClicked action", async () => {
|
||||
render(<AppWithProviders />)
|
||||
|
||||
act(() => {
|
||||
triggerMessage("marketplaceButtonClicked")
|
||||
})
|
||||
|
||||
const marketplaceView = await screen.findByTestId("marketplace-view")
|
||||
expect(marketplaceView).toBeInTheDocument()
|
||||
|
||||
const chatView = screen.getByTestId("chat-view")
|
||||
expect(chatView.getAttribute("data-hidden")).toBe("true")
|
||||
})
|
||||
|
||||
it("returns to chat view when clicking done in marketplace view", async () => {
|
||||
render(<AppWithProviders />)
|
||||
|
||||
act(() => {
|
||||
triggerMessage("marketplaceButtonClicked")
|
||||
})
|
||||
|
||||
const marketplaceView = await screen.findByTestId("marketplace-view")
|
||||
|
||||
act(() => {
|
||||
marketplaceView.click()
|
||||
})
|
||||
|
||||
const chatView = screen.getByTestId("chat-view")
|
||||
expect(chatView.getAttribute("data-hidden")).toBe("false")
|
||||
expect(screen.queryByTestId("marketplace-view")).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -307,21 +307,6 @@ export const ModeSelector = ({
|
|||
{/* Bottom bar with buttons on left and title on right */}
|
||||
<div className="flex flex-row items-center justify-between px-2 py-2 border-t border-vscode-dropdown-border">
|
||||
<div className="flex flex-row gap-1">
|
||||
<IconButton
|
||||
iconClass="codicon-extensions"
|
||||
title={t("chat:modeSelector.marketplace")}
|
||||
onClick={() => {
|
||||
window.postMessage(
|
||||
{
|
||||
type: "action",
|
||||
action: "marketplaceButtonClicked",
|
||||
values: { marketplaceTab: "mode" },
|
||||
},
|
||||
"*",
|
||||
)
|
||||
setOpen(false)
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
iconClass="codicon-settings-gear"
|
||||
title={t("chat:modeSelector.settings")}
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
import React from "react"
|
||||
import { Trans } from "react-i18next"
|
||||
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
export const IssueFooter: React.FC = () => {
|
||||
return (
|
||||
<div className="text-xs text-vscode-descriptionForeground p-3">
|
||||
<Trans i18nKey="marketplace:footer.issueText">
|
||||
<VSCodeLink
|
||||
href="https://github.com/RooCodeInc/Roo-Code/issues/new?template=marketplace.yml"
|
||||
style={{ display: "inline", fontSize: "inherit" }}>
|
||||
Open a GitHub issue
|
||||
</VSCodeLink>
|
||||
</Trans>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,294 +0,0 @@
|
|||
import * as React from "react"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"
|
||||
import { X, ChevronsUpDown } from "lucide-react"
|
||||
import { MarketplaceItemCard } from "./components/MarketplaceItemCard"
|
||||
import { MarketplaceViewStateManager } from "./MarketplaceViewStateManager"
|
||||
import { useAppTranslation } from "@/i18n/TranslationContext"
|
||||
import { useStateManager } from "./useStateManager"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { IssueFooter } from "./IssueFooter"
|
||||
|
||||
export interface MarketplaceListViewProps {
|
||||
stateManager: MarketplaceViewStateManager
|
||||
allTags: string[]
|
||||
filteredTags: string[]
|
||||
filterByType?: "mcp" | "mode"
|
||||
}
|
||||
|
||||
export function MarketplaceListView({ stateManager, allTags, filteredTags, filterByType }: MarketplaceListViewProps) {
|
||||
const [state, manager] = useStateManager(stateManager)
|
||||
const { t } = useAppTranslation()
|
||||
const { marketplaceInstalledMetadata, cloudUserInfo } = useExtensionState()
|
||||
const [isTagPopoverOpen, setIsTagPopoverOpen] = React.useState(false)
|
||||
const [tagSearch, setTagSearch] = React.useState("")
|
||||
const allItems = state.displayItems || []
|
||||
const organizationMcps = state.displayOrganizationMcps || []
|
||||
|
||||
// NOTE: installed metadata is already synchronized into the state manager via handleMessage("state"/"marketplaceData")
|
||||
// in MarketplaceViewStateManager; avoid dispatching UPDATE_FILTERS here to prevent render loops.
|
||||
|
||||
// Filter items by type if specified
|
||||
const items = filterByType ? allItems.filter((item) => item.type === filterByType) : allItems
|
||||
const orgMcps = filterByType === "mcp" ? organizationMcps : []
|
||||
|
||||
const isEmpty = items.length === 0 && orgMcps.length === 0
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<div className="relative">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={
|
||||
filterByType === "mcp"
|
||||
? t("marketplace:filters.search.placeholderMcp")
|
||||
: filterByType === "mode"
|
||||
? t("marketplace:filters.search.placeholderMode")
|
||||
: 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 flex gap-2">
|
||||
<Select
|
||||
value={state.filters.installed}
|
||||
onValueChange={(value: "all" | "installed" | "not_installed") =>
|
||||
manager.transition({
|
||||
type: "UPDATE_FILTERS",
|
||||
payload: { filters: { installed: value } },
|
||||
})
|
||||
}>
|
||||
<SelectTrigger className="flex-1 h-7">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">{t("marketplace:filters.installed.all")}</SelectItem>
|
||||
<SelectItem value="installed">{t("marketplace:filters.installed.installed")}</SelectItem>
|
||||
<SelectItem value="not_installed">
|
||||
{t("marketplace:filters.installed.notInstalled")}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{allTags.length > 0 && (
|
||||
<div className="flex-1">
|
||||
<Popover open={isTagPopoverOpen} onOpenChange={(open) => setIsTagPopoverOpen(open)}>
|
||||
<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(", ")
|
||||
: t("marketplace:filters.tags.label")}
|
||||
</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"
|
||||
onClick={(e) => e.stopPropagation()}>
|
||||
<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.stopPropagation()
|
||||
e.preventDefault()
|
||||
}}>
|
||||
{state.filters.tags.includes(tag) ? (
|
||||
<span className="codicon codicon-check" />
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
{tag}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{state.filters.tags.length > 0 && (
|
||||
<div className="text-xs text-vscode-descriptionForeground mt-2 flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<span className="codicon codicon-tag mr-1"></span>
|
||||
{t("marketplace:filters.tags.selected")}
|
||||
</div>
|
||||
<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()
|
||||
manager.transition({
|
||||
type: "UPDATE_FILTERS",
|
||||
payload: { filters: { tags: [] } },
|
||||
})
|
||||
}}>
|
||||
<span className="codicon codicon-close"></span>
|
||||
{t("marketplace:filters.tags.clear")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{state.isFetching && isEmpty && (
|
||||
<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">{t("marketplace:items.refresh.mayTakeMoment")}</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">{t("marketplace:items.empty.adjustFilters")}</p>
|
||||
<Button
|
||||
onClick={() =>
|
||||
manager.transition({
|
||||
type: "UPDATE_FILTERS",
|
||||
payload: { filters: { search: "", type: "", tags: [], installed: "all" } },
|
||||
})
|
||||
}
|
||||
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>
|
||||
{t("marketplace:items.empty.clearAllFilters")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!state.isFetching && !isEmpty && (
|
||||
<div className="pb-3">
|
||||
{orgMcps.length > 0 && (
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center gap-2 mb-3 px-1">
|
||||
<span className="codicon codicon-organization text-lg"></span>
|
||||
<h3 className="text-sm font-semibold text-vscode-foreground">
|
||||
{t("marketplace:sections.organizationMcps", {
|
||||
organization: cloudUserInfo?.organizationName,
|
||||
})}
|
||||
</h3>
|
||||
<div className="flex-1 h-px bg-vscode-input-border"></div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-1 xl:grid-cols-2 gap-3">
|
||||
{orgMcps.map((item) => (
|
||||
<MarketplaceItemCard
|
||||
key={`org-${item.id}`}
|
||||
item={item}
|
||||
filters={state.filters}
|
||||
setFilters={(filters) =>
|
||||
manager.transition({
|
||||
type: "UPDATE_FILTERS",
|
||||
payload: { filters },
|
||||
})
|
||||
}
|
||||
installed={{
|
||||
project: marketplaceInstalledMetadata?.project?.[item.id],
|
||||
global: marketplaceInstalledMetadata?.global?.[item.id],
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{items.length > 0 && (
|
||||
<div>
|
||||
{orgMcps.length > 0 && (
|
||||
<div className="flex items-center gap-2 mb-3 px-1">
|
||||
<span className="codicon codicon-globe text-lg"></span>
|
||||
<h3 className="text-sm font-semibold text-vscode-foreground">
|
||||
{t("marketplace:sections.marketplace")}
|
||||
</h3>
|
||||
<div className="flex-1 h-px bg-vscode-input-border"></div>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-1 xl:grid-cols-2 gap-3">
|
||||
{items.map((item) => (
|
||||
<MarketplaceItemCard
|
||||
key={item.id}
|
||||
item={item}
|
||||
filters={state.filters}
|
||||
setFilters={(filters) =>
|
||||
manager.transition({
|
||||
type: "UPDATE_FILTERS",
|
||||
payload: { filters },
|
||||
})
|
||||
}
|
||||
installed={{
|
||||
project: marketplaceInstalledMetadata?.project?.[item.id],
|
||||
global: marketplaceInstalledMetadata?.global?.[item.id],
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<IssueFooter />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,167 +0,0 @@
|
|||
import { useState, useEffect, useMemo, useContext } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { ArrowLeft } from "lucide-react"
|
||||
import { Tab, TabContent, TabHeader } from "../common/Tab"
|
||||
import { MarketplaceViewStateManager } from "./MarketplaceViewStateManager"
|
||||
import { useStateManager } from "./useStateManager"
|
||||
import { useAppTranslation } from "@/i18n/TranslationContext"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { MarketplaceListView } from "./MarketplaceListView"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { TooltipProvider } from "@/components/ui/tooltip"
|
||||
import { ExtensionStateContext } from "@/context/ExtensionStateContext"
|
||||
|
||||
interface MarketplaceViewProps {
|
||||
onDone?: () => void
|
||||
stateManager: MarketplaceViewStateManager
|
||||
targetTab?: "mcp" | "mode"
|
||||
}
|
||||
export function MarketplaceView({ stateManager, onDone, targetTab }: MarketplaceViewProps) {
|
||||
const { t } = useAppTranslation()
|
||||
const [state, manager] = useStateManager(stateManager)
|
||||
const [hasReceivedInitialState, setHasReceivedInitialState] = useState(false)
|
||||
const extensionState = useContext(ExtensionStateContext)
|
||||
const [lastOrganizationSettingsVersion, setLastOrganizationSettingsVersion] = useState<number>(
|
||||
extensionState?.organizationSettingsVersion ?? -1,
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const currentVersion = extensionState?.organizationSettingsVersion ?? -1
|
||||
if (currentVersion !== lastOrganizationSettingsVersion) {
|
||||
vscode.postMessage({
|
||||
type: "fetchMarketplaceData",
|
||||
})
|
||||
}
|
||||
setLastOrganizationSettingsVersion(currentVersion)
|
||||
}, [extensionState?.organizationSettingsVersion, lastOrganizationSettingsVersion])
|
||||
|
||||
// Track when we receive the initial state
|
||||
useEffect(() => {
|
||||
// Check if we already have items (state might have been received before mount)
|
||||
if (state.allItems.length > 0 && !hasReceivedInitialState) {
|
||||
setHasReceivedInitialState(true)
|
||||
}
|
||||
}, [state.allItems, hasReceivedInitialState])
|
||||
|
||||
useEffect(() => {
|
||||
if (targetTab && (targetTab === "mcp" || targetTab === "mode")) {
|
||||
manager.transition({ type: "SET_ACTIVE_TAB", payload: { tab: targetTab } })
|
||||
}
|
||||
}, [targetTab, manager])
|
||||
|
||||
// Ensure marketplace state manager processes messages when component mounts
|
||||
useEffect(() => {
|
||||
// When the marketplace view first mounts, we need to trigger a state update
|
||||
// to ensure we get the current marketplace items. We do this by sending
|
||||
// a filter message with empty filters, which will cause the extension to
|
||||
// send back the full state including all marketplace items.
|
||||
if (!hasReceivedInitialState && state.allItems.length === 0) {
|
||||
// Fetch marketplace data on demand
|
||||
// Note: isFetching is already true by default for initial load
|
||||
vscode.postMessage({
|
||||
type: "fetchMarketplaceData",
|
||||
})
|
||||
}
|
||||
|
||||
// Listen for state changes to know when initial data arrives
|
||||
const unsubscribe = manager.onStateChange((newState) => {
|
||||
// Mark as received initial state when we get any state update
|
||||
// This prevents infinite loops and ensures proper state handling
|
||||
if (!hasReceivedInitialState && (newState.allItems.length > 0 || newState.displayItems !== undefined)) {
|
||||
setHasReceivedInitialState(true)
|
||||
}
|
||||
})
|
||||
|
||||
const handleVisibilityMessage = (event: MessageEvent) => {
|
||||
const message = event.data
|
||||
if (message.type === "webviewVisible" && message.visible === true) {
|
||||
// Data will be automatically fresh when panel becomes visible
|
||||
// No manual fetching needed since we removed caching
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("message", handleVisibilityMessage)
|
||||
return () => {
|
||||
window.removeEventListener("message", handleVisibilityMessage)
|
||||
unsubscribe()
|
||||
}
|
||||
}, [manager, hasReceivedInitialState, state.allItems.length])
|
||||
|
||||
// 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(() => allTags, [allTags])
|
||||
|
||||
return (
|
||||
<TooltipProvider delayDuration={300}>
|
||||
<Tab>
|
||||
<TabHeader className="flex flex-col sticky top-0 z-10 px-3 py-2">
|
||||
<div className="flex items-center justify-between gap-2 px-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="px-1.5 -ml-2"
|
||||
onClick={() => onDone?.()}
|
||||
aria-label={t("settings:back")}>
|
||||
<ArrowLeft />
|
||||
<span className="sr-only">{t("settings:back")}</span>
|
||||
</Button>
|
||||
<h3 className="font-bold m-0">{t("marketplace:title")}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full mt-2">
|
||||
<div className="flex relative py-1">
|
||||
<div className="absolute w-full h-[2px] -bottom-[2px] bg-vscode-input-border">
|
||||
<div
|
||||
className={cn(
|
||||
"absolute w-1/2 h-[2px] bottom-0 bg-vscode-button-background transition-all duration-300 ease-in-out",
|
||||
{
|
||||
"left-0": state.activeTab === "mcp",
|
||||
"left-1/2": state.activeTab === "mode",
|
||||
},
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="cursor-pointer flex items-center justify-center gap-2 flex-1 text-sm font-medium rounded-sm transition-colors duration-300 relative z-10 text-vscode-foreground"
|
||||
onClick={() => manager.transition({ type: "SET_ACTIVE_TAB", payload: { tab: "mcp" } })}>
|
||||
MCP
|
||||
</button>
|
||||
<button
|
||||
className="cursor-pointer flex items-center justify-center gap-2 flex-1 text-sm font-medium rounded-sm transition-colors duration-300 relative z-10 text-vscode-foreground"
|
||||
onClick={() =>
|
||||
manager.transition({ type: "SET_ACTIVE_TAB", payload: { tab: "mode" } })
|
||||
}>
|
||||
Modes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</TabHeader>
|
||||
|
||||
<TabContent className="p-3 pt-2">
|
||||
{state.activeTab === "mcp" && (
|
||||
<MarketplaceListView
|
||||
stateManager={stateManager}
|
||||
allTags={allTags}
|
||||
filteredTags={filteredTags}
|
||||
filterByType="mcp"
|
||||
/>
|
||||
)}
|
||||
{state.activeTab === "mode" && (
|
||||
<MarketplaceListView
|
||||
stateManager={stateManager}
|
||||
allTags={allTags}
|
||||
filteredTags={filteredTags}
|
||||
filterByType="mode"
|
||||
/>
|
||||
)}
|
||||
</TabContent>
|
||||
</Tab>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,486 +0,0 @@
|
|||
/**
|
||||
* 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, MarketplaceInstalledMetadata } from "@roo-code/types"
|
||||
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { WebviewMessage } from "../../../../src/shared/WebviewMessage"
|
||||
|
||||
export interface ViewState {
|
||||
allItems: MarketplaceItem[]
|
||||
organizationMcps: MarketplaceItem[]
|
||||
displayItems?: MarketplaceItem[] // Items currently being displayed (filtered or all)
|
||||
displayOrganizationMcps?: MarketplaceItem[] // Organization MCPs currently being displayed (filtered or all)
|
||||
isFetching: boolean
|
||||
activeTab: "mcp" | "mode"
|
||||
filters: {
|
||||
type: string
|
||||
search: string
|
||||
tags: string[]
|
||||
installed: "all" | "installed" | "not_installed" // Filter by installation status
|
||||
}
|
||||
installedMetadata?: MarketplaceInstalledMetadata // Store installed metadata for filtering
|
||||
}
|
||||
|
||||
type TransitionPayloads = {
|
||||
FETCH_ITEMS: undefined
|
||||
FETCH_COMPLETE: { items: MarketplaceItem[] }
|
||||
FETCH_ERROR: undefined
|
||||
SET_ACTIVE_TAB: { tab: ViewState["activeTab"] }
|
||||
UPDATE_FILTERS: { filters: Partial<ViewState["filters"]> }
|
||||
}
|
||||
|
||||
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 {
|
||||
// Always start with default state - no sessionStorage caching
|
||||
// This ensures fresh data from the extension is always used
|
||||
return this.getDefaultState()
|
||||
}
|
||||
|
||||
private getDefaultState(): ViewState {
|
||||
return {
|
||||
allItems: [],
|
||||
organizationMcps: [],
|
||||
displayItems: [], // Always initialize as empty array, not undefined
|
||||
displayOrganizationMcps: [], // Always initialize as empty array, not undefined
|
||||
isFetching: true, // Start with loading state for initial load
|
||||
activeTab: "mcp",
|
||||
filters: {
|
||||
type: "",
|
||||
search: "",
|
||||
tags: [],
|
||||
installed: "all",
|
||||
},
|
||||
}
|
||||
}
|
||||
// Removed auto-polling timeout
|
||||
private stateChangeHandlers: Set<StateChangeHandler> = new Set()
|
||||
|
||||
// Empty constructor is required for test initialization
|
||||
constructor() {
|
||||
// Initialize is now handled by the loadInitialState call in the property initialization
|
||||
}
|
||||
|
||||
public initialize(): void {
|
||||
// Set initial state
|
||||
this.state = this.getDefaultState()
|
||||
}
|
||||
|
||||
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 organizationMcps = this.state.organizationMcps.length ? [...this.state.organizationMcps] : []
|
||||
// Ensure displayItems is always an array, never undefined
|
||||
// If displayItems is undefined or null, fall back to allItems
|
||||
const displayItems = this.state.displayItems ? [...this.state.displayItems] : [...allItems]
|
||||
const displayOrganizationMcps = this.state.displayOrganizationMcps
|
||||
? [...this.state.displayOrganizationMcps]
|
||||
: [...organizationMcps]
|
||||
const tags = this.state.filters.tags.length ? [...this.state.filters.tags] : []
|
||||
|
||||
// Create minimal new state object
|
||||
return {
|
||||
...this.state,
|
||||
allItems,
|
||||
organizationMcps,
|
||||
displayItems,
|
||||
displayOrganizationMcps,
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
// Removed sessionStorage caching to ensure fresh data from extension is always used
|
||||
// This prevents old cached marketplace items from overriding fresh data
|
||||
}
|
||||
|
||||
public async transition(transition: ViewStateTransition): Promise<void> {
|
||||
switch (transition.type) {
|
||||
case "FETCH_ITEMS": {
|
||||
// Set fetching state to show loading indicator
|
||||
this.state = {
|
||||
...this.state,
|
||||
isFetching: true,
|
||||
}
|
||||
this.notifyStateChange()
|
||||
break
|
||||
}
|
||||
|
||||
case "FETCH_COMPLETE": {
|
||||
const { items } = transition.payload as TransitionPayloads["FETCH_COMPLETE"]
|
||||
// No timeout to clear anymore
|
||||
|
||||
// Compare with current state to avoid unnecessary updates
|
||||
if (JSON.stringify(items) === JSON.stringify(this.state.allItems)) {
|
||||
// No changes: update only isFetching flag and send minimal update
|
||||
this.state.isFetching = false
|
||||
this.stateChangeHandlers.forEach((handler) => {
|
||||
handler({
|
||||
...this.getState(),
|
||||
isFetching: false,
|
||||
})
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
// Calculate display items based on current filters
|
||||
let newDisplayItems: MarketplaceItem[]
|
||||
let newDisplayOrganizationMcps: MarketplaceItem[]
|
||||
if (this.isFilterActive()) {
|
||||
newDisplayItems = this.filterItems([...items], this.state.installedMetadata)
|
||||
newDisplayOrganizationMcps = this.filterItems(
|
||||
[...this.state.organizationMcps],
|
||||
this.state.installedMetadata,
|
||||
)
|
||||
} else {
|
||||
// No filters active - show all items
|
||||
newDisplayItems = [...items]
|
||||
newDisplayOrganizationMcps = [...this.state.organizationMcps]
|
||||
}
|
||||
|
||||
// Update allItems as source of truth
|
||||
this.state = {
|
||||
...this.state,
|
||||
allItems: [...items],
|
||||
displayItems: newDisplayItems,
|
||||
displayOrganizationMcps: newDisplayOrganizationMcps,
|
||||
isFetching: false,
|
||||
}
|
||||
|
||||
// Notify state change
|
||||
this.notifyStateChange()
|
||||
break
|
||||
}
|
||||
|
||||
case "FETCH_ERROR": {
|
||||
// Preserve current filters and items
|
||||
const { filters, activeTab, allItems, displayItems } = this.state
|
||||
|
||||
// Reset state but preserve filters and items
|
||||
this.state = {
|
||||
...this.getDefaultState(),
|
||||
filters,
|
||||
activeTab,
|
||||
allItems,
|
||||
displayItems,
|
||||
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,
|
||||
}
|
||||
|
||||
// Tab switching no longer triggers fetch - data comes automatically from extension
|
||||
|
||||
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,
|
||||
installed: filters.installed !== undefined ? filters.installed : this.state.filters.installed,
|
||||
}
|
||||
|
||||
// Update filters first
|
||||
this.state = {
|
||||
...this.state,
|
||||
filters: updatedFilters,
|
||||
}
|
||||
|
||||
// Apply filters to displayItems and displayOrganizationMcps with the updated filters
|
||||
const newDisplayItems = this.filterItems(this.state.allItems, this.state.installedMetadata)
|
||||
const newDisplayOrganizationMcps = this.filterItems(
|
||||
this.state.organizationMcps,
|
||||
this.state.installedMetadata,
|
||||
)
|
||||
|
||||
// Update state with filtered items
|
||||
this.state = {
|
||||
...this.state,
|
||||
displayItems: newDisplayItems,
|
||||
displayOrganizationMcps: newDisplayOrganizationMcps,
|
||||
}
|
||||
|
||||
// Send filter message
|
||||
vscode.postMessage({
|
||||
type: "filterMarketplaceItems",
|
||||
filters: updatedFilters,
|
||||
} as WebviewMessage)
|
||||
|
||||
this.notifyStateChange()
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public isFilterActive(): boolean {
|
||||
return !!(
|
||||
this.state.filters.type ||
|
||||
this.state.filters.search ||
|
||||
this.state.filters.tags.length > 0 ||
|
||||
this.state.filters.installed !== "all"
|
||||
)
|
||||
}
|
||||
|
||||
public filterItems(items: MarketplaceItem[], installedMetadata?: MarketplaceInstalledMetadata): MarketplaceItem[] {
|
||||
const { type, search, tags, installed } = this.state.filters
|
||||
const searchLower = search?.toLowerCase()
|
||||
|
||||
return items.filter((item) => {
|
||||
// Check type match
|
||||
if (type && item.type !== type) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check search match
|
||||
if (searchLower) {
|
||||
const nameMatch = item.name.toLowerCase().includes(searchLower)
|
||||
const descriptionMatch = (item.description || "").toLowerCase().includes(searchLower)
|
||||
if (!nameMatch && !descriptionMatch) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Check tag match
|
||||
if (tags.length > 0 && !item.tags?.some((tag) => tags.includes(tag))) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check installed status if filter is active
|
||||
if (installed !== "all" && installedMetadata) {
|
||||
const isInstalledGlobally = !!installedMetadata?.global?.[item.id]
|
||||
const isInstalledInProject = !!installedMetadata?.project?.[item.id]
|
||||
const isInstalled = isInstalledGlobally || isInstalledInProject
|
||||
|
||||
if (installed === "installed" && !isInstalled) {
|
||||
return false
|
||||
}
|
||||
if (installed === "not_installed" && isInstalled) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
public async handleMessage(message: any): Promise<void> {
|
||||
// Handle empty or invalid message
|
||||
if (!message || !message.type || message.type === "invalidType") {
|
||||
this.state = {
|
||||
...this.getDefaultState(),
|
||||
}
|
||||
this.notifyStateChange()
|
||||
return
|
||||
}
|
||||
|
||||
// Handle state updates
|
||||
if (message.type === "state") {
|
||||
// Handle empty state
|
||||
if (!message.state) {
|
||||
this.state = {
|
||||
...this.getDefaultState(),
|
||||
}
|
||||
this.notifyStateChange()
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
const marketplaceInstalledMetadata = message.state.marketplaceInstalledMetadata
|
||||
|
||||
if (marketplaceItems !== undefined) {
|
||||
// Always use the marketplace items from the extension when they're provided
|
||||
// This ensures fresh data is always displayed
|
||||
const items = [...marketplaceItems]
|
||||
|
||||
// Update installed metadata if provided
|
||||
if (marketplaceInstalledMetadata !== undefined) {
|
||||
this.state.installedMetadata = marketplaceInstalledMetadata
|
||||
}
|
||||
|
||||
// Calculate display items based on current filters
|
||||
// If no filters are active, show all items
|
||||
// If filters are active, apply filtering
|
||||
let newDisplayItems: MarketplaceItem[]
|
||||
let newDisplayOrganizationMcps: MarketplaceItem[]
|
||||
if (this.isFilterActive()) {
|
||||
newDisplayItems = this.filterItems(items, this.state.installedMetadata)
|
||||
newDisplayOrganizationMcps = this.filterItems(
|
||||
this.state.organizationMcps,
|
||||
this.state.installedMetadata,
|
||||
)
|
||||
} else {
|
||||
// No filters active - show all items
|
||||
newDisplayItems = items
|
||||
newDisplayOrganizationMcps = this.state.organizationMcps
|
||||
}
|
||||
|
||||
// Update state in a single operation
|
||||
this.state = {
|
||||
...this.state,
|
||||
isFetching: false,
|
||||
allItems: items,
|
||||
displayItems: newDisplayItems,
|
||||
displayOrganizationMcps: newDisplayOrganizationMcps,
|
||||
installedMetadata: marketplaceInstalledMetadata || this.state.installedMetadata,
|
||||
}
|
||||
// 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 isOnMcpTab = this.state.activeTab === "mcp"
|
||||
const hasCurrentItems = (this.state.allItems || []).length > 0
|
||||
const preserveTab = !isOnMcpTab && hasCurrentItems
|
||||
|
||||
this.notifyStateChange(preserveTab)
|
||||
}
|
||||
|
||||
// Handle marketplace button clicks
|
||||
if (message.type === "marketplaceButtonClicked") {
|
||||
if (message.text) {
|
||||
// Error case
|
||||
void this.transition({ type: "FETCH_ERROR" })
|
||||
} else {
|
||||
// Check if a specific tab is requested
|
||||
if (
|
||||
message.values?.marketplaceTab &&
|
||||
(message.values.marketplaceTab === "mcp" || message.values.marketplaceTab === "mode")
|
||||
) {
|
||||
// Set the active tab
|
||||
void this.transition({
|
||||
type: "SET_ACTIVE_TAB",
|
||||
payload: { tab: message.values.marketplaceTab },
|
||||
})
|
||||
}
|
||||
|
||||
// Refresh request
|
||||
void this.transition({ type: "FETCH_ITEMS" })
|
||||
}
|
||||
}
|
||||
|
||||
// Handle marketplace data updates (fetched on demand)
|
||||
if (message.type === "marketplaceData") {
|
||||
const marketplaceItems = message.marketplaceItems
|
||||
const organizationMcps = message.organizationMcps || []
|
||||
const marketplaceInstalledMetadata = message.marketplaceInstalledMetadata
|
||||
|
||||
if (marketplaceItems !== undefined) {
|
||||
// Always use the marketplace items from the extension when they're provided
|
||||
// This ensures fresh data is always displayed
|
||||
const items = [...marketplaceItems]
|
||||
const orgMcps = [...organizationMcps]
|
||||
|
||||
// Update installed metadata if provided
|
||||
if (marketplaceInstalledMetadata !== undefined) {
|
||||
this.state.installedMetadata = marketplaceInstalledMetadata
|
||||
}
|
||||
|
||||
const newDisplayItems = this.isFilterActive()
|
||||
? this.filterItems(items, this.state.installedMetadata)
|
||||
: items
|
||||
const newDisplayOrganizationMcps = this.isFilterActive()
|
||||
? this.filterItems(orgMcps, this.state.installedMetadata)
|
||||
: orgMcps
|
||||
|
||||
// Update state in a single operation
|
||||
this.state = {
|
||||
...this.state,
|
||||
isFetching: false,
|
||||
allItems: items,
|
||||
organizationMcps: orgMcps,
|
||||
displayItems: newDisplayItems,
|
||||
displayOrganizationMcps: newDisplayOrganizationMcps,
|
||||
installedMetadata: marketplaceInstalledMetadata || this.state.installedMetadata,
|
||||
}
|
||||
}
|
||||
|
||||
// Notify state change
|
||||
this.notifyStateChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,131 +0,0 @@
|
|||
// npx vitest run src/components/marketplace/__tests__/MarketplaceListView.spec.tsx
|
||||
|
||||
import { render, screen, fireEvent } from "@/utils/test-utils"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
|
||||
import { TooltipProvider } from "@/components/ui/tooltip"
|
||||
import { ExtensionStateContextProvider } from "@/context/ExtensionStateContext"
|
||||
|
||||
import { MarketplaceListView } from "../MarketplaceListView"
|
||||
import { ViewState } from "../MarketplaceViewStateManager"
|
||||
|
||||
vi.mock("@/i18n/TranslationContext", () => ({
|
||||
useAppTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockTransition = vi.fn()
|
||||
const mockState: ViewState = {
|
||||
allItems: [],
|
||||
organizationMcps: [],
|
||||
displayItems: [],
|
||||
displayOrganizationMcps: [],
|
||||
isFetching: false,
|
||||
activeTab: "mcp",
|
||||
filters: {
|
||||
type: "",
|
||||
search: "",
|
||||
tags: [],
|
||||
installed: "all",
|
||||
},
|
||||
}
|
||||
|
||||
vi.mock("../useStateManager", () => ({
|
||||
useStateManager: () => [mockState, { transition: mockTransition }],
|
||||
}))
|
||||
|
||||
const defaultProps = {
|
||||
stateManager: {} as any,
|
||||
allTags: ["tag1", "tag2"],
|
||||
filteredTags: ["tag1", "tag2"],
|
||||
}
|
||||
|
||||
describe("MarketplaceListView", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockState.filters.tags = []
|
||||
mockState.isFetching = false
|
||||
mockState.displayItems = []
|
||||
})
|
||||
|
||||
const renderWithProviders = (props = {}) =>
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<TooltipProvider delayDuration={300}>
|
||||
<MarketplaceListView {...defaultProps} {...props} />
|
||||
</TooltipProvider>
|
||||
</ExtensionStateContextProvider>,
|
||||
)
|
||||
|
||||
it("renders search input", () => {
|
||||
renderWithProviders()
|
||||
|
||||
const searchInput = screen.getByPlaceholderText("marketplace:filters.search.placeholder")
|
||||
expect(searchInput).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("does not render type filter (removed in simplified interface)", () => {
|
||||
renderWithProviders()
|
||||
|
||||
expect(screen.queryByText("marketplace:filters.type.label")).not.toBeInTheDocument()
|
||||
expect(screen.queryByText("marketplace:filters.type.all")).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("does not render sort options (removed in simplified interface)", () => {
|
||||
renderWithProviders()
|
||||
|
||||
expect(screen.queryByText("marketplace:filters.sort.label")).not.toBeInTheDocument()
|
||||
expect(screen.queryByText("marketplace:filters.sort.name")).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders tags section when tags are available", () => {
|
||||
renderWithProviders()
|
||||
|
||||
expect(screen.getByText("marketplace:filters.tags.label")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("shows loading state when fetching", () => {
|
||||
mockState.isFetching = true
|
||||
|
||||
renderWithProviders()
|
||||
|
||||
expect(screen.getByText("marketplace:items.refresh.refreshing")).toBeInTheDocument()
|
||||
expect(screen.getByText("marketplace:items.refresh.mayTakeMoment")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("shows empty state when no items and not fetching", () => {
|
||||
renderWithProviders()
|
||||
|
||||
expect(screen.getByText("marketplace:items.empty.noItems")).toBeInTheDocument()
|
||||
expect(screen.getByText("marketplace:items.empty.adjustFilters")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("updates search filter when typing", () => {
|
||||
renderWithProviders()
|
||||
|
||||
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"]
|
||||
|
||||
renderWithProviders()
|
||||
|
||||
const clearButton = screen.getByText("marketplace:filters.tags.clear")
|
||||
expect(clearButton).toBeInTheDocument()
|
||||
|
||||
await user.click(clearButton)
|
||||
expect(mockTransition).toHaveBeenCalledWith({
|
||||
type: "UPDATE_FILTERS",
|
||||
payload: { filters: { tags: [] } },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
import { render, waitFor } from "@testing-library/react"
|
||||
|
||||
import { ExtensionStateContext } from "@/context/ExtensionStateContext"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
|
||||
import { MarketplaceView } from "../MarketplaceView"
|
||||
import { MarketplaceViewStateManager } from "../MarketplaceViewStateManager"
|
||||
import { DEFAULT_CHECKPOINT_TIMEOUT_SECONDS } from "@roo-code/types"
|
||||
|
||||
vi.mock("@/utils/vscode", () => ({
|
||||
vscode: {
|
||||
postMessage: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("@/i18n/TranslationContext", () => ({
|
||||
useAppTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
describe("MarketplaceView", () => {
|
||||
let stateManager: MarketplaceViewStateManager
|
||||
let mockExtensionState: any
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
stateManager = new MarketplaceViewStateManager()
|
||||
|
||||
// Initialize state manager with some test data
|
||||
stateManager.transition({
|
||||
type: "FETCH_COMPLETE",
|
||||
payload: {
|
||||
items: [
|
||||
{
|
||||
id: "test-mcp",
|
||||
name: "Test MCP",
|
||||
type: "mcp" as const,
|
||||
description: "Test MCP server",
|
||||
tags: ["test"],
|
||||
content: "Test content",
|
||||
url: "https://test.com",
|
||||
author: "Test Author",
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
mockExtensionState = {
|
||||
organizationSettingsVersion: 1,
|
||||
// Add other required properties for the context
|
||||
didHydrateState: true,
|
||||
showWelcome: false,
|
||||
theme: {},
|
||||
mcpServers: [],
|
||||
filePaths: [],
|
||||
openedTabs: [],
|
||||
commands: [],
|
||||
organizationAllowList: { allowAll: true, providers: {} },
|
||||
cloudIsAuthenticated: false,
|
||||
sharingEnabled: false,
|
||||
hasOpenedModeSelector: false,
|
||||
setHasOpenedModeSelector: vi.fn(),
|
||||
alwaysAllowFollowupQuestions: false,
|
||||
setAlwaysAllowFollowupQuestions: vi.fn(),
|
||||
followupAutoApproveTimeoutMs: 60000,
|
||||
setFollowupAutoApproveTimeoutMs: vi.fn(),
|
||||
profileThresholds: {},
|
||||
setProfileThresholds: vi.fn(),
|
||||
checkpointTimeout: DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
|
||||
// ... other required context properties
|
||||
}
|
||||
})
|
||||
|
||||
it("should trigger fetchMarketplaceData when organization settings version changes", async () => {
|
||||
const { rerender } = render(
|
||||
<ExtensionStateContext.Provider value={mockExtensionState}>
|
||||
<MarketplaceView stateManager={stateManager} />
|
||||
</ExtensionStateContext.Provider>,
|
||||
)
|
||||
|
||||
// Initial render should not trigger fetch (version hasn't changed)
|
||||
expect(vscode.postMessage).not.toHaveBeenCalledWith({
|
||||
type: "fetchMarketplaceData",
|
||||
})
|
||||
|
||||
// Update the organization settings version
|
||||
mockExtensionState = {
|
||||
...mockExtensionState,
|
||||
organizationSettingsVersion: 2,
|
||||
checkpointTimeout: DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
|
||||
}
|
||||
|
||||
// Re-render with updated context
|
||||
rerender(
|
||||
<ExtensionStateContext.Provider value={mockExtensionState}>
|
||||
<MarketplaceView stateManager={stateManager} />
|
||||
</ExtensionStateContext.Provider>,
|
||||
)
|
||||
|
||||
// Wait for the effect to run
|
||||
await waitFor(() => {
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "fetchMarketplaceData",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("should trigger fetchMarketplaceData when organization settings version changes from -1", async () => {
|
||||
// Start with -1 version (default)
|
||||
mockExtensionState = {
|
||||
...mockExtensionState,
|
||||
organizationSettingsVersion: -1,
|
||||
}
|
||||
|
||||
const { rerender } = render(
|
||||
<ExtensionStateContext.Provider value={mockExtensionState}>
|
||||
<MarketplaceView stateManager={stateManager} />
|
||||
</ExtensionStateContext.Provider>,
|
||||
)
|
||||
|
||||
// Clear any initial calls
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Update to a defined version
|
||||
mockExtensionState = {
|
||||
...mockExtensionState,
|
||||
organizationSettingsVersion: 1,
|
||||
}
|
||||
|
||||
rerender(
|
||||
<ExtensionStateContext.Provider value={mockExtensionState}>
|
||||
<MarketplaceView stateManager={stateManager} />
|
||||
</ExtensionStateContext.Provider>,
|
||||
)
|
||||
|
||||
// Should trigger fetch when transitioning from -1 to 1
|
||||
await waitFor(() => {
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "fetchMarketplaceData",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("should not trigger fetchMarketplaceData when organization settings version remains the same", async () => {
|
||||
const { rerender } = render(
|
||||
<ExtensionStateContext.Provider value={mockExtensionState}>
|
||||
<MarketplaceView stateManager={stateManager} />
|
||||
</ExtensionStateContext.Provider>,
|
||||
)
|
||||
|
||||
// Re-render with same version
|
||||
rerender(
|
||||
<ExtensionStateContext.Provider value={mockExtensionState}>
|
||||
<MarketplaceView stateManager={stateManager} />
|
||||
</ExtensionStateContext.Provider>,
|
||||
)
|
||||
|
||||
// Should not trigger fetch when version hasn't changed
|
||||
await waitFor(() => {
|
||||
expect(vscode.postMessage).not.toHaveBeenCalledWith({
|
||||
type: "fetchMarketplaceData",
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,398 +0,0 @@
|
|||
import { MarketplaceViewStateManager, ViewStateTransition } from "../MarketplaceViewStateManager"
|
||||
import { MarketplaceItem } from "@roo-code/types"
|
||||
|
||||
// Mock vscode module
|
||||
vi.mock("@/utils/vscode", () => ({
|
||||
vscode: {
|
||||
postMessage: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe("MarketplaceViewStateManager", () => {
|
||||
let stateManager: MarketplaceViewStateManager
|
||||
let mockStateChangeHandler: ReturnType<typeof vi.fn>
|
||||
|
||||
const mockMarketplaceItems: MarketplaceItem[] = [
|
||||
{
|
||||
id: "test-mcp-1",
|
||||
name: "Test MCP Server 1",
|
||||
description: "A test MCP server",
|
||||
type: "mcp",
|
||||
url: "https://example.com/test-mcp-1",
|
||||
content: "test content",
|
||||
tags: ["test", "mcp"],
|
||||
},
|
||||
{
|
||||
id: "test-mode-1",
|
||||
name: "Test Mode 1",
|
||||
description: "A test mode",
|
||||
type: "mode",
|
||||
content: "test content",
|
||||
tags: ["test", "mode"],
|
||||
},
|
||||
{
|
||||
id: "test-mcp-2",
|
||||
name: "Test MCP Server 2",
|
||||
description: "Another test MCP server",
|
||||
type: "mcp",
|
||||
url: "https://example.com/test-mcp-2",
|
||||
content: "test content",
|
||||
tags: ["test", "server"],
|
||||
},
|
||||
]
|
||||
|
||||
beforeEach(() => {
|
||||
stateManager = new MarketplaceViewStateManager()
|
||||
mockStateChangeHandler = vi.fn()
|
||||
stateManager.onStateChange(mockStateChangeHandler)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
stateManager.cleanup()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("initialization", () => {
|
||||
it("should initialize with default state", () => {
|
||||
const state = stateManager.getState()
|
||||
|
||||
expect(state.allItems).toEqual([])
|
||||
expect(state.displayItems).toEqual([])
|
||||
expect(state.isFetching).toBe(true)
|
||||
expect(state.activeTab).toBe("mcp")
|
||||
expect(state.filters).toEqual({
|
||||
type: "",
|
||||
search: "",
|
||||
tags: [],
|
||||
installed: "all",
|
||||
})
|
||||
})
|
||||
|
||||
it("should ensure displayItems is never undefined in getState", () => {
|
||||
const state = stateManager.getState()
|
||||
|
||||
expect(state.displayItems).toBeDefined()
|
||||
expect(Array.isArray(state.displayItems)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("displayItems initialization fix", () => {
|
||||
it("should fall back to allItems when displayItems is undefined", () => {
|
||||
// Simulate the scenario where displayItems might be undefined
|
||||
const transition: ViewStateTransition = {
|
||||
type: "FETCH_COMPLETE",
|
||||
payload: { items: mockMarketplaceItems },
|
||||
}
|
||||
|
||||
stateManager.transition(transition)
|
||||
const state = stateManager.getState()
|
||||
|
||||
// Verify that displayItems is properly initialized with allItems when no filters are active
|
||||
expect(state.displayItems).toEqual(mockMarketplaceItems)
|
||||
expect(state.allItems).toEqual(mockMarketplaceItems)
|
||||
})
|
||||
|
||||
it("should ensure displayItems defaults to allItems when no filters are active", async () => {
|
||||
// Handle message with marketplace items (simulating the fix scenario)
|
||||
const message = {
|
||||
type: "state",
|
||||
state: {
|
||||
marketplaceItems: mockMarketplaceItems,
|
||||
},
|
||||
}
|
||||
|
||||
await stateManager.handleMessage(message)
|
||||
const state = stateManager.getState()
|
||||
|
||||
// Verify the fix: displayItems should equal allItems when no filters are active
|
||||
expect(state.displayItems).toEqual(mockMarketplaceItems)
|
||||
expect(state.allItems).toEqual(mockMarketplaceItems)
|
||||
expect(state.displayItems?.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it("should prevent marketplace blanking by ensuring displayItems is never empty when allItems has content", async () => {
|
||||
// This test specifically addresses the bug described in the PR
|
||||
const message = {
|
||||
type: "state",
|
||||
state: {
|
||||
marketplaceItems: mockMarketplaceItems,
|
||||
},
|
||||
}
|
||||
|
||||
await stateManager.handleMessage(message)
|
||||
const state = stateManager.getState()
|
||||
|
||||
// The key fix: displayItems should never be empty when allItems has content and no filters are active
|
||||
expect(state.allItems.length).toBeGreaterThan(0)
|
||||
expect(state.displayItems?.length).toBeGreaterThan(0)
|
||||
expect(state.displayItems).toEqual(state.allItems)
|
||||
})
|
||||
})
|
||||
|
||||
describe("state change notifications", () => {
|
||||
it("should notify handlers when marketplace items are loaded", async () => {
|
||||
const message = {
|
||||
type: "state",
|
||||
state: {
|
||||
marketplaceItems: mockMarketplaceItems,
|
||||
},
|
||||
}
|
||||
|
||||
await stateManager.handleMessage(message)
|
||||
|
||||
expect(mockStateChangeHandler).toHaveBeenCalled()
|
||||
const notifiedState = mockStateChangeHandler.mock.calls[0][0]
|
||||
expect(notifiedState.allItems).toEqual(mockMarketplaceItems)
|
||||
expect(notifiedState.displayItems).toEqual(mockMarketplaceItems)
|
||||
})
|
||||
|
||||
it("should prevent infinite loops by properly handling initial state", async () => {
|
||||
// Simulate multiple rapid state updates that could cause infinite loops
|
||||
const message1 = {
|
||||
type: "state",
|
||||
state: {
|
||||
marketplaceItems: [],
|
||||
},
|
||||
}
|
||||
|
||||
const message2 = {
|
||||
type: "state",
|
||||
state: {
|
||||
marketplaceItems: mockMarketplaceItems,
|
||||
},
|
||||
}
|
||||
|
||||
await stateManager.handleMessage(message1)
|
||||
await stateManager.handleMessage(message2)
|
||||
|
||||
// Should have been called twice, not infinitely
|
||||
expect(mockStateChangeHandler).toHaveBeenCalledTimes(2)
|
||||
|
||||
const finalState = stateManager.getState()
|
||||
expect(finalState.displayItems).toEqual(mockMarketplaceItems)
|
||||
})
|
||||
})
|
||||
|
||||
describe("filtering behavior", () => {
|
||||
beforeEach(async () => {
|
||||
// Set up initial state with items
|
||||
const message = {
|
||||
type: "state",
|
||||
state: {
|
||||
marketplaceItems: mockMarketplaceItems,
|
||||
},
|
||||
}
|
||||
await stateManager.handleMessage(message)
|
||||
})
|
||||
|
||||
it("should show all items when no filters are active", () => {
|
||||
const state = stateManager.getState()
|
||||
|
||||
expect(stateManager.isFilterActive()).toBe(false)
|
||||
expect(state.displayItems).toEqual(mockMarketplaceItems)
|
||||
})
|
||||
|
||||
it("should filter items when filters are applied", async () => {
|
||||
// First update the filters
|
||||
const filterTransition: ViewStateTransition = {
|
||||
type: "UPDATE_FILTERS",
|
||||
payload: {
|
||||
filters: { type: "mcp" },
|
||||
},
|
||||
}
|
||||
|
||||
await stateManager.transition(filterTransition)
|
||||
|
||||
// Then simulate a state message that would trigger filtering
|
||||
const message = {
|
||||
type: "state",
|
||||
state: {
|
||||
marketplaceItems: mockMarketplaceItems,
|
||||
},
|
||||
}
|
||||
await stateManager.handleMessage(message)
|
||||
|
||||
const state = stateManager.getState()
|
||||
|
||||
expect(stateManager.isFilterActive()).toBe(true)
|
||||
expect(state.displayItems?.length).toBe(2) // Only MCP items
|
||||
expect(state.displayItems?.every((item) => item.type === "mcp")).toBe(true)
|
||||
})
|
||||
|
||||
it("should restore all items when filters are cleared", async () => {
|
||||
// First apply a filter
|
||||
await stateManager.transition({
|
||||
type: "UPDATE_FILTERS",
|
||||
payload: { filters: { type: "mcp" } },
|
||||
})
|
||||
|
||||
// Simulate state message with filter active
|
||||
let message = {
|
||||
type: "state",
|
||||
state: {
|
||||
marketplaceItems: mockMarketplaceItems,
|
||||
},
|
||||
}
|
||||
await stateManager.handleMessage(message)
|
||||
|
||||
// Then clear the filter
|
||||
await stateManager.transition({
|
||||
type: "UPDATE_FILTERS",
|
||||
payload: { filters: { type: "" } },
|
||||
})
|
||||
|
||||
// Simulate state message with filter cleared
|
||||
message = {
|
||||
type: "state",
|
||||
state: {
|
||||
marketplaceItems: mockMarketplaceItems,
|
||||
},
|
||||
}
|
||||
await stateManager.handleMessage(message)
|
||||
|
||||
const state = stateManager.getState()
|
||||
expect(stateManager.isFilterActive()).toBe(false)
|
||||
expect(state.displayItems).toEqual(mockMarketplaceItems)
|
||||
})
|
||||
|
||||
it("should handle search filters correctly", async () => {
|
||||
await stateManager.transition({
|
||||
type: "UPDATE_FILTERS",
|
||||
payload: { filters: { search: "Mode" } },
|
||||
})
|
||||
|
||||
// Simulate state message to trigger filtering
|
||||
const message = {
|
||||
type: "state",
|
||||
state: {
|
||||
marketplaceItems: mockMarketplaceItems,
|
||||
},
|
||||
}
|
||||
await stateManager.handleMessage(message)
|
||||
|
||||
const state = stateManager.getState()
|
||||
expect(state.displayItems?.length).toBe(1)
|
||||
expect(state.displayItems?.[0].name).toBe("Test Mode 1")
|
||||
})
|
||||
|
||||
it("should handle tag filters correctly", async () => {
|
||||
await stateManager.transition({
|
||||
type: "UPDATE_FILTERS",
|
||||
payload: { filters: { tags: ["server"] } },
|
||||
})
|
||||
|
||||
// Simulate state message to trigger filtering
|
||||
const message = {
|
||||
type: "state",
|
||||
state: {
|
||||
marketplaceItems: mockMarketplaceItems,
|
||||
},
|
||||
}
|
||||
await stateManager.handleMessage(message)
|
||||
|
||||
const state = stateManager.getState()
|
||||
expect(state.displayItems?.length).toBe(1)
|
||||
expect(state.displayItems?.[0].name).toBe("Test MCP Server 2")
|
||||
})
|
||||
})
|
||||
|
||||
describe("tab switching", () => {
|
||||
it("should update active tab", async () => {
|
||||
await stateManager.transition({
|
||||
type: "SET_ACTIVE_TAB",
|
||||
payload: { tab: "mode" },
|
||||
})
|
||||
|
||||
const state = stateManager.getState()
|
||||
expect(state.activeTab).toBe("mode")
|
||||
})
|
||||
|
||||
it("should preserve items when switching tabs", async () => {
|
||||
// Load items first
|
||||
const message = {
|
||||
type: "state",
|
||||
state: {
|
||||
marketplaceItems: mockMarketplaceItems,
|
||||
},
|
||||
}
|
||||
await stateManager.handleMessage(message)
|
||||
|
||||
// Switch tab
|
||||
await stateManager.transition({
|
||||
type: "SET_ACTIVE_TAB",
|
||||
payload: { tab: "mode" },
|
||||
})
|
||||
|
||||
const state = stateManager.getState()
|
||||
expect(state.activeTab).toBe("mode")
|
||||
expect(state.allItems).toEqual(mockMarketplaceItems)
|
||||
expect(state.displayItems).toEqual(mockMarketplaceItems)
|
||||
})
|
||||
})
|
||||
|
||||
describe("error handling", () => {
|
||||
it("should handle empty or invalid messages gracefully", async () => {
|
||||
await stateManager.handleMessage(null)
|
||||
await stateManager.handleMessage({})
|
||||
await stateManager.handleMessage({ type: "invalidType" })
|
||||
|
||||
const state = stateManager.getState()
|
||||
expect(state.allItems).toEqual([])
|
||||
expect(state.displayItems).toEqual([])
|
||||
})
|
||||
|
||||
it("should handle fetch errors", async () => {
|
||||
// First load some items
|
||||
const message = {
|
||||
type: "state",
|
||||
state: {
|
||||
marketplaceItems: mockMarketplaceItems,
|
||||
},
|
||||
}
|
||||
await stateManager.handleMessage(message)
|
||||
|
||||
// Then trigger an error
|
||||
await stateManager.transition({ type: "FETCH_ERROR" })
|
||||
|
||||
const state = stateManager.getState()
|
||||
expect(state.isFetching).toBe(false)
|
||||
// Items should be preserved during error
|
||||
expect(state.allItems).toEqual(mockMarketplaceItems)
|
||||
})
|
||||
})
|
||||
|
||||
describe("state copying and immutability", () => {
|
||||
it("should return new arrays in getState to prevent mutation", () => {
|
||||
const state1 = stateManager.getState()
|
||||
const state2 = stateManager.getState()
|
||||
|
||||
expect(state1.allItems).not.toBe(state2.allItems)
|
||||
expect(state1.displayItems).not.toBe(state2.displayItems)
|
||||
expect(state1.filters.tags).not.toBe(state2.filters.tags)
|
||||
})
|
||||
|
||||
it("should not mutate original state when modifying returned state", async () => {
|
||||
const message = {
|
||||
type: "state",
|
||||
state: {
|
||||
marketplaceItems: mockMarketplaceItems,
|
||||
},
|
||||
}
|
||||
await stateManager.handleMessage(message)
|
||||
|
||||
const state = stateManager.getState()
|
||||
state.allItems.push({
|
||||
id: "mutated",
|
||||
name: "Mutated Item",
|
||||
description: "Should not affect original",
|
||||
type: "mcp",
|
||||
url: "https://example.com/mutated",
|
||||
content: "test",
|
||||
})
|
||||
|
||||
const newState = stateManager.getState()
|
||||
expect(newState.allItems.length).toBe(mockMarketplaceItems.length)
|
||||
expect(newState.allItems.find((item) => item.id === "mutated")).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,386 +0,0 @@
|
|||
import React, { useState, useMemo, useEffect } from "react"
|
||||
import { MarketplaceItem, McpParameter, McpInstallationMethod } from "@roo-code/types"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { useAppTranslation } from "@/i18n/TranslationContext"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
|
||||
interface MarketplaceInstallModalProps {
|
||||
item: MarketplaceItem | null
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
hasWorkspace: boolean
|
||||
}
|
||||
|
||||
export const MarketplaceInstallModal: React.FC<MarketplaceInstallModalProps> = ({
|
||||
item,
|
||||
isOpen,
|
||||
onClose,
|
||||
hasWorkspace,
|
||||
}) => {
|
||||
const { t } = useAppTranslation()
|
||||
const [scope, setScope] = useState<"project" | "global">(hasWorkspace ? "project" : "global")
|
||||
const [selectedMethodIndex, setSelectedMethodIndex] = useState(0)
|
||||
const [parameterValues, setParameterValues] = useState<Record<string, string>>({})
|
||||
const [validationError, setValidationError] = useState<string | null>(null)
|
||||
const [installationComplete, setInstallationComplete] = useState(false)
|
||||
|
||||
// Reset state when item changes
|
||||
React.useEffect(() => {
|
||||
if (item) {
|
||||
setSelectedMethodIndex(0)
|
||||
setParameterValues({})
|
||||
setValidationError(null)
|
||||
setInstallationComplete(false)
|
||||
}
|
||||
}, [item])
|
||||
|
||||
// Check if item has multiple installation methods
|
||||
const hasMultipleMethods = useMemo(() => {
|
||||
return item && Array.isArray(item.content) && item.content.length > 1
|
||||
}, [item])
|
||||
|
||||
// Get installation method names (for display in dropdown)
|
||||
const methodNames = useMemo(() => {
|
||||
if (!item || !Array.isArray(item.content)) return []
|
||||
|
||||
// Content is an array of McpInstallationMethod objects
|
||||
return (item.content as Array<{ name: string; content: string }>).map((method) => method.name)
|
||||
}, [item])
|
||||
|
||||
// Get effective parameters for the selected method (global + method-specific)
|
||||
const effectiveParameters = useMemo(() => {
|
||||
if (!item) return []
|
||||
|
||||
const globalParams = item.type === "mcp" ? item.parameters || [] : []
|
||||
let methodParams: McpParameter[] = []
|
||||
|
||||
// Get method-specific parameters if content is an array
|
||||
if (Array.isArray(item.content)) {
|
||||
const selectedMethod = item.content[selectedMethodIndex] as McpInstallationMethod
|
||||
methodParams = selectedMethod?.parameters || []
|
||||
}
|
||||
|
||||
// Create map with global params first, then override with method-specific ones
|
||||
const paramMap = new Map<string, McpParameter>()
|
||||
globalParams.forEach((p) => paramMap.set(p.key, p))
|
||||
methodParams.forEach((p) => paramMap.set(p.key, p))
|
||||
|
||||
return Array.from(paramMap.values())
|
||||
}, [item, selectedMethodIndex])
|
||||
|
||||
// Get effective prerequisites for the selected method (global + method-specific)
|
||||
const effectivePrerequisites = useMemo(() => {
|
||||
if (!item) return []
|
||||
|
||||
const globalPrereqs = item.prerequisites || []
|
||||
let methodPrereqs: string[] = []
|
||||
|
||||
// Get method-specific prerequisites if content is an array
|
||||
if (Array.isArray(item.content)) {
|
||||
const selectedMethod = item.content[selectedMethodIndex] as McpInstallationMethod
|
||||
methodPrereqs = selectedMethod?.prerequisites || []
|
||||
}
|
||||
|
||||
// Combine and deduplicate prerequisites
|
||||
const allPrereqs = [...globalPrereqs, ...methodPrereqs]
|
||||
return Array.from(new Set(allPrereqs))
|
||||
}, [item, selectedMethodIndex])
|
||||
|
||||
// Update parameter values when method changes
|
||||
React.useEffect(() => {
|
||||
if (item) {
|
||||
// Get effective parameters for current method
|
||||
const globalParams = item.type === "mcp" ? item.parameters || [] : []
|
||||
let methodParams: McpParameter[] = []
|
||||
|
||||
if (Array.isArray(item.content)) {
|
||||
const selectedMethod = item.content[selectedMethodIndex] as McpInstallationMethod
|
||||
methodParams = selectedMethod?.parameters || []
|
||||
}
|
||||
|
||||
// Create map with global params first, then override with method-specific ones
|
||||
const paramMap = new Map<string, McpParameter>()
|
||||
globalParams.forEach((p) => paramMap.set(p.key, p))
|
||||
methodParams.forEach((p) => paramMap.set(p.key, p))
|
||||
|
||||
const currentEffectiveParams = Array.from(paramMap.values())
|
||||
|
||||
// Initialize parameter values for effective parameters
|
||||
setParameterValues((prev) => {
|
||||
const newValues: Record<string, string> = {}
|
||||
currentEffectiveParams.forEach((param) => {
|
||||
// Keep existing value if it exists, otherwise empty string
|
||||
newValues[param.key] = prev[param.key] || ""
|
||||
})
|
||||
return newValues
|
||||
})
|
||||
}
|
||||
}, [item, selectedMethodIndex])
|
||||
|
||||
// Listen for installation result messages
|
||||
useEffect(() => {
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
const message = event.data
|
||||
if (message.type === "marketplaceInstallResult" && message.slug === item?.id) {
|
||||
if (message.success) {
|
||||
// Installation succeeded - show success state
|
||||
setInstallationComplete(true)
|
||||
setValidationError(null)
|
||||
|
||||
// Request fresh marketplace data to update installed status
|
||||
vscode.postMessage({
|
||||
type: "fetchMarketplaceData",
|
||||
})
|
||||
} else {
|
||||
// Installation failed - show error
|
||||
setValidationError(message.error || "Installation failed")
|
||||
setInstallationComplete(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("message", handleMessage)
|
||||
return () => window.removeEventListener("message", handleMessage)
|
||||
}, [item?.id])
|
||||
|
||||
const handleInstall = () => {
|
||||
if (!item) return
|
||||
|
||||
// Clear previous validation error
|
||||
setValidationError(null)
|
||||
|
||||
// Validate required parameters from effective parameters (global + method-specific)
|
||||
for (const param of effectiveParameters) {
|
||||
// Only validate if parameter is not optional (optional defaults to false)
|
||||
if (!param.optional && !parameterValues[param.key]?.trim()) {
|
||||
setValidationError(t("marketplace:install.validationRequired", { paramName: param.name }))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare parameters - ensure optional parameters have empty string if not provided
|
||||
const finalParameters: Record<string, any> = { ...parameterValues }
|
||||
for (const param of effectiveParameters) {
|
||||
if (param.optional && !finalParameters[param.key]) {
|
||||
finalParameters[param.key] = ""
|
||||
}
|
||||
}
|
||||
|
||||
// Send install message with parameters
|
||||
vscode.postMessage({
|
||||
type: "installMarketplaceItem",
|
||||
mpItem: item,
|
||||
mpInstallOptions: {
|
||||
target: scope,
|
||||
parameters: {
|
||||
...finalParameters,
|
||||
_selectedIndex: hasMultipleMethods ? selectedMethodIndex : undefined,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Don't show success immediately - wait for backend result
|
||||
// The success state will be shown when installation actually succeeds
|
||||
setValidationError(null)
|
||||
}
|
||||
|
||||
const handlePostInstallAction = (tab: "mcp" | "modes") => {
|
||||
const section = tab === "mcp" ? "mcp" : "modes"
|
||||
|
||||
vscode.postMessage({
|
||||
type: "switchTab",
|
||||
tab: "settings",
|
||||
values: { section },
|
||||
})
|
||||
|
||||
// Close the modal
|
||||
onClose()
|
||||
}
|
||||
|
||||
if (!item) return null
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{installationComplete
|
||||
? t("marketplace:install.successTitle", { name: item.name })
|
||||
: item.type === "mcp"
|
||||
? t("marketplace:install.titleMcp", { name: item.name })
|
||||
: t("marketplace:install.titleMode", { name: item.name })}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{installationComplete ? (
|
||||
t("marketplace:install.successDescription")
|
||||
) : item.type === "mcp" && item.url ? (
|
||||
<a
|
||||
href={item.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline inline-flex items-center gap-1">
|
||||
{t("marketplace:install.moreInfoMcp", { name: item.name })}
|
||||
</a>
|
||||
) : null}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{installationComplete ? (
|
||||
// Post-installation options
|
||||
<div className="space-y-4 py-2">
|
||||
<div className="text-center space-y-4">
|
||||
<div className="text-green-500 text-lg">✓ {t("marketplace:install.installed")}</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{item.type === "mcp"
|
||||
? t("marketplace:install.whatNextMcp")
|
||||
: t("marketplace:install.whatNextMode")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// Installation configuration
|
||||
<div className="space-y-4 py-2">
|
||||
{/* Installation Scope */}
|
||||
<div className="space-y-2">
|
||||
<div className="text-base font-semibold">{t("marketplace:install.scope")}</div>
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center space-x-2">
|
||||
<input
|
||||
type="radio"
|
||||
name="scope"
|
||||
value="project"
|
||||
checked={scope === "project"}
|
||||
onChange={() => setScope("project")}
|
||||
disabled={!hasWorkspace}
|
||||
className="rounded-full"
|
||||
/>
|
||||
<span className={!hasWorkspace ? "opacity-50" : ""}>
|
||||
{t("marketplace:install.project")}
|
||||
</span>
|
||||
</label>
|
||||
<label className="flex items-center space-x-2">
|
||||
<input
|
||||
type="radio"
|
||||
name="scope"
|
||||
value="global"
|
||||
checked={scope === "global"}
|
||||
onChange={() => setScope("global")}
|
||||
className="rounded-full"
|
||||
/>
|
||||
<span>{t("marketplace:install.global")}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Installation Method (if multiple) */}
|
||||
{hasMultipleMethods && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-base font-semibold">{t("marketplace:install.method")}</div>
|
||||
<Select
|
||||
value={String(selectedMethodIndex)}
|
||||
onValueChange={(value) => setSelectedMethodIndex(Number(value))}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{methodNames.map((name, index) => (
|
||||
<SelectItem key={index} value={String(index)}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Prerequisites */}
|
||||
{effectivePrerequisites.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-base font-semibold">{t("marketplace:install.prerequisites")}</div>
|
||||
<ul className="list-disc list-inside space-y-1 text-sm">
|
||||
{effectivePrerequisites.map((prereq, index) => (
|
||||
<li key={index} className="text-muted-foreground">
|
||||
{prereq}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Parameters */}
|
||||
{effectiveParameters.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<div className="text-base font-semibold">
|
||||
{t("marketplace:install.configuration")}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t("marketplace:install.configurationDescription")}
|
||||
</div>
|
||||
</div>
|
||||
{effectiveParameters.map((param) => (
|
||||
<div key={param.key} className="space-y-1">
|
||||
<label htmlFor={param.key} className="text-sm">
|
||||
{param.name}
|
||||
{param.optional ? " (optional)" : ""}
|
||||
</label>
|
||||
<Input
|
||||
id={param.key}
|
||||
type="text"
|
||||
placeholder={param.placeholder}
|
||||
value={parameterValues[param.key] || ""}
|
||||
onChange={(e) =>
|
||||
setParameterValues((prev) => ({
|
||||
...prev,
|
||||
[param.key]: e.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{/* Validation Error */}
|
||||
{validationError && (
|
||||
<div className="text-sm text-red-500 bg-red-500/10 border border-red-500/20 rounded p-2">
|
||||
{validationError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
{installationComplete ? (
|
||||
<>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
{t("marketplace:install.done")}
|
||||
</Button>
|
||||
<Button onClick={() => handlePostInstallAction(item.type === "mcp" ? "mcp" : "modes")}>
|
||||
{item.type === "mcp"
|
||||
? t("marketplace:install.goToMcp")
|
||||
: t("marketplace:install.goToModes")}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
{t("common:answers.cancel")}
|
||||
</Button>
|
||||
<Button onClick={handleInstall}>{t("marketplace:install.button")}</Button>
|
||||
</>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,276 +0,0 @@
|
|||
import React, { useMemo, useState, useEffect } from "react"
|
||||
import { MarketplaceItem } from "@roo-code/types"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { ViewState } from "../MarketplaceViewStateManager"
|
||||
import { useAppTranslation } from "@/i18n/TranslationContext"
|
||||
import { isValidUrl } from "../../../utils/url"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { StandardTooltip } from "@/components/ui"
|
||||
import { MarketplaceInstallModal } from "./MarketplaceInstallModal"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui"
|
||||
|
||||
interface ItemInstalledMetadata {
|
||||
type: string
|
||||
}
|
||||
|
||||
interface MarketplaceItemCardProps {
|
||||
item: MarketplaceItem
|
||||
filters: ViewState["filters"]
|
||||
setFilters: (filters: Partial<ViewState["filters"]>) => void
|
||||
installed: {
|
||||
project: ItemInstalledMetadata | undefined
|
||||
global: ItemInstalledMetadata | undefined
|
||||
}
|
||||
}
|
||||
|
||||
export const MarketplaceItemCard: React.FC<MarketplaceItemCardProps> = ({ item, filters, setFilters, installed }) => {
|
||||
const { t } = useAppTranslation()
|
||||
const { cwd } = useExtensionState()
|
||||
const [showInstallModal, setShowInstallModal] = useState(false)
|
||||
const [showRemoveConfirm, setShowRemoveConfirm] = useState(false)
|
||||
const [removeTarget, setRemoveTarget] = useState<"project" | "global">("project")
|
||||
const [removeError, setRemoveError] = useState<string | null>(null)
|
||||
|
||||
// Listen for removal result messages
|
||||
useEffect(() => {
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
const message = event.data
|
||||
if (message.type === "marketplaceRemoveResult" && message.slug === item.id) {
|
||||
if (message.success) {
|
||||
// Removal succeeded - refresh marketplace data
|
||||
vscode.postMessage({
|
||||
type: "fetchMarketplaceData",
|
||||
})
|
||||
} else {
|
||||
// Removal failed - show error message to user
|
||||
setRemoveError(message.error || t("marketplace:items.unknownError"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("message", handleMessage)
|
||||
return () => window.removeEventListener("message", handleMessage)
|
||||
}, [item.id, t])
|
||||
|
||||
const typeLabel = useMemo(() => {
|
||||
const labels: Partial<Record<MarketplaceItem["type"], string>> = {
|
||||
mode: t("marketplace:filters.type.mode"),
|
||||
mcp: t("marketplace:filters.type.mcpServer"),
|
||||
}
|
||||
return labels[item.type] ?? "N/A"
|
||||
}, [item.type, t])
|
||||
|
||||
// Determine installation status
|
||||
const isInstalledGlobally = !!installed.global
|
||||
const isInstalledInProject = !!installed.project
|
||||
const isInstalled = isInstalledGlobally || isInstalledInProject
|
||||
|
||||
const handleInstallClick = () => {
|
||||
// Show modal for all item types (MCP and modes)
|
||||
setShowInstallModal(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="border border-vscode-panel-border rounded-xl cursor-default p-3 transition-colors bg-vscode-editor-background hover:bg-vscode-editor-foreground/5">
|
||||
<div className="flex gap-2 items-start justify-between">
|
||||
<div className="flex gap-2 items-start">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-vscode-foreground mt-0 mb-1 leading-none">
|
||||
{item.type === "mcp" && item.url && isValidUrl(item.url) ? (
|
||||
<Button
|
||||
variant="link"
|
||||
className="p-0 h-auto text-lg font-semibold text-vscode-foreground hover:underline"
|
||||
onClick={() => vscode.postMessage({ type: "openExternal", url: item.url })}>
|
||||
{item.name}
|
||||
</Button>
|
||||
) : (
|
||||
item.name
|
||||
)}
|
||||
</h3>
|
||||
<AuthorInfo item={item} typeLabel={typeLabel} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{isInstalled ? (
|
||||
/* Single Remove button when installed */
|
||||
<StandardTooltip
|
||||
content={
|
||||
isInstalledInProject
|
||||
? t("marketplace:items.card.removeProjectTooltip")
|
||||
: t("marketplace:items.card.removeGlobalTooltip")
|
||||
}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="text-xs h-5 py-0 px-2"
|
||||
onClick={() => {
|
||||
// Determine which installation to remove (prefer project over global)
|
||||
const target = isInstalledInProject ? "project" : "global"
|
||||
setRemoveTarget(target)
|
||||
setShowRemoveConfirm(true)
|
||||
}}>
|
||||
{t("marketplace:items.card.remove")}
|
||||
</Button>
|
||||
</StandardTooltip>
|
||||
) : (
|
||||
/* Single Install button when not installed */
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
className="text-xs h-5 py-0 px-2"
|
||||
onClick={handleInstallClick}>
|
||||
{t("marketplace:items.card.install")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Error message display */}
|
||||
{removeError && (
|
||||
<div className="text-vscode-errorForeground text-sm mt-2">
|
||||
{t("marketplace:items.removeFailed", { error: removeError })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="my-2 text-vscode-foreground">{item.description}</p>
|
||||
|
||||
{/* Installation status badges and tags in the same row */}
|
||||
{(isInstalled || (item.tags && item.tags.length > 0)) && (
|
||||
<div className="relative flex flex-wrap gap-1 my-2">
|
||||
{/* Installation status badge on the left */}
|
||||
{isInstalled && (
|
||||
<span className="text-xs px-2 py-0.5 rounded-sm h-5 flex items-center bg-green-600/20 text-green-400 border border-green-600/30 shrink-0">
|
||||
{t("marketplace:items.card.installed")}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Tags on the right */}
|
||||
{item.tags &&
|
||||
item.tags.length > 0 &&
|
||||
item.tags.map((tag) => (
|
||||
<StandardTooltip
|
||||
key={tag}
|
||||
content={
|
||||
filters.tags.includes(tag)
|
||||
? t("marketplace:filters.tags.clear", { count: tag })
|
||||
: t("marketplace:filters.tags.clickToFilter")
|
||||
}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className={cn("rounded-sm capitalize text-xs px-2 h-5", {
|
||||
"border-solid border-primary text-primary": filters.tags.includes(tag),
|
||||
})}
|
||||
onClick={() => {
|
||||
const newTags = filters.tags.includes(tag)
|
||||
? filters.tags.filter((t: string) => t !== tag)
|
||||
: [...filters.tags, tag]
|
||||
setFilters({ tags: newTags })
|
||||
}}>
|
||||
{tag}
|
||||
</Button>
|
||||
</StandardTooltip>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Installation Modal - Outside the clickable card */}
|
||||
<MarketplaceInstallModal
|
||||
item={item}
|
||||
isOpen={showInstallModal}
|
||||
onClose={() => setShowInstallModal(false)}
|
||||
hasWorkspace={!!cwd}
|
||||
/>
|
||||
|
||||
{/* Remove Confirmation Dialog */}
|
||||
<AlertDialog open={showRemoveConfirm} onOpenChange={setShowRemoveConfirm}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
{item.type === "mode"
|
||||
? t("marketplace:removeConfirm.mode.title")
|
||||
: t("marketplace:removeConfirm.mcp.title")}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{item.type === "mode" ? (
|
||||
<>
|
||||
{t("marketplace:removeConfirm.mode.message", { modeName: item.name })}
|
||||
<div className="mt-2 text-sm">
|
||||
{t("marketplace:removeConfirm.mode.rulesWarning")}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
t("marketplace:removeConfirm.mcp.message", { mcpName: item.name })
|
||||
)}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t("marketplace:removeConfirm.cancel")}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => {
|
||||
// Clear any previous error
|
||||
setRemoveError(null)
|
||||
|
||||
vscode.postMessage({
|
||||
type: "removeInstalledMarketplaceItem",
|
||||
mpItem: item,
|
||||
mpInstallOptions: { target: removeTarget },
|
||||
})
|
||||
|
||||
setShowRemoveConfirm(false)
|
||||
}}>
|
||||
{t("marketplace:removeConfirm.confirm")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
interface AuthorInfoProps {
|
||||
item: MarketplaceItem
|
||||
typeLabel: string
|
||||
}
|
||||
|
||||
const AuthorInfo: React.FC<AuthorInfoProps> = ({ item, typeLabel }) => {
|
||||
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">
|
||||
{typeLabel}{" "}
|
||||
{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
|
||||
}
|
||||
|
|
@ -1,157 +0,0 @@
|
|||
import { render, screen, fireEvent, waitFor } from "@/utils/test-utils"
|
||||
|
||||
import { MarketplaceItem } from "@roo-code/types"
|
||||
|
||||
import { MarketplaceInstallModal } from "../MarketplaceInstallModal"
|
||||
|
||||
vi.mock("@/utils/vscode", () => ({
|
||||
vscode: {
|
||||
postMessage: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
import { vscode } from "@/utils/vscode"
|
||||
const mockPostMessage = vscode.postMessage as any
|
||||
|
||||
vi.mock("@/i18n/TranslationContext", () => ({
|
||||
useAppTranslation: () => ({
|
||||
t: (key: string, params?: any) => {
|
||||
// Simple mock translation
|
||||
if (key === "marketplace:install.configuration") return "Configuration"
|
||||
if (key === "marketplace:install.button") return "Install"
|
||||
if (key === "common:answers.cancel") return "Cancel"
|
||||
if (key === "marketplace:install.validationRequired") {
|
||||
return `Please provide a value for ${params?.paramName || "parameter"}`
|
||||
}
|
||||
return key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
describe("MarketplaceInstallModal - Optional Parameters", () => {
|
||||
const mockOnClose = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
const createMcpItemWithParams = (parameters: any[]): MarketplaceItem => ({
|
||||
id: "test-mcp",
|
||||
name: "Test MCP",
|
||||
description: "Test MCP with parameters",
|
||||
type: "mcp",
|
||||
url: "https://example.com/test-mcp",
|
||||
content: '{"test-server": {"command": "test", "args": ["--key", "{{api_key}}", "--endpoint", "{{endpoint}}"]}}',
|
||||
parameters,
|
||||
})
|
||||
|
||||
it("should show (optional) label for optional parameters", () => {
|
||||
const item = createMcpItemWithParams([
|
||||
{
|
||||
name: "API Key",
|
||||
key: "api_key",
|
||||
placeholder: "Enter API key",
|
||||
optional: false,
|
||||
},
|
||||
{
|
||||
name: "Custom Endpoint",
|
||||
key: "endpoint",
|
||||
placeholder: "Leave empty for default",
|
||||
optional: true,
|
||||
},
|
||||
])
|
||||
|
||||
render(<MarketplaceInstallModal item={item} isOpen={true} onClose={mockOnClose} hasWorkspace={true} />)
|
||||
|
||||
expect(screen.getByText("API Key")).toBeInTheDocument()
|
||||
expect(screen.getByText("Custom Endpoint (optional)")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should render input fields correctly for optional parameters", () => {
|
||||
const item = createMcpItemWithParams([
|
||||
{
|
||||
name: "API Key",
|
||||
key: "api_key",
|
||||
placeholder: "Enter API key",
|
||||
optional: false,
|
||||
},
|
||||
{
|
||||
name: "Custom Endpoint",
|
||||
key: "endpoint",
|
||||
placeholder: "Leave empty for default",
|
||||
optional: true,
|
||||
},
|
||||
])
|
||||
|
||||
render(<MarketplaceInstallModal item={item} isOpen={true} onClose={mockOnClose} hasWorkspace={true} />)
|
||||
|
||||
// Check that input fields are rendered
|
||||
const apiKeyInput = screen.getByPlaceholderText("Enter API key")
|
||||
const endpointInput = screen.getByPlaceholderText("Leave empty for default")
|
||||
|
||||
expect(apiKeyInput).toBeInTheDocument()
|
||||
expect(endpointInput).toBeInTheDocument()
|
||||
expect(endpointInput).toHaveValue("")
|
||||
})
|
||||
|
||||
it("should require non-optional parameters", async () => {
|
||||
const item = createMcpItemWithParams([
|
||||
{
|
||||
name: "API Key",
|
||||
key: "api_key",
|
||||
placeholder: "Enter API key",
|
||||
optional: false,
|
||||
},
|
||||
{
|
||||
name: "Custom Endpoint",
|
||||
key: "endpoint",
|
||||
placeholder: "Leave empty for default",
|
||||
optional: true,
|
||||
},
|
||||
])
|
||||
|
||||
render(<MarketplaceInstallModal item={item} isOpen={true} onClose={mockOnClose} hasWorkspace={true} />)
|
||||
|
||||
// Leave required parameter empty, fill optional one
|
||||
const endpointInput = screen.getByPlaceholderText("Leave empty for default")
|
||||
fireEvent.change(endpointInput, { target: { value: "https://custom.endpoint.com" } })
|
||||
|
||||
// Click install without filling required parameter
|
||||
const installButton = screen.getByText("Install")
|
||||
fireEvent.click(installButton)
|
||||
|
||||
// Should show validation error
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Please provide a value for API Key")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Should not call postMessage
|
||||
expect(mockPostMessage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should handle parameters without optional field (defaults to required)", async () => {
|
||||
const item = createMcpItemWithParams([
|
||||
{
|
||||
name: "API Key",
|
||||
key: "api_key",
|
||||
placeholder: "Enter API key",
|
||||
// No optional field - should default to required
|
||||
},
|
||||
])
|
||||
|
||||
render(<MarketplaceInstallModal item={item} isOpen={true} onClose={mockOnClose} hasWorkspace={true} />)
|
||||
|
||||
// Should not show (optional) label
|
||||
expect(screen.getByText("API Key")).toBeInTheDocument()
|
||||
expect(screen.queryByText("API Key (optional)")).not.toBeInTheDocument()
|
||||
|
||||
// Click install without filling parameter
|
||||
const installButton = screen.getByText("Install")
|
||||
fireEvent.click(installButton)
|
||||
|
||||
// Should show validation error
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Please provide a value for API Key")).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,218 +0,0 @@
|
|||
import { render, screen, fireEvent, waitFor } from "@/utils/test-utils"
|
||||
|
||||
import { MarketplaceItem } from "@roo-code/types"
|
||||
|
||||
import { MarketplaceInstallModal } from "../MarketplaceInstallModal"
|
||||
|
||||
vi.mock("@/utils/vscode", () => ({
|
||||
vscode: {
|
||||
postMessage: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
// Import the mocked vscode after setting up the mock
|
||||
import { vscode } from "@/utils/vscode"
|
||||
const mockedVscode = vscode as any
|
||||
|
||||
// Mock the translation hook
|
||||
vi.mock("@/i18n/TranslationContext", () => ({
|
||||
useAppTranslation: () => ({
|
||||
t: (key: string, params?: any) => {
|
||||
// Simple mock translation that returns the key with params
|
||||
if (key === "marketplace:install.validationRequired") {
|
||||
return `Please provide a value for ${params?.paramName || "parameter"}`
|
||||
}
|
||||
if (params) {
|
||||
return `${key}:${JSON.stringify(params)}`
|
||||
}
|
||||
return key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
describe("MarketplaceInstallModal - Nested Parameters", () => {
|
||||
const mockOnClose = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
// Reset the mock function
|
||||
mockedVscode.postMessage.mockClear()
|
||||
})
|
||||
|
||||
const createMockItem = (hasNestedParams = false): MarketplaceItem => ({
|
||||
id: "test-item",
|
||||
name: "Test MCP Server",
|
||||
description: "A test MCP server",
|
||||
type: "mcp",
|
||||
url: "https://example.com/test-mcp",
|
||||
author: "Test Author",
|
||||
tags: ["test"],
|
||||
// Global parameters
|
||||
parameters: [
|
||||
{
|
||||
name: "Global API Key",
|
||||
key: "apiKey",
|
||||
placeholder: "Enter your API key",
|
||||
optional: false,
|
||||
},
|
||||
{
|
||||
name: "Global Optional Setting",
|
||||
key: "globalOptional",
|
||||
placeholder: "Optional setting",
|
||||
optional: true,
|
||||
},
|
||||
],
|
||||
content: hasNestedParams
|
||||
? [
|
||||
{
|
||||
name: "NPM Installation",
|
||||
content: "npm install {{packageName}}",
|
||||
parameters: [
|
||||
{
|
||||
name: "Package Name",
|
||||
key: "packageName",
|
||||
placeholder: "Enter package name",
|
||||
optional: false,
|
||||
},
|
||||
// Override global parameter
|
||||
{
|
||||
name: "NPM API Key",
|
||||
key: "apiKey",
|
||||
placeholder: "Enter NPM API key",
|
||||
optional: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Docker Installation",
|
||||
content: "docker run {{imageName}}",
|
||||
parameters: [
|
||||
{
|
||||
name: "Docker Image",
|
||||
key: "imageName",
|
||||
placeholder: "Enter image name",
|
||||
optional: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
: "npm install test-package",
|
||||
})
|
||||
|
||||
it("should display global parameters when no nested parameters exist", () => {
|
||||
const item = createMockItem(false)
|
||||
render(<MarketplaceInstallModal item={item} isOpen={true} onClose={mockOnClose} hasWorkspace={true} />)
|
||||
|
||||
// Should show global parameters
|
||||
expect(screen.getByPlaceholderText("Enter your API key")).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText("Optional setting")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should display effective parameters for selected installation method", () => {
|
||||
const item = createMockItem(true)
|
||||
render(<MarketplaceInstallModal item={item} isOpen={true} onClose={mockOnClose} hasWorkspace={true} />)
|
||||
|
||||
// Should show method dropdown for multiple methods
|
||||
expect(screen.getByRole("combobox")).toBeInTheDocument()
|
||||
|
||||
// Should show effective parameters (global + method-specific for NPM method)
|
||||
expect(screen.getByPlaceholderText("Enter package name")).toBeInTheDocument() // Method-specific
|
||||
expect(screen.getByPlaceholderText("Enter NPM API key")).toBeInTheDocument() // Overridden global
|
||||
expect(screen.getByPlaceholderText("Optional setting")).toBeInTheDocument() // Global optional
|
||||
})
|
||||
|
||||
it("should update parameters when switching installation methods", async () => {
|
||||
const item = createMockItem(true)
|
||||
render(<MarketplaceInstallModal item={item} isOpen={true} onClose={mockOnClose} hasWorkspace={true} />)
|
||||
|
||||
// Initially should show NPM method parameters
|
||||
expect(screen.getByPlaceholderText("Enter package name")).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText("Enter NPM API key")).toBeInTheDocument()
|
||||
|
||||
// Switch to Docker method
|
||||
const methodSelect = screen.getByRole("combobox")
|
||||
fireEvent.click(methodSelect)
|
||||
|
||||
// Find and click Docker option
|
||||
await waitFor(() => {
|
||||
const dockerOption = screen.getByText("Docker Installation")
|
||||
fireEvent.click(dockerOption)
|
||||
})
|
||||
|
||||
// Should now show Docker method parameters
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText("Enter image name")).toBeInTheDocument()
|
||||
// Should still show global API key (not overridden in Docker method)
|
||||
expect(screen.getByPlaceholderText("Enter your API key")).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText("Optional setting")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Package name parameter should no longer be visible
|
||||
expect(screen.queryByPlaceholderText("Enter package name")).not.toBeInTheDocument()
|
||||
expect(screen.queryByPlaceholderText("Enter NPM API key")).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should validate required parameters from effective parameters", async () => {
|
||||
const item = createMockItem(true)
|
||||
render(<MarketplaceInstallModal item={item} isOpen={true} onClose={mockOnClose} hasWorkspace={true} />)
|
||||
|
||||
// Try to install without filling required parameters
|
||||
const installButton = screen.getByText("marketplace:install.button")
|
||||
fireEvent.click(installButton)
|
||||
|
||||
// Should show validation error for missing required parameter
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Please provide a value for/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Fill in the required parameters
|
||||
const packageNameInput = screen.getByPlaceholderText("Enter package name")
|
||||
const apiKeyInput = screen.getByPlaceholderText("Enter NPM API key")
|
||||
|
||||
fireEvent.change(packageNameInput, { target: { value: "test-package" } })
|
||||
fireEvent.change(apiKeyInput, { target: { value: "test-api-key" } })
|
||||
|
||||
// Now install should work
|
||||
fireEvent.click(installButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedVscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "installMarketplaceItem",
|
||||
mpItem: item,
|
||||
mpInstallOptions: {
|
||||
target: "project",
|
||||
parameters: {
|
||||
packageName: "test-package",
|
||||
apiKey: "test-api-key", // Overridden value
|
||||
globalOptional: "", // Optional parameter with empty string
|
||||
_selectedIndex: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("should preserve parameter values when switching methods if keys match", async () => {
|
||||
const item = createMockItem(true)
|
||||
render(<MarketplaceInstallModal item={item} isOpen={true} onClose={mockOnClose} hasWorkspace={true} />)
|
||||
|
||||
// Fill in global optional parameter
|
||||
const globalOptionalInput = screen.getByPlaceholderText("Optional setting")
|
||||
fireEvent.change(globalOptionalInput, { target: { value: "test-value" } })
|
||||
|
||||
// Switch to Docker method
|
||||
const methodSelect = screen.getByRole("combobox")
|
||||
fireEvent.click(methodSelect)
|
||||
|
||||
await waitFor(() => {
|
||||
const dockerOption = screen.getByText("Docker Installation")
|
||||
fireEvent.click(dockerOption)
|
||||
})
|
||||
|
||||
// Global optional parameter value should be preserved
|
||||
await waitFor(() => {
|
||||
const preservedInput = screen.getByPlaceholderText("Optional setting")
|
||||
expect(preservedInput).toHaveValue("test-value")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,229 +0,0 @@
|
|||
import { render, screen } from "@/utils/test-utils"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
|
||||
import { MarketplaceItem } from "@roo-code/types"
|
||||
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { TooltipProvider } from "@/components/ui/tooltip"
|
||||
|
||||
import { MarketplaceItemCard } from "../MarketplaceItemCard"
|
||||
|
||||
vi.mock("@/utils/vscode", () => ({
|
||||
vscode: {
|
||||
postMessage: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("@/context/ExtensionStateContext", () => ({
|
||||
useExtensionState: () => ({
|
||||
cwd: "/test/workspace",
|
||||
filePaths: ["/test/workspace/file1.ts", "/test/workspace/file2.ts"],
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock("@/i18n/TranslationContext", () => ({
|
||||
useAppTranslation: () => ({
|
||||
t: (key: string, params?: any) => {
|
||||
if (key === "marketplace:items.card.by") {
|
||||
return `by ${params.author}`
|
||||
}
|
||||
const translations: Record<string, any> = {
|
||||
"marketplace:filters.type.mode": "Mode",
|
||||
"marketplace:filters.type.mcpServer": "MCP Server",
|
||||
"marketplace:filters.tags.clear": "Remove filter",
|
||||
"marketplace:filters.tags.clickToFilter": "Add filter",
|
||||
"marketplace:items.components": "Components", // This should be a string for the title prop
|
||||
"marketplace:items.card.install": "Install",
|
||||
"marketplace:items.card.installed": "Installed",
|
||||
"marketplace:items.card.installProject": "Install Project",
|
||||
"marketplace:items.card.removeProject": "Remove Project",
|
||||
"marketplace:items.card.remove": "Remove",
|
||||
"marketplace:items.card.removeProjectTooltip": "Remove from current project",
|
||||
"marketplace:items.card.removeGlobalTooltip": "Remove from global configuration",
|
||||
"marketplace:items.card.noWorkspaceTooltip": "Open a workspace to install marketplace items",
|
||||
"marketplace:items.matched": "matched",
|
||||
}
|
||||
// Special handling for "marketplace:items.components" when it's used as a badge with count
|
||||
if (key === "marketplace:items.components" && params?.count !== undefined) {
|
||||
return `${params.count} Components`
|
||||
}
|
||||
// Special handling for "marketplace:items.matched" when it's used as a badge with count
|
||||
if (key === "marketplace:items.matched" && params?.count !== undefined) {
|
||||
return `${params.count} matched`
|
||||
}
|
||||
return translations[key] || key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
const renderWithProviders = (ui: React.ReactElement) => {
|
||||
return render(<TooltipProvider delayDuration={300}>{ui}</TooltipProvider>)
|
||||
}
|
||||
|
||||
describe("MarketplaceItemCard", () => {
|
||||
const defaultItem: MarketplaceItem = {
|
||||
id: "test-item",
|
||||
name: "Test Item",
|
||||
description: "Test Description",
|
||||
type: "mode",
|
||||
author: "Test Author",
|
||||
authorUrl: "https://example.com",
|
||||
tags: ["test", "example"],
|
||||
content: "test content",
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
item: defaultItem,
|
||||
filters: {
|
||||
type: "",
|
||||
search: "",
|
||||
tags: [],
|
||||
installed: "all" as "all" | "installed" | "not_installed",
|
||||
},
|
||||
setFilters: vi.fn(),
|
||||
installed: {
|
||||
project: undefined,
|
||||
global: undefined,
|
||||
},
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.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()
|
||||
})
|
||||
|
||||
it("renders install button", () => {
|
||||
renderWithProviders(<MarketplaceItemCard {...defaultProps} />)
|
||||
|
||||
// Should show install button
|
||||
expect(screen.getByText("Install")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders tags and handles tag clicks", async () => {
|
||||
const user = userEvent.setup()
|
||||
const setFilters = vi.fn()
|
||||
|
||||
renderWithProviders(<MarketplaceItemCard {...defaultProps} setFilters={setFilters} />)
|
||||
|
||||
const tagButton = screen.getByText("test")
|
||||
await user.click(tagButton)
|
||||
|
||||
expect(setFilters).toHaveBeenCalledWith({ tags: ["test"] })
|
||||
})
|
||||
|
||||
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("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/) // Changed to regex
|
||||
expect(authorText.tagName).not.toBe("BUTTON")
|
||||
})
|
||||
|
||||
describe("MarketplaceItemCard install button", () => {
|
||||
it("renders install button", () => {
|
||||
const setFilters = vi.fn()
|
||||
const item: MarketplaceItem = {
|
||||
id: "test-item",
|
||||
name: "Test Item",
|
||||
description: "Test Description",
|
||||
type: "mode",
|
||||
author: "Test Author",
|
||||
authorUrl: "https://example.com",
|
||||
tags: ["test", "example"],
|
||||
content: "test content",
|
||||
}
|
||||
renderWithProviders(
|
||||
<MarketplaceItemCard
|
||||
item={item}
|
||||
filters={{
|
||||
type: "",
|
||||
search: "",
|
||||
tags: [],
|
||||
installed: "all" as "all" | "installed" | "not_installed",
|
||||
}}
|
||||
setFilters={setFilters}
|
||||
installed={{
|
||||
project: undefined,
|
||||
global: undefined,
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText("Install")).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it("shows install button when no workspace is open", async () => {
|
||||
// Mock useExtensionState to simulate no workspace
|
||||
vi.spyOn(await import("@/context/ExtensionStateContext"), "useExtensionState").mockReturnValue({
|
||||
cwd: undefined,
|
||||
filePaths: [],
|
||||
} as any)
|
||||
|
||||
renderWithProviders(<MarketplaceItemCard {...defaultProps} />)
|
||||
|
||||
// Should still show the Install button (dropdown behavior is handled by MarketplaceItemActionsMenu)
|
||||
expect(screen.getByText("Install")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("shows single Installed badge when item is installed", () => {
|
||||
const installedProps = {
|
||||
...defaultProps,
|
||||
installed: {
|
||||
project: { type: "mode" },
|
||||
global: undefined,
|
||||
},
|
||||
}
|
||||
|
||||
renderWithProviders(<MarketplaceItemCard {...installedProps} />)
|
||||
|
||||
// Should show single "Installed" badge
|
||||
expect(screen.getByText("Installed")).toBeInTheDocument()
|
||||
// Should show Remove button instead of Install
|
||||
expect(screen.getByText("Remove")).toBeInTheDocument()
|
||||
// Should not show Install button
|
||||
expect(screen.queryByText("Install")).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("shows single Installed badge even when installed in both locations", () => {
|
||||
const installedProps = {
|
||||
...defaultProps,
|
||||
installed: {
|
||||
project: { type: "mode" },
|
||||
global: { type: "mode" },
|
||||
},
|
||||
}
|
||||
|
||||
renderWithProviders(<MarketplaceItemCard {...installedProps} />)
|
||||
|
||||
// Should show only one "Installed" badge
|
||||
const installedBadges = screen.getAllByText("Installed")
|
||||
expect(installedBadges).toHaveLength(1)
|
||||
// Should show Remove button
|
||||
expect(screen.getByText("Remove")).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
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.organizationMcps) !== JSON.stringify(newState.organizationMcps) ||
|
||||
JSON.stringify(prevState.displayItems) !== JSON.stringify(newState.displayItems) ||
|
||||
JSON.stringify(prevState.displayOrganizationMcps) !==
|
||||
JSON.stringify(newState.displayOrganizationMcps) ||
|
||||
JSON.stringify(prevState.filters) !== JSON.stringify(newState.filters)
|
||||
|
||||
return hasChanged ? newState : prevState
|
||||
})
|
||||
}
|
||||
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
manager.handleMessage(event.data)
|
||||
}
|
||||
|
||||
// Register message handler immediately
|
||||
window.addEventListener("message", handleMessage)
|
||||
|
||||
// Register state change handler
|
||||
const unsubscribe = manager.onStateChange(handleStateChange)
|
||||
|
||||
// Force initial state sync
|
||||
handleStateChange(manager.getState())
|
||||
|
||||
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
|
||||
}
|
||||
|
|
@ -17,7 +17,6 @@ import {
|
|||
DialogDescription,
|
||||
DialogFooter,
|
||||
ToggleSwitch,
|
||||
StandardTooltip,
|
||||
} from "@src/components/ui"
|
||||
import { buildDocLink } from "@src/utils/docLinks"
|
||||
import { Section } from "@src/components/settings/Section"
|
||||
|
|
@ -133,24 +132,6 @@ const McpView = () => {
|
|||
<span className="codicon codicon-refresh" style={{ marginRight: "6px" }}></span>
|
||||
{t("mcp:refreshMCP")}
|
||||
</Button>
|
||||
<StandardTooltip content={t("mcp:marketplace")}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
style={{ width: "100%" }}
|
||||
onClick={() => {
|
||||
window.postMessage(
|
||||
{
|
||||
type: "action",
|
||||
action: "marketplaceButtonClicked",
|
||||
values: { marketplaceTab: "mcp" },
|
||||
},
|
||||
"*",
|
||||
)
|
||||
}}>
|
||||
<span className="codicon codicon-extensions" style={{ marginRight: "6px" }}></span>
|
||||
{t("mcp:marketplace")}
|
||||
</Button>
|
||||
</StandardTooltip>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
|
|
|
|||
|
|
@ -653,24 +653,6 @@ const ModesView = () => {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
<StandardTooltip content={t("chat:modeSelector.marketplace")}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
window.postMessage(
|
||||
{
|
||||
type: "action",
|
||||
action: "marketplaceButtonClicked",
|
||||
values: { marketplaceTab: "mode" },
|
||||
},
|
||||
"*",
|
||||
)
|
||||
}}>
|
||||
<span className="codicon codicon-extensions"></span>
|
||||
</Button>
|
||||
</StandardTooltip>
|
||||
|
||||
<StandardTooltip content={t("prompts:modes.importMode")}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useState, useEffect, useMemo, useCallback } from "react"
|
||||
import { Plus, Globe, Folder, Edit, Trash2, Settings } from "lucide-react"
|
||||
import { Trans } from "react-i18next"
|
||||
import { Plus, Globe, Folder, Edit, Trash2, Settings } from "lucide-react"
|
||||
|
||||
import type { SkillMetadata } from "@roo-code/types"
|
||||
|
||||
|
|
@ -276,30 +276,6 @@ export const SkillsSettings: React.FC = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Fixed Footer */}
|
||||
<div className="px-6 py-1 text-sm border-t border-vscode-panel-border text-muted-foreground">
|
||||
<Trans
|
||||
i18nKey="settings:skills.footer"
|
||||
components={{
|
||||
MarketplaceLink: (
|
||||
<span
|
||||
onClick={() => {
|
||||
window.postMessage(
|
||||
{
|
||||
type: "action",
|
||||
action: "marketplaceButtonClicked",
|
||||
values: { marketplaceTab: "mode" },
|
||||
},
|
||||
"*",
|
||||
)
|
||||
}}
|
||||
className="text-vscode-textLink-foreground hover:underline cursor-pointer"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import {
|
|||
type CloudOrganizationMembership,
|
||||
type ExtensionMessage,
|
||||
type ExtensionState,
|
||||
type MarketplaceInstalledMetadata,
|
||||
type SkillMetadata,
|
||||
type Command,
|
||||
type McpServer,
|
||||
|
|
@ -53,8 +52,6 @@ export interface ExtensionStateContextType extends ExtensionState {
|
|||
setAlwaysAllowFollowupQuestions: (value: boolean) => void // Setter for the new property
|
||||
followupAutoApproveTimeoutMs: number | undefined // Timeout in ms for auto-approving follow-up questions
|
||||
setFollowupAutoApproveTimeoutMs: (value: number) => void // Setter for the timeout
|
||||
marketplaceItems?: any[]
|
||||
marketplaceInstalledMetadata?: MarketplaceInstalledMetadata
|
||||
profileThresholds: Record<string, number>
|
||||
setProfileThresholds: (value: Record<string, number>) => void
|
||||
setApiConfiguration: (config: ProviderSettings) => void
|
||||
|
|
@ -269,13 +266,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
const [mcpServers, setMcpServers] = useState<McpServer[]>([])
|
||||
const [currentCheckpoint, setCurrentCheckpoint] = useState<string>()
|
||||
const [extensionRouterModels, setExtensionRouterModels] = useState<RouterModels | undefined>(undefined)
|
||||
const [marketplaceItems, setMarketplaceItems] = useState<any[]>([])
|
||||
const [alwaysAllowFollowupQuestions, setAlwaysAllowFollowupQuestions] = useState(false) // Add state for follow-up questions auto-approve
|
||||
const [followupAutoApproveTimeoutMs, setFollowupAutoApproveTimeoutMs] = useState<number | undefined>(undefined) // Will be set from global settings
|
||||
const [marketplaceInstalledMetadata, setMarketplaceInstalledMetadata] = useState<MarketplaceInstalledMetadata>({
|
||||
project: {},
|
||||
global: {},
|
||||
})
|
||||
const [skills, setSkills] = useState<SkillMetadata[]>([])
|
||||
const [includeTaskHistoryInEnhance, setIncludeTaskHistoryInEnhance] = useState(true)
|
||||
const [prevCloudIsAuthenticated, setPrevCloudIsAuthenticated] = useState(false)
|
||||
|
|
@ -326,13 +318,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
if ((newState as any).includeCurrentCost !== undefined) {
|
||||
setIncludeCurrentCost((newState as any).includeCurrentCost)
|
||||
}
|
||||
// Handle marketplace data if present in state message
|
||||
if (newState.marketplaceItems !== undefined) {
|
||||
setMarketplaceItems(newState.marketplaceItems)
|
||||
}
|
||||
if (newState.marketplaceInstalledMetadata !== undefined) {
|
||||
setMarketplaceInstalledMetadata(newState.marketplaceInstalledMetadata)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "action": {
|
||||
|
|
@ -409,15 +394,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
setExtensionRouterModels(message.routerModels)
|
||||
break
|
||||
}
|
||||
case "marketplaceData": {
|
||||
if (message.marketplaceItems !== undefined) {
|
||||
setMarketplaceItems(message.marketplaceItems)
|
||||
}
|
||||
if (message.marketplaceInstalledMetadata !== undefined) {
|
||||
setMarketplaceInstalledMetadata(message.marketplaceInstalledMetadata)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "taskHistoryUpdated": {
|
||||
// Efficiently update just the task history without replacing entire state
|
||||
if (message.taskHistory !== undefined) {
|
||||
|
|
@ -498,8 +474,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
cloudIsAuthenticated: state.cloudIsAuthenticated ?? false,
|
||||
cloudOrganizations: state.cloudOrganizations ?? [],
|
||||
organizationSettingsVersion: state.organizationSettingsVersion ?? -1,
|
||||
marketplaceItems,
|
||||
marketplaceInstalledMetadata,
|
||||
profileThresholds: state.profileThresholds ?? {},
|
||||
alwaysAllowFollowupQuestions,
|
||||
followupAutoApproveTimeoutMs,
|
||||
|
|
|
|||
1
webview-ui/src/i18n/locales/ca/chat.json
generated
1
webview-ui/src/i18n/locales/ca/chat.json
generated
|
|
@ -128,7 +128,6 @@
|
|||
"enhancePromptDescription": "El botó 'Millora la sol·licitud' ajuda a millorar la teva sol·licitud proporcionant context addicional, aclariments o reformulacions. Prova d'escriure una sol·licitud aquí i fes clic al botó de nou per veure com funciona.",
|
||||
"modeSelector": {
|
||||
"title": "Modes",
|
||||
"marketplace": "Marketplace de Modes",
|
||||
"settings": "Configuració de Modes",
|
||||
"description": "Personalitats especialitzades que adapten el comportament de Roo.",
|
||||
"searchPlaceholder": "Cerca modes...",
|
||||
|
|
|
|||
158
webview-ui/src/i18n/locales/ca/marketplace.json
generated
158
webview-ui/src/i18n/locales/ca/marketplace.json
generated
|
|
@ -1,158 +0,0 @@
|
|||
{
|
||||
"title": "Roo Marketplace",
|
||||
"tabs": {
|
||||
"installed": "Instal·lat",
|
||||
"settings": "Configuració",
|
||||
"browse": "Navegar"
|
||||
},
|
||||
"done": "Fet",
|
||||
"refresh": "Actualitzar",
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "Cercar elements del marketplace...",
|
||||
"placeholderMcp": "Cercar MCPs...",
|
||||
"placeholderMode": "Cercar modes..."
|
||||
},
|
||||
"installed": {
|
||||
"label": "Filtra per estat",
|
||||
"all": "Tots els articles",
|
||||
"installed": "Instal·lats",
|
||||
"notInstalled": "No instal·lats"
|
||||
},
|
||||
"type": {
|
||||
"label": "Filtrar per tipus:",
|
||||
"all": "Tots els tipus",
|
||||
"mode": "Mode",
|
||||
"mcpServer": "Servidor MCP"
|
||||
},
|
||||
"sort": {
|
||||
"label": "Ordenar per:",
|
||||
"name": "Nom",
|
||||
"author": "Autor",
|
||||
"lastUpdated": "Última actualització"
|
||||
},
|
||||
"tags": {
|
||||
"label": "Filtrar per etiquetes:",
|
||||
"clear": "Netejar etiquetes",
|
||||
"placeholder": "Escriu per cercar i seleccionar etiquetes...",
|
||||
"noResults": "No s'han trobat etiquetes coincidents",
|
||||
"selected": "Mostrant elements amb qualsevol de les etiquetes seleccionades",
|
||||
"clickToFilter": "Feu clic a les etiquetes per filtrar elements"
|
||||
},
|
||||
"none": "Cap"
|
||||
},
|
||||
"sections": {
|
||||
"organizationMcps": "MCPs de {{organization}}",
|
||||
"marketplace": "Mercat"
|
||||
},
|
||||
"type-group": {
|
||||
"modes": "Modes",
|
||||
"mcps": "Servidors MCP"
|
||||
},
|
||||
"items": {
|
||||
"empty": {
|
||||
"noItems": "No s'han trobat elements del marketplace",
|
||||
"withFilters": "Prova d'ajustar els filtres",
|
||||
"noSources": "Prova d'afegir una font a la pestanya Fonts",
|
||||
"adjustFilters": "Prova d'ajustar els filtres o termes de cerca",
|
||||
"clearAllFilters": "Netejar tots els filtres"
|
||||
},
|
||||
"count": "{{count}} elements trobats",
|
||||
"components": "{{count}} components",
|
||||
"matched": "{{count}} coincidents",
|
||||
"refresh": {
|
||||
"button": "Actualitzar",
|
||||
"refreshing": "Actualitzant...",
|
||||
"mayTakeMoment": "Això pot trigar un moment."
|
||||
},
|
||||
"card": {
|
||||
"by": "per {{author}}",
|
||||
"from": "de {{source}}",
|
||||
"install": "Instal·lar",
|
||||
"installProject": "Instal·lar",
|
||||
"installGlobal": "Instal·lar (Global)",
|
||||
"remove": "Eliminar",
|
||||
"removeProject": "Eliminar",
|
||||
"removeGlobal": "Eliminar (Global)",
|
||||
"viewSource": "Veure",
|
||||
"viewOnSource": "Veure a {{source}}",
|
||||
"noWorkspaceTooltip": "Obre un espai de treball per instal·lar elements del marketplace",
|
||||
"installed": "Instal·lat",
|
||||
"removeProjectTooltip": "Eliminar del projecte actual",
|
||||
"removeGlobalTooltip": "Eliminar de la configuració global",
|
||||
"actionsMenuLabel": "Més accions",
|
||||
"official": "Oficial",
|
||||
"verified": "Verificat"
|
||||
},
|
||||
"removeFailed": "No s'ha pogut eliminar l'element: {{error}}",
|
||||
"unknownError": "S'ha produït un error desconegut"
|
||||
},
|
||||
"install": {
|
||||
"title": "Instal·lar {{name}}",
|
||||
"titleMode": "Instal·lar mode {{name}}",
|
||||
"titleMcp": "Instal·lar MCP {{name}}",
|
||||
"scope": "Àmbit d'instal·lació",
|
||||
"project": "Projecte (espai de treball actual)",
|
||||
"global": "Global (tots els espais de treball)",
|
||||
"method": "Mètode d'instal·lació",
|
||||
"configuration": "Configuració",
|
||||
"configurationDescription": "Configura els paràmetres necessaris per a aquest servidor MCP",
|
||||
"button": "Instal·lar",
|
||||
"successTitle": "{{name}} instal·lat",
|
||||
"successDescription": "Instal·lació completada amb èxit",
|
||||
"installed": "Instal·lat amb èxit!",
|
||||
"whatNextMcp": "Ara pots configurar i utilitzar aquest servidor MCP. Feu clic a la icona MCP de la barra lateral per canviar de pestanya.",
|
||||
"whatNextMode": "Ara pots utilitzar aquest mode. Feu clic a la icona Modes de la barra lateral per canviar de pestanya.",
|
||||
"done": "Fet",
|
||||
"goToMcp": "Anar a la pestanya MCP",
|
||||
"goToModes": "Anar a la configuració de Modes",
|
||||
"moreInfoMcp": "Veure documentació MCP de {{name}}",
|
||||
"validationRequired": "Si us plau, proporciona un valor per a {{paramName}}",
|
||||
"prerequisites": "Prerequisits"
|
||||
},
|
||||
"sources": {
|
||||
"title": "Configurar fonts del marketplace",
|
||||
"description": "Afegeix repositoris Git que continguin elements del marketplace. Aquests repositoris es recuperaran quan navegueu pel marketplace.",
|
||||
"add": {
|
||||
"title": "Afegir nova font",
|
||||
"urlPlaceholder": "URL del repositori Git (p. ex., https://github.com/username/repo)",
|
||||
"urlFormats": "Formats compatibles: HTTPS (https://github.com/username/repo), SSH (git@github.com:username/repo.git), o protocol Git (git://github.com/username/repo.git)",
|
||||
"namePlaceholder": "Nom de visualització (màx. 20 caràcters)",
|
||||
"button": "Afegir font"
|
||||
},
|
||||
"current": {
|
||||
"title": "Fonts actuals",
|
||||
"empty": "No hi ha fonts configurades. Afegeix una font per començar.",
|
||||
"refresh": "Actualitzar aquesta font",
|
||||
"remove": "Eliminar font"
|
||||
},
|
||||
"errors": {
|
||||
"emptyUrl": "La URL no pot estar buida",
|
||||
"invalidUrl": "Format d'URL no vàlid",
|
||||
"nonVisibleChars": "La URL conté caràcters no visibles a part dels espais",
|
||||
"invalidGitUrl": "La URL ha de ser una URL de repositori Git vàlida (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 a part dels espais",
|
||||
"duplicateName": "Aquest nom ja s'està utilitzant (coincidència insensible a majúscules i espais)",
|
||||
"emojiName": "Els caràcters emoji poden causar problemes de visualització",
|
||||
"maxSources": "Màxim de {{max}} fonts permeses"
|
||||
}
|
||||
},
|
||||
"removeConfirm": {
|
||||
"mode": {
|
||||
"title": "Eliminar el mode",
|
||||
"message": "Estàs segur que vols eliminar el mode \"{{modeName}}\"?",
|
||||
"rulesWarning": "Això també eliminarà qualsevol fitxer de regles associat per a aquest mode."
|
||||
},
|
||||
"mcp": {
|
||||
"title": "Eliminar el servidor MCP",
|
||||
"message": "Estàs segur que vols eliminar el servidor MCP \"{{mcpName}}\"?"
|
||||
},
|
||||
"cancel": "Cancel·lar",
|
||||
"confirm": "Eliminar"
|
||||
},
|
||||
"footer": {
|
||||
"issueText": "Has trobat un problema amb un element del marketplace o tens suggeriments per a nous elements? <0>Obre una incidència de GitHub</0> per fer-nos-ho saber!"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue