From 7de562eab83b81e7e83c0ae88e5b6716d5a77d57 Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Fri, 20 Jun 2025 10:51:06 -0500 Subject: [PATCH 1/3] fix: resolve marketplace timeout issues and display installed MCPs (#4817) - Separate marketplace data fetching from main state updates - Add lazy loading for marketplace items to prevent UI freezes - Fix marketplaceInstalledMetadata not being displayed correctly - Add dedicated fetchMarketplaceData mechanism triggered only when needed - Ensure marketplace timeouts don't block core functionality This prevents the 'timeout of 10000ms exceeded' error and UI freezes in restricted network environments by fetching marketplace data asynchronously only when the marketplace tab is accessed. --- src/core/webview/ClineProvider.ts | 32 ++++++++++++++++--- src/core/webview/webviewMessageHandler.ts | 6 ++++ src/shared/ExtensionMessage.ts | 3 ++ src/shared/WebviewMessage.ts | 1 + .../marketplace/MarketplaceView.tsx | 9 ++---- .../MarketplaceViewStateManager.ts | 23 +++++++++++++ .../src/context/ExtensionStateContext.tsx | 25 +++++++++++++++ 7 files changed, 88 insertions(+), 11 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index d99aa644b2..4dc83aa63e 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1254,6 +1254,33 @@ export class ClineProvider } } + /** + * Fetches marketplace data on demand to avoid blocking main state updates + */ + async fetchMarketplaceData() { + try { + const [marketplaceItems, marketplaceInstalledMetadata] = await Promise.all([ + this.marketplaceManager.getCurrentItems().catch(() => []), + this.marketplaceManager.getInstallationMetadata().catch(() => ({ project: {}, global: {} })), + ]) + + // Send marketplace data separately + this.postMessageToWebview({ + type: "marketplaceData", + marketplaceItems: marketplaceItems || [], + marketplaceInstalledMetadata: marketplaceInstalledMetadata || { project: {}, global: {} }, + }) + } catch (error) { + console.error("Failed to fetch marketplace data:", error) + // Send empty data on error to prevent UI from hanging + this.postMessageToWebview({ + type: "marketplaceData", + marketplaceItems: [], + marketplaceInstalledMetadata: { project: {}, global: {} }, + }) + } + } + /** * Checks if there is a file-based system prompt override for the given mode */ @@ -1342,13 +1369,10 @@ export class ClineProvider const allowedCommands = vscode.workspace.getConfiguration(Package.name).get("allowedCommands") || [] const cwd = this.cwd - // Fetch marketplace data + // Initialize marketplace data as empty - will be fetched on demand let marketplaceItems: any[] = [] let marketplaceInstalledMetadata: any = { project: {}, global: {} } - 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 const hasSystemPromptOverride = await this.hasFileBasedSystemPromptOverride(currentMode) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 423120dfb7..c6ef3c839c 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1508,6 +1508,12 @@ export const webviewMessageHandler = async ( break } + case "fetchMarketplaceData": { + // Fetch marketplace data on demand + await provider.fetchMarketplaceData() + break + } + case "installMarketplaceItem": { if (marketplaceManager && message.mpItem && message.mpInstallOptions) { try { diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index ac19ba0ef2..dd2c364e28 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -90,6 +90,7 @@ export interface ExtensionMessage { | "indexCleared" | "codebaseIndexConfig" | "marketplaceInstallResult" + | "marketplaceData" text?: string payload?: any // Add a generic payload for now, can refine later action?: @@ -136,6 +137,8 @@ export interface ExtensionMessage { userInfo?: CloudUserInfo organizationAllowList?: OrganizationAllowList tab?: string + marketplaceItems?: MarketplaceItem[] + marketplaceInstalledMetadata?: { project: Record; global: Record } } export type ExtensionState = Pick< diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 97dc9ccdf0..9ea0e192eb 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -167,6 +167,7 @@ export interface WebviewMessage { | "cancelMarketplaceInstall" | "removeInstalledMarketplaceItem" | "marketplaceInstallResult" + | "fetchMarketplaceData" | "switchTab" text?: string tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account" diff --git a/webview-ui/src/components/marketplace/MarketplaceView.tsx b/webview-ui/src/components/marketplace/MarketplaceView.tsx index 8d831d8674..b1b08247f3 100644 --- a/webview-ui/src/components/marketplace/MarketplaceView.tsx +++ b/webview-ui/src/components/marketplace/MarketplaceView.tsx @@ -33,14 +33,9 @@ export function MarketplaceView({ stateManager, onDone }: MarketplaceViewProps) // 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) { - // Send empty filter to trigger state update + // Fetch marketplace data on demand vscode.postMessage({ - type: "filterMarketplaceItems", - filters: { - type: "", - search: "", - tags: [], - }, + type: "fetchMarketplaceData", }) } diff --git a/webview-ui/src/components/marketplace/MarketplaceViewStateManager.ts b/webview-ui/src/components/marketplace/MarketplaceViewStateManager.ts index 7f7324f581..9b83895bb3 100644 --- a/webview-ui/src/components/marketplace/MarketplaceViewStateManager.ts +++ b/webview-ui/src/components/marketplace/MarketplaceViewStateManager.ts @@ -341,5 +341,28 @@ export class MarketplaceViewStateManager { void this.transition({ type: "FETCH_ITEMS" }) } } + + // Handle marketplace data updates (fetched on demand) + if (message.type === "marketplaceData") { + const marketplaceItems = message.marketplaceItems + + 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 newDisplayItems = this.isFilterActive() ? this.filterItems(items) : items + + // Update state in a single operation + this.state = { + ...this.state, + isFetching: false, + allItems: items, + displayItems: newDisplayItems, + } + } + + // Notify state change + this.notifyStateChange() + } } } diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 456ae02671..5788ff3ec8 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -42,6 +42,8 @@ export interface ExtensionStateContextType extends ExtensionState { setCondensingApiConfigId: (value: string) => void customCondensingPrompt?: string setCustomCondensingPrompt: (value: string) => void + marketplaceItems?: any[] + marketplaceInstalledMetadata?: { project: Record; global: Record } setApiConfiguration: (config: ProviderSettings) => void setCustomInstructions: (value?: string) => void setAlwaysAllowReadOnly: (value: boolean) => void @@ -217,6 +219,11 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode const [mcpServers, setMcpServers] = useState([]) const [currentCheckpoint, setCurrentCheckpoint] = useState() const [extensionRouterModels, setExtensionRouterModels] = useState(undefined) + const [marketplaceItems, setMarketplaceItems] = useState([]) + const [marketplaceInstalledMetadata, setMarketplaceInstalledMetadata] = useState<{ + project: Record + global: Record + }>({ project: {}, global: {} }) const setListApiConfigMeta = useCallback( (value: ProviderSettingsEntry[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })), @@ -242,6 +249,13 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setState((prevState) => mergeExtensionState(prevState, newState)) setShowWelcome(!checkExistKey(newState.apiConfiguration)) setDidHydrateState(true) + // Handle marketplace data if present in state message + if (newState.marketplaceItems !== undefined) { + setMarketplaceItems(newState.marketplaceItems) + } + if (newState.marketplaceInstalledMetadata !== undefined) { + setMarketplaceInstalledMetadata(newState.marketplaceInstalledMetadata) + } break } case "theme": { @@ -288,6 +302,15 @@ 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 + } } }, [setListApiConfigMeta], @@ -320,6 +343,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode screenshotQuality: state.screenshotQuality, routerModels: extensionRouterModels, cloudIsAuthenticated: state.cloudIsAuthenticated ?? false, + marketplaceItems, + marketplaceInstalledMetadata, setExperimentEnabled: (id, enabled) => setState((prevState) => ({ ...prevState, experiments: { ...prevState.experiments, [id]: enabled } })), setApiConfiguration, From 3e083fc7add5fcc26f855562f88abd5de18eec0c Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Fri, 20 Jun 2025 11:35:04 -0500 Subject: [PATCH 2/3] feat: enhance marketplace data handling with improved error logging and user notifications --- src/core/webview/ClineProvider.ts | 26 ++++++++++++------- src/shared/ExtensionMessage.ts | 8 +++++- .../marketplace/MarketplaceView.tsx | 1 + .../MarketplaceViewStateManager.ts | 21 ++++++++++++--- .../src/context/ExtensionStateContext.tsx | 12 ++++----- 5 files changed, 48 insertions(+), 20 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 4dc83aa63e..27b3ffba53 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -23,6 +23,7 @@ import { type TerminalActionPromptType, type HistoryItem, type CloudUserInfo, + type MarketplaceItem, requestyDefaultModelId, openRouterDefaultModelId, glamaDefaultModelId, @@ -37,7 +38,7 @@ import { Package } from "../../shared/package" import { findLast } from "../../shared/array" import { supportPrompt } from "../../shared/support-prompt" import { GlobalFileNames } from "../../shared/globalFileNames" -import { ExtensionMessage } from "../../shared/ExtensionMessage" +import { ExtensionMessage, MarketplaceInstalledMetadata } from "../../shared/ExtensionMessage" import { Mode, defaultModeSlug } from "../../shared/modes" import { experimentDefault, experiments, EXPERIMENT_IDS } from "../../shared/experiments" import { formatLanguage } from "../../shared/language" @@ -1260,8 +1261,14 @@ export class ClineProvider async fetchMarketplaceData() { try { const [marketplaceItems, marketplaceInstalledMetadata] = await Promise.all([ - this.marketplaceManager.getCurrentItems().catch(() => []), - this.marketplaceManager.getInstallationMetadata().catch(() => ({ project: {}, global: {} })), + this.marketplaceManager.getCurrentItems().catch((error) => { + console.error("Failed to fetch marketplace items:", error) + return [] as MarketplaceItem[] + }), + this.marketplaceManager.getInstallationMetadata().catch((error) => { + console.error("Failed to fetch installation metadata:", error) + return { project: {}, global: {} } as MarketplaceInstalledMetadata + }), ]) // Send marketplace data separately @@ -1278,6 +1285,13 @@ export class ClineProvider marketplaceItems: [], marketplaceInstalledMetadata: { project: {}, global: {} }, }) + + // 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.", + ) + } } } @@ -1369,18 +1383,12 @@ export class ClineProvider const allowedCommands = vscode.workspace.getConfiguration(Package.name).get("allowedCommands") || [] const cwd = this.cwd - // Initialize marketplace data as empty - will be fetched on demand - let marketplaceItems: any[] = [] - let marketplaceInstalledMetadata: any = { project: {}, global: {} } - // Check if there's a system prompt override for the current mode const currentMode = mode ?? defaultModeSlug const hasSystemPromptOverride = await this.hasFileBasedSystemPromptOverride(currentMode) return { version: this.context.extension?.packageJSON?.version ?? "", - marketplaceItems, - marketplaceInstalledMetadata, apiConfiguration, customInstructions, alwaysAllowReadOnly: alwaysAllowReadOnly ?? false, diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index dd2c364e28..a515a2d004 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -18,6 +18,12 @@ import { Mode } from "./modes" import { RouterModels } from "./api" import type { MarketplaceItem } from "@roo-code/types" +// Type for marketplace installed metadata +export interface MarketplaceInstalledMetadata { + project: Record + global: Record +} + // Indexing status types export interface IndexingStatus { systemStatus: string @@ -138,7 +144,7 @@ export interface ExtensionMessage { organizationAllowList?: OrganizationAllowList tab?: string marketplaceItems?: MarketplaceItem[] - marketplaceInstalledMetadata?: { project: Record; global: Record } + marketplaceInstalledMetadata?: MarketplaceInstalledMetadata } export type ExtensionState = Pick< diff --git a/webview-ui/src/components/marketplace/MarketplaceView.tsx b/webview-ui/src/components/marketplace/MarketplaceView.tsx index b1b08247f3..73bbfc2d22 100644 --- a/webview-ui/src/components/marketplace/MarketplaceView.tsx +++ b/webview-ui/src/components/marketplace/MarketplaceView.tsx @@ -34,6 +34,7 @@ export function MarketplaceView({ stateManager, onDone }: MarketplaceViewProps) // 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", }) diff --git a/webview-ui/src/components/marketplace/MarketplaceViewStateManager.ts b/webview-ui/src/components/marketplace/MarketplaceViewStateManager.ts index 9b83895bb3..9d9b408d6e 100644 --- a/webview-ui/src/components/marketplace/MarketplaceViewStateManager.ts +++ b/webview-ui/src/components/marketplace/MarketplaceViewStateManager.ts @@ -55,7 +55,7 @@ export class MarketplaceViewStateManager { return { allItems: [], displayItems: [], // Always initialize as empty array, not undefined - isFetching: false, + isFetching: true, // Start with loading state for initial load activeTab: "mcp", filters: { type: "", @@ -148,8 +148,12 @@ export class MarketplaceViewStateManager { public async transition(transition: ViewStateTransition): Promise { switch (transition.type) { case "FETCH_ITEMS": { - // Fetch functionality removed - data comes automatically from extension - // No manual fetching needed since we removed caching + // Set fetching state to show loading indicator + this.state = { + ...this.state, + isFetching: true, + } + this.notifyStateChange() break } @@ -225,12 +229,21 @@ export class MarketplaceViewStateManager { tags: filters.tags !== undefined ? filters.tags : this.state.filters.tags, } - // Update state + // Update filters first this.state = { ...this.state, filters: updatedFilters, } + // Apply filters to displayItems with the updated filters + const newDisplayItems = this.filterItems(this.state.allItems) + + // Update state with filtered items + this.state = { + ...this.state, + displayItems: newDisplayItems, + } + // Send filter message vscode.postMessage({ type: "filterMarketplaceItems", diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 5788ff3ec8..e0f6e0d3ad 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -10,7 +10,7 @@ import { ORGANIZATION_ALLOW_ALL, } from "@roo-code/types" -import { ExtensionMessage, ExtensionState } from "@roo/ExtensionMessage" +import { ExtensionMessage, ExtensionState, MarketplaceInstalledMetadata } from "@roo/ExtensionMessage" import { findLastIndex } from "@roo/array" import { McpServer } from "@roo/mcp" import { checkExistKey } from "@roo/checkExistApiConfig" @@ -43,7 +43,7 @@ export interface ExtensionStateContextType extends ExtensionState { customCondensingPrompt?: string setCustomCondensingPrompt: (value: string) => void marketplaceItems?: any[] - marketplaceInstalledMetadata?: { project: Record; global: Record } + marketplaceInstalledMetadata?: MarketplaceInstalledMetadata setApiConfiguration: (config: ProviderSettings) => void setCustomInstructions: (value?: string) => void setAlwaysAllowReadOnly: (value: boolean) => void @@ -220,10 +220,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode const [currentCheckpoint, setCurrentCheckpoint] = useState() const [extensionRouterModels, setExtensionRouterModels] = useState(undefined) const [marketplaceItems, setMarketplaceItems] = useState([]) - const [marketplaceInstalledMetadata, setMarketplaceInstalledMetadata] = useState<{ - project: Record - global: Record - }>({ project: {}, global: {} }) + const [marketplaceInstalledMetadata, setMarketplaceInstalledMetadata] = useState({ + project: {}, + global: {}, + }) const setListApiConfigMeta = useCallback( (value: ProviderSettingsEntry[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })), From 8d1cf83a88e354e14ca76790fa4d22a5c78bddda Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Fri, 20 Jun 2025 11:56:41 -0500 Subject: [PATCH 3/3] feat: request fresh marketplace data after installation and removal actions --- .../marketplace/components/MarketplaceInstallModal.tsx | 5 +++++ .../marketplace/components/MarketplaceItemCard.tsx | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/webview-ui/src/components/marketplace/components/MarketplaceInstallModal.tsx b/webview-ui/src/components/marketplace/components/MarketplaceInstallModal.tsx index 4333db50f4..876f5f3f86 100644 --- a/webview-ui/src/components/marketplace/components/MarketplaceInstallModal.tsx +++ b/webview-ui/src/components/marketplace/components/MarketplaceInstallModal.tsx @@ -136,6 +136,11 @@ export const MarketplaceInstallModal: React.FC = ( // 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") diff --git a/webview-ui/src/components/marketplace/components/MarketplaceItemCard.tsx b/webview-ui/src/components/marketplace/components/MarketplaceItemCard.tsx index 3979fe2fb4..b59291a35b 100644 --- a/webview-ui/src/components/marketplace/components/MarketplaceItemCard.tsx +++ b/webview-ui/src/components/marketplace/components/MarketplaceItemCard.tsx @@ -94,6 +94,11 @@ export const MarketplaceItemCard: React.FC = ({ item, mpItem: item, mpInstallOptions: { target }, }) + + // Request fresh marketplace data to update installed status + vscode.postMessage({ + type: "fetchMarketplaceData", + }) }}> {t("marketplace:items.card.remove")}