Turn on marketplace (#4788)

This commit is contained in:
Matt Rubens 2025-06-17 14:00:27 -04:00 committed by GitHub
parent 3e2aec2ca7
commit 7b5a1d987d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 43 additions and 202 deletions

View file

@ -6,7 +6,7 @@ import type { Keys, Equals, AssertEqual } from "./type-fu.js"
* ExperimentId
*/
export const experimentIds = ["powerSteering", "disableCompletionCommand", "marketplace", "multiFileApplyDiff"] as const
export const experimentIds = ["powerSteering", "disableCompletionCommand", "multiFileApplyDiff"] as const
export const experimentIdsSchema = z.enum(experimentIds)
@ -19,7 +19,6 @@ export type ExperimentId = z.infer<typeof experimentIdsSchema>
export const experimentsSchema = z.object({
powerSteering: z.boolean().optional(),
disableCompletionCommand: z.boolean().optional(),
marketplace: z.boolean().optional(),
multiFileApplyDiff: z.boolean().optional(),
})

View file

@ -502,9 +502,6 @@ export class ClineProvider
// If the extension is starting a new session, clear previous task state.
await this.removeClineFromStack()
// Set initial VSCode context for experiments
await this.updateVSCodeContext()
this.log("Webview view resolved")
}
@ -1245,29 +1242,12 @@ export class ClineProvider
const state = await this.getStateToPostToWebview()
this.postMessageToWebview({ type: "state", state })
// Update VSCode context for experiments
await this.updateVSCodeContext()
// Check MDM compliance and send user to account tab if not compliant
if (!this.checkMdmCompliance()) {
await this.postMessageToWebview({ type: "action", action: "accountButtonClicked" })
}
}
/**
* Updates VSCode context variables for experiments so they can be used in when clauses
*/
private async updateVSCodeContext() {
const { experiments } = await this.getState()
// Set context for marketplace experiment
await vscode.commands.executeCommand(
"setContext",
`${Package.name}.marketplaceEnabled`,
experiments.marketplace ?? false,
)
}
/**
* Checks if there is a file-based system prompt override for the given mode
*/
@ -1356,14 +1336,12 @@ export class ClineProvider
const allowedCommands = vscode.workspace.getConfiguration(Package.name).get<string[]>("allowedCommands") || []
const cwd = this.cwd
// Only fetch marketplace data if the feature is enabled
// Fetch marketplace data
let marketplaceItems: any[] = []
let marketplaceInstalledMetadata: any = { project: {}, global: {} }
if (experiments.marketplace) {
marketplaceItems = (await this.marketplaceManager.getCurrentItems()) || []
marketplaceInstalledMetadata = await this.marketplaceManager.getInstallationMetadata()
}
marketplaceItems = (await this.marketplaceManager.getCurrentItems()) || []
marketplaceInstalledMetadata = await this.marketplaceManager.getInstallationMetadata()
// Check if there's a system prompt override for the current mode
const currentMode = mode ?? defaultModeSlug

View file

@ -1465,13 +1465,6 @@ export const webviewMessageHandler = async (
break
}
case "filterMarketplaceItems": {
// Check if marketplace is enabled before making API calls
const { experiments } = await provider.getState()
if (!experiments.marketplace) {
console.log("Marketplace: Feature disabled, skipping API call")
break
}
if (marketplaceManager && message.filters) {
try {
await marketplaceManager.updateWithFilteredItems({
@ -1489,13 +1482,6 @@ export const webviewMessageHandler = async (
}
case "installMarketplaceItem": {
// Check if marketplace is enabled before installing
const { experiments } = await provider.getState()
if (!experiments.marketplace) {
console.log("Marketplace: Feature disabled, skipping installation")
break
}
if (marketplaceManager && message.mpItem && message.mpInstallOptions) {
try {
const configFilePath = await marketplaceManager.installMarketplaceItem(
@ -1525,13 +1511,6 @@ export const webviewMessageHandler = async (
}
case "removeInstalledMarketplaceItem": {
// Check if marketplace is enabled before removing
const { experiments } = await provider.getState()
if (!experiments.marketplace) {
console.log("Marketplace: Feature disabled, skipping removal")
break
}
if (marketplaceManager && message.mpItem && message.mpInstallOptions) {
try {
await marketplaceManager.removeInstalledMarketplaceItem(message.mpItem, message.mpInstallOptions)
@ -1544,13 +1523,6 @@ export const webviewMessageHandler = async (
}
case "installMarketplaceItemWithParameters": {
// Check if marketplace is enabled before installing with parameters
const { experiments } = await provider.getState()
if (!experiments.marketplace) {
console.log("Marketplace: Feature disabled, skipping installation with parameters")
break
}
if (marketplaceManager && message.payload && "item" in message.payload && "parameters" in message.payload) {
try {
const configFilePath = await marketplaceManager.installMarketplaceItem(message.payload.item, {

View file

@ -232,7 +232,7 @@
{
"command": "roo-cline.marketplaceButtonClicked",
"group": "navigation@4",
"when": "view == roo-cline.SidebarProvider && roo-cline.marketplaceEnabled"
"when": "view == roo-cline.SidebarProvider"
},
{
"command": "roo-cline.historyButtonClicked",
@ -274,7 +274,7 @@
{
"command": "roo-cline.marketplaceButtonClicked",
"group": "navigation@4",
"when": "activeWebviewPanelId == roo-cline.TabPanelProvider && roo-cline.marketplaceEnabled"
"when": "activeWebviewPanelId == roo-cline.TabPanelProvider"
},
{
"command": "roo-cline.historyButtonClicked",

View file

@ -6,21 +6,22 @@ import { webviewMessageHandler } from "../../../core/webview/webviewMessageHandl
const mockProvider = {
getState: vi.fn(),
postStateToWebview: vi.fn(),
postMessageToWebview: vi.fn(),
} as any
const mockMarketplaceManager = {
updateWithFilteredItems: vi.fn(),
} as any
describe("Marketplace Setting Check", () => {
describe("Marketplace General Availability", () => {
beforeEach(() => {
vi.clearAllMocks()
})
it("should skip API calls when marketplace is disabled", async () => {
// Mock experiments with marketplace disabled
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: { marketplace: false },
experiments: {},
})
const message = {
@ -30,25 +31,7 @@ describe("Marketplace Setting Check", () => {
await webviewMessageHandler(mockProvider, message, mockMarketplaceManager)
// Should not call marketplace manager methods
expect(mockMarketplaceManager.updateWithFilteredItems).not.toHaveBeenCalled()
expect(mockProvider.postStateToWebview).not.toHaveBeenCalled()
})
it("should allow API calls when marketplace is enabled", async () => {
// Mock experiments with marketplace enabled
mockProvider.getState.mockResolvedValue({
experiments: { marketplace: true },
})
const message = {
type: "filterMarketplaceItems" as const,
filters: { type: "mcp", search: "", tags: [] },
}
await webviewMessageHandler(mockProvider, message, mockMarketplaceManager)
// Should call marketplace manager methods
// Should call marketplace manager methods since marketplace is generally available
expect(mockMarketplaceManager.updateWithFilteredItems).toHaveBeenCalledWith({
type: "mcp",
search: "",
@ -57,13 +40,13 @@ describe("Marketplace Setting Check", () => {
expect(mockProvider.postStateToWebview).toHaveBeenCalled()
})
it("should skip installation when marketplace is disabled", async () => {
// Mock experiments with marketplace disabled
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: { marketplace: false },
experiments: {},
})
const mockInstallMarketplaceItem = vi.fn()
const mockInstallMarketplaceItem = vi.fn().mockResolvedValue(undefined)
const mockMarketplaceManagerWithInstall = {
installMarketplaceItem: mockInstallMarketplaceItem,
}
@ -83,7 +66,7 @@ describe("Marketplace Setting Check", () => {
await webviewMessageHandler(mockProvider, message, mockMarketplaceManagerWithInstall as any)
// Should not call install method
expect(mockInstallMarketplaceItem).not.toHaveBeenCalled()
// Should call install method since marketplace is generally available
expect(mockInstallMarketplaceItem).toHaveBeenCalledWith(message.mpItem, message.mpInstallOptions)
})
})

View file

@ -27,7 +27,6 @@ describe("experiments", () => {
it("returns false when POWER_STEERING experiment is not enabled", () => {
const experiments: Record<ExperimentId, boolean> = {
powerSteering: false,
marketplace: false,
disableCompletionCommand: false,
multiFileApplyDiff: false,
}
@ -37,7 +36,6 @@ describe("experiments", () => {
it("returns true when experiment POWER_STEERING is enabled", () => {
const experiments: Record<ExperimentId, boolean> = {
powerSteering: true,
marketplace: false,
disableCompletionCommand: false,
multiFileApplyDiff: false,
}
@ -47,49 +45,10 @@ describe("experiments", () => {
it("returns false when experiment is not present", () => {
const experiments: Record<ExperimentId, boolean> = {
powerSteering: false,
marketplace: false,
disableCompletionCommand: false,
multiFileApplyDiff: false,
}
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false)
})
})
describe("MARKETPLACE", () => {
it("is configured correctly", () => {
expect(EXPERIMENT_IDS.MARKETPLACE).toBe("marketplace")
expect(experimentConfigsMap.MARKETPLACE).toMatchObject({
enabled: false,
})
})
})
describe("isEnabled for MARKETPLACE", () => {
it("returns false when MARKETPLACE experiment is not enabled", () => {
const experiments: Record<ExperimentId, boolean> = {
powerSteering: false,
marketplace: false,
disableCompletionCommand: false,
multiFileApplyDiff: false,
}
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.MARKETPLACE)).toBe(false)
})
it("returns true when MARKETPLACE experiment is enabled", () => {
const experiments: Record<ExperimentId, boolean> = {
powerSteering: false,
marketplace: true,
disableCompletionCommand: false,
multiFileApplyDiff: false,
}
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.MARKETPLACE)).toBe(true)
})
it("returns false when MARKETPLACE experiment is not present", () => {
const experiments: Record<ExperimentId, boolean> = {
powerSteering: false,
// marketplace missing
} as any
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.MARKETPLACE)).toBe(false)
})
})
})

View file

@ -1,7 +1,6 @@
import type { AssertEqual, Equals, Keys, Values, ExperimentId, Experiments } from "@roo-code/types"
export const EXPERIMENT_IDS = {
MARKETPLACE: "marketplace",
MULTI_FILE_APPLY_DIFF: "multiFileApplyDiff",
DISABLE_COMPLETION_COMMAND: "disableCompletionCommand",
POWER_STEERING: "powerSteering",
@ -16,7 +15,6 @@ interface ExperimentConfig {
}
export const experimentConfigsMap: Record<ExperimentKey, ExperimentConfig> = {
MARKETPLACE: { enabled: false },
MULTI_FILE_APPLY_DIFF: { enabled: false },
DISABLE_COMPLETION_COMMAND: { enabled: false },
POWER_STEERING: { enabled: false },

View file

@ -40,7 +40,6 @@ const App = () => {
telemetrySetting,
telemetryKey,
machineId,
experiments,
cloudUserInfo,
cloudIsAuthenticated,
mdmCompliant,
@ -93,10 +92,6 @@ const App = () => {
// Handle switchTab action with tab parameter
if (message.action === "switchTab" && message.tab) {
const targetTab = message.tab as Tab
// Don't switch to marketplace tab if the experiment is disabled
if (targetTab === "marketplace" && !experiments.marketplace) {
return
}
switchTab(targetTab)
setCurrentSection(undefined)
} else {
@ -105,10 +100,6 @@ const App = () => {
const section = message.values?.section as string | undefined
if (newTab) {
// Don't switch to marketplace tab if the experiment is disabled
if (newTab === "marketplace" && !experiments.marketplace) {
return
}
switchTab(newTab)
setCurrentSection(section)
}
@ -124,7 +115,7 @@ const App = () => {
chatViewRef.current?.acceptInput()
}
},
[switchTab, experiments],
[switchTab],
)
useEvent("message", onMessage)
@ -147,10 +138,10 @@ const App = () => {
// Track marketplace tab views
useEffect(() => {
if (tab === "marketplace" && experiments.marketplace) {
if (tab === "marketplace") {
telemetryClient.capture(TelemetryEventName.MARKETPLACE_TAB_VIEWED)
}
}, [tab, experiments.marketplace])
}, [tab])
if (!didHydrateState) {
return null

View file

@ -104,7 +104,7 @@ describe("App", () => {
didHydrateState: true,
showWelcome: false,
shouldShowAnnouncement: false,
experiments: { marketplace: false },
experiments: {},
language: "en",
})
})
@ -224,74 +224,35 @@ describe("App", () => {
expect(screen.queryByTestId(`${view}-view`)).not.toBeInTheDocument()
})
describe("marketplace experiment", () => {
it("does not switch to marketplace tab when experiment is disabled", async () => {
mockUseExtensionState.mockReturnValue({
didHydrateState: true,
showWelcome: false,
shouldShowAnnouncement: false,
experiments: { marketplace: false },
language: "en",
})
it("switches to marketplace view when receiving marketplaceButtonClicked action", async () => {
render(<AppWithProviders />)
render(<AppWithProviders />)
act(() => {
triggerMessage("marketplaceButtonClicked")
})
// Should remain on chat view
const chatView = screen.getByTestId("chat-view")
expect(chatView.getAttribute("data-hidden")).toBe("false")
expect(screen.queryByTestId("marketplace-view")).not.toBeInTheDocument()
act(() => {
triggerMessage("marketplaceButtonClicked")
})
it("switches to marketplace tab when experiment is enabled", async () => {
mockUseExtensionState.mockReturnValue({
didHydrateState: true,
showWelcome: false,
shouldShowAnnouncement: false,
experiments: { marketplace: true },
language: "en",
})
const marketplaceView = await screen.findByTestId("marketplace-view")
expect(marketplaceView).toBeInTheDocument()
render(<AppWithProviders />)
const chatView = screen.getByTestId("chat-view")
expect(chatView.getAttribute("data-hidden")).toBe("true")
})
act(() => {
triggerMessage("marketplaceButtonClicked")
})
it("returns to chat view when clicking done in marketplace view", async () => {
render(<AppWithProviders />)
const marketplaceView = await screen.findByTestId("marketplace-view")
expect(marketplaceView).toBeInTheDocument()
const chatView = screen.getByTestId("chat-view")
expect(chatView.getAttribute("data-hidden")).toBe("true")
act(() => {
triggerMessage("marketplaceButtonClicked")
})
it("returns to chat view when clicking done in marketplace view", async () => {
mockUseExtensionState.mockReturnValue({
didHydrateState: true,
showWelcome: false,
shouldShowAnnouncement: false,
experiments: { marketplace: true },
language: "en",
})
const marketplaceView = await screen.findByTestId("marketplace-view")
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()
act(() => {
marketplaceView.click()
})
const chatView = screen.getByTestId("chat-view")
expect(chatView.getAttribute("data-hidden")).toBe("false")
expect(screen.queryByTestId("marketplace-view")).not.toBeInTheDocument()
})
})

View file

@ -52,7 +52,7 @@ export const ExperimentalSettings = ({
<Section>
{Object.entries(experimentConfigsMap)
.filter((config) => config[0] !== "DIFF_STRATEGY" && config[0] !== "MULTI_SEARCH_AND_REPLACE")
.filter(([key]) => key in EXPERIMENT_IDS)
.map((config) => {
if (config[0] === "MULTI_FILE_APPLY_DIFF") {
return (