Просмотр исходного кода

fix: resolve marketplace timeout issues and display installed MCPs (#4817) (#4941)

Daniel 6 месяцев назад
Родитель
Сommit
9a5f6ce7ea

+ 42 - 10
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"
@@ -1254,6 +1255,46 @@ 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((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
+			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: {} },
+			})
+
+			// 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.",
+				)
+			}
+		}
+	}
+
 	/**
 	 * Checks if there is a file-based system prompt override for the given mode
 	 */
@@ -1342,21 +1383,12 @@ export class ClineProvider
 		const allowedCommands = vscode.workspace.getConfiguration(Package.name).get<string[]>("allowedCommands") || []
 		const cwd = this.cwd
 
-		// Fetch marketplace data
-		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)
 
 		return {
 			version: this.context.extension?.packageJSON?.version ?? "",
-			marketplaceItems,
-			marketplaceInstalledMetadata,
 			apiConfiguration,
 			customInstructions,
 			alwaysAllowReadOnly: alwaysAllowReadOnly ?? false,

+ 6 - 0
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 {

+ 9 - 0
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<string, { type: string }>
+	global: Record<string, { type: string }>
+}
+
 // Indexing status types
 export interface IndexingStatus {
 	systemStatus: string
@@ -90,6 +96,7 @@ export interface ExtensionMessage {
 		| "indexCleared"
 		| "codebaseIndexConfig"
 		| "marketplaceInstallResult"
+		| "marketplaceData"
 	text?: string
 	payload?: any // Add a generic payload for now, can refine later
 	action?:
@@ -136,6 +143,8 @@ export interface ExtensionMessage {
 	userInfo?: CloudUserInfo
 	organizationAllowList?: OrganizationAllowList
 	tab?: string
+	marketplaceItems?: MarketplaceItem[]
+	marketplaceInstalledMetadata?: MarketplaceInstalledMetadata
 }
 
 export type ExtensionState = Pick<

+ 1 - 0
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"

+ 3 - 7
webview-ui/src/components/marketplace/MarketplaceView.tsx

@@ -33,14 +33,10 @@ 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
+			// Note: isFetching is already true by default for initial load
 			vscode.postMessage({
-				type: "filterMarketplaceItems",
-				filters: {
-					type: "",
-					search: "",
-					tags: [],
-				},
+				type: "fetchMarketplaceData",
 			})
 		}
 

+ 40 - 4
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<void> {
 		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",
@@ -341,5 +354,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()
+		}
 	}
 }

+ 5 - 0
webview-ui/src/components/marketplace/components/MarketplaceInstallModal.tsx

@@ -136,6 +136,11 @@ export const MarketplaceInstallModal: React.FC<MarketplaceInstallModalProps> = (
 					// 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")

+ 5 - 0
webview-ui/src/components/marketplace/components/MarketplaceItemCard.tsx

@@ -94,6 +94,11 @@ export const MarketplaceItemCard: React.FC<MarketplaceItemCardProps> = ({ item,
 													mpItem: item,
 													mpInstallOptions: { target },
 												})
+
+												// Request fresh marketplace data to update installed status
+												vscode.postMessage({
+													type: "fetchMarketplaceData",
+												})
 											}}>
 											{t("marketplace:items.card.remove")}
 										</Button>

+ 26 - 1
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"
@@ -42,6 +42,8 @@ export interface ExtensionStateContextType extends ExtensionState {
 	setCondensingApiConfigId: (value: string) => void
 	customCondensingPrompt?: string
 	setCustomCondensingPrompt: (value: string) => void
+	marketplaceItems?: any[]
+	marketplaceInstalledMetadata?: MarketplaceInstalledMetadata
 	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<McpServer[]>([])
 	const [currentCheckpoint, setCurrentCheckpoint] = useState<string>()
 	const [extensionRouterModels, setExtensionRouterModels] = useState<RouterModels | undefined>(undefined)
+	const [marketplaceItems, setMarketplaceItems] = useState<any[]>([])
+	const [marketplaceInstalledMetadata, setMarketplaceInstalledMetadata] = useState<MarketplaceInstalledMetadata>({
+		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,