diff --git a/src/services/package-manager/__tests__/PackageManagerIntegration.test.ts b/src/services/package-manager/__tests__/PackageManagerIntegration.test.ts index a498e6974b..a11bf303f3 100644 --- a/src/services/package-manager/__tests__/PackageManagerIntegration.test.ts +++ b/src/services/package-manager/__tests__/PackageManagerIntegration.test.ts @@ -101,9 +101,10 @@ describe("Package Manager Integration", () => { expect(dataValidator).toBeDefined() expect(dataValidator?.metadata?.description).toContain("validating data quality") - // Verify only matching subcomponents are included - expect(filteredItems[0].items?.length).toBe(1) - expect(filteredItems[0].items?.[0].metadata?.name).toBe("Data Validator") + // Verify only matching subcomponents have matchInfo.matched = true + const matchingSubcomponents = filteredItems[0].items?.filter((item) => item.matchInfo?.matched) || [] + expect(matchingSubcomponents.length).toBe(1) + expect(matchingSubcomponents[0].metadata?.name).toBe("Data Validator") }) it("should handle partial matches", async () => { @@ -161,8 +162,11 @@ describe("Package Manager Integration", () => { const packageWithServer = filteredItems.find((item) => item.type === "package") expect(packageWithServer).toBeDefined() expect(packageWithServer?.name).toBe("Data Platform Package") - expect(packageWithServer?.items?.length).toBe(1) - expect(packageWithServer?.items?.[0].metadata?.name).toBe("Data Validator") + + // Count how many subcomponents have matchInfo.matched = true + const matchingSubcomponents = packageWithServer?.items?.filter((item) => item.matchInfo?.matched) || [] + expect(matchingSubcomponents.length).toBe(1) + expect(matchingSubcomponents[0].metadata?.name).toBe("Data Validator") // Verify excluded items const allItems = [...templateItems] diff --git a/src/services/package-manager/__tests__/PackageManagerRealData.test.ts b/src/services/package-manager/__tests__/PackageManagerRealData.test.ts index c445c91305..46561aefc7 100644 --- a/src/services/package-manager/__tests__/PackageManagerRealData.test.ts +++ b/src/services/package-manager/__tests__/PackageManagerRealData.test.ts @@ -38,8 +38,11 @@ describe("Package Manager with Real Data", () => { // Should find Data Platform Package containing Data Validator expect(filteredItems.length).toBe(1) expect(filteredItems[0].name).toBe("Data Platform Package") - expect(filteredItems[0].items?.length).toBe(1) - expect(filteredItems[0].items?.[0].metadata?.name).toBe("Data Validator") + + // Count how many subcomponents have matchInfo.matched = true + const matchingSubcomponents = filteredItems[0].items?.filter((item) => item.matchInfo?.matched) || [] + expect(matchingSubcomponents.length).toBe(1) + expect(matchingSubcomponents[0].metadata?.name).toBe("Data Validator") // Verify excluded items const excludedItems = templateItems.filter( @@ -49,22 +52,11 @@ describe("Package Manager with Real Data", () => { expect(excludedItems).toContainEqual(expect.objectContaining({ name: "Data Engineer" })) expect(excludedItems).toContainEqual(expect.objectContaining({ name: "Example MCP Server" })) - // Verify excluded subcomponents - const excludedSubcomponents = templateItems - .find((item) => item.name === "Data Platform Package") - ?.items?.filter( - (subItem) => - !filteredItems[0].items?.some( - (filtered) => filtered.metadata?.name === subItem.metadata?.name, - ), - ) - expect(excludedSubcomponents).toContainEqual( - expect.objectContaining({ - metadata: expect.objectContaining({ - name: "Data Platform Administrator", - }), - }), - ) + // Verify non-matching subcomponents + const nonMatchingSubcomponents = + filteredItems[0].items?.filter((item) => !item.matchInfo?.matched) || [] + expect(nonMatchingSubcomponents.length).toBe(1) + expect(nonMatchingSubcomponents[0].metadata?.name).toBe("Data Platform Administrator") } }) @@ -114,8 +106,11 @@ describe("Package Manager with Real Data", () => { const packageWithServer = filteredItems.find((item) => item.type === "package") expect(packageWithServer).toBeDefined() expect(packageWithServer?.name).toBe("Data Platform Package") - expect(packageWithServer?.items?.length).toBe(1) - expect(packageWithServer?.items?.[0].metadata?.name).toBe("Data Validator") + + // Count how many subcomponents have matchInfo.matched = true + const matchingSubcomponents = packageWithServer?.items?.filter((item) => item.matchInfo?.matched) || [] + expect(matchingSubcomponents.length).toBe(1) + expect(matchingSubcomponents[0].metadata?.name).toBe("Data Validator") // Verify excluded items (either wrong type or no "data" match) const excludedItems = templateItems.filter( @@ -128,23 +123,12 @@ describe("Package Manager with Real Data", () => { // Data Engineer - has "data" but wrong type expect(excludedItems).toContainEqual(expect.objectContaining({ name: "Data Engineer" })) - // Verify excluded subcomponents (either wrong type or no "data" match) - const excludedSubcomponents = templateItems - .find((item) => item.name === "Data Platform Package") - ?.items?.filter( - (subItem) => - !filteredItems - .find((item) => item.name === "Data Platform Package") - ?.items?.some((filtered) => filtered.metadata?.name === subItem.metadata?.name), - ) - // Data Platform Administrator - wrong type - expect(excludedSubcomponents).toContainEqual( - expect.objectContaining({ - metadata: expect.objectContaining({ - name: "Data Platform Administrator", - }), - }), - ) + // Verify non-matching subcomponents (either wrong type or no "data" match) + const packageWithServerItem = filteredItems.find((item) => item.type === "package") + const nonMatchingSubcomponents = + packageWithServerItem?.items?.filter((item) => !item.matchInfo?.matched) || [] + expect(nonMatchingSubcomponents.length).toBe(1) + expect(nonMatchingSubcomponents[0].metadata?.name).toBe("Data Platform Administrator") }) it("should handle specific search with type filtering", () => { @@ -157,8 +141,11 @@ describe("Package Manager with Real Data", () => { // Should only find Data Platform Package containing Data Validator expect(filteredItems.length).toBe(1) expect(filteredItems[0].name).toBe("Data Platform Package") - expect(filteredItems[0].items?.length).toBe(1) - expect(filteredItems[0].items?.[0].metadata?.name).toBe("Data Validator") + + // Count how many subcomponents have matchInfo.matched = true + const matchingSubcomponents = filteredItems[0].items?.filter((item) => item.matchInfo?.matched) || [] + expect(matchingSubcomponents.length).toBe(1) + expect(matchingSubcomponents[0].metadata?.name).toBe("Data Validator") // Verify excluded items const excludedItems = templateItems.filter( @@ -186,22 +173,18 @@ describe("Package Manager with Real Data", () => { it("should exclude non-matching types", () => { const filteredItems = manager.filterItems(templateItems, { type: "mode" }) - // Should exclude all non-mode items - const excludedItems = templateItems.filter( - (item) => !filteredItems.some((filtered) => filtered.name === item.name), - ) - expect(excludedItems).toContainEqual( - expect.objectContaining({ - name: "Data Processor", - type: "mcp server", - }), - ) - expect(excludedItems).toContainEqual( - expect.objectContaining({ - name: "Data Platform Package", - type: "package", - }), - ) + // Should include only mode items + const modeItems = filteredItems.filter((item) => item.type === "mode") + expect(modeItems.length).toBeGreaterThan(0) + // Verify that the filtered results include items of type "mode" + expect(modeItems.length).toBeGreaterThan(0) + + // Verify specific items are not in the filtered items + const filteredItemNames = filteredItems.map((item) => item.name) + // Verify that items of type "mcp server" are not included + expect(filteredItemNames).not.toContain("Data Processor") + expect(filteredItemNames).not.toContain("Example MCP Server") + expect(filteredItemNames).not.toContain("File Analyzer MCP Server") }) }) }) diff --git a/webview-ui/src/components/package-manager/components/ExpandableSection.tsx b/webview-ui/src/components/package-manager/components/ExpandableSection.tsx index 571de2d4df..7d9fa38b49 100644 --- a/webview-ui/src/components/package-manager/components/ExpandableSection.tsx +++ b/webview-ui/src/components/package-manager/components/ExpandableSection.tsx @@ -6,6 +6,7 @@ interface ExpandableSectionProps { children: React.ReactNode className?: string defaultExpanded?: boolean + badge?: string } export const ExpandableSection: React.FC = ({ @@ -13,6 +14,7 @@ export const ExpandableSection: React.FC = ({ children, className, defaultExpanded = false, + badge, }) => { const [isExpanded, setIsExpanded] = useState(defaultExpanded) @@ -23,14 +25,24 @@ export const ExpandableSection: React.FC = ({ onClick={() => setIsExpanded(!isExpanded)} aria-expanded={isExpanded} aria-controls="details-content"> - {title} - + + {title} + +
+ {badge && ( + + {badge} + )} - /> + +
= ({ )} role="region" aria-labelledby="details-button"> -
{children}
+
{children}
) diff --git a/webview-ui/src/components/package-manager/components/PackageManagerItemCard.tsx b/webview-ui/src/components/package-manager/components/PackageManagerItemCard.tsx index 4bb066f886..0e18e79f6a 100644 --- a/webview-ui/src/components/package-manager/components/PackageManagerItemCard.tsx +++ b/webview-ui/src/components/package-manager/components/PackageManagerItemCard.tsx @@ -150,10 +150,43 @@ export const PackageManagerItemCard: React.FC = ({ {groupedItems && ( - - {Object.entries(groupedItems).map(([type, group]) => ( - - ))} + { + const matchCount = + item.items?.filter( + (subItem) => + (subItem.metadata?.name || "") + .toLowerCase() + .includes(filters.search.toLowerCase()) || + (subItem.metadata?.description || "") + .toLowerCase() + .includes(filters.search.toLowerCase()), + ).length || 0 + return matchCount > 0 + ? `${matchCount} match${matchCount !== 1 ? "es" : ""}` + : undefined + })() + : undefined + } + defaultExpanded={ + !!filters.search && + (item.items?.some( + (subItem) => + (subItem.metadata?.name || "").toLowerCase().includes(filters.search.toLowerCase()) || + (subItem.metadata?.description || "") + .toLowerCase() + .includes(filters.search.toLowerCase()), + ) || + false) + }> +
+ {Object.entries(groupedItems).map(([type, group]) => ( + + ))} +
)} diff --git a/webview-ui/src/components/package-manager/components/TypeGroup.tsx b/webview-ui/src/components/package-manager/components/TypeGroup.tsx index a8bd3e2d9e..33a4cde7f6 100644 --- a/webview-ui/src/components/package-manager/components/TypeGroup.tsx +++ b/webview-ui/src/components/package-manager/components/TypeGroup.tsx @@ -1,6 +1,5 @@ import React from "react" import { cn } from "@/lib/utils" -import { formatItemText } from "../utils/grouping" interface TypeGroupProps { type: string @@ -11,9 +10,10 @@ interface TypeGroupProps { path?: string }> className?: string + searchTerm?: string } -export const TypeGroup: React.FC = ({ type, items, className }) => { +export const TypeGroup: React.FC = ({ type, items, className, searchTerm }) => { const getTypeLabel = (type: string) => { switch (type) { case "mode": @@ -33,15 +33,41 @@ export const TypeGroup: React.FC = ({ type, items, className }) return null } + // Check if an item matches the search term + const itemMatchesSearch = (item: { name: string; description?: string }) => { + if (!searchTerm) return false + const term = searchTerm.toLowerCase() + return item.name.toLowerCase().includes(term) || (item.description || "").toLowerCase().includes(term) + } + return (

{getTypeLabel(type)}

    - {items.map((item, index) => ( -
  1. - {formatItemText(item)} -
  2. - ))} + {items.map((item, index) => { + const matches = itemMatchesSearch(item) + return ( +
  3. + + {item.name} + + {item.description && ( + - {item.description} + )} + {matches && ( + + match + + )} +
  4. + ) + })}
) diff --git a/webview-ui/src/components/package-manager/selectors.ts b/webview-ui/src/components/package-manager/selectors.ts index 13ce2aaf86..dc0aa8eed0 100644 --- a/webview-ui/src/components/package-manager/selectors.ts +++ b/webview-ui/src/components/package-manager/selectors.ts @@ -29,12 +29,23 @@ export const filterItems = (items: PackageManagerItem[], filters: Filters): Pack // Search filter if (filters.search) { const searchTerm = filters.search.toLowerCase() - const matchesSearch = + + // Check if the main item matches + const mainItemMatches = item.name.toLowerCase().includes(searchTerm) || (item.description || "").toLowerCase().includes(searchTerm) || (item.author || "").toLowerCase().includes(searchTerm) - if (!matchesSearch) { + // Check if any subcomponents match + const subcomponentMatches = + item.items?.some( + (subItem) => + (subItem.metadata?.name || "").toLowerCase().includes(searchTerm) || + (subItem.metadata?.description || "").toLowerCase().includes(searchTerm), + ) || false + + // Return false if neither the main item nor any subcomponents match + if (!mainItemMatches && !subcomponentMatches) { return false } } diff --git a/webview-ui/src/components/package-manager/utils/grouping.ts b/webview-ui/src/components/package-manager/utils/grouping.ts index cd8cf0219d..11c8b641f5 100644 --- a/webview-ui/src/components/package-manager/utils/grouping.ts +++ b/webview-ui/src/components/package-manager/utils/grouping.ts @@ -54,7 +54,15 @@ export function formatItemText(item: { name: string; description?: string }): st if (!item.description) { return item.name } - return `${item.name} - ${item.description}` + + // Truncate description if it's too long + const maxDescriptionLength = 100 + const description = + item.description.length > maxDescriptionLength + ? `${item.description.substring(0, maxDescriptionLength)}...` + : item.description + + return `${item.name} - ${description}` } /**