Skip to content

Commit 5324f65

Browse files
committed
feat: add "Installed" filter to Roo Marketplace
- Added installed boolean filter to ViewState interface - Implemented UI checkbox for "Show installed only" filter - Updated filterItems method to filter by installation status - Added translation strings for the new filter - Updated tests to include the new filter property Fixes #7004
1 parent bbe3362 commit 5324f65

File tree

6 files changed

+102
-14
lines changed

6 files changed

+102
-14
lines changed

webview-ui/src/components/marketplace/MarketplaceListView.tsx

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,17 @@ export function MarketplaceListView({ stateManager, allTags, filteredTags, filte
2727
const allItems = state.displayItems || []
2828
const organizationMcps = state.displayOrganizationMcps || []
2929

30+
// Update state manager with installed metadata when it changes
31+
React.useEffect(() => {
32+
if (marketplaceInstalledMetadata && state.installedMetadata !== marketplaceInstalledMetadata) {
33+
// Update the state manager's installed metadata
34+
manager.transition({
35+
type: "UPDATE_FILTERS",
36+
payload: { filters: state.filters },
37+
})
38+
}
39+
}, [marketplaceInstalledMetadata, state.installedMetadata, state.filters, manager])
40+
3041
// Filter items by type if specified
3142
const items = filterByType ? allItems.filter((item) => item.type === filterByType) : allItems
3243
const orgMcps = filterByType === "mcp" ? organizationMcps : []
@@ -55,6 +66,28 @@ export function MarketplaceListView({ stateManager, allTags, filteredTags, filte
5566
}
5667
/>
5768
</div>
69+
{/* Installed filter toggle */}
70+
<div className="mt-2 flex items-center gap-2">
71+
<label className="flex items-center gap-2 cursor-pointer">
72+
<input
73+
type="checkbox"
74+
className="rounded border-vscode-input-border"
75+
checked={state.filters.installed}
76+
onChange={(e) =>
77+
manager.transition({
78+
type: "UPDATE_FILTERS",
79+
payload: { filters: { installed: e.target.checked } },
80+
})
81+
}
82+
/>
83+
<span className="text-sm">{t("marketplace:filters.installed.label")}</span>
84+
</label>
85+
{state.filters.installed && (
86+
<span className="text-xs text-vscode-descriptionForeground">
87+
({t("marketplace:filters.installed.description")})
88+
</span>
89+
)}
90+
</div>
5891
{allTags.length > 0 && (
5992
<div className="mt-2">
6093
<div className="flex items-center justify-between mb-1">
@@ -187,7 +220,7 @@ export function MarketplaceListView({ stateManager, allTags, filteredTags, filte
187220
onClick={() =>
188221
manager.transition({
189222
type: "UPDATE_FILTERS",
190-
payload: { filters: { search: "", type: "", tags: [] } },
223+
payload: { filters: { search: "", type: "", tags: [], installed: false } },
191224
})
192225
}
193226
className="mt-4 bg-vscode-button-secondaryBackground text-vscode-button-secondaryForeground hover:bg-vscode-button-secondaryHoverBackground transition-colors">

webview-ui/src/components/marketplace/MarketplaceViewStateManager.ts

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ export interface ViewState {
2626
type: string
2727
search: string
2828
tags: string[]
29+
installed: boolean // New filter to show only installed items
2930
}
31+
installedMetadata?: any // Store installed metadata for filtering
3032
}
3133

3234
type TransitionPayloads = {
@@ -65,6 +67,7 @@ export class MarketplaceViewStateManager {
6567
type: "",
6668
search: "",
6769
tags: [],
70+
installed: false,
6871
},
6972
}
7073
}
@@ -189,8 +192,11 @@ export class MarketplaceViewStateManager {
189192
let newDisplayItems: MarketplaceItem[]
190193
let newDisplayOrganizationMcps: MarketplaceItem[]
191194
if (this.isFilterActive()) {
192-
newDisplayItems = this.filterItems([...items])
193-
newDisplayOrganizationMcps = this.filterItems([...this.state.organizationMcps])
195+
newDisplayItems = this.filterItems([...items], this.state.installedMetadata)
196+
newDisplayOrganizationMcps = this.filterItems(
197+
[...this.state.organizationMcps],
198+
this.state.installedMetadata,
199+
)
194200
} else {
195201
// No filters active - show all items
196202
newDisplayItems = [...items]
@@ -251,6 +257,7 @@ export class MarketplaceViewStateManager {
251257
type: filters.type !== undefined ? filters.type : this.state.filters.type,
252258
search: filters.search !== undefined ? filters.search : this.state.filters.search,
253259
tags: filters.tags !== undefined ? filters.tags : this.state.filters.tags,
260+
installed: filters.installed !== undefined ? filters.installed : this.state.filters.installed,
254261
}
255262

256263
// Update filters first
@@ -260,8 +267,11 @@ export class MarketplaceViewStateManager {
260267
}
261268

262269
// Apply filters to displayItems and displayOrganizationMcps with the updated filters
263-
const newDisplayItems = this.filterItems(this.state.allItems)
264-
const newDisplayOrganizationMcps = this.filterItems(this.state.organizationMcps)
270+
const newDisplayItems = this.filterItems(this.state.allItems, this.state.installedMetadata)
271+
const newDisplayOrganizationMcps = this.filterItems(
272+
this.state.organizationMcps,
273+
this.state.installedMetadata,
274+
)
265275

266276
// Update state with filtered items
267277
this.state = {
@@ -284,11 +294,16 @@ export class MarketplaceViewStateManager {
284294
}
285295

286296
public isFilterActive(): boolean {
287-
return !!(this.state.filters.type || this.state.filters.search || this.state.filters.tags.length > 0)
297+
return !!(
298+
this.state.filters.type ||
299+
this.state.filters.search ||
300+
this.state.filters.tags.length > 0 ||
301+
this.state.filters.installed
302+
)
288303
}
289304

290-
public filterItems(items: MarketplaceItem[]): MarketplaceItem[] {
291-
const { type, search, tags } = this.state.filters
305+
public filterItems(items: MarketplaceItem[], installedMetadata?: any): MarketplaceItem[] {
306+
const { type, search, tags, installed } = this.state.filters
292307

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

321+
// Check installed status if filter is active
322+
let installedMatch = true
323+
if (installed && installedMetadata) {
324+
const isInstalledGlobally = !!installedMetadata?.global?.[item.id]
325+
const isInstalledInProject = !!installedMetadata?.project?.[item.id]
326+
installedMatch = isInstalledGlobally || isInstalledInProject
327+
}
328+
306329
// Determine if the main item matches all filters
307330
const mainItemMatches =
308-
typeMatch && (!search || nameMatch || descriptionMatch) && (!tags.length || tagMatch)
331+
typeMatch &&
332+
(!search || nameMatch || descriptionMatch) &&
333+
(!tags.length || tagMatch) &&
334+
installedMatch
309335

310336
const hasMatchingSubcomponents = false
311337

@@ -343,20 +369,29 @@ export class MarketplaceViewStateManager {
343369
// Handle state updates for marketplace items
344370
// The state.marketplaceItems come from ClineProvider, see the file src/core/webview/ClineProvider.ts
345371
const marketplaceItems = message.state.marketplaceItems
372+
const marketplaceInstalledMetadata = message.state.marketplaceInstalledMetadata
346373

347374
if (marketplaceItems !== undefined) {
348375
// Always use the marketplace items from the extension when they're provided
349376
// This ensures fresh data is always displayed
350377
const items = [...marketplaceItems]
351378

379+
// Update installed metadata if provided
380+
if (marketplaceInstalledMetadata !== undefined) {
381+
this.state.installedMetadata = marketplaceInstalledMetadata
382+
}
383+
352384
// Calculate display items based on current filters
353385
// If no filters are active, show all items
354386
// If filters are active, apply filtering
355387
let newDisplayItems: MarketplaceItem[]
356388
let newDisplayOrganizationMcps: MarketplaceItem[]
357389
if (this.isFilterActive()) {
358-
newDisplayItems = this.filterItems(items)
359-
newDisplayOrganizationMcps = this.filterItems(this.state.organizationMcps)
390+
newDisplayItems = this.filterItems(items, this.state.installedMetadata)
391+
newDisplayOrganizationMcps = this.filterItems(
392+
this.state.organizationMcps,
393+
this.state.installedMetadata,
394+
)
360395
} else {
361396
// No filters active - show all items
362397
newDisplayItems = items
@@ -370,6 +405,7 @@ export class MarketplaceViewStateManager {
370405
allItems: items,
371406
displayItems: newDisplayItems,
372407
displayOrganizationMcps: newDisplayOrganizationMcps,
408+
installedMetadata: marketplaceInstalledMetadata || this.state.installedMetadata,
373409
}
374410
// Notification is handled below after all state parts are processed
375411
}
@@ -411,14 +447,25 @@ export class MarketplaceViewStateManager {
411447
if (message.type === "marketplaceData") {
412448
const marketplaceItems = message.marketplaceItems
413449
const organizationMcps = message.organizationMcps || []
450+
const marketplaceInstalledMetadata = message.marketplaceInstalledMetadata
414451

415452
if (marketplaceItems !== undefined) {
416453
// Always use the marketplace items from the extension when they're provided
417454
// This ensures fresh data is always displayed
418455
const items = [...marketplaceItems]
419456
const orgMcps = [...organizationMcps]
420-
const newDisplayItems = this.isFilterActive() ? this.filterItems(items) : items
421-
const newDisplayOrganizationMcps = this.isFilterActive() ? this.filterItems(orgMcps) : orgMcps
457+
458+
// Update installed metadata if provided
459+
if (marketplaceInstalledMetadata !== undefined) {
460+
this.state.installedMetadata = marketplaceInstalledMetadata
461+
}
462+
463+
const newDisplayItems = this.isFilterActive()
464+
? this.filterItems(items, this.state.installedMetadata)
465+
: items
466+
const newDisplayOrganizationMcps = this.isFilterActive()
467+
? this.filterItems(orgMcps, this.state.installedMetadata)
468+
: orgMcps
422469

423470
// Update state in a single operation
424471
this.state = {
@@ -428,6 +475,7 @@ export class MarketplaceViewStateManager {
428475
organizationMcps: orgMcps,
429476
displayItems: newDisplayItems,
430477
displayOrganizationMcps: newDisplayOrganizationMcps,
478+
installedMetadata: marketplaceInstalledMetadata || this.state.installedMetadata,
431479
}
432480
}
433481

webview-ui/src/components/marketplace/__tests__/MarketplaceListView.spec.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const mockState: ViewState = {
2727
type: "",
2828
search: "",
2929
tags: [],
30+
installed: false,
3031
},
3132
}
3233

webview-ui/src/components/marketplace/__tests__/MarketplaceViewStateManager.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ describe("MarketplaceViewStateManager", () => {
6464
type: "",
6565
search: "",
6666
tags: [],
67+
installed: false,
6768
})
6869
})
6970

webview-ui/src/components/marketplace/components/__tests__/MarketplaceItemCard.spec.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ describe("MarketplaceItemCard", () => {
7878
type: "",
7979
search: "",
8080
tags: [],
81+
installed: false,
8182
},
8283
setFilters: vi.fn(),
8384
installed: {
@@ -158,7 +159,7 @@ describe("MarketplaceItemCard", () => {
158159
renderWithProviders(
159160
<MarketplaceItemCard
160161
item={item}
161-
filters={{ type: "", search: "", tags: [] }}
162+
filters={{ type: "", search: "", tags: [], installed: false }}
162163
setFilters={setFilters}
163164
installed={{
164165
project: undefined,

webview-ui/src/i18n/locales/en/marketplace.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
"placeholderMcp": "Search MCPs...",
1414
"placeholderMode": "Search Modes..."
1515
},
16+
"installed": {
17+
"label": "Show installed only",
18+
"description": "Filtering by installed items"
19+
},
1620
"type": {
1721
"label": "Filter by type:",
1822
"all": "All types",

0 commit comments

Comments
 (0)