Skip to content

feat: add "Installed" filter to Roo Marketplace #7007

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
35 changes: 34 additions & 1 deletion webview-ui/src/components/marketplace/MarketplaceListView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ export function MarketplaceListView({ stateManager, allTags, filteredTags, filte
const allItems = state.displayItems || []
const organizationMcps = state.displayOrganizationMcps || []

// Update state manager with installed metadata when it changes
React.useEffect(() => {
if (marketplaceInstalledMetadata && state.installedMetadata !== marketplaceInstalledMetadata) {
// Update the state manager's installed metadata
manager.transition({
type: "UPDATE_FILTERS",
payload: { filters: state.filters },
})
}
}, [marketplaceInstalledMetadata, state.installedMetadata, state.filters, manager])
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this useEffect intentional? It seems redundant since the state manager already handles installedMetadata updates through the handleMessage method. This could cause unnecessary re-renders and state updates when the metadata changes.


// Filter items by type if specified
const items = filterByType ? allItems.filter((item) => item.type === filterByType) : allItems
const orgMcps = filterByType === "mcp" ? organizationMcps : []
Expand Down Expand Up @@ -55,6 +66,28 @@ export function MarketplaceListView({ stateManager, allTags, filteredTags, filte
}
/>
</div>
{/* Installed filter toggle */}
<div className="mt-2 flex items-center gap-2">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
className="rounded border-vscode-input-border"
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add proper ARIA labels here for better accessibility? Screen reader users might have difficulty understanding the purpose of this checkbox without proper labeling.

Suggested change
className="rounded border-vscode-input-border"
<input
type="checkbox"
className="rounded border-vscode-input-border"
checked={state.filters.installed}
aria-label="Show installed items only"
onChange={(e) =>

checked={state.filters.installed}
onChange={(e) =>
manager.transition({
type: "UPDATE_FILTERS",
payload: { filters: { installed: e.target.checked } },
})
}
/>
<span className="text-sm">{t("marketplace:filters.installed.label")}</span>
</label>
{state.filters.installed && (
<span className="text-xs text-vscode-descriptionForeground">
({t("marketplace:filters.installed.description")})
</span>
)}
</div>
{allTags.length > 0 && (
<div className="mt-2">
<div className="flex items-center justify-between mb-1">
Expand Down Expand Up @@ -187,7 +220,7 @@ export function MarketplaceListView({ stateManager, allTags, filteredTags, filte
onClick={() =>
manager.transition({
type: "UPDATE_FILTERS",
payload: { filters: { search: "", type: "", tags: [] } },
payload: { filters: { search: "", type: "", tags: [], installed: false } },
})
}
className="mt-4 bg-vscode-button-secondaryBackground text-vscode-button-secondaryForeground hover:bg-vscode-button-secondaryHoverBackground transition-colors">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ export interface ViewState {
type: string
search: string
tags: string[]
installed: boolean // New filter to show only installed items
}
installedMetadata?: any // Store installed metadata for filtering
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type safety concern: Consider creating a proper TypeScript interface for the installed metadata structure instead of using any. This would improve type safety and make the code more maintainable.

Suggested change
installedMetadata?: any // Store installed metadata for filtering
installedMetadata?: {
global?: Record<string, any>;
project?: Record<string, any>;
} // Store installed metadata for filtering

}

type TransitionPayloads = {
Expand Down Expand Up @@ -65,6 +67,7 @@ export class MarketplaceViewStateManager {
type: "",
search: "",
tags: [],
installed: false,
},
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The installedMetadata field should be initialized in the getDefaultState() method to avoid potential undefined behavior on first load. Consider adding:

Suggested change
},
installed: false,
},
installedMetadata: undefined,

}
}
Expand Down Expand Up @@ -189,8 +192,11 @@ export class MarketplaceViewStateManager {
let newDisplayItems: MarketplaceItem[]
let newDisplayOrganizationMcps: MarketplaceItem[]
if (this.isFilterActive()) {
newDisplayItems = this.filterItems([...items])
newDisplayOrganizationMcps = this.filterItems([...this.state.organizationMcps])
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]
Expand Down Expand Up @@ -251,6 +257,7 @@ export class MarketplaceViewStateManager {
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
Expand All @@ -260,8 +267,11 @@ export class MarketplaceViewStateManager {
}

// Apply filters to displayItems and displayOrganizationMcps with the updated filters
const newDisplayItems = this.filterItems(this.state.allItems)
const newDisplayOrganizationMcps = this.filterItems(this.state.organizationMcps)
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 = {
Expand All @@ -284,11 +294,16 @@ export class MarketplaceViewStateManager {
}

public isFilterActive(): boolean {
return !!(this.state.filters.type || this.state.filters.search || this.state.filters.tags.length > 0)
return !!(
this.state.filters.type ||
this.state.filters.search ||
this.state.filters.tags.length > 0 ||
this.state.filters.installed
)
}

public filterItems(items: MarketplaceItem[]): MarketplaceItem[] {
const { type, search, tags } = this.state.filters
public filterItems(items: MarketplaceItem[], installedMetadata?: any): MarketplaceItem[] {
const { type, search, tags, installed } = this.state.filters

return items
.map((item) => {
Expand All @@ -303,9 +318,20 @@ export class MarketplaceViewStateManager {
: false
const tagMatch = tags.length > 0 ? item.tags?.some((tag) => tags.includes(tag)) : false

// Check installed status if filter is active
let installedMatch = true
if (installed && installedMetadata) {
const isInstalledGlobally = !!installedMetadata?.global?.[item.id]
const isInstalledInProject = !!installedMetadata?.project?.[item.id]
installedMatch = isInstalledGlobally || isInstalledInProject
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance consideration: The installed filter logic checks both global and project metadata for every item when the filter is active. Could we memoize the installed status calculation or compute it once when metadata updates rather than on every filter operation? This would improve performance for large item lists.

}

// Determine if the main item matches all filters
const mainItemMatches =
typeMatch && (!search || nameMatch || descriptionMatch) && (!tags.length || tagMatch)
typeMatch &&
(!search || nameMatch || descriptionMatch) &&
(!tags.length || tagMatch) &&
installedMatch

const hasMatchingSubcomponents = false

Expand Down Expand Up @@ -343,20 +369,29 @@ export class MarketplaceViewStateManager {
// 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)
newDisplayOrganizationMcps = this.filterItems(this.state.organizationMcps)
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
Expand All @@ -370,6 +405,7 @@ export class MarketplaceViewStateManager {
allItems: items,
displayItems: newDisplayItems,
displayOrganizationMcps: newDisplayOrganizationMcps,
installedMetadata: marketplaceInstalledMetadata || this.state.installedMetadata,
}
// Notification is handled below after all state parts are processed
}
Expand Down Expand Up @@ -411,14 +447,25 @@ export class MarketplaceViewStateManager {
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]
const newDisplayItems = this.isFilterActive() ? this.filterItems(items) : items
const newDisplayOrganizationMcps = this.isFilterActive() ? this.filterItems(orgMcps) : orgMcps

// 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 = {
Expand All @@ -428,6 +475,7 @@ export class MarketplaceViewStateManager {
organizationMcps: orgMcps,
displayItems: newDisplayItems,
displayOrganizationMcps: newDisplayOrganizationMcps,
installedMetadata: marketplaceInstalledMetadata || this.state.installedMetadata,
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const mockState: ViewState = {
type: "",
search: "",
tags: [],
installed: false,
},
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ describe("MarketplaceViewStateManager", () => {
type: "",
search: "",
tags: [],
installed: false,
})
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ describe("MarketplaceItemCard", () => {
type: "",
search: "",
tags: [],
installed: false,
},
setFilters: vi.fn(),
installed: {
Expand Down Expand Up @@ -158,7 +159,7 @@ describe("MarketplaceItemCard", () => {
renderWithProviders(
<MarketplaceItemCard
item={item}
filters={{ type: "", search: "", tags: [] }}
filters={{ type: "", search: "", tags: [], installed: false }}
setFilters={setFilters}
installed={{
project: undefined,
Expand Down
4 changes: 4 additions & 0 deletions webview-ui/src/i18n/locales/en/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
"placeholderMcp": "Search MCPs...",
"placeholderMode": "Search Modes..."
},
"installed": {
"label": "Show installed only",
"description": "Filtering by installed items"
},
"type": {
"label": "Filter by type:",
"all": "All types",
Expand Down
Loading