diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index d99aa644b2..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" @@ -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("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, 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..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 @@ -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< 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..73bbfc2d22 100644 --- a/webview-ui/src/components/marketplace/MarketplaceView.tsx +++ b/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", }) } diff --git a/webview-ui/src/components/marketplace/MarketplaceViewStateManager.ts b/webview-ui/src/components/marketplace/MarketplaceViewStateManager.ts index 7f7324f581..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", @@ -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() + } } } 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")} diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 456ae02671..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" @@ -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([]) const [currentCheckpoint, setCurrentCheckpoint] = useState() const [extensionRouterModels, setExtensionRouterModels] = useState(undefined) + const [marketplaceItems, setMarketplaceItems] = useState([]) + const [marketplaceInstalledMetadata, setMarketplaceInstalledMetadata] = useState({ + 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,