From af6c27bdff28781717ea941ad5b36585466b1d83 Mon Sep 17 00:00:00 2001 From: Smartsheet-JB-Brown Date: Mon, 14 Apr 2025 13:33:58 -0700 Subject: [PATCH] locale fixes for metadata --- .../implementation/01-architecture.md | 199 ++-- .../implementation/02-core-components.md | 560 ++++----- .../implementation/03-data-structures.md | 238 ++-- .../implementation/04-search-and-filter.md | 806 ++++++------- .../implementation/05-ui-components.md | 1013 ++++++++--------- .../implementation/06-testing-strategy.md | 718 ++++++------ .../implementation/07-extending.md | 890 +++++++-------- .../localization-improvements.md | 378 +----- .../user-guide/04-working-with-details.md | 21 +- src/services/package-manager/GitFetcher.ts | 14 +- .../package-manager/MetadataScanner.ts | 144 ++- .../package-manager/PackageManagerManager.ts | 18 +- .../__tests__/GetLocalizedMetadata.test.ts | 79 ++ .../__tests__/LocalizationFallback.test.ts | 9 + src/services/package-manager/constants.ts | 3 +- src/services/package-manager/types.ts | 8 + src/services/package-manager/utils.ts | 13 + 17 files changed, 2449 insertions(+), 2662 deletions(-) create mode 100644 src/services/package-manager/__tests__/GetLocalizedMetadata.test.ts create mode 100644 src/services/package-manager/__tests__/LocalizationFallback.test.ts create mode 100644 src/services/package-manager/utils.ts diff --git a/cline_docs/package-manager/implementation/01-architecture.md b/cline_docs/package-manager/implementation/01-architecture.md index 954f528194..a2c5a9dc3c 100644 --- a/cline_docs/package-manager/implementation/01-architecture.md +++ b/cline_docs/package-manager/implementation/01-architecture.md @@ -34,19 +34,21 @@ The Package Manager components interact through a well-defined message flow: ### Core Interaction Patterns 1. **Data Loading**: - - MetadataScanner loads package data from repositories - - PackageManagerManager stores and manages this data - - UI requests data through the message handler + + - MetadataScanner loads package data from repositories + - PackageManagerManager stores and manages this data + - UI requests data through the message handler 2. **Filtering and Search**: - - UI sends filter/search criteria to the backend - - PackageManagerManager applies filters to the data - - Filtered results are returned to the UI + + - UI sends filter/search criteria to the backend + - PackageManagerManager applies filters to the data + - Filtered results are returned to the UI 3. **Source Management**: - - UI sends source management commands - - PackageManagerManager updates source configurations - - MetadataScanner reloads data from updated sources + - UI sends source management commands + - PackageManagerManager updates source configurations + - MetadataScanner reloads data from updated sources ## Data Flow Diagram @@ -235,48 +237,53 @@ classDiagram ### Backend Components 1. **MetadataScanner** - - Scans directories and repositories for package metadata - - Parses YAML metadata files - - Builds component hierarchies - - Handles file system and Git operations + + - Scans directories and repositories for package metadata + - Parses YAML metadata files + - Builds component hierarchies + - Handles file system and Git operations 2. **PackageManagerManager** - - Stores and manages package items - - Applies filters and search criteria - - Manages package sources - - Handles package operations + + - Stores and manages package items + - Applies filters and search criteria + - Manages package sources + - Handles package operations 3. **packageManagerMessageHandler** - - Routes messages between UI and backend - - Processes commands from the UI - - Returns data and status updates to the UI - - Handles error conditions + - Routes messages between UI and backend + - Processes commands from the UI + - Returns data and status updates to the UI + - Handles error conditions ### Frontend Components 1. **PackageManagerView** - - Main container component - - Manages overall UI state - - Handles tab navigation - - Displays filter controls + + - Main container component + - Manages overall UI state + - Handles tab navigation + - Displays filter controls 2. **PackageManagerItemCard** - - Displays individual package information - - Handles tag interactions - - Manages expandable details section - - Provides action buttons + + - Displays individual package information + - Handles tag interactions + - Manages expandable details section + - Provides action buttons 3. **ExpandableSection** - - Provides collapsible UI sections - - Manages expand/collapse state - - Handles animations - - Displays section headers and badges + + - Provides collapsible UI sections + - Manages expand/collapse state + - Handles animations + - Displays section headers and badges 4. **TypeGroup** - - Groups and displays components by type - - Formats item lists - - Highlights search matches - - Provides consistent styling + - Groups and displays components by type + - Formats item lists + - Highlights search matches + - Provides consistent styling ## Data Flow Patterns @@ -285,104 +292,114 @@ classDiagram The Package Manager uses a message-based architecture for communication between the frontend and backend: 1. **Message Structure**: - ```typescript - { - type: string; // The message type (e.g., "search", "filter", "addSource") - payload: any; // The message data - } - ``` + + ```typescript + { + type: string // The message type (e.g., "search", "filter", "addSource") + payload: any // The message data + } + ``` 2. **Common Message Types**: - - `search`: Apply a search term filter - - `filter`: Apply type or tag filters - - `addSource`: Add a new package source - - `removeSource`: Remove a package source - - `refreshSources`: Reload data from sources + + - `search`: Apply a search term filter + - `filter`: Apply type or tag filters + - `addSource`: Add a new package source + - `removeSource`: Remove a package source + - `refreshSources`: Reload data from sources 3. **Response Structure**: - ```typescript - { - type: string; // The response type - data: any; // The response data - error?: string; // Optional error message - } - ``` + ```typescript + { + type: string; // The response type + data: any; // The response data + error?: string; // Optional error message + } + ``` ### State Management The Package Manager maintains state in several places: 1. **Backend State**: - - Current items in the PackageManagerManager - - Source configurations - - Cached metadata + + - Current items in the PackageManagerManager + - Source configurations + - Cached metadata 2. **Frontend State**: - - Current filters and search terms - - UI state (active tab, expanded sections) - - Display preferences + + - Current filters and search terms + - UI state (active tab, expanded sections) + - Display preferences 3. **Persistent State**: - - Source configurations stored in extension settings - - User preferences + - Source configurations stored in extension settings + - User preferences ## Performance Considerations The Package Manager architecture addresses several performance challenges: 1. **Lazy Loading**: - - Metadata is loaded on demand - - Repositories are scanned only when needed - - UI components render incrementally + + - Metadata is loaded on demand + - Repositories are scanned only when needed + - UI components render incrementally 2. **Efficient Filtering**: - - Filtering happens on the backend to reduce data transfer - - Search algorithms optimize for common patterns - - Results are cached when possible + + - Filtering happens on the backend to reduce data transfer + - Search algorithms optimize for common patterns + - Results are cached when possible 3. **Responsive UI**: - - Asynchronous operations prevent UI blocking - - Animations provide feedback during loading - - Pagination limits the number of items displayed at once + - Asynchronous operations prevent UI blocking + - Animations provide feedback during loading + - Pagination limits the number of items displayed at once ## Error Handling The architecture includes robust error handling: 1. **Source Errors**: - - Invalid repositories are marked with error states - - Users are notified of access issues - - The system continues to function with other sources + + - Invalid repositories are marked with error states + - Users are notified of access issues + - The system continues to function with other sources 2. **Parsing Errors**: - - Malformed metadata is gracefully handled - - Partial results are displayed when possible - - Error details are logged for debugging + + - Malformed metadata is gracefully handled + - Partial results are displayed when possible + - Error details are logged for debugging 3. **Network Errors**: - - Timeouts and retries for network operations - - Offline mode with cached data - - Clear error messages for user troubleshooting + - Timeouts and retries for network operations + - Offline mode with cached data + - Clear error messages for user troubleshooting ## Extensibility Points The Package Manager architecture is designed for extensibility: 1. **New Component Types**: - - The system can be extended to support new component types - - Type-specific rendering can be added to the UI - - Backend processing adapts to new types + + - The system can be extended to support new component types + - Type-specific rendering can be added to the UI + - Backend processing adapts to new types 2. **Additional Filters**: - - New filter types can be added to the system - - Filter logic can be extended in the PackageManagerManager - - UI can be updated to display new filter controls + + - New filter types can be added to the system + - Filter logic can be extended in the PackageManagerManager + - UI can be updated to display new filter controls 3. **Custom Sources**: - - The source system supports various repository types - - Custom source providers can be implemented - - Authentication mechanisms can be extended + - The source system supports various repository types + - Custom source providers can be implemented + - Authentication mechanisms can be extended --- -**Previous**: [Adding Custom Package Sources](../user-guide/06-adding-custom-sources.md) | **Next**: [Core Components](./02-core-components.md) \ No newline at end of file +**Previous**: [Adding Custom Package Sources](../user-guide/06-adding-custom-sources.md) | **Next**: [Core Components](./02-core-components.md) diff --git a/cline_docs/package-manager/implementation/02-core-components.md b/cline_docs/package-manager/implementation/02-core-components.md index c731e538bc..e2123e6bce 100644 --- a/cline_docs/package-manager/implementation/02-core-components.md +++ b/cline_docs/package-manager/implementation/02-core-components.md @@ -18,42 +18,42 @@ The MetadataScanner is responsible for reading and parsing package metadata from ```typescript class MetadataScanner { - /** - * Scans a directory for package metadata - * @param directoryPath Path to the directory to scan - * @param baseUrl Base URL for the repository (for remote sources) - * @returns Array of package items - */ - public async scanDirectory(directoryPath: string, baseUrl?: string): Promise { - // Implementation details - } + /** + * Scans a directory for package metadata + * @param directoryPath Path to the directory to scan + * @param baseUrl Base URL for the repository (for remote sources) + * @returns Array of package items + */ + public async scanDirectory(directoryPath: string, baseUrl?: string): Promise { + // Implementation details + } - /** - * Scans a Git repository for package metadata - * @param repoUrl URL of the Git repository - * @returns Array of package items - */ - public async scanRepository(repoUrl: string): Promise { - // Implementation details - } + /** + * Scans a Git repository for package metadata + * @param repoUrl URL of the Git repository + * @returns Array of package items + */ + public async scanRepository(repoUrl: string): Promise { + // Implementation details + } - /** - * Parses a YAML metadata file - * @param filePath Path to the metadata file - * @returns Parsed metadata object - */ - private async parseMetadataFile(filePath: string): Promise { - // Implementation details - } + /** + * Parses a YAML metadata file + * @param filePath Path to the metadata file + * @returns Parsed metadata object + */ + private async parseMetadataFile(filePath: string): Promise { + // Implementation details + } - /** - * Builds a component hierarchy from flat items - * @param items Array of items to organize - * @returns Hierarchical structure of items - */ - private buildComponentHierarchy(items: any[]): PackageManagerItem[] { - // Implementation details - } + /** + * Builds a component hierarchy from flat items + * @param items Array of items to organize + * @returns Hierarchical structure of items + */ + private buildComponentHierarchy(items: any[]): PackageManagerItem[] { + // Implementation details + } } ``` @@ -67,9 +67,9 @@ The directory scanning algorithm recursively traverses directories looking for m 2. Look for `metadata.*.yml` files in the current directory 3. Parse found metadata files 4. For each subdirectory: - - Determine the component type based on directory name - - Recursively scan the subdirectory - - Associate child components with parent components + - Determine the component type based on directory name + - Recursively scan the subdirectory + - Associate child components with parent components 5. Build the component hierarchy #### Metadata Parsing @@ -109,67 +109,67 @@ The PackageManagerManager is the central component that manages package items, a ```typescript class PackageManagerManager { - private currentItems: PackageManagerItem[] = []; - private sources: PackageManagerSource[] = []; + private currentItems: PackageManagerItem[] = [] + private sources: PackageManagerSource[] = [] - /** - * Constructor - * @param context VS Code extension context - */ - constructor(private context: vscode.ExtensionContext) { - // Initialize from stored state - } + /** + * Constructor + * @param context VS Code extension context + */ + constructor(private context: vscode.ExtensionContext) { + // Initialize from stored state + } - /** - * Get all items - * @returns Array of all package items - */ - public getItems(): PackageManagerItem[] { - return this.currentItems; - } + /** + * Get all items + * @returns Array of all package items + */ + public getItems(): PackageManagerItem[] { + return this.currentItems + } - /** - * Filter items based on criteria - * @param filters Filter criteria - * @returns Filtered array of items - */ - public filterItems(filters: { type?: string; search?: string; tags?: string[] }): PackageManagerItem[] { - // Implementation details - } + /** + * Filter items based on criteria + * @param filters Filter criteria + * @returns Filtered array of items + */ + public filterItems(filters: { type?: string; search?: string; tags?: string[] }): PackageManagerItem[] { + // Implementation details + } - /** - * Add a new package source - * @param url Source repository URL - * @param name Optional source name - * @returns Success status - */ - public async addSource(url: string, name?: string): Promise { - // Implementation details - } + /** + * Add a new package source + * @param url Source repository URL + * @param name Optional source name + * @returns Success status + */ + public async addSource(url: string, name?: string): Promise { + // Implementation details + } - /** - * Remove a package source - * @param url Source repository URL - * @returns Success status - */ - public removeSource(url: string): boolean { - // Implementation details - } + /** + * Remove a package source + * @param url Source repository URL + * @returns Success status + */ + public removeSource(url: string): boolean { + // Implementation details + } - /** - * Refresh all sources - * @returns Updated items - */ - public async refreshSources(): Promise { - // Implementation details - } + /** + * Refresh all sources + * @returns Updated items + */ + public async refreshSources(): Promise { + // Implementation details + } - /** - * Save state to persistent storage - */ - private saveState(): void { - // Implementation details - } + /** + * Save state to persistent storage + */ + private saveState(): void { + // Implementation details + } } ``` @@ -181,14 +181,14 @@ The filtering algorithm applies multiple criteria to the package items: 1. Start with the complete set of items 2. If a type filter is specified: - - Keep only items matching the specified type + - Keep only items matching the specified type 3. If a search term is specified: - - Check item name, description, and author for matches - - Check subcomponents for matches - - Keep items that match or have matching subcomponents - - Add match information to the items + - Check item name, description, and author for matches + - Check subcomponents for matches + - Keep items that match or have matching subcomponents + - Add match information to the items 4. If tag filters are specified: - - Keep only items that have at least one of the specified tags + - Keep only items that have at least one of the specified tags 5. Return the filtered items with match information #### Source Management @@ -196,25 +196,27 @@ The filtering algorithm applies multiple criteria to the package items: The source management process handles adding, removing, and refreshing sources: 1. For adding a source: - - Validate the repository URL - - Check if the source already exists - - Add the source to the list - - Scan the repository for items - - Add the items to the current set - - Save the updated source list + + - Validate the repository URL + - Check if the source already exists + - Add the source to the list + - Scan the repository for items + - Add the items to the current set + - Save the updated source list 2. For removing a source: - - Find the source in the list - - Remove items from that source - - Remove the source from the list - - Save the updated source list + + - Find the source in the list + - Remove items from that source + - Remove the source from the list + - Save the updated source list 3. For refreshing sources: - - Clear the current items - - For each enabled source: - - Scan the repository for items - - Add the items to the current set - - Return the updated items + - Clear the current items + - For each enabled source: + - Scan the repository for items + - Add the items to the current set + - Return the updated items ### State Persistence @@ -247,49 +249,46 @@ The packageManagerMessageHandler is responsible for routing messages between the * @param packageManager The package manager instance * @returns Response object */ -export async function handlePackageManagerMessages( - message: any, - packageManager: PackageManagerManager -): Promise { - switch (message.type) { - case "getItems": - return { - type: "items", - data: packageManager.getItems() - }; +export async function handlePackageManagerMessages(message: any, packageManager: PackageManagerManager): Promise { + switch (message.type) { + case "getItems": + return { + type: "items", + data: packageManager.getItems(), + } - case "search": - return { - type: "searchResults", - data: packageManager.filterItems({ - search: message.search, - type: message.typeFilter, - tags: message.tagFilters - }) - }; + case "search": + return { + type: "searchResults", + data: packageManager.filterItems({ + search: message.search, + type: message.typeFilter, + tags: message.tagFilters, + }), + } - case "addSource": - try { - const success = await packageManager.addSource(message.url, message.name); - return { - type: "sourceAdded", - data: { success } - }; - } catch (error) { - return { - type: "error", - error: error.message - }; - } + case "addSource": + try { + const success = await packageManager.addSource(message.url, message.name) + return { + type: "sourceAdded", + data: { success }, + } + } catch (error) { + return { + type: "error", + error: error.message, + } + } - // Additional message handlers... + // Additional message handlers... - default: - return { - type: "error", - error: `Unknown message type: ${message.type}` - }; - } + default: + return { + type: "error", + error: `Unknown message type: ${message.type}`, + } + } } ``` @@ -300,75 +299,86 @@ The message handler processes several types of messages: #### Input Messages 1. **getItems**: Request all package items - ```typescript - { type: "getItems" } - ``` + + ```typescript + { + type: "getItems" + } + ``` 2. **search**: Apply search and filter criteria - ```typescript - { - type: "search", - search: "search term", - typeFilter: "mode", - tagFilters: ["tag1", "tag2"] - } - ``` + + ```typescript + { + type: "search", + search: "search term", + typeFilter: "mode", + tagFilters: ["tag1", "tag2"] + } + ``` 3. **addSource**: Add a new package source - ```typescript - { - type: "addSource", - url: "https://github.com/username/repo.git", - name: "Custom Source" - } - ``` + + ```typescript + { + type: "addSource", + url: "https://github.com/username/repo.git", + name: "Custom Source" + } + ``` 4. **removeSource**: Remove a package source - ```typescript - { - type: "removeSource", - url: "https://github.com/username/repo.git" - } - ``` + + ```typescript + { + type: "removeSource", + url: "https://github.com/username/repo.git" + } + ``` 5. **refreshSources**: Refresh all sources - ```typescript - { type: "refreshSources" } - ``` + ```typescript + { + type: "refreshSources" + } + ``` #### Output Messages 1. **items**: Response with all items - ```typescript - { - type: "items", - data: [/* package items */] - } - ``` + + ```typescript + { + type: "items", + data: [/* package items */] + } + ``` 2. **searchResults**: Response with filtered items - ```typescript - { - type: "searchResults", - data: [/* filtered items */] - } - ``` + + ```typescript + { + type: "searchResults", + data: [/* filtered items */] + } + ``` 3. **sourceAdded**: Response after adding a source - ```typescript - { - type: "sourceAdded", - data: { success: true } - } - ``` + + ```typescript + { + type: "sourceAdded", + data: { success: true } + } + ``` 4. **error**: Error response - ```typescript - { - type: "error", - error: "Error message" - } - ``` + ```typescript + { + type: "error", + error: "Error message" + } + ``` ### Asynchronous Processing @@ -389,54 +399,45 @@ The main container component that manages the overall UI: ```tsx const PackageManagerView: React.FC = () => { - const [items, setItems] = useState([]); - const [filters, setFilters] = useState({ type: "", search: "", tags: [] }); - const [activeTab, setActiveTab] = useState<"browse" | "sources">("browse"); + const [items, setItems] = useState([]) + const [filters, setFilters] = useState({ type: "", search: "", tags: [] }) + const [activeTab, setActiveTab] = useState<"browse" | "sources">("browse") - // Implementation details... + // Implementation details... - return ( -
-
- - -
+ return ( +
+
+ + +
- {activeTab === "browse" ? ( -
- -
- {items.map(item => ( - - ))} -
-
- ) : ( - - )} -
- ); -}; + {activeTab === "browse" ? ( +
+ +
+ {items.map((item) => ( + + ))} +
+
+ ) : ( + + )} +
+ ) +} ``` ### Component Interactions @@ -444,37 +445,41 @@ const PackageManagerView: React.FC = () => { The UI components interact through props and state: 1. **Parent-Child Communication**: - - Parent components pass data and callbacks to children - - Children invoke callbacks to notify parents of events + + - Parent components pass data and callbacks to children + - Children invoke callbacks to notify parents of events 2. **State Management**: - - Component state for UI-specific state - - Shared state for filters and active tab - - Backend state accessed through messages + + - Component state for UI-specific state + - Shared state for filters and active tab + - Backend state accessed through messages 3. **Event Handling**: - - UI events trigger state updates - - State updates cause re-renders - - Messages are sent to the backend when needed + - UI events trigger state updates + - State updates cause re-renders + - Messages are sent to the backend when needed ### Accessibility Features The UI components include several accessibility features: 1. **Keyboard Navigation**: - - Tab order follows logical flow - - Focus indicators are visible - - Keyboard shortcuts for common actions + + - Tab order follows logical flow + - Focus indicators are visible + - Keyboard shortcuts for common actions 2. **Screen Reader Support**: - - ARIA attributes for dynamic content - - Semantic HTML structure - - Descriptive labels and announcements + + - ARIA attributes for dynamic content + - Semantic HTML structure + - Descriptive labels and announcements 3. **Visual Accessibility**: - - High contrast mode support - - Resizable text - - Color schemes that work with color blindness + - High contrast mode support + - Resizable text + - Color schemes that work with color blindness ## Component Integration @@ -509,25 +514,28 @@ The core components work together to provide a complete package management exper The core components include several performance optimizations: 1. **Lazy Loading**: - - Items are loaded on demand - - Heavy operations are deferred - - Components render incrementally + + - Items are loaded on demand + - Heavy operations are deferred + - Components render incrementally 2. **Caching**: - - Parsed metadata is cached - - Filter results can be cached - - Repository data is cached when possible + + - Parsed metadata is cached + - Filter results can be cached + - Repository data is cached when possible 3. **Efficient Filtering**: - - Filtering happens on the backend - - Only necessary data is transferred - - Algorithms optimize for common cases + + - Filtering happens on the backend + - Only necessary data is transferred + - Algorithms optimize for common cases 4. **UI Optimizations**: - - Virtual scrolling for large lists - - Debounced search input - - Optimized rendering of complex components + - Virtual scrolling for large lists + - Debounced search input + - Optimized rendering of complex components --- -**Previous**: [Package Manager Architecture](./01-architecture.md) | **Next**: [Data Structures](./03-data-structures.md) \ No newline at end of file +**Previous**: [Package Manager Architecture](./01-architecture.md) | **Next**: [Data Structures](./03-data-structures.md) diff --git a/cline_docs/package-manager/implementation/03-data-structures.md b/cline_docs/package-manager/implementation/03-data-structures.md index da105ce603..b231c8a713 100644 --- a/cline_docs/package-manager/implementation/03-data-structures.md +++ b/cline_docs/package-manager/implementation/03-data-structures.md @@ -12,7 +12,7 @@ The Package Manager uses a type system to categorize different kinds of componen /** * Supported component types */ -export type ComponentType = "mode" | "prompt" | "package" | "mcp server"; +export type ComponentType = "mode" | "prompt" | "package" | "mcp server" ``` These types represent the different kinds of components that can be managed by the Package Manager: @@ -35,10 +35,10 @@ The Package Manager uses a set of interfaces to define the structure of metadata * Base metadata interface */ export interface BaseMetadata { - name: string; - description: string; - version: string; - tags?: string[]; + name: string + description: string + version: string + tags?: string[] } ``` @@ -67,7 +67,7 @@ This interface represents the metadata for a package source repository. It curre * Component metadata with type */ export interface ComponentMetadata extends BaseMetadata { - type: ComponentType; + type: ComponentType } ``` @@ -80,12 +80,12 @@ This interface extends BaseMetadata to include a type field, which specifies the * Package metadata with optional subcomponents */ export interface PackageMetadata extends ComponentMetadata { - type: "package"; - items?: { - type: ComponentType; - path: string; - metadata?: ComponentMetadata; - }[]; + type: "package" + items?: { + type: ComponentType + path: string + metadata?: ComponentMetadata + }[] } ``` @@ -93,9 +93,9 @@ This interface represents packages that can contain subcomponents: - **type**: Always "package" for this interface - **items**: Optional array of subcomponents, each with: - - **type**: The subcomponent type - - **path**: The file system path to the subcomponent - - **metadata**: Optional metadata for the subcomponent + - **type**: The subcomponent type + - **path**: The file system path to the subcomponent + - **metadata**: Optional metadata for the subcomponent ### SubcomponentMetadata @@ -104,10 +104,10 @@ This interface represents packages that can contain subcomponents: * Subcomponent metadata with parent reference */ export interface SubcomponentMetadata extends ComponentMetadata { - parentPackage: { - name: string; - path: string; - }; + parentPackage: { + name: string + path: string + } } ``` @@ -115,8 +115,8 @@ This interface represents components that are part of a parent package: - All fields from ComponentMetadata - **parentPackage**: Reference to the parent package - - **name**: The name of the parent package - - **path**: The file system path to the parent package + - **name**: The name of the parent package + - **path**: The file system path to the parent package ## Item Structures @@ -129,13 +129,13 @@ The Package Manager uses several interfaces to represent items in the UI: * Information about why an item matched search/filter criteria */ export interface MatchInfo { - matched: boolean; - matchReason?: { - nameMatch?: boolean; - descriptionMatch?: boolean; - tagMatch?: boolean; - hasMatchingSubcomponents?: boolean; - }; + matched: boolean + matchReason?: { + nameMatch?: boolean + descriptionMatch?: boolean + tagMatch?: boolean + hasMatchingSubcomponents?: boolean + } } ``` @@ -143,10 +143,10 @@ This interface provides information about why an item matched search or filter c - **matched**: Boolean indicating if the item matched - **matchReason**: Optional object with specific match reasons - - **nameMatch**: True if the name matched - - **descriptionMatch**: True if the description matched - - **tagMatch**: True if a tag matched - - **hasMatchingSubcomponents**: True if a subcomponent matched + - **nameMatch**: True if the name matched + - **descriptionMatch**: True if the description matched + - **tagMatch**: True if a tag matched + - **hasMatchingSubcomponents**: True if a subcomponent matched ### PackageManagerItem @@ -155,25 +155,25 @@ This interface provides information about why an item matched search or filter c * Represents an individual package manager item */ export interface PackageManagerItem { - name: string; - description: string; - type: ComponentType; - url: string; - repoUrl: string; - sourceName?: string; - author?: string; - tags?: string[]; - version?: string; - lastUpdated?: string; - sourceUrl?: string; - items?: { - type: ComponentType; - path: string; - metadata?: ComponentMetadata; - lastUpdated?: string; - matchInfo?: MatchInfo; - }[]; - matchInfo?: MatchInfo; + name: string + description: string + type: ComponentType + url: string + repoUrl: string + sourceName?: string + author?: string + tags?: string[] + version?: string + lastUpdated?: string + sourceUrl?: string + items?: { + type: ComponentType + path: string + metadata?: ComponentMetadata + lastUpdated?: string + matchInfo?: MatchInfo + }[] + matchInfo?: MatchInfo } ``` @@ -200,9 +200,9 @@ This interface represents a complete package manager item as displayed in the UI * Represents a Git repository source for package manager items */ export interface PackageManagerSource { - url: string; - name?: string; - enabled: boolean; + url: string + name?: string + enabled: boolean } ``` @@ -219,10 +219,10 @@ This interface represents a package source repository: * Represents a repository with its metadata and items */ export interface PackageManagerRepository { - metadata: RepositoryMetadata; - items: PackageManagerItem[]; - url: string; - error?: string; + metadata: RepositoryMetadata + items: PackageManagerItem[] + url: string + error?: string } ``` @@ -240,8 +240,8 @@ This interface represents a complete repository with its metadata and items: * Utility type for metadata files with locale */ export type LocalizedMetadata = { - [locale: string]: T; -}; + [locale: string]: T +} ``` This utility type represents metadata that can be localized to different languages: @@ -257,11 +257,11 @@ The Package Manager UI components use several prop interfaces: ```typescript interface PackageManagerItemCardProps { - item: PackageManagerItem; - filters: { type: string; search: string; tags: string[] }; - setFilters: React.Dispatch>; - activeTab: "browse" | "sources"; - setActiveTab: React.Dispatch>; + item: PackageManagerItem + filters: { type: string; search: string; tags: string[] } + setFilters: React.Dispatch> + activeTab: "browse" | "sources" + setActiveTab: React.Dispatch> } ``` @@ -277,11 +277,11 @@ This interface defines the props for the PackageManagerItemCard component: ```typescript interface ExpandableSectionProps { - title: string; - children: React.ReactNode; - className?: string; - defaultExpanded?: boolean; - badge?: string; + title: string + children: React.ReactNode + className?: string + defaultExpanded?: boolean + badge?: string } ``` @@ -297,15 +297,15 @@ This interface defines the props for the ExpandableSection component: ```typescript interface TypeGroupProps { - type: string; - items: Array<{ - name: string; - description?: string; - metadata?: any; - path?: string; - }>; - className?: string; - searchTerm?: string; + type: string + items: Array<{ + name: string + description?: string + metadata?: any + path?: string + }> + className?: string + searchTerm?: string } ``` @@ -324,15 +324,15 @@ The Package Manager uses a specialized structure for grouping items by type: ```typescript export interface GroupedItems { - [type: string]: { - type: string; - items: Array<{ - name: string; - description?: string; - metadata?: any; - path?: string; - }>; - }; + [type: string]: { + type: string + items: Array<{ + name: string + description?: string + metadata?: any + path?: string + }> + } } ``` @@ -341,10 +341,10 @@ This interface represents items grouped by their type: - **[type: string]**: Keys are component types - **type**: The component type (redundant with the key) - **items**: Array of items of this type - - **name**: The item name - - **description**: Optional item description - - **metadata**: Optional additional metadata - - **path**: Optional file system path + - **name**: The item name + - **description**: Optional item description + - **metadata**: Optional additional metadata + - **path**: Optional file system path ## Filter and Sort Structures @@ -354,9 +354,9 @@ The Package Manager uses several structures for filtering and sorting: ```typescript interface Filters { - type: string; - search: string; - tags: string[]; + type: string + search: string + tags: string[] } ``` @@ -370,8 +370,8 @@ This interface represents the filter criteria: ```typescript interface SortConfig { - by: string; - order: "asc" | "desc"; + by: string + order: "asc" | "desc" } ``` @@ -521,19 +521,19 @@ The Package Manager includes validation at several levels: ```typescript function validateMetadata(metadata: any): boolean { - // Required fields - if (!metadata.name || !metadata.description || !metadata.version) { - return false; - } + // Required fields + if (!metadata.name || !metadata.description || !metadata.version) { + return false + } - // Type validation for components - if (metadata.type && !["mode", "prompt", "package", "mcp server"].includes(metadata.type)) { - return false; - } + // Type validation for components + if (metadata.type && !["mode", "prompt", "package", "mcp server"].includes(metadata.type)) { + return false + } - // Additional validation... + // Additional validation... - return true; + return true } ``` @@ -541,12 +541,12 @@ function validateMetadata(metadata: any): boolean { ```typescript function isValidUrl(urlString: string): boolean { - try { - new URL(urlString); - return true; - } catch (e) { - return false; - } + try { + new URL(urlString) + return true + } catch (e) { + return false + } } ``` @@ -554,13 +554,11 @@ function isValidUrl(urlString: string): boolean { ```typescript function validateTags(tags: any[]): string[] { - if (!Array.isArray(tags)) { - return []; - } + if (!Array.isArray(tags)) { + return [] + } - return tags - .filter(tag => typeof tag === "string" && tag.trim().length > 0) - .map(tag => tag.trim()); + return tags.filter((tag) => typeof tag === "string" && tag.trim().length > 0).map((tag) => tag.trim()) } ``` @@ -667,4 +665,4 @@ The Package Manager's data structures are designed for evolution: --- -**Previous**: [Core Components](./02-core-components.md) | **Next**: [Search and Filter Implementation](./04-search-and-filter.md) \ No newline at end of file +**Previous**: [Core Components](./02-core-components.md) | **Next**: [Search and Filter Implementation](./04-search-and-filter.md) diff --git a/cline_docs/package-manager/implementation/04-search-and-filter.md b/cline_docs/package-manager/implementation/04-search-and-filter.md index e4c285620d..3b1fb905a0 100644 --- a/cline_docs/package-manager/implementation/04-search-and-filter.md +++ b/cline_docs/package-manager/implementation/04-search-and-filter.md @@ -18,15 +18,16 @@ The core of the search functionality is the `containsSearchTerm` function, which * @returns True if the text contains the search term */ export function containsSearchTerm(text: string | undefined, searchTerm: string): boolean { - if (!text || !searchTerm) { - return false; - } + if (!text || !searchTerm) { + return false + } - return text.toLowerCase().includes(searchTerm.toLowerCase()); + return text.toLowerCase().includes(searchTerm.toLowerCase()) } ``` This function: + - Handles undefined inputs gracefully - Performs case-insensitive matching - Uses JavaScript's native `includes` method for performance @@ -43,56 +44,63 @@ The main search function applies the search term to multiple fields: * @returns Match information */ function itemMatchesSearch(item: PackageManagerItem, searchTerm: string): MatchInfo { - if (!searchTerm) { - return { matched: true }; - } + if (!searchTerm) { + return { matched: true } + } - const term = searchTerm.toLowerCase(); + const term = searchTerm.toLowerCase() - // Check main item fields - const nameMatch = containsSearchTerm(item.name, term); - const descriptionMatch = containsSearchTerm(item.description, term); - const authorMatch = containsSearchTerm(item.author, term); + // Check main item fields + const nameMatch = containsSearchTerm(item.name, term) + const descriptionMatch = containsSearchTerm(item.description, term) + const authorMatch = containsSearchTerm(item.author, term) - // Check subcomponents - let hasMatchingSubcomponents = false; + // Check subcomponents + let hasMatchingSubcomponents = false - if (item.items?.length) { - hasMatchingSubcomponents = item.items.some(subItem => - containsSearchTerm(subItem.metadata?.name, term) || - containsSearchTerm(subItem.metadata?.description, term) - ); + if (item.items?.length) { + hasMatchingSubcomponents = item.items.some( + (subItem) => + containsSearchTerm(subItem.metadata?.name, term) || + containsSearchTerm(subItem.metadata?.description, term), + ) - // Add match info to subcomponents - item.items.forEach(subItem => { - const subNameMatch = containsSearchTerm(subItem.metadata?.name, term); - const subDescMatch = containsSearchTerm(subItem.metadata?.description, term); + // Add match info to subcomponents + item.items.forEach((subItem) => { + const subNameMatch = containsSearchTerm(subItem.metadata?.name, term) + const subDescMatch = containsSearchTerm(subItem.metadata?.description, term) - subItem.matchInfo = { - matched: subNameMatch || subDescMatch, - matchReason: subNameMatch || subDescMatch ? { - nameMatch: subNameMatch, - descriptionMatch: subDescMatch - } : undefined - }; - }); - } + subItem.matchInfo = { + matched: subNameMatch || subDescMatch, + matchReason: + subNameMatch || subDescMatch + ? { + nameMatch: subNameMatch, + descriptionMatch: subDescMatch, + } + : undefined, + } + }) + } - const matched = nameMatch || descriptionMatch || authorMatch || hasMatchingSubcomponents; + const matched = nameMatch || descriptionMatch || authorMatch || hasMatchingSubcomponents - return { - matched, - matchReason: matched ? { - nameMatch, - descriptionMatch, - authorMatch, - hasMatchingSubcomponents - } : undefined - }; + return { + matched, + matchReason: matched + ? { + nameMatch, + descriptionMatch, + authorMatch, + hasMatchingSubcomponents, + } + : undefined, + } } ``` This function: + - Checks the item's name, description, and author - Recursively checks subcomponents - Adds match information to both the item and its subcomponents @@ -103,22 +111,25 @@ This function: The search implementation includes several optimizations: 1. **Early Termination**: - - Returns as soon as any field matches - - Avoids unnecessary checks after a match is found + + - Returns as soon as any field matches + - Avoids unnecessary checks after a match is found 2. **Efficient String Operations**: - - Uses native string methods for performance - - Converts to lowercase once per string - - Avoids regular expressions for simple matching + + - Uses native string methods for performance + - Converts to lowercase once per string + - Avoids regular expressions for simple matching 3. **Match Caching**: - - Stores match information on items - - Avoids recalculating matches for the same search term - - Clears cache when the search term changes + + - Stores match information on items + - Avoids recalculating matches for the same search term + - Clears cache when the search term changes 4. **Lazy Evaluation**: - - Only checks subcomponents if main fields don't match - - Processes subcomponents only when necessary + - Only checks subcomponents if main fields don't match + - Processes subcomponents only when necessary ## Filter Logic @@ -136,11 +147,11 @@ Type filtering restricts results to components of a specific type: * @returns Filtered items */ function filterByType(items: PackageManagerItem[], type: string): PackageManagerItem[] { - if (!type) { - return items; - } + if (!type) { + return items + } - return items.filter(item => item.type === type); + return items.filter((item) => item.type === type) } ``` @@ -156,18 +167,18 @@ Tag filtering shows only items with specific tags: * @returns Filtered items */ function filterByTags(items: PackageManagerItem[], tags: string[]): PackageManagerItem[] { - if (!tags.length) { - return items; - } + if (!tags.length) { + return items + } - return items.filter(item => { - if (!item.tags?.length) { - return false; - } + return items.filter((item) => { + if (!item.tags?.length) { + return false + } - // Item must have at least one of the specified tags - return item.tags.some(tag => tags.includes(tag)); - }); + // Item must have at least one of the specified tags + return item.tags.some((tag) => tags.includes(tag)) + }) } ``` @@ -183,39 +194,40 @@ The main filter function combines all filter types: * @returns Filtered items */ export function filterItems( - items: PackageManagerItem[], - filters: { type?: string; search?: string; tags?: string[] } + items: PackageManagerItem[], + filters: { type?: string; search?: string; tags?: string[] }, ): PackageManagerItem[] { - if (!isFilterActive(filters)) { - return items; - } + if (!isFilterActive(filters)) { + return items + } - let result = items; + let result = items - // Apply type filter - if (filters.type) { - result = filterByType(result, filters.type); - } + // Apply type filter + if (filters.type) { + result = filterByType(result, filters.type) + } - // Apply search filter - if (filters.search) { - result = result.filter(item => { - const matchInfo = itemMatchesSearch(item, filters.search!); - item.matchInfo = matchInfo; - return matchInfo.matched; - }); - } + // Apply search filter + if (filters.search) { + result = result.filter((item) => { + const matchInfo = itemMatchesSearch(item, filters.search!) + item.matchInfo = matchInfo + return matchInfo.matched + }) + } - // Apply tag filter - if (filters.tags?.length) { - result = filterByTags(result, filters.tags); - } + // Apply tag filter + if (filters.tags?.length) { + result = filterByTags(result, filters.tags) + } - return result; + return result } ``` This function: + - Applies filters in a specific order (type, search, tags) - Short-circuits if no filters are active - Adds match information to items @@ -226,22 +238,25 @@ This function: The filter implementation includes several optimizations: 1. **Filter Order**: - - Applies the most restrictive filters first - - Reduces the number of items for subsequent filters - - Improves performance for large datasets + + - Applies the most restrictive filters first + - Reduces the number of items for subsequent filters + - Improves performance for large datasets 2. **Short-Circuit Evaluation**: - - Skips filtering entirely if no filters are active - - Returns early when possible + + - Skips filtering entirely if no filters are active + - Returns early when possible 3. **Immutable Operations**: - - Creates new arrays rather than modifying existing ones - - Ensures predictable behavior - - Supports undo/redo functionality + + - Creates new arrays rather than modifying existing ones + - Ensures predictable behavior + - Supports undo/redo functionality 4. **Selective Processing**: - - Only processes necessary fields for each filter - - Avoids redundant calculations + - Only processes necessary fields for each filter + - Avoids redundant calculations ## Selector Functions @@ -256,8 +271,8 @@ The Package Manager uses selector functions to extract and transform data for th * @returns True if any filters are active */ export const isFilterActive = (filters: Filters): boolean => { - return !!(filters.type || filters.search || filters.tags.length > 0); -}; + return !!(filters.type || filters.search || filters.tags.length > 0) +} ``` ### Display Items Selector @@ -271,13 +286,13 @@ export const isFilterActive = (filters: Filters): boolean => { * @returns Filtered and sorted items */ export const getDisplayedItems = ( - items: PackageManagerItem[], - filters: Filters, - sortConfig: SortConfig, + items: PackageManagerItem[], + filters: Filters, + sortConfig: SortConfig, ): PackageManagerItem[] => { - const filteredItems = filterItems(items, filters); - return sortItems(filteredItems, sortConfig); -}; + const filteredItems = filterItems(items, filters) + return sortItems(filteredItems, sortConfig) +} ``` ### Sort Function @@ -290,26 +305,26 @@ export const getDisplayedItems = ( * @returns Sorted items */ export const sortItems = (items: PackageManagerItem[], config: SortConfig): PackageManagerItem[] => { - return [...items].sort((a, b) => { - let comparison = 0; + return [...items].sort((a, b) => { + let comparison = 0 - switch (config.by) { - case "name": - comparison = a.name.localeCompare(b.name); - break; - case "author": - comparison = (a.author || "").localeCompare(b.author || ""); - break; - case "lastUpdated": - comparison = (a.lastUpdated || "").localeCompare(b.lastUpdated || ""); - break; - default: - comparison = a.name.localeCompare(b.name); - } + switch (config.by) { + case "name": + comparison = a.name.localeCompare(b.name) + break + case "author": + comparison = (a.author || "").localeCompare(b.author || "") + break + case "lastUpdated": + comparison = (a.lastUpdated || "").localeCompare(b.lastUpdated || "") + break + default: + comparison = a.name.localeCompare(b.name) + } - return config.order === "asc" ? comparison : -comparison; - }); -}; + return config.order === "asc" ? comparison : -comparison + }) +} ``` ## Grouping Implementation @@ -325,31 +340,31 @@ The Package Manager includes functionality to group items by type: * @returns Object with items grouped by type */ export function groupItemsByType(items: PackageManagerItem["items"] = []): GroupedItems { - if (!items?.length) { - return {}; - } + if (!items?.length) { + return {} + } - return items.reduce((groups: GroupedItems, item) => { - if (!item.type) { - return groups; - } + return items.reduce((groups: GroupedItems, item) => { + if (!item.type) { + return groups + } - if (!groups[item.type]) { - groups[item.type] = { - type: item.type, - items: [], - }; - } + if (!groups[item.type]) { + groups[item.type] = { + type: item.type, + items: [], + } + } - groups[item.type].items.push({ - name: item.metadata?.name || "Unnamed item", - description: item.metadata?.description, - metadata: item.metadata, - path: item.path, - }); + groups[item.type].items.push({ + name: item.metadata?.name || "Unnamed item", + description: item.metadata?.description, + metadata: item.metadata, + path: item.path, + }) - return groups; - }, {}); + return groups + }, {}) } ``` @@ -362,7 +377,7 @@ export function groupItemsByType(items: PackageManagerItem["items"] = []): Group * @returns Total number of items */ export function getTotalItemCount(groups: GroupedItems): number { - return Object.values(groups).reduce((total, group) => total + group.items.length, 0); + return Object.values(groups).reduce((total, group) => total + group.items.length, 0) } /** @@ -371,7 +386,7 @@ export function getTotalItemCount(groups: GroupedItems): number { * @returns Array of type strings */ export function getUniqueTypes(groups: GroupedItems): string[] { - return Object.keys(groups).sort(); + return Object.keys(groups).sort() } ``` @@ -383,112 +398,107 @@ The search and filter functionality is integrated with the UI through several co ```tsx const SearchInput: React.FC<{ - value: string; - onChange: (value: string) => void; + value: string + onChange: (value: string) => void }> = ({ value, onChange }) => { - // Debounce search input to avoid excessive filtering - const debouncedOnChange = useDebounce(onChange, 300); + // Debounce search input to avoid excessive filtering + const debouncedOnChange = useDebounce(onChange, 300) - return ( -
- - debouncedOnChange(e.target.value)} - placeholder="Search packages..." - className="search-input" - aria-label="Search packages" - /> - {value && ( - - )} -
- ); -}; + return ( +
+ + debouncedOnChange(e.target.value)} + placeholder="Search packages..." + className="search-input" + aria-label="Search packages" + /> + {value && ( + + )} +
+ ) +} ``` ### Type Filter Component ```tsx const TypeFilter: React.FC<{ - value: string; - onChange: (value: string) => void; - types: string[]; + value: string + onChange: (value: string) => void + types: string[] }> = ({ value, onChange, types }) => { - return ( -
-

Filter by Type

-
- + return ( +
+

Filter by Type

+
+ - {types.map((type) => ( - - ))} -
-
- ); -}; + {types.map((type) => ( + + ))} +
+
+ ) +} ``` ### Tag Filter Component ```tsx const TagFilter: React.FC<{ - selectedTags: string[]; - onChange: (tags: string[]) => void; - availableTags: string[]; + selectedTags: string[] + onChange: (tags: string[]) => void + availableTags: string[] }> = ({ selectedTags, onChange, availableTags }) => { - const toggleTag = (tag: string) => { - if (selectedTags.includes(tag)) { - onChange(selectedTags.filter(t => t !== tag)); - } else { - onChange([...selectedTags, tag]); - } - }; + const toggleTag = (tag: string) => { + if (selectedTags.includes(tag)) { + onChange(selectedTags.filter((t) => t !== tag)) + } else { + onChange([...selectedTags, tag]) + } + } - return ( -
-

Filter by Tags

-
- {availableTags.map((tag) => ( - - ))} -
-
- ); -}; + return ( +
+

Filter by Tags

+
+ {availableTags.map((tag) => ( + + ))} +
+
+ ) +} ``` ## Performance Considerations @@ -500,71 +510,77 @@ The search and filter implementation includes several performance optimizations: For large datasets, the Package Manager implements: 1. **Pagination**: - - Limits the number of items displayed at once - - Implements virtual scrolling for smooth performance - - Loads additional items as needed + + - Limits the number of items displayed at once + - Implements virtual scrolling for smooth performance + - Loads additional items as needed 2. **Progressive Loading**: - - Shows initial results quickly - - Loads additional details asynchronously - - Provides visual feedback during loading + + - Shows initial results quickly + - Loads additional details asynchronously + - Provides visual feedback during loading 3. **Background Processing**: - - Performs heavy operations in a web worker - - Keeps the UI responsive during filtering - - Updates results incrementally + - Performs heavy operations in a web worker + - Keeps the UI responsive during filtering + - Updates results incrementally ### Search Optimizations For efficient searching: 1. **Debounced Input**: - ```typescript - function useDebounce(value: T, delay: number): T { - const [debouncedValue, setDebouncedValue] = useState(value); - useEffect(() => { - const timer = setTimeout(() => { - setDebouncedValue(value); - }, delay); + ```typescript + function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value) - return () => { - clearTimeout(timer); - }; - }, [value, delay]); + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedValue(value) + }, delay) - return debouncedValue; - } - ``` + return () => { + clearTimeout(timer) + } + }, [value, delay]) + + return debouncedValue + } + ``` 2. **Incremental Matching**: - - Matches characters in sequence - - Prioritizes prefix matches - - Supports fuzzy matching for better results + + - Matches characters in sequence + - Prioritizes prefix matches + - Supports fuzzy matching for better results 3. **Result Highlighting**: - - Highlights matching text portions - - Provides visual feedback on match quality - - Improves user understanding of results + - Highlights matching text portions + - Provides visual feedback on match quality + - Improves user understanding of results ### Filter Combinations For efficient filter combinations: 1. **Filter Order Optimization**: - - Applies most restrictive filters first - - Reduces dataset size early in the pipeline - - Improves performance for complex filter combinations + + - Applies most restrictive filters first + - Reduces dataset size early in the pipeline + - Improves performance for complex filter combinations 2. **Filter Caching**: - - Caches results for recent filter combinations - - Avoids recomputing the same filters - - Clears cache when underlying data changes + + - Caches results for recent filter combinations + - Avoids recomputing the same filters + - Clears cache when underlying data changes 3. **Progressive Filtering**: - - Shows initial results based on simple filters - - Applies complex filters incrementally - - Provides feedback during filtering process + - Shows initial results based on simple filters + - Applies complex filters incrementally + - Provides feedback during filtering process ## Edge Cases and Error Handling @@ -576,27 +592,27 @@ When no items match the filters: ```tsx const NoResults: React.FC<{ - filters: Filters; - clearFilters: () => void; + filters: Filters + clearFilters: () => void }> = ({ filters, clearFilters }) => { - return ( -
- -

No matching packages found

-

- No packages match your current filters. - {isFilterActive(filters) && ( - <> -
- - - )} -

-
- ); -}; + return ( +
+ +

No matching packages found

+

+ No packages match your current filters. + {isFilterActive(filters) && ( + <> +
+ + + )} +

+
+ ) +} ``` ### Invalid Search Terms @@ -625,130 +641,130 @@ The search and filter functionality includes comprehensive tests: ```typescript describe("Search Utils", () => { - describe("containsSearchTerm", () => { - it("should return true for exact matches", () => { - expect(containsSearchTerm("hello world", "hello")).toBe(true); - }); + describe("containsSearchTerm", () => { + it("should return true for exact matches", () => { + expect(containsSearchTerm("hello world", "hello")).toBe(true) + }) - it("should be case insensitive", () => { - expect(containsSearchTerm("Hello World", "hello")).toBe(true); - expect(containsSearchTerm("hello world", "WORLD")).toBe(true); - }); + it("should be case insensitive", () => { + expect(containsSearchTerm("Hello World", "hello")).toBe(true) + expect(containsSearchTerm("hello world", "WORLD")).toBe(true) + }) - it("should handle undefined inputs", () => { - expect(containsSearchTerm(undefined, "test")).toBe(false); - expect(containsSearchTerm("test", "")).toBe(false); - }); - }); + it("should handle undefined inputs", () => { + expect(containsSearchTerm(undefined, "test")).toBe(false) + expect(containsSearchTerm("test", "")).toBe(false) + }) + }) - describe("filterItems", () => { - const items = [ - { - name: "Test Package", - description: "A test package", - type: "package", - tags: ["test", "example"] - }, - { - name: "Another Package", - description: "Another test package", - type: "mode", - tags: ["example"] - } - ]; + describe("filterItems", () => { + const items = [ + { + name: "Test Package", + description: "A test package", + type: "package", + tags: ["test", "example"], + }, + { + name: "Another Package", + description: "Another test package", + type: "mode", + tags: ["example"], + }, + ] - it("should filter by type", () => { - const result = filterItems(items, { type: "package" }); - expect(result).toHaveLength(1); - expect(result[0].name).toBe("Test Package"); - }); + it("should filter by type", () => { + const result = filterItems(items, { type: "package" }) + expect(result).toHaveLength(1) + expect(result[0].name).toBe("Test Package") + }) - it("should filter by search term", () => { - const result = filterItems(items, { search: "another" }); - expect(result).toHaveLength(1); - expect(result[0].name).toBe("Another Package"); - }); + it("should filter by search term", () => { + const result = filterItems(items, { search: "another" }) + expect(result).toHaveLength(1) + expect(result[0].name).toBe("Another Package") + }) - it("should filter by tags", () => { - const result = filterItems(items, { tags: ["test"] }); - expect(result).toHaveLength(1); - expect(result[0].name).toBe("Test Package"); - }); + it("should filter by tags", () => { + const result = filterItems(items, { tags: ["test"] }) + expect(result).toHaveLength(1) + expect(result[0].name).toBe("Test Package") + }) - it("should combine filters", () => { - const result = filterItems(items, { - type: "package", - tags: ["example"] - }); - expect(result).toHaveLength(1); - expect(result[0].name).toBe("Test Package"); - }); - }); -}); + it("should combine filters", () => { + const result = filterItems(items, { + type: "package", + tags: ["example"], + }) + expect(result).toHaveLength(1) + expect(result[0].name).toBe("Test Package") + }) + }) +}) ``` ### Integration Tests ```typescript describe("Package Manager Search Integration", () => { - let manager: PackageManagerManager; - let metadataScanner: MetadataScanner; - let templateItems: PackageManagerItem[]; + let manager: PackageManagerManager + let metadataScanner: MetadataScanner + let templateItems: PackageManagerItem[] - beforeAll(async () => { - // Load real data from template - metadataScanner = new MetadataScanner(); - const templatePath = path.resolve(__dirname, "../../../../package-manager-template"); - templateItems = await metadataScanner.scanDirectory(templatePath, "https://example.com"); - }); + beforeAll(async () => { + // Load real data from template + metadataScanner = new MetadataScanner() + const templatePath = path.resolve(__dirname, "../../../../package-manager-template") + templateItems = await metadataScanner.scanDirectory(templatePath, "https://example.com") + }) - beforeEach(() => { - // Create a real context-like object - const context = { - extensionPath: path.resolve(__dirname, "../../../../"), - globalStorageUri: { fsPath: path.resolve(__dirname, "../../../../mock/settings/path") }, - } as vscode.ExtensionContext; + beforeEach(() => { + // Create a real context-like object + const context = { + extensionPath: path.resolve(__dirname, "../../../../"), + globalStorageUri: { fsPath: path.resolve(__dirname, "../../../../mock/settings/path") }, + } as vscode.ExtensionContext - // Create real instances - manager = new PackageManagerManager(context); + // Create real instances + manager = new PackageManagerManager(context) - // Set up manager with template data - manager["currentItems"] = [...templateItems]; - }); + // Set up manager with template data + manager["currentItems"] = [...templateItems] + }) - it("should find items by name", () => { - const message = { - type: "search", - search: "data platform", - typeFilter: "", - tagFilters: [] - }; + it("should find items by name", () => { + const message = { + type: "search", + search: "data platform", + typeFilter: "", + tagFilters: [], + } - const result = handlePackageManagerMessages(message, manager); - expect(result.data).toHaveLength(1); - expect(result.data[0].name).toContain("Data Platform"); - }); + const result = handlePackageManagerMessages(message, manager) + expect(result.data).toHaveLength(1) + expect(result.data[0].name).toContain("Data Platform") + }) - it("should find items with matching subcomponents", () => { - const message = { - type: "search", - search: "validator", - typeFilter: "", - tagFilters: [] - }; + it("should find items with matching subcomponents", () => { + const message = { + type: "search", + search: "validator", + typeFilter: "", + tagFilters: [], + } - const result = handlePackageManagerMessages(message, manager); - expect(result.data.length).toBeGreaterThan(0); + const result = handlePackageManagerMessages(message, manager) + expect(result.data.length).toBeGreaterThan(0) - // Check that subcomponents are marked as matches - const hasMatchingSubcomponent = result.data.some(item => - item.items?.some(subItem => subItem.matchInfo?.matched) - ); - expect(hasMatchingSubcomponent).toBe(true); - }); -}); + // Check that subcomponents are marked as matches + const hasMatchingSubcomponent = result.data.some((item) => + item.items?.some((subItem) => subItem.matchInfo?.matched), + ) + expect(hasMatchingSubcomponent).toBe(true) + }) +}) ``` --- -**Previous**: [Data Structures](./03-data-structures.md) | **Next**: [UI Component Design](./05-ui-components.md) \ No newline at end of file +**Previous**: [Data Structures](./03-data-structures.md) | **Next**: [UI Component Design](./05-ui-components.md) diff --git a/cline_docs/package-manager/implementation/05-ui-components.md b/cline_docs/package-manager/implementation/05-ui-components.md index fab6499e71..24f949cc23 100644 --- a/cline_docs/package-manager/implementation/05-ui-components.md +++ b/cline_docs/package-manager/implementation/05-ui-components.md @@ -10,214 +10,217 @@ The PackageManagerItemCard is the primary component for displaying package infor ```tsx export const PackageManagerItemCard: React.FC = ({ - item, - filters, - setFilters, - activeTab, - setActiveTab, + item, + filters, + setFilters, + activeTab, + setActiveTab, }) => { - // URL validation helper - const isValidUrl = (urlString: string): boolean => { - try { - new URL(urlString); - return true; - } catch (e) { - return false; - } - }; + // URL validation helper + const isValidUrl = (urlString: string): boolean => { + try { + new URL(urlString) + return true + } catch (e) { + return false + } + } - // Type label and color helpers - const getTypeLabel = (type: string) => { - switch (type) { - case "mode": - return "Mode"; - case "mcp server": - return "MCP Server"; - case "prompt": - return "Prompt"; - case "package": - return "Package"; - default: - return "Other"; - } - }; + // Type label and color helpers + const getTypeLabel = (type: string) => { + switch (type) { + case "mode": + return "Mode" + case "mcp server": + return "MCP Server" + case "prompt": + return "Prompt" + case "package": + return "Package" + default: + return "Other" + } + } - const getTypeColor = (type: string) => { - switch (type) { - case "mode": - return "bg-blue-600"; - case "mcp server": - return "bg-green-600"; - case "prompt": - return "bg-purple-600"; - case "package": - return "bg-orange-600"; - default: - return "bg-gray-600"; - } - }; + const getTypeColor = (type: string) => { + switch (type) { + case "mode": + return "bg-blue-600" + case "mcp server": + return "bg-green-600" + case "prompt": + return "bg-purple-600" + case "package": + return "bg-orange-600" + default: + return "bg-gray-600" + } + } - // URL opening handler - const handleOpenUrl = () => { - const urlToOpen = item.sourceUrl && isValidUrl(item.sourceUrl) ? item.sourceUrl : item.repoUrl; - vscode.postMessage({ - type: "openExternal", - url: urlToOpen, - }); - }; + // URL opening handler + const handleOpenUrl = () => { + const urlToOpen = item.sourceUrl && isValidUrl(item.sourceUrl) ? item.sourceUrl : item.repoUrl + vscode.postMessage({ + type: "openExternal", + url: urlToOpen, + }) + } - // Group items by type - const groupedItems = useMemo(() => { - if (!item.items?.length) { - return null; - } - return groupItemsByType(item.items); - }, [item.items]) as GroupedItems | null; + // Group items by type + const groupedItems = useMemo(() => { + if (!item.items?.length) { + return null + } + return groupItemsByType(item.items) + }, [item.items]) as GroupedItems | null - return ( -
- {/* Header section with name, author, and type badge */} -
-
-

{item.name}

- {item.author &&

{`by ${item.author}`}

} -
- - {getTypeLabel(item.type)} - -
+ return ( +
+ {/* Header section with name, author, and type badge */} +
+
+

{item.name}

+ {item.author &&

{`by ${item.author}`}

} +
+ + {getTypeLabel(item.type)} + +
- {/* Description */} -

{item.description}

+ {/* Description */} +

{item.description}

- {/* Tags section */} - {item.tags && item.tags.length > 0 && ( -
- {item.tags.map((tag) => ( - - ))} -
- )} + {/* Tags section */} + {item.tags && item.tags.length > 0 && ( +
+ {item.tags.map((tag) => ( + + ))} +
+ )} - {/* Footer section with metadata and action button */} -
-
- {item.version && ( - - - {item.version} - - )} - {item.lastUpdated && ( - - - {new Date(item.lastUpdated).toLocaleDateString(undefined, { - year: "numeric", - month: "short", - day: "numeric", - })} - - )} -
+ {/* Footer section with metadata and action button */} +
+
+ {item.version && ( + + + {item.version} + + )} + {item.lastUpdated && ( + + + {new Date(item.lastUpdated).toLocaleDateString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + })} + + )} +
- -
+ +
- {/* Details section with subcomponents */} - {groupedItems && ( - { - 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]) => ( - - ))} -
-
- )} -
- ); -}; + {/* Details section with subcomponents */} + {groupedItems && ( + { + 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]) => ( + + ))} +
+
+ )} +
+ ) +} ``` ### Design Considerations 1. **Visual Hierarchy**: - - Clear distinction between header, content, and footer - - Type badge stands out with color coding - - Important information is emphasized with typography + + - Clear distinction between header, content, and footer + - Type badge stands out with color coding + - Important information is emphasized with typography 2. **Interactive Elements**: - - Tags are clickable for filtering - - External link button for source access - - Expandable details section for subcomponents + + - Tags are clickable for filtering + - External link button for source access + - Expandable details section for subcomponents 3. **Information Density**: - - Balanced display of essential information - - Optional elements only shown when available - - Expandable section for additional details + + - Balanced display of essential information + - Optional elements only shown when available + - Expandable section for additional details 4. **VSCode Integration**: - - Uses VSCode theme variables for colors - - Matches VSCode UI patterns - - Integrates with VSCode messaging system + - Uses VSCode theme variables for colors + - Matches VSCode UI patterns + - Integrates with VSCode messaging system ## ExpandableSection @@ -227,76 +230,79 @@ The ExpandableSection component provides a collapsible container for content tha ```tsx export const ExpandableSection: React.FC = ({ - title, - children, - className, - defaultExpanded = false, - badge, + title, + children, + className, + defaultExpanded = false, + badge, }) => { - const [isExpanded, setIsExpanded] = useState(defaultExpanded); + const [isExpanded, setIsExpanded] = useState(defaultExpanded) - return ( -
- -
-
{children}
-
-
- ); -}; + return ( +
+ +
+
{children}
+
+
+ ) +} ``` ### Design Considerations 1. **Animation**: - - Smooth height transition for expand/collapse - - Opacity change for better visual feedback - - Chevron icon rotation for state indication + + - Smooth height transition for expand/collapse + - Opacity change for better visual feedback + - Chevron icon rotation for state indication 2. **Accessibility**: - - Proper ARIA attributes for screen readers - - Keyboard navigation support - - Clear visual indication of interactive state + + - Proper ARIA attributes for screen readers + - Keyboard navigation support + - Clear visual indication of interactive state 3. **Flexibility**: - - Accepts any content as children - - Optional badge for additional information - - Customizable through className prop + + - Accepts any content as children + - Optional badge for additional information + - Customizable through className prop 4. **State Management**: - - Internal state for expanded/collapsed - - Can be controlled through defaultExpanded prop - - Preserves state during component lifecycle + - Internal state for expanded/collapsed + - Can be controlled through defaultExpanded prop + - Preserves state during component lifecycle ## TypeGroup @@ -306,87 +312,90 @@ The TypeGroup component displays a collection of items of the same type, with sp ```tsx export const TypeGroup: React.FC = ({ type, items, className, searchTerm }) => { - const getTypeLabel = (type: string) => { - switch (type) { - case "mode": - return "Modes"; - case "mcp server": - return "MCP Servers"; - case "prompt": - return "Prompts"; - case "package": - return "Packages"; - default: - return `${type.charAt(0).toUpperCase()}${type.slice(1)}s`; - } - }; + const getTypeLabel = (type: string) => { + switch (type) { + case "mode": + return "Modes" + case "mcp server": + return "MCP Servers" + case "prompt": + return "Prompts" + case "package": + return "Packages" + default: + return `${type.charAt(0).toUpperCase()}${type.slice(1)}s` + } + } - if (!items?.length) { - return null; - } + if (!items?.length) { + 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); - }; + // 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) => { - const matches = itemMatchesSearch(item); - return ( -
  1. - - {item.name} - - {item.description && ( - - {item.description} - )} - {matches && ( - - match - - )} -
  2. - ); - })} -
-
- ); -}; + return ( +
+

{getTypeLabel(type)}

+
    + {items.map((item, index) => { + const matches = itemMatchesSearch(item) + return ( +
  1. + + {item.name} + + {item.description && ( + - {item.description} + )} + {matches && ( + + match + + )} +
  2. + ) + })} +
+
+ ) +} ``` ### Design Considerations 1. **List Presentation**: - - Ordered list with automatic numbering - - Clear type heading for context - - Consistent spacing for readability + + - Ordered list with automatic numbering + - Clear type heading for context + - Consistent spacing for readability 2. **Search Match Highlighting**: - - Visual distinction for matching items - - "match" badge for quick identification - - Color change for matched text + + - Visual distinction for matching items + - "match" badge for quick identification + - Color change for matched text 3. **Information Display**: - - Name and description clearly separated - - Tooltip shows path information on hover - - Truncation for very long descriptions + + - Name and description clearly separated + - Tooltip shows path information on hover + - Truncation for very long descriptions 4. **Empty State Handling**: - - Returns null when no items are present - - Avoids rendering empty containers - - Prevents unnecessary UI elements + - Returns null when no items are present + - Avoids rendering empty containers + - Prevents unnecessary UI elements ## Filter Components @@ -396,112 +405,107 @@ The Package Manager includes several components for filtering and searching. ```tsx const SearchInput: React.FC<{ - value: string; - onChange: (value: string) => void; + value: string + onChange: (value: string) => void }> = ({ value, onChange }) => { - // Debounce search input to avoid excessive filtering - const debouncedOnChange = useDebounce(onChange, 300); + // Debounce search input to avoid excessive filtering + const debouncedOnChange = useDebounce(onChange, 300) - return ( -
- - debouncedOnChange(e.target.value)} - placeholder="Search packages..." - className="search-input" - aria-label="Search packages" - /> - {value && ( - - )} -
- ); -}; + return ( +
+ + debouncedOnChange(e.target.value)} + placeholder="Search packages..." + className="search-input" + aria-label="Search packages" + /> + {value && ( + + )} +
+ ) +} ``` ### TypeFilterGroup ```tsx const TypeFilterGroup: React.FC<{ - selectedType: string; - onChange: (type: string) => void; - availableTypes: string[]; + selectedType: string + onChange: (type: string) => void + availableTypes: string[] }> = ({ selectedType, onChange, availableTypes }) => { - return ( -
-

Filter by Type

-
- + return ( +
+

Filter by Type

+
+ - {availableTypes.map((type) => ( - - ))} -
-
- ); -}; + {availableTypes.map((type) => ( + + ))} +
+
+ ) +} ``` ### TagFilterGroup ```tsx const TagFilterGroup: React.FC<{ - selectedTags: string[]; - onChange: (tags: string[]) => void; - availableTags: string[]; + selectedTags: string[] + onChange: (tags: string[]) => void + availableTags: string[] }> = ({ selectedTags, onChange, availableTags }) => { - const toggleTag = (tag: string) => { - if (selectedTags.includes(tag)) { - onChange(selectedTags.filter(t => t !== tag)); - } else { - onChange([...selectedTags, tag]); - } - }; + const toggleTag = (tag: string) => { + if (selectedTags.includes(tag)) { + onChange(selectedTags.filter((t) => t !== tag)) + } else { + onChange([...selectedTags, tag]) + } + } - return ( -
-

Filter by Tags

-
- {availableTags.map((tag) => ( - - ))} -
-
- ); -}; + return ( +
+

Filter by Tags

+
+ {availableTags.map((tag) => ( + + ))} +
+
+ ) +} ``` ## Styling Approach @@ -515,21 +519,21 @@ The components use VSCode theme variables to ensure they match the user's select ```css /* Example of VSCode theme variable usage */ .package-card { - background-color: var(--vscode-panel-background); - border-color: var(--vscode-panel-border); - color: var(--vscode-foreground); + background-color: var(--vscode-panel-background); + border-color: var(--vscode-panel-border); + color: var(--vscode-foreground); } .package-description { - color: var(--vscode-descriptionForeground); + color: var(--vscode-descriptionForeground); } .package-link { - color: var(--vscode-textLink-foreground); + color: var(--vscode-textLink-foreground); } .package-link:hover { - color: var(--vscode-textLink-activeForeground); + color: var(--vscode-textLink-activeForeground); } ``` @@ -540,10 +544,8 @@ Tailwind CSS is used for utility-based styling: ```tsx // Example of Tailwind CSS usage
-

{item.name}

- - {getTypeLabel(item.type)} - +

{item.name}

+ {getTypeLabel(item.type)}
``` @@ -553,11 +555,11 @@ The UI uses utility functions for class name composition: ```typescript // cn utility for conditional class names -import { clsx, type ClassValue } from "clsx"; -import { twMerge } from "tailwind-merge"; +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); + return twMerge(clsx(inputs)) } ``` @@ -570,9 +572,9 @@ The Package Manager UI is designed to work across different viewport sizes: ```tsx // Example of responsive layout
- {items.map(item => ( - - ))} + {items.map((item) => ( + + ))}
``` @@ -581,19 +583,21 @@ The Package Manager UI is designed to work across different viewport sizes: For smaller screens: 1. **Stacked Layout**: - - Cards stack vertically on small screens - - Filter panel collapses to a dropdown - - Full-width elements for better touch targets + + - Cards stack vertically on small screens + - Filter panel collapses to a dropdown + - Full-width elements for better touch targets 2. **Touch Optimization**: - - Larger touch targets for mobile users - - Swipe gestures for common actions - - Simplified interactions for touch devices + + - Larger touch targets for mobile users + - Swipe gestures for common actions + - Simplified interactions for touch devices 3. **Content Prioritization**: - - Critical information shown first - - Less important details hidden behind expandable sections - - Reduced information density on small screens + - Critical information shown first + - Less important details hidden behind expandable sections + - Reduced information density on small screens ## Accessibility Features @@ -604,19 +608,18 @@ The Package Manager UI includes several accessibility features: ```tsx // Example of keyboard navigation support ``` @@ -624,21 +627,13 @@ The Package Manager UI includes several accessibility features: ```tsx // Example of screen reader support -
- - +
+ +
``` @@ -646,19 +641,19 @@ The Package Manager UI includes several accessibility features: ```tsx // Example of focus management -const buttonRef = useRef(null); +const buttonRef = useRef(null) useEffect(() => { - if (isOpen && buttonRef.current) { - buttonRef.current.focus(); - } -}, [isOpen]); + if (isOpen && buttonRef.current) { + buttonRef.current.focus() + } +}, [isOpen]) return ( - -); + +) ``` ### Color Contrast @@ -678,12 +673,11 @@ The Package Manager UI uses subtle animations to enhance the user experience: ```tsx // Example of expand/collapse animation
- {children} + className={cn( + "overflow-hidden transition-[max-height,opacity] duration-200 ease-in-out", + isExpanded ? "max-h-[500px] opacity-100" : "max-h-0 opacity-0", + )}> + {children}
``` @@ -692,7 +686,7 @@ The Package Manager UI uses subtle animations to enhance the user experience: ```tsx // Example of hover effects ``` @@ -701,8 +695,8 @@ The Package Manager UI uses subtle animations to enhance the user experience: ```tsx // Example of loading state animation
-
- Loading packages... +
+ Loading packages...
``` @@ -715,22 +709,19 @@ The Package Manager UI includes graceful error handling: ```tsx // Example of error state display const ErrorDisplay: React.FC<{ error: string; retry: () => void }> = ({ error, retry }) => { - return ( -
-
- -

Error loading packages

-
-

{error}

- -
- ); -}; + return ( +
+
+ +

Error loading packages

+
+

{error}

+ +
+ ) +} ``` ### Empty States @@ -738,13 +729,13 @@ const ErrorDisplay: React.FC<{ error: string; retry: () => void }> = ({ error, r ```tsx // Example of empty state display const EmptyState: React.FC<{ message: string }> = ({ message }) => { - return ( -
-
-

{message}

-
- ); -}; + return ( +
+
+

{message}

+
+ ) +} ``` ### Loading States @@ -752,24 +743,24 @@ const EmptyState: React.FC<{ message: string }> = ({ message }) => { ```tsx // Example of loading state with skeleton const PackageCardSkeleton: React.FC = () => { - return ( -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ); -}; + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ) +} ``` ## Component Testing @@ -875,4 +866,4 @@ it("meets accessibility requirements", async () => { --- -**Previous**: [Search and Filter Implementation](./04-search-and-filter.md) | **Next**: [Testing Strategy](./06-testing-strategy.md) \ No newline at end of file +**Previous**: [Search and Filter Implementation](./04-search-and-filter.md) | **Next**: [Testing Strategy](./06-testing-strategy.md) diff --git a/cline_docs/package-manager/implementation/06-testing-strategy.md b/cline_docs/package-manager/implementation/06-testing-strategy.md index 251be86a9c..605c363581 100644 --- a/cline_docs/package-manager/implementation/06-testing-strategy.md +++ b/cline_docs/package-manager/implementation/06-testing-strategy.md @@ -24,150 +24,155 @@ Backend unit tests verify the functionality of core services and utilities: ```typescript describe("MetadataScanner", () => { - let scanner: MetadataScanner; + let scanner: MetadataScanner - beforeEach(() => { - scanner = new MetadataScanner(); - }); + beforeEach(() => { + scanner = new MetadataScanner() + }) - describe("parseMetadataFile", () => { - it("should parse valid YAML metadata", async () => { - // Mock file system - jest.spyOn(fs, "readFile").mockImplementation((path, options, callback) => { - callback(null, Buffer.from(` + describe("parseMetadataFile", () => { + it("should parse valid YAML metadata", async () => { + // Mock file system + jest.spyOn(fs, "readFile").mockImplementation((path, options, callback) => { + callback( + null, + Buffer.from(` name: "Test Package" description: "A test package" version: "1.0.0" type: "package" - `)); - }); + `), + ) + }) - const result = await scanner["parseMetadataFile"]("test/path/metadata.en.yml"); + const result = await scanner["parseMetadataFile"]("test/path/metadata.en.yml") - expect(result).toEqual({ - name: "Test Package", - description: "A test package", - version: "1.0.0", - type: "package" - }); - }); + expect(result).toEqual({ + name: "Test Package", + description: "A test package", + version: "1.0.0", + type: "package", + }) + }) - it("should handle invalid YAML", async () => { - // Mock file system with invalid YAML - jest.spyOn(fs, "readFile").mockImplementation((path, options, callback) => { - callback(null, Buffer.from(` + it("should handle invalid YAML", async () => { + // Mock file system with invalid YAML + jest.spyOn(fs, "readFile").mockImplementation((path, options, callback) => { + callback( + null, + Buffer.from(` name: "Invalid YAML description: Missing quote - `)); - }); + `), + ) + }) - await expect(scanner["parseMetadataFile"]("test/path/metadata.en.yml")) - .rejects.toThrow(); - }); - }); + await expect(scanner["parseMetadataFile"]("test/path/metadata.en.yml")).rejects.toThrow() + }) + }) - describe("scanDirectory", () => { - // Tests for directory scanning - }); -}); + describe("scanDirectory", () => { + // Tests for directory scanning + }) +}) ``` #### PackageManagerManager Tests ```typescript describe("PackageManagerManager", () => { - let manager: PackageManagerManager; - let mockContext: vscode.ExtensionContext; + let manager: PackageManagerManager + let mockContext: vscode.ExtensionContext - beforeEach(() => { - // Create mock context - mockContext = { - extensionPath: "/test/path", - globalStorageUri: { fsPath: "/test/storage" }, - globalState: { - get: jest.fn().mockImplementation((key, defaultValue) => defaultValue), - update: jest.fn().mockResolvedValue(undefined) - } - } as unknown as vscode.ExtensionContext; + beforeEach(() => { + // Create mock context + mockContext = { + extensionPath: "/test/path", + globalStorageUri: { fsPath: "/test/storage" }, + globalState: { + get: jest.fn().mockImplementation((key, defaultValue) => defaultValue), + update: jest.fn().mockResolvedValue(undefined), + }, + } as unknown as vscode.ExtensionContext - manager = new PackageManagerManager(mockContext); - }); + manager = new PackageManagerManager(mockContext) + }) - describe("filterItems", () => { - it("should filter by type", () => { - // Set up test data - manager["currentItems"] = [ - { name: "Item 1", type: "mode", description: "Test item 1" }, - { name: "Item 2", type: "package", description: "Test item 2" } - ] as PackageManagerItem[]; + describe("filterItems", () => { + it("should filter by type", () => { + // Set up test data + manager["currentItems"] = [ + { name: "Item 1", type: "mode", description: "Test item 1" }, + { name: "Item 2", type: "package", description: "Test item 2" }, + ] as PackageManagerItem[] - const result = manager.filterItems({ type: "mode" }); + const result = manager.filterItems({ type: "mode" }) - expect(result).toHaveLength(1); - expect(result[0].name).toBe("Item 1"); - }); + expect(result).toHaveLength(1) + expect(result[0].name).toBe("Item 1") + }) - it("should filter by search term", () => { - // Set up test data - manager["currentItems"] = [ - { name: "Alpha Item", type: "mode", description: "Test item" }, - { name: "Beta Item", type: "package", description: "Another test" } - ] as PackageManagerItem[]; + it("should filter by search term", () => { + // Set up test data + manager["currentItems"] = [ + { name: "Alpha Item", type: "mode", description: "Test item" }, + { name: "Beta Item", type: "package", description: "Another test" }, + ] as PackageManagerItem[] - const result = manager.filterItems({ search: "alpha" }); + const result = manager.filterItems({ search: "alpha" }) - expect(result).toHaveLength(1); - expect(result[0].name).toBe("Alpha Item"); - }); + expect(result).toHaveLength(1) + expect(result[0].name).toBe("Alpha Item") + }) - // More filter tests... - }); + // More filter tests... + }) - describe("addSource", () => { - // Tests for adding sources - }); -}); + describe("addSource", () => { + // Tests for adding sources + }) +}) ``` #### Search Utilities Tests ```typescript describe("searchUtils", () => { - describe("containsSearchTerm", () => { - it("should return true for exact matches", () => { - expect(containsSearchTerm("hello world", "hello")).toBe(true); - }); + describe("containsSearchTerm", () => { + it("should return true for exact matches", () => { + expect(containsSearchTerm("hello world", "hello")).toBe(true) + }) - it("should be case insensitive", () => { - expect(containsSearchTerm("Hello World", "hello")).toBe(true); - expect(containsSearchTerm("hello world", "WORLD")).toBe(true); - }); + it("should be case insensitive", () => { + expect(containsSearchTerm("Hello World", "hello")).toBe(true) + expect(containsSearchTerm("hello world", "WORLD")).toBe(true) + }) - it("should handle undefined inputs", () => { - expect(containsSearchTerm(undefined, "test")).toBe(false); - expect(containsSearchTerm("test", "")).toBe(false); - }); - }); + it("should handle undefined inputs", () => { + expect(containsSearchTerm(undefined, "test")).toBe(false) + expect(containsSearchTerm("test", "")).toBe(false) + }) + }) - describe("itemMatchesSearch", () => { - it("should match on name", () => { - const item = { - name: "Test Item", - description: "Description" - }; + describe("itemMatchesSearch", () => { + it("should match on name", () => { + const item = { + name: "Test Item", + description: "Description", + } - expect(itemMatchesSearch(item, "test")).toEqual({ - matched: true, - matchReason: { - nameMatch: true, - descriptionMatch: false - } - }); - }); + expect(itemMatchesSearch(item, "test")).toEqual({ + matched: true, + matchReason: { + nameMatch: true, + descriptionMatch: false, + }, + }) + }) - // More search matching tests... - }); -}); + // More search matching tests... + }) +}) ``` ### Frontend Unit Tests @@ -318,87 +323,87 @@ Integration tests verify that different components work together correctly. ```typescript describe("Package Manager Integration", () => { - let manager: PackageManagerManager; - let metadataScanner: MetadataScanner; - let templateItems: PackageManagerItem[]; + let manager: PackageManagerManager + let metadataScanner: MetadataScanner + let templateItems: PackageManagerItem[] - beforeAll(async () => { - // Load real data from template - metadataScanner = new MetadataScanner(); - const templatePath = path.resolve(__dirname, "../../../../package-manager-template"); - templateItems = await metadataScanner.scanDirectory(templatePath, "https://example.com"); - }); + beforeAll(async () => { + // Load real data from template + metadataScanner = new MetadataScanner() + const templatePath = path.resolve(__dirname, "../../../../package-manager-template") + templateItems = await metadataScanner.scanDirectory(templatePath, "https://example.com") + }) - beforeEach(() => { - // Create a real context-like object - const context = { - extensionPath: path.resolve(__dirname, "../../../../"), - globalStorageUri: { fsPath: path.resolve(__dirname, "../../../../mock/settings/path") }, - } as vscode.ExtensionContext; + beforeEach(() => { + // Create a real context-like object + const context = { + extensionPath: path.resolve(__dirname, "../../../../"), + globalStorageUri: { fsPath: path.resolve(__dirname, "../../../../mock/settings/path") }, + } as vscode.ExtensionContext - // Create real instances - manager = new PackageManagerManager(context); + // Create real instances + manager = new PackageManagerManager(context) - // Set up manager with template data - manager["currentItems"] = [...templateItems]; - }); + // Set up manager with template data + manager["currentItems"] = [...templateItems] + }) - describe("Message Handler Integration", () => { - it("should handle search messages", async () => { - const message = { - type: "search", - search: "data platform", - typeFilter: "", - tagFilters: [] - }; + describe("Message Handler Integration", () => { + it("should handle search messages", async () => { + const message = { + type: "search", + search: "data platform", + typeFilter: "", + tagFilters: [], + } - const result = await handlePackageManagerMessages(message, manager); + const result = await handlePackageManagerMessages(message, manager) - expect(result.type).toBe("searchResults"); - expect(result.data).toHaveLength(1); - expect(result.data[0].name).toContain("Data Platform"); - }); + expect(result.type).toBe("searchResults") + expect(result.data).toHaveLength(1) + expect(result.data[0].name).toContain("Data Platform") + }) - it("should handle type filter messages", async () => { - const message = { - type: "search", - search: "", - typeFilter: "mode", - tagFilters: [] - }; + it("should handle type filter messages", async () => { + const message = { + type: "search", + search: "", + typeFilter: "mode", + tagFilters: [], + } - const result = await handlePackageManagerMessages(message, manager); + const result = await handlePackageManagerMessages(message, manager) - expect(result.type).toBe("searchResults"); - expect(result.data.every(item => item.type === "mode")).toBe(true); - }); + expect(result.type).toBe("searchResults") + expect(result.data.every((item) => item.type === "mode")).toBe(true) + }) - // More message handler tests... - }); + // More message handler tests... + }) - describe("End-to-End Flow", () => { - it("should find items with matching subcomponents", async () => { - const message = { - type: "search", - search: "validator", - typeFilter: "", - tagFilters: [] - }; + describe("End-to-End Flow", () => { + it("should find items with matching subcomponents", async () => { + const message = { + type: "search", + search: "validator", + typeFilter: "", + tagFilters: [], + } - const result = await handlePackageManagerMessages(message, manager); + const result = await handlePackageManagerMessages(message, manager) - expect(result.data.length).toBeGreaterThan(0); + expect(result.data.length).toBeGreaterThan(0) - // Check that subcomponents are marked as matches - const hasMatchingSubcomponent = result.data.some(item => - item.items?.some(subItem => subItem.matchInfo?.matched) - ); - expect(hasMatchingSubcomponent).toBe(true); - }); + // Check that subcomponents are marked as matches + const hasMatchingSubcomponent = result.data.some((item) => + item.items?.some((subItem) => subItem.matchInfo?.matched), + ) + expect(hasMatchingSubcomponent).toBe(true) + }) - // More end-to-end flow tests... - }); -}); + // More end-to-end flow tests... + }) +}) ``` ### Frontend Integration Tests @@ -490,17 +495,17 @@ Mock data is used for simple unit tests: ```typescript const mockItems: PackageManagerItem[] = [ - { - name: "Test Package", - description: "A test package", - type: "package", - url: "https://example.com", - repoUrl: "https://github.com/example/repo", - tags: ["test", "example"], - version: "1.0.0" - }, - // More mock items... -]; + { + name: "Test Package", + description: "A test package", + type: "package", + url: "https://example.com", + repoUrl: "https://github.com/example/repo", + tags: ["test", "example"], + version: "1.0.0", + }, + // More mock items... +] ``` ### Test Fixtures @@ -510,48 +515,48 @@ Test fixtures provide more complex data structures: ```typescript // fixtures/metadata.ts export const metadataFixtures = { - basic: { - name: "Basic Package", - description: "A basic package for testing", - version: "1.0.0", - type: "package" - }, + basic: { + name: "Basic Package", + description: "A basic package for testing", + version: "1.0.0", + type: "package", + }, - withTags: { - name: "Tagged Package", - description: "A package with tags", - version: "1.0.0", - type: "package", - tags: ["test", "fixture", "example"] - }, + withTags: { + name: "Tagged Package", + description: "A package with tags", + version: "1.0.0", + type: "package", + tags: ["test", "fixture", "example"], + }, - withSubcomponents: { - name: "Complex Package", - description: "A package with subcomponents", - version: "1.0.0", - type: "package", - items: [ - { - type: "mode", - path: "/test/path/mode", - metadata: { - name: "Test Mode", - description: "A test mode", - type: "mode" - } - }, - { - type: "mcp server", - path: "/test/path/server", - metadata: { - name: "Test Server", - description: "A test server", - type: "mcp server" - } - } - ] - } -}; + withSubcomponents: { + name: "Complex Package", + description: "A package with subcomponents", + version: "1.0.0", + type: "package", + items: [ + { + type: "mode", + path: "/test/path/mode", + metadata: { + name: "Test Mode", + description: "A test mode", + type: "mode", + }, + }, + { + type: "mcp server", + path: "/test/path/server", + metadata: { + name: "Test Server", + description: "A test server", + type: "mcp server", + }, + }, + ], + }, +} ``` ### Template Data @@ -560,11 +565,11 @@ Real template data is used for integration tests: ```typescript beforeAll(async () => { - // Load real data from template - metadataScanner = new MetadataScanner(); - const templatePath = path.resolve(__dirname, "../../../../package-manager-template"); - templateItems = await metadataScanner.scanDirectory(templatePath, "https://example.com"); -}); + // Load real data from template + metadataScanner = new MetadataScanner() + const templatePath = path.resolve(__dirname, "../../../../package-manager-template") + templateItems = await metadataScanner.scanDirectory(templatePath, "https://example.com") +}) ``` ### Test Data Generators @@ -574,45 +579,43 @@ Generators create varied test data: ```typescript // Test data generator function generatePackageItems(count: number): PackageManagerItem[] { - const types: ComponentType[] = ["mode", "mcp server", "package", "prompt"]; - const tags = ["test", "example", "data", "ui", "server", "client"]; + const types: ComponentType[] = ["mode", "mcp server", "package", "prompt"] + const tags = ["test", "example", "data", "ui", "server", "client"] - return Array.from({ length: count }, (_, i) => { - const type = types[i % types.length]; - const randomTags = tags - .filter(() => Math.random() > 0.5) - .slice(0, Math.floor(Math.random() * 4)); + return Array.from({ length: count }, (_, i) => { + const type = types[i % types.length] + const randomTags = tags.filter(() => Math.random() > 0.5).slice(0, Math.floor(Math.random() * 4)) - return { - name: `Test ${type} ${i + 1}`, - description: `This is a test ${type} for testing purposes`, - type, - url: `https://example.com/${type}/${i + 1}`, - repoUrl: "https://github.com/example/repo", - tags: randomTags.length ? randomTags : undefined, - version: "1.0.0", - lastUpdated: new Date().toISOString(), - items: type === "package" ? generateSubcomponents(Math.floor(Math.random() * 5) + 1) : undefined - }; - }); + return { + name: `Test ${type} ${i + 1}`, + description: `This is a test ${type} for testing purposes`, + type, + url: `https://example.com/${type}/${i + 1}`, + repoUrl: "https://github.com/example/repo", + tags: randomTags.length ? randomTags : undefined, + version: "1.0.0", + lastUpdated: new Date().toISOString(), + items: type === "package" ? generateSubcomponents(Math.floor(Math.random() * 5) + 1) : undefined, + } + }) } function generateSubcomponents(count: number): PackageManagerItem["items"] { - const types: ComponentType[] = ["mode", "mcp server", "prompt"]; + const types: ComponentType[] = ["mode", "mcp server", "prompt"] - return Array.from({ length: count }, (_, i) => { - const type = types[i % types.length]; + return Array.from({ length: count }, (_, i) => { + const type = types[i % types.length] - return { - type, - path: `/test/path/${type}/${i + 1}`, - metadata: { - name: `Test ${type} ${i + 1}`, - description: `This is a test ${type} subcomponent`, - type - } - }; - }); + return { + type, + path: `/test/path/${type}/${i + 1}`, + metadata: { + name: `Test ${type} ${i + 1}`, + description: `This is a test ${type} subcomponent`, + type, + }, + } + }) } ``` @@ -635,20 +638,20 @@ Tests are organized into logical groups: ```typescript describe("Package Manager", () => { - // Shared setup + // Shared setup - describe("Direct Filtering", () => { - // Tests for filtering functionality - }); + describe("Direct Filtering", () => { + // Tests for filtering functionality + }) - describe("Message Handler Integration", () => { - // Tests for message handling - }); + describe("Message Handler Integration", () => { + // Tests for message handling + }) - describe("Sorting", () => { - // Tests for sorting functionality - }); -}); + describe("Sorting", () => { + // Tests for sorting functionality + }) +}) ``` ## Test Coverage @@ -666,24 +669,24 @@ The Package Manager maintains high test coverage: ```typescript // jest.config.js module.exports = { - // ...other config - collectCoverage: true, - coverageReporters: ["text", "lcov", "html"], - coverageThreshold: { - global: { - branches: 80, - functions: 85, - lines: 85, - statements: 85 - }, - "src/services/package-manager/*.ts": { - branches: 90, - functions: 90, - lines: 90, - statements: 90 - } - } -}; + // ...other config + collectCoverage: true, + coverageReporters: ["text", "lcov", "html"], + coverageThreshold: { + global: { + branches: 80, + functions: 85, + lines: 85, + statements: 85, + }, + "src/services/package-manager/*.ts": { + branches: 90, + functions: 90, + lines: 90, + statements: 90, + }, + }, +} ``` ### Critical Path Testing @@ -703,12 +706,12 @@ The Package Manager tests are optimized for performance: ```typescript // Fast unit tests with minimal dependencies describe("containsSearchTerm", () => { - it("should return true for exact matches", () => { - expect(containsSearchTerm("hello world", "hello")).toBe(true); - }); + it("should return true for exact matches", () => { + expect(containsSearchTerm("hello world", "hello")).toBe(true) + }) - // More tests... -}); + // More tests... +}) ``` ### Optimized Integration Tests @@ -716,19 +719,19 @@ describe("containsSearchTerm", () => { ```typescript // Optimized integration tests describe("Package Manager Integration", () => { - // Load template data once for all tests - beforeAll(async () => { - templateItems = await metadataScanner.scanDirectory(templatePath); - }); + // Load template data once for all tests + beforeAll(async () => { + templateItems = await metadataScanner.scanDirectory(templatePath) + }) - // Create fresh manager for each test - beforeEach(() => { - manager = new PackageManagerManager(mockContext); - manager["currentItems"] = [...templateItems]; - }); + // Create fresh manager for each test + beforeEach(() => { + manager = new PackageManagerManager(mockContext) + manager["currentItems"] = [...templateItems] + }) - // Tests... -}); + // Tests... +}) ``` ### Parallel Test Execution @@ -736,10 +739,10 @@ describe("Package Manager Integration", () => { ```typescript // jest.config.js module.exports = { - // ...other config - maxWorkers: "50%", // Use 50% of available cores - maxConcurrency: 5 // Run up to 5 tests concurrently -}; + // ...other config + maxWorkers: "50%", // Use 50% of available cores + maxConcurrency: 5, // Run up to 5 tests concurrently +} ``` ## Continuous Integration @@ -753,33 +756,33 @@ The Package Manager tests are integrated into the CI/CD pipeline: name: Tests on: - push: - branches: [ main ] - pull_request: - branches: [ main ] + push: + branches: [main] + pull_request: + branches: [main] jobs: - test: - runs-on: ubuntu-latest + test: + runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 + steps: + - uses: actions/checkout@v2 - - name: Setup Node.js - uses: actions/setup-node@v2 - with: - node-version: '16' + - name: Setup Node.js + uses: actions/setup-node@v2 + with: + node-version: "16" - - name: Install dependencies - run: npm ci + - name: Install dependencies + run: npm ci - - name: Run tests - run: npm test + - name: Run tests + run: npm test - - name: Upload coverage - uses: codecov/codecov-action@v2 - with: - file: ./coverage/lcov.info + - name: Upload coverage + uses: codecov/codecov-action@v2 + with: + file: ./coverage/lcov.info ``` ### Pre-commit Hooks @@ -787,17 +790,14 @@ jobs: ```json // package.json { - "husky": { - "hooks": { - "pre-commit": "lint-staged" - } - }, - "lint-staged": { - "*.{ts,tsx}": [ - "eslint --fix", - "jest --findRelatedTests" - ] - } + "husky": { + "hooks": { + "pre-commit": "lint-staged" + } + }, + "lint-staged": { + "*.{ts,tsx}": ["eslint --fix", "jest --findRelatedTests"] + } } ``` @@ -810,17 +810,17 @@ The Package Manager includes tools for debugging tests: ```typescript // Debug logging in tests describe("Complex integration test", () => { - it("should handle complex search", async () => { - // Enable debug logging for this test - const originalDebug = process.env.DEBUG; - process.env.DEBUG = "package-manager:*"; + it("should handle complex search", async () => { + // Enable debug logging for this test + const originalDebug = process.env.DEBUG + process.env.DEBUG = "package-manager:*" - // Test logic... + // Test logic... - // Restore debug setting - process.env.DEBUG = originalDebug; - }); -}); + // Restore debug setting + process.env.DEBUG = originalDebug + }) +}) ``` ### Visual Debugging @@ -860,26 +860,26 @@ The Package Manager tests include comprehensive documentation: * - Matching in subcomponents */ describe("Search functionality", () => { - // Tests... -}); + // Tests... +}) ``` ### Test Scenarios ```typescript describe("Package filtering", () => { - /** - * Scenario: User filters by type and search term - * Given: A list of packages of different types - * When: The user selects a type filter and enters a search term - * Then: Only packages of the selected type containing the search term should be shown - */ - it("should combine type and search filters", () => { - // Test implementation... - }); -}); + /** + * Scenario: User filters by type and search term + * Given: A list of packages of different types + * When: The user selects a type filter and enters a search term + * Then: Only packages of the selected type containing the search term should be shown + */ + it("should combine type and search filters", () => { + // Test implementation... + }) +}) ``` --- -**Previous**: [UI Component Design](./05-ui-components.md) | **Next**: [Extending the Package Manager](./07-extending.md) \ No newline at end of file +**Previous**: [UI Component Design](./05-ui-components.md) | **Next**: [Extending the Package Manager](./07-extending.md) diff --git a/cline_docs/package-manager/implementation/07-extending.md b/cline_docs/package-manager/implementation/07-extending.md index 939ceac780..f0f55778f8 100644 --- a/cline_docs/package-manager/implementation/07-extending.md +++ b/cline_docs/package-manager/implementation/07-extending.md @@ -16,70 +16,70 @@ To add a new component type: /** * Supported component types */ -export type ComponentType = "mode" | "prompt" | "package" | "mcp server" | "your-new-type"; +export type ComponentType = "mode" | "prompt" | "package" | "mcp server" | "your-new-type" ``` 2. **Update Type Label Functions**: ```typescript const getTypeLabel = (type: string) => { - switch (type) { - case "mode": - return "Mode"; - case "mcp server": - return "MCP Server"; - case "prompt": - return "Prompt"; - case "package": - return "Package"; - case "your-new-type": - return "Your New Type"; - default: - return "Other"; - } -}; + switch (type) { + case "mode": + return "Mode" + case "mcp server": + return "MCP Server" + case "prompt": + return "Prompt" + case "package": + return "Package" + case "your-new-type": + return "Your New Type" + default: + return "Other" + } +} ``` 3. **Update Type Color Functions**: ```typescript const getTypeColor = (type: string) => { - switch (type) { - case "mode": - return "bg-blue-600"; - case "mcp server": - return "bg-green-600"; - case "prompt": - return "bg-purple-600"; - case "package": - return "bg-orange-600"; - case "your-new-type": - return "bg-yellow-600"; // Choose a distinctive color - default: - return "bg-gray-600"; - } -}; + switch (type) { + case "mode": + return "bg-blue-600" + case "mcp server": + return "bg-green-600" + case "prompt": + return "bg-purple-600" + case "package": + return "bg-orange-600" + case "your-new-type": + return "bg-yellow-600" // Choose a distinctive color + default: + return "bg-gray-600" + } +} ``` 4. **Update Type Group Labels**: ```typescript const getTypeGroupLabel = (type: string) => { - switch (type) { - case "mode": - return "Modes"; - case "mcp server": - return "MCP Servers"; - case "prompt": - return "Prompts"; - case "package": - return "Packages"; - case "your-new-type": - return "Your New Types"; - default: - return `${type.charAt(0).toUpperCase()}${type.slice(1)}s`; - } -}; + switch (type) { + case "mode": + return "Modes" + case "mcp server": + return "MCP Servers" + case "prompt": + return "Prompts" + case "package": + return "Packages" + case "your-new-type": + return "Your New Types" + default: + return `${type.charAt(0).toUpperCase()}${type.slice(1)}s` + } +} ``` ### Directory Structure for New Types @@ -107,8 +107,8 @@ description: "Description of your component" version: "1.0.0" type: "your-new-type" tags: - - relevant-tag-1 - - relevant-tag-2 + - relevant-tag-1 + - relevant-tag-2 ``` ### UI Considerations for New Types @@ -116,34 +116,36 @@ tags: When adding a new component type, consider these UI aspects: 1. **Type Filtering**: - - Add your new type to the type filter options - - Ensure proper labeling and styling + + - Add your new type to the type filter options + - Ensure proper labeling and styling 2. **Type-Specific Rendering**: - - Consider if your type needs special rendering in the UI - - Add any type-specific UI components or styles + + - Consider if your type needs special rendering in the UI + - Add any type-specific UI components or styles 3. **Type Icons**: - - Choose an appropriate icon for your type - - Add it to the icon mapping + - Choose an appropriate icon for your type + - Add it to the icon mapping ```typescript const getTypeIcon = (type: string) => { - switch (type) { - case "mode": - return "codicon-person"; - case "mcp server": - return "codicon-server"; - case "prompt": - return "codicon-comment"; - case "package": - return "codicon-package"; - case "your-new-type": - return "codicon-your-icon"; // Choose an appropriate icon - default: - return "codicon-symbol-misc"; - } -}; + switch (type) { + case "mode": + return "codicon-person" + case "mcp server": + return "codicon-server" + case "prompt": + return "codicon-comment" + case "package": + return "codicon-package" + case "your-new-type": + return "codicon-your-icon" // Choose an appropriate icon + default: + return "codicon-symbol-misc" + } +} ``` ## Creating Custom Templates @@ -181,9 +183,9 @@ Register your template with the Package Manager: ```typescript // In your extension code const registerTemplates = (context: vscode.ExtensionContext) => { - const templatePath = path.join(context.extensionPath, "templates", "your-template"); - packageManager.registerTemplate(templatePath); -}; + const templatePath = path.join(context.extensionPath, "templates", "your-template") + packageManager.registerTemplate(templatePath) +} ``` ### Template Usage @@ -193,11 +195,11 @@ Users can create new components from your template: ```typescript // In the UI const createFromTemplate = (templateName: string) => { - vscode.postMessage({ - type: "createFromTemplate", - templateName - }); -}; + vscode.postMessage({ + type: "createFromTemplate", + templateName, + }) +} ``` ## Implementing New Features @@ -212,10 +214,10 @@ To add a new filter type (beyond type, search, and tags): ```typescript interface Filters { - type: string; - search: string; - tags: string[]; - yourNewFilter: string; // Add your new filter + type: string + search: string + tags: string[] + yourNewFilter: string // Add your new filter } ``` @@ -223,25 +225,25 @@ interface Filters { ```typescript export function filterItems( - items: PackageManagerItem[], - filters: { - type?: string; - search?: string; - tags?: string[]; - yourNewFilter?: string; // Add your new filter - } + items: PackageManagerItem[], + filters: { + type?: string + search?: string + tags?: string[] + yourNewFilter?: string // Add your new filter + }, ): PackageManagerItem[] { - // Existing filter logic... + // Existing filter logic... - // Add your new filter logic - if (filters.yourNewFilter) { - result = result.filter(item => { - // Your filter implementation - return yourFilterLogic(item, filters.yourNewFilter); - }); - } + // Add your new filter logic + if (filters.yourNewFilter) { + result = result.filter((item) => { + // Your filter implementation + return yourFilterLogic(item, filters.yourNewFilter) + }) + } - return result; + return result } ``` @@ -249,40 +251,26 @@ export function filterItems( ```tsx const YourNewFilterControl: React.FC<{ - value: string; - onChange: (value: string) => void; + value: string + onChange: (value: string) => void }> = ({ value, onChange }) => { - return ( -
-

Your New Filter

- {/* Your filter UI controls */} -
- ); -}; + return ( +
+

Your New Filter

+ {/* Your filter UI controls */} +
+ ) +} ``` 4. **Integrate with the Main UI**: ```tsx - - - - + + + + ``` @@ -293,85 +281,72 @@ To add a new view mode (beyond the card view): 1. **Add a View Mode State**: ```typescript -type ViewMode = "card" | "list" | "yourNewView"; +type ViewMode = "card" | "list" | "yourNewView" -const [viewMode, setViewMode] = useState("card"); +const [viewMode, setViewMode] = useState("card") ``` 2. **Create the View Component**: ```tsx const YourNewView: React.FC<{ - items: PackageManagerItem[]; - filters: Filters; - setFilters: (filters: Filters) => void; + items: PackageManagerItem[] + filters: Filters + setFilters: (filters: Filters) => void }> = ({ items, filters, setFilters }) => { - return ( -
- {/* Your view implementation */} -
- ); -}; + return
{/* Your view implementation */}
+} ``` 3. **Add View Switching Controls**: ```tsx const ViewModeSelector: React.FC<{ - viewMode: ViewMode; - setViewMode: (mode: ViewMode) => void; + viewMode: ViewMode + setViewMode: (mode: ViewMode) => void }> = ({ viewMode, setViewMode }) => { - return ( -
- - - -
- ); -}; + return ( +
+ + + +
+ ) +} ``` 4. **Integrate with the Main UI**: ```tsx
-
- - {/* Other toolbar items */} -
+
+ + {/* Other toolbar items */} +
-
- {viewMode === "card" && ( - - )} - {viewMode === "list" && ( - - )} - {viewMode === "yourNewView" && ( - - )} -
+
+ {viewMode === "card" && } + {viewMode === "list" && } + {viewMode === "yourNewView" && } +
``` @@ -383,23 +358,20 @@ To add custom actions for package items: ```typescript const handleCustomAction = (item: PackageManagerItem) => { - vscode.postMessage({ - type: "customAction", - item: item.name, - itemType: item.type - }); -}; + vscode.postMessage({ + type: "customAction", + item: item.name, + itemType: item.type, + }) +} ``` 2. **Add Action Button to the UI**: ```tsx - ``` @@ -430,10 +402,10 @@ To customize the styling: ```css /* In your CSS file */ :root { - --package-card-bg: var(--vscode-panel-background); - --package-card-border: var(--vscode-panel-border); - --package-card-hover: var(--vscode-list-hoverBackground); - --your-custom-variable: #your-color; + --package-card-bg: var(--vscode-panel-background); + --package-card-border: var(--vscode-panel-border); + --package-card-hover: var(--vscode-list-hoverBackground); + --your-custom-variable: #your-color; } ``` @@ -441,32 +413,30 @@ To customize the styling: ```tsx
-
- {/* Your custom UI */} -
+
{/* Your custom UI */}
``` 3. **Add Custom Themes**: ```typescript -type Theme = "default" | "compact" | "detailed" | "yourCustomTheme"; +type Theme = "default" | "compact" | "detailed" | "yourCustomTheme" -const [theme, setTheme] = useState("default"); +const [theme, setTheme] = useState("default") // Theme-specific styles const getThemeClasses = (theme: Theme) => { - switch (theme) { - case "compact": - return "compact-theme"; - case "detailed": - return "detailed-theme"; - case "yourCustomTheme": - return "your-custom-theme"; - default: - return "default-theme"; - } -}; + switch (theme) { + case "compact": + return "compact-theme" + case "detailed": + return "detailed-theme" + case "yourCustomTheme": + return "your-custom-theme" + default: + return "default-theme" + } +} ``` ### Custom Components @@ -477,51 +447,49 @@ To replace or extend existing components: ```tsx const CustomPackageCard: React.FC = (props) => { - // Your custom implementation - return ( -
- {/* Your custom UI */} -

{props.item.name}

- {/* Additional custom elements */} -
- {/* Custom footer content */} -
-
- ); -}; + // Your custom implementation + return ( +
+ {/* Your custom UI */} +

{props.item.name}

+ {/* Additional custom elements */} +
{/* Custom footer content */}
+
+ ) +} ``` 2. **Use Component Injection**: ```tsx interface ComponentOverrides { - PackageCard?: React.ComponentType; - ExpandableSection?: React.ComponentType; - TypeGroup?: React.ComponentType; + PackageCard?: React.ComponentType + ExpandableSection?: React.ComponentType + TypeGroup?: React.ComponentType } const PackageManagerView: React.FC<{ - initialItems: PackageManagerItem[]; - componentOverrides?: ComponentOverrides; + initialItems: PackageManagerItem[] + componentOverrides?: ComponentOverrides }> = ({ initialItems, componentOverrides = {} }) => { - // Component selection logic - const PackageCard = componentOverrides.PackageCard || PackageManagerItemCard; + // Component selection logic + const PackageCard = componentOverrides.PackageCard || PackageManagerItemCard - return ( -
- {items.map(item => ( - - ))} -
- ); -}; + return ( +
+ {items.map((item) => ( + + ))} +
+ ) +} ``` ### Custom Layouts @@ -532,51 +500,47 @@ To implement custom layouts: ```tsx const CustomLayout: React.FC<{ - sidebar: React.ReactNode; - content: React.ReactNode; - footer?: React.ReactNode; + sidebar: React.ReactNode + content: React.ReactNode + footer?: React.ReactNode }> = ({ sidebar, content, footer }) => { - return ( -
-
{sidebar}
-
{content}
- {footer &&
{footer}
} -
- ); -}; + return ( +
+
{sidebar}
+
{content}
+ {footer &&
{footer}
} +
+ ) +} ``` 2. **Use the Layout in the Main UI**: ```tsx - } - content={ -
- {filteredItems.map(item => ( - - ))} -
- } - footer={ -
- {`Showing ${filteredItems.length} of ${items.length} packages`} -
- } + sidebar={ + + } + content={ +
+ {filteredItems.map((item) => ( + + ))} +
+ } + footer={
{`Showing ${filteredItems.length} of ${items.length} packages`}
} /> ``` @@ -592,9 +556,9 @@ To add support for new source types: ```typescript interface SourceProvider { - type: string; - canHandle(url: string): boolean; - fetchItems(url: string): Promise; + type: string + canHandle(url: string): boolean + fetchItems(url: string): Promise } ``` @@ -602,17 +566,17 @@ interface SourceProvider { ```typescript class CustomSourceProvider implements SourceProvider { - type = "custom"; + type = "custom" - canHandle(url: string): boolean { - return url.startsWith("custom://"); - } + canHandle(url: string): boolean { + return url.startsWith("custom://") + } - async fetchItems(url: string): Promise { - // Your custom implementation - // Fetch items from your custom source - return items; - } + async fetchItems(url: string): Promise { + // Your custom implementation + // Fetch items from your custom source + return items + } } ``` @@ -621,8 +585,8 @@ class CustomSourceProvider implements SourceProvider { ```typescript // In your extension code const registerSourceProviders = (packageManager: PackageManagerManager) => { - packageManager.registerSourceProvider(new CustomSourceProvider()); -}; + packageManager.registerSourceProvider(new CustomSourceProvider()) +} ``` ### Custom Metadata Processors @@ -633,8 +597,8 @@ To add support for custom metadata formats: ```typescript interface MetadataProcessor { - canProcess(filePath: string): boolean; - process(filePath: string, content: string): Promise; + canProcess(filePath: string): boolean + process(filePath: string, content: string): Promise } ``` @@ -642,14 +606,14 @@ interface MetadataProcessor { ```typescript class CustomMetadataProcessor implements MetadataProcessor { - canProcess(filePath: string): boolean { - return filePath.endsWith(".custom"); - } + canProcess(filePath: string): boolean { + return filePath.endsWith(".custom") + } - async process(filePath: string, content: string): Promise { - // Your custom processing logic - return processedMetadata; - } + async process(filePath: string, content: string): Promise { + // Your custom processing logic + return processedMetadata + } } ``` @@ -658,8 +622,8 @@ class CustomMetadataProcessor implements MetadataProcessor { ```typescript // In your extension code const registerMetadataProcessors = (metadataScanner: MetadataScanner) => { - metadataScanner.registerProcessor(new CustomMetadataProcessor()); -}; + metadataScanner.registerProcessor(new CustomMetadataProcessor()) +} ``` ### Custom Message Handlers @@ -671,34 +635,36 @@ To add support for custom messages: ```typescript // In your extension code const extendMessageHandler = () => { - const originalHandler = handlePackageManagerMessages; + const originalHandler = handlePackageManagerMessages - return async (message: any, packageManager: PackageManagerManager) => { - // Handle custom messages - if (message.type === "yourCustomMessage") { - // Your custom message handling - return { - type: "yourCustomResponse", - data: { /* response data */ } - }; - } + return async (message: any, packageManager: PackageManagerManager) => { + // Handle custom messages + if (message.type === "yourCustomMessage") { + // Your custom message handling + return { + type: "yourCustomResponse", + data: { + /* response data */ + }, + } + } - // Fall back to the original handler - return originalHandler(message, packageManager); - }; -}; + // Fall back to the original handler + return originalHandler(message, packageManager) + } +} ``` 2. **Register the Extended Handler**: ```typescript // In your extension code -const customMessageHandler = extendMessageHandler(); +const customMessageHandler = extendMessageHandler() context.subscriptions.push( - vscode.commands.registerCommand("packageManager.handleMessage", (message) => { - return customMessageHandler(message, packageManager); - }) -); + vscode.commands.registerCommand("packageManager.handleMessage", (message) => { + return customMessageHandler(message, packageManager) + }), +) ``` ## Integration with Other Systems @@ -713,26 +679,26 @@ To integrate with external APIs: ```typescript class ExternalApiClient { - private baseUrl: string; + private baseUrl: string - constructor(baseUrl: string) { - this.baseUrl = baseUrl; - } + constructor(baseUrl: string) { + this.baseUrl = baseUrl + } - async fetchPackages(): Promise { - const response = await fetch(`${this.baseUrl}/packages`); - const data = await response.json(); + async fetchPackages(): Promise { + const response = await fetch(`${this.baseUrl}/packages`) + const data = await response.json() - // Transform API data to PackageManagerItem format - return data.map(item => ({ - name: item.name, - description: item.description, - type: item.type, - url: item.url, - repoUrl: item.repository_url, - // Map other fields - })); - } + // Transform API data to PackageManagerItem format + return data.map((item) => ({ + name: item.name, + description: item.description, + type: item.type, + url: item.url, + repoUrl: item.repository_url, + // Map other fields + })) + } } ``` @@ -740,21 +706,21 @@ class ExternalApiClient { ```typescript class ApiSourceProvider implements SourceProvider { - private apiClient: ExternalApiClient; + private apiClient: ExternalApiClient - constructor(apiUrl: string) { - this.apiClient = new ExternalApiClient(apiUrl); - } + constructor(apiUrl: string) { + this.apiClient = new ExternalApiClient(apiUrl) + } - type = "api"; + type = "api" - canHandle(url: string): boolean { - return url.startsWith("api://"); - } + canHandle(url: string): boolean { + return url.startsWith("api://") + } - async fetchItems(url: string): Promise { - return this.apiClient.fetchPackages(); - } + async fetchItems(url: string): Promise { + return this.apiClient.fetchPackages() + } } ``` @@ -763,10 +729,8 @@ class ApiSourceProvider implements SourceProvider { ```typescript // In your extension code const registerApiProvider = (packageManager: PackageManagerManager) => { - packageManager.registerSourceProvider( - new ApiSourceProvider("https://your-api.example.com") - ); -}; + packageManager.registerSourceProvider(new ApiSourceProvider("https://your-api.example.com")) +} ``` ### Integration with Authentication Systems @@ -777,24 +741,24 @@ To integrate with authentication systems: ```typescript class AuthProvider { - private token: string | null = null; + private token: string | null = null - async login(): Promise { - // Your authentication logic - this.token = "your-auth-token"; - return true; - } + async login(): Promise { + // Your authentication logic + this.token = "your-auth-token" + return true + } - async getToken(): Promise { - if (!this.token) { - await this.login(); - } - return this.token; - } + async getToken(): Promise { + if (!this.token) { + await this.login() + } + return this.token + } - isAuthenticated(): boolean { - return !!this.token; - } + isAuthenticated(): boolean { + return !!this.token + } } ``` @@ -802,28 +766,28 @@ class AuthProvider { ```typescript class AuthenticatedApiClient extends ExternalApiClient { - private authProvider: AuthProvider; + private authProvider: AuthProvider - constructor(baseUrl: string, authProvider: AuthProvider) { - super(baseUrl); - this.authProvider = authProvider; - } + constructor(baseUrl: string, authProvider: AuthProvider) { + super(baseUrl) + this.authProvider = authProvider + } - async fetchPackages(): Promise { - const token = await this.authProvider.getToken(); + async fetchPackages(): Promise { + const token = await this.authProvider.getToken() - if (!token) { - throw new Error("Authentication required"); - } + if (!token) { + throw new Error("Authentication required") + } - const response = await fetch(`${this.baseUrl}/packages`, { - headers: { - Authorization: `Bearer ${token}` - } - }); + const response = await fetch(`${this.baseUrl}/packages`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }) - // Process response as before - } + // Process response as before + } } ``` @@ -835,33 +799,33 @@ To integrate with local development tools: ```typescript class LocalDevProvider { - private workspacePath: string; + private workspacePath: string - constructor(workspacePath: string) { - this.workspacePath = workspacePath; - } + constructor(workspacePath: string) { + this.workspacePath = workspacePath + } - async createLocalPackage(template: string, name: string): Promise { - const targetPath = path.join(this.workspacePath, name); + async createLocalPackage(template: string, name: string): Promise { + const targetPath = path.join(this.workspacePath, name) - // Create directory - await fs.promises.mkdir(targetPath, { recursive: true }); + // Create directory + await fs.promises.mkdir(targetPath, { recursive: true }) - // Copy template files - // Your implementation + // Copy template files + // Your implementation - return targetPath; - } + return targetPath + } - async buildLocalPackage(packagePath: string): Promise { - // Your build implementation - return true; - } + async buildLocalPackage(packagePath: string): Promise { + // Your build implementation + return true + } - async testLocalPackage(packagePath: string): Promise { - // Your test implementation - return true; - } + async testLocalPackage(packagePath: string): Promise { + // Your test implementation + return true + } } ``` @@ -870,30 +834,30 @@ class LocalDevProvider { ```typescript // In your extension code const registerLocalDevTools = (context: vscode.ExtensionContext) => { - const workspaceFolders = vscode.workspace.workspaceFolders; + const workspaceFolders = vscode.workspace.workspaceFolders - if (!workspaceFolders) { - return; - } + if (!workspaceFolders) { + return + } - const workspacePath = workspaceFolders[0].uri.fsPath; - const localDevProvider = new LocalDevProvider(workspacePath); + const workspacePath = workspaceFolders[0].uri.fsPath + const localDevProvider = new LocalDevProvider(workspacePath) - // Register commands - context.subscriptions.push( - vscode.commands.registerCommand("packageManager.createLocal", async (template, name) => { - return localDevProvider.createLocalPackage(template, name); - }), + // Register commands + context.subscriptions.push( + vscode.commands.registerCommand("packageManager.createLocal", async (template, name) => { + return localDevProvider.createLocalPackage(template, name) + }), - vscode.commands.registerCommand("packageManager.buildLocal", async (packagePath) => { - return localDevProvider.buildLocalPackage(packagePath); - }), + vscode.commands.registerCommand("packageManager.buildLocal", async (packagePath) => { + return localDevProvider.buildLocalPackage(packagePath) + }), - vscode.commands.registerCommand("packageManager.testLocal", async (packagePath) => { - return localDevProvider.testLocalPackage(packagePath); - }) - ); -}; + vscode.commands.registerCommand("packageManager.testLocal", async (packagePath) => { + return localDevProvider.testLocalPackage(packagePath) + }), + ) +} ``` ## Best Practices for Extensions @@ -903,54 +867,60 @@ When extending the Package Manager, follow these best practices: ### Maintainable Code 1. **Follow the Existing Patterns**: - - Use similar naming conventions - - Follow the same code structure - - Maintain consistent error handling + + - Use similar naming conventions + - Follow the same code structure + - Maintain consistent error handling 2. **Document Your Extensions**: - - Add JSDoc comments to functions and classes - - Explain the purpose of your extensions - - Document any configuration options + + - Add JSDoc comments to functions and classes + - Explain the purpose of your extensions + - Document any configuration options 3. **Write Tests**: - - Add unit tests for new functionality - - Update integration tests as needed - - Ensure test coverage remains high + - Add unit tests for new functionality + - Update integration tests as needed + - Ensure test coverage remains high ### Performance Considerations 1. **Lazy Loading**: - - Load data only when needed - - Defer expensive operations - - Use pagination for large datasets + + - Load data only when needed + - Defer expensive operations + - Use pagination for large datasets 2. **Efficient Data Processing**: - - Minimize data transformations - - Use memoization for expensive calculations - - Batch operations when possible + + - Minimize data transformations + - Use memoization for expensive calculations + - Batch operations when possible 3. **UI Responsiveness**: - - Keep the UI responsive during operations - - Show loading indicators for async operations - - Use debouncing for frequent events + - Keep the UI responsive during operations + - Show loading indicators for async operations + - Use debouncing for frequent events ### Compatibility 1. **VSCode API Compatibility**: - - Use stable VSCode API features - - Handle API version differences - - Test with multiple VSCode versions + + - Use stable VSCode API features + - Handle API version differences + - Test with multiple VSCode versions 2. **Cross-Platform Support**: - - Test on Windows, macOS, and Linux - - Use path.join for file paths - - Handle file system differences + + - Test on Windows, macOS, and Linux + - Use path.join for file paths + - Handle file system differences 3. **Theme Compatibility**: - - Use VSCode theme variables - - Test with light and dark themes - - Support high contrast mode + - Use VSCode theme variables + - Test with light and dark themes + - Support high contrast mode --- -**Previous**: [Testing Strategy](./06-testing-strategy.md) \ No newline at end of file +**Previous**: [Testing Strategy](./06-testing-strategy.md) diff --git a/cline_docs/package-manager/implementation/localization-improvements.md b/cline_docs/package-manager/implementation/localization-improvements.md index 9df407f125..2ce1f40e26 100644 --- a/cline_docs/package-manager/implementation/localization-improvements.md +++ b/cline_docs/package-manager/implementation/localization-improvements.md @@ -1,371 +1,3 @@ -# Package Manager Localization Improvements - -## Issue Identified - -The current implementation of the Package Manager only uses English metadata (`metadata.en.yml`) for all functionality, regardless of the user's locale. While the system loads metadata files for other locales, it doesn't actually use them. The correct behavior should be: - -1. Use the locale-specific version for each package item if it is present -2. Fall back to the English version if the locale-specific version is not available -3. Skip the item if neither the locale-specific nor the English version is available - -## Implementation Changes Needed - -### 1. Add User Locale Detection - -```typescript -// Add to src/services/package-manager/types.ts -export interface LocalizationOptions { - userLocale: string; - fallbackLocale: string; -} -``` - -```typescript -// Add to src/services/package-manager/utils.ts -export function getUserLocale(): string { - // Get from VS Code API or system locale - const vscodeLocale = vscode.env.language; - // Extract just the language part (e.g., "en-US" -> "en") - return vscodeLocale.split('-')[0].toLowerCase(); -} -``` - -### 2. Modify MetadataScanner to Use Locale Preference - -```typescript -// Update MetadataScanner constructor -constructor(git?: SimpleGit, private localizationOptions?: LocalizationOptions) { - this.git = git; - this.localizationOptions = localizationOptions || { - userLocale: getUserLocale(), - fallbackLocale: 'en' - }; -} -``` - -### 3. Update Component Creation Logic - -```typescript -// Update scanDirectory method in MetadataScanner.ts -async scanDirectory(rootDir: string, repoUrl: string, sourceName?: string): Promise { - const items: PackageManagerItem[] = []; - - try { - const entries = await fs.readdir(rootDir, { withFileTypes: true }); - - for (const entry of entries) { - if (!entry.isDirectory()) continue; - - const componentDir = path.join(rootDir, entry.name); - const metadata = await this.loadComponentMetadata(componentDir); - - // Skip if no metadata found at all - if (!metadata) continue; - - // Get localized metadata with fallback - const localizedMetadata = this.getLocalizedMetadata(metadata); - if (!localizedMetadata) continue; - - const item = await this.createPackageManagerItem(localizedMetadata, componentDir, repoUrl, sourceName); - if (item) { - // Process package subcomponents with the same localization logic - // ...rest of the method - } - } - } catch (error) { - console.error(`Error scanning directory ${rootDir}:`, error); - } - - return items; -} -``` - -### 4. Add Localization Selection Helper - -```typescript -// Add to MetadataScanner.ts -private getLocalizedMetadata(metadata: LocalizedMetadata): ComponentMetadata | null { - const { userLocale, fallbackLocale } = this.localizationOptions; - - // First try user's locale - if (metadata[userLocale]) { - return metadata[userLocale]; - } - - // Fall back to English - if (metadata[fallbackLocale]) { - return metadata[fallbackLocale]; - } - - // No suitable metadata found - return null; -} -``` - -### 5. Update Subcomponent Processing - -```typescript -// Update the subcomponent processing in scanDirectory -if (this.isPackageMetadata(localizedMetadata)) { - // Load metadata for items listed in package metadata - if (localizedMetadata.items) { - const subcomponents = await Promise.all( - localizedMetadata.items.map(async (subItem) => { - const subPath = path.join(componentDir, subItem.path); - const subMetadata = await this.loadComponentMetadata(subPath); - - // Skip if no metadata found - if (!subMetadata) return null; - - // Get localized metadata with fallback - const localizedSubMetadata = this.getLocalizedMetadata(subMetadata); - if (!localizedSubMetadata) return null; - - return { - type: subItem.type, - path: subItem.path, - metadata: localizedSubMetadata, - lastUpdated: await this.getLastModifiedDate(subPath), - }; - }), - ); - item.items = subcomponents.filter((sub): sub is NonNullable => sub !== null); - } - - // Also scan directory for unlisted subcomponents with localization support - await this.scanPackageSubcomponents(componentDir, item); -} -``` - -### 6. Update scanPackageSubcomponents Method - -```typescript -// Update scanPackageSubcomponents in MetadataScanner.ts -private async scanPackageSubcomponents( - packageDir: string, - packageItem: PackageManagerItem, - parentPath: string = "", -): Promise { - const entries = await fs.readdir(packageDir, { withFileTypes: true }); - - for (const entry of entries) { - if (!entry.isDirectory()) continue; - - const subPath = path.join(packageDir, entry.name); - const relativePath = parentPath ? path.join(parentPath, entry.name) : entry.name; - - // Try to load metadata directly - const subMetadata = await this.loadComponentMetadata(subPath); - - if (subMetadata) { - const isListed = packageItem.items?.some((i) => i.path === relativePath); - - if (!isListed) { - // Get localized metadata with fallback - const localizedSubMetadata = this.getLocalizedMetadata(subMetadata); - if (localizedSubMetadata) { - const subItem = { - type: localizedSubMetadata.type, - path: relativePath, - metadata: localizedSubMetadata, - lastUpdated: await this.getLastModifiedDate(subPath), - }; - packageItem.items = packageItem.items || []; - packageItem.items.push(subItem); - } - } - } - - // Recursively scan this directory - await this.scanPackageSubcomponents(subPath, packageItem, relativePath); - } -} -``` - -### 7. Update PackageManagerManager to Pass Locale - -```typescript -// Update PackageManagerManager.ts -constructor(private readonly context: vscode.ExtensionContext) { - const userLocale = getUserLocale(); - this.gitFetcher = new GitFetcher(context, { userLocale, fallbackLocale: 'en' }); -} -``` - -## Test Cases - -### Unit Tests - -1. **Test Locale Fallback Logic** - -```typescript -describe('Localization Fallback', () => { - let metadataScanner: MetadataScanner; - - beforeEach(() => { - // Mock fs and other dependencies - }); - - test('should use user locale when available', async () => { - // Setup mock metadata with both user locale and English - const mockMetadata = { - 'en': { name: 'English Name', description: 'English Description' }, - 'fr': { name: 'Nom Français', description: 'Description Française' } - }; - - // Initialize with French locale - metadataScanner = new MetadataScanner(null, { userLocale: 'fr', fallbackLocale: 'en' }); - - // Call the getLocalizedMetadata method - const result = metadataScanner['getLocalizedMetadata'](mockMetadata); - - // Expect French metadata to be used - expect(result.name).toBe('Nom Français'); - expect(result.description).toBe('Description Française'); - }); - - test('should fall back to English when user locale not available', async () => { - // Setup mock metadata with only English - const mockMetadata = { - 'en': { name: 'English Name', description: 'English Description' } - }; - - // Initialize with French locale - metadataScanner = new MetadataScanner(null, { userLocale: 'fr', fallbackLocale: 'en' }); - - // Call the getLocalizedMetadata method - const result = metadataScanner['getLocalizedMetadata'](mockMetadata); - - // Expect English metadata to be used as fallback - expect(result.name).toBe('English Name'); - expect(result.description).toBe('English Description'); - }); - - test('should return null when neither user locale nor English available', async () => { - // Setup mock metadata with neither user locale nor English - const mockMetadata = { - 'de': { name: 'Deutscher Name', description: 'Deutsche Beschreibung' } - }; - - // Initialize with French locale - metadataScanner = new MetadataScanner(null, { userLocale: 'fr', fallbackLocale: 'en' }); - - // Call the getLocalizedMetadata method - const result = metadataScanner['getLocalizedMetadata'](mockMetadata); - - // Expect null result - expect(result).toBeNull(); - }); -}); -``` - -2. **Test Component Loading with Localization** - -```typescript -describe('Component Loading with Localization', () => { - let metadataScanner: MetadataScanner; - - beforeEach(() => { - // Mock fs and other dependencies - }); - - test('should load components with user locale preference', async () => { - // Setup mock directory structure with multiple locales - mockFs.readdir.mockImplementation((dir, options) => { - if (dir === '/test/repo') { - return Promise.resolve([ - { name: 'component1', isDirectory: () => true }, - { name: 'component2', isDirectory: () => true } - ]); - } - return Promise.resolve([]); - }); - - // Mock loadComponentMetadata to return different locales - jest.spyOn(MetadataScanner.prototype, 'loadComponentMetadata').mockImplementation((dir) => { - if (dir === '/test/repo/component1') { - return Promise.resolve({ - 'en': { name: 'Component 1 EN', description: 'Description EN', type: 'mode' }, - 'fr': { name: 'Component 1 FR', description: 'Description FR', type: 'mode' } - }); - } else if (dir === '/test/repo/component2') { - return Promise.resolve({ - 'en': { name: 'Component 2 EN', description: 'Description EN', type: 'mcp server' } - }); - } - return Promise.resolve(null); - }); - - // Initialize with French locale - metadataScanner = new MetadataScanner(null, { userLocale: 'fr', fallbackLocale: 'en' }); - - // Scan directory - const items = await metadataScanner.scanDirectory('/test/repo', 'https://example.com'); - - // Expect French for component1, English for component2 - expect(items.length).toBe(2); - expect(items[0].name).toBe('Component 1 FR'); - expect(items[1].name).toBe('Component 2 EN'); - }); -}); -``` - -3. **Test Subcomponent Processing with Localization** - -```typescript -describe('Subcomponent Processing with Localization', () => { - // Similar tests for subcomponents -}); -``` - -### Integration Tests - -1. **Test End-to-End Localization Flow** - -```typescript -describe('End-to-End Localization', () => { - test('should display components in user locale with fallback', async () => { - // Setup test repository with multiple locales - // Initialize PackageManagerManager with specific locale - // Verify that components are displayed in the correct locale - }); -}); -``` - -2. **Test with Real Package Repository** - -```typescript -describe('Real Package Repository with Localization', () => { - test('should handle real-world package repository with multiple locales', async () => { - // Use a real package repository with multiple locales - // Verify correct locale selection and fallback - }); -}); -``` - -## UI Changes - -1. **Add Locale Selector in UI (Optional Enhancement)** - -```typescript -// Add to webview-ui/src/components/package-manager/PackageManagerView.tsx -const [currentLocale, setCurrentLocale] = useState(getUserLocale()); - -// Add locale selector dropdown - -``` - ## Documentation Updates Update the documentation to reflect the correct localization behavior: @@ -380,18 +12,10 @@ You can provide metadata in multiple languages by using locale-specific files: - `metadata.fr.yml` - French metadata **Important Notes on Localization:** + - Only files with the pattern `metadata.{locale}.yml` are supported - The Package Manager will display metadata in the user's locale if available - If the user's locale is not available, it will fall back to English - The English locale (`metadata.en.yml`) is required as a fallback - Files without a locale code (e.g., just `metadata.yml`) are not supported ``` - -## Implementation Plan - -1. Add localization options and user locale detection -2. Modify MetadataScanner to use locale preference with fallback -3. Update component creation logic to handle localization -4. Add tests to verify localization behavior -5. Update documentation to reflect the correct behavior -6. (Optional) Add UI controls for locale selection \ No newline at end of file diff --git a/cline_docs/package-manager/user-guide/04-working-with-details.md b/cline_docs/package-manager/user-guide/04-working-with-details.md index 06ae2b5b3e..460b997519 100644 --- a/cline_docs/package-manager/user-guide/04-working-with-details.md +++ b/cline_docs/package-manager/user-guide/04-working-with-details.md @@ -34,20 +34,23 @@ Components within packages are grouped by their type to make them easier to find ### Common Component Types 1. **Modes** - - AI assistant personalities with specialized capabilities - - Examples: Code Mode, Architect Mode, Debug Mode + + - AI assistant personalities with specialized capabilities + - Examples: Code Mode, Architect Mode, Debug Mode 2. **MCP Servers** - - Model Context Protocol servers that provide additional functionality - - Examples: File Analyzer, Data Validator, Image Generator + + - Model Context Protocol servers that provide additional functionality + - Examples: File Analyzer, Data Validator, Image Generator 3. **Prompts** - - Pre-configured instructions for specific tasks - - Examples: Code Review, Documentation Generator, Test Case Creator + + - Pre-configured instructions for specific tasks + - Examples: Code Review, Documentation Generator, Test Case Creator 4. **Packages** - - Nested collections of related components - - Can contain any of the other component types + - Nested collections of related components + - Can contain any of the other component types ### Type Presentation @@ -136,4 +139,4 @@ If you search for "validator": --- -**Previous**: [Searching and Filtering](./03-searching-and-filtering.md) | **Next**: [Adding Packages](./05-adding-packages.md) \ No newline at end of file +**Previous**: [Searching and Filtering](./03-searching-and-filtering.md) | **Next**: [Adding Packages](./05-adding-packages.md) diff --git a/src/services/package-manager/GitFetcher.ts b/src/services/package-manager/GitFetcher.ts index 57459f8e71..bc14c19836 100644 --- a/src/services/package-manager/GitFetcher.ts +++ b/src/services/package-manager/GitFetcher.ts @@ -5,7 +5,8 @@ import * as yaml from "js-yaml" import simpleGit, { SimpleGit } from "simple-git" import { MetadataScanner } from "./MetadataScanner" import { validateAnyMetadata } from "./schemas" -import { PackageManagerItem, PackageManagerRepository, RepositoryMetadata } from "./types" +import { LocalizationOptions, PackageManagerItem, PackageManagerRepository, RepositoryMetadata } from "./types" +import { getUserLocale } from "./utils" /** * Handles fetching and caching package manager repositories @@ -14,10 +15,15 @@ export class GitFetcher { private readonly cacheDir: string private metadataScanner: MetadataScanner private git?: SimpleGit + private localizationOptions: LocalizationOptions - constructor(context: vscode.ExtensionContext) { + constructor(context: vscode.ExtensionContext, localizationOptions?: LocalizationOptions) { this.cacheDir = path.join(context.globalStorageUri.fsPath, "package-manager-cache") - this.metadataScanner = new MetadataScanner() + this.localizationOptions = localizationOptions || { + userLocale: getUserLocale(), + fallbackLocale: "en", + } + this.metadataScanner = new MetadataScanner(undefined, this.localizationOptions) } /** @@ -27,7 +33,7 @@ export class GitFetcher { private initGit(repoDir: string): void { this.git = simpleGit(repoDir) // Update MetadataScanner with new git instance - this.metadataScanner = new MetadataScanner(this.git) + this.metadataScanner = new MetadataScanner(this.git, this.localizationOptions) } /** diff --git a/src/services/package-manager/MetadataScanner.ts b/src/services/package-manager/MetadataScanner.ts index e3c0a359ae..f5e7f30d94 100644 --- a/src/services/package-manager/MetadataScanner.ts +++ b/src/services/package-manager/MetadataScanner.ts @@ -4,16 +4,29 @@ import * as vscode from "vscode" import * as yaml from "js-yaml" import { SimpleGit } from "simple-git" import { validateAnyMetadata } from "./schemas" -import { ComponentMetadata, ComponentType, LocalizedMetadata, PackageManagerItem, PackageMetadata } from "./types" +import { + ComponentMetadata, + ComponentType, + LocalizationOptions, + LocalizedMetadata, + PackageManagerItem, + PackageMetadata, +} from "./types" +import { getUserLocale } from "./utils" /** * Handles component discovery and metadata loading */ export class MetadataScanner { private readonly git?: SimpleGit + private localizationOptions: LocalizationOptions - constructor(git?: SimpleGit) { + constructor(git?: SimpleGit, localizationOptions?: LocalizationOptions) { this.git = git + this.localizationOptions = localizationOptions || { + userLocale: getUserLocale(), + fallbackLocale: "en", + } } /** @@ -35,44 +48,54 @@ export class MetadataScanner { const componentDir = path.join(rootDir, entry.name) const metadata = await this.loadComponentMetadata(componentDir) - if (metadata?.["en"]) { - const item = await this.createPackageManagerItem(metadata["en"], componentDir, repoUrl, sourceName) - if (item) { - // If this is a package, scan for subcomponents - if (this.isPackageMetadata(metadata["en"])) { - // Load metadata for items listed in package metadata - if (metadata["en"].items) { - const subcomponents = await Promise.all( - metadata["en"].items.map(async (subItem) => { - const subPath = path.join(componentDir, subItem.path) - const subMetadata = await this.loadComponentMetadata(subPath) - if (subMetadata?.["en"]) { - return { - type: subItem.type, - path: subItem.path, - metadata: subMetadata["en"], - lastUpdated: await this.getLastModifiedDate(subPath), - } - } - return null - }), - ) - item.items = subcomponents.filter((sub): sub is NonNullable => sub !== null) - } + // Skip if no metadata found at all + if (!metadata) continue - // Also scan directory for unlisted subcomponents - await this.scanPackageSubcomponents(componentDir, item) - } - items.push(item) - // Skip recursion if this is a package directory - if (this.isPackageMetadata(metadata["en"])) { - continue + // Get localized metadata with fallback + const localizedMetadata = this.getLocalizedMetadata(metadata) + if (!localizedMetadata) continue + + const item = await this.createPackageManagerItem(localizedMetadata, componentDir, repoUrl, sourceName) + if (item) { + // If this is a package, scan for subcomponents + if (this.isPackageMetadata(localizedMetadata)) { + // Load metadata for items listed in package metadata + if (localizedMetadata.items) { + const subcomponents = await Promise.all( + localizedMetadata.items.map(async (subItem) => { + const subPath = path.join(componentDir, subItem.path) + const subMetadata = await this.loadComponentMetadata(subPath) + + // Skip if no metadata found + if (!subMetadata) return null + + // Get localized metadata with fallback + const localizedSubMetadata = this.getLocalizedMetadata(subMetadata) + if (!localizedSubMetadata) return null + + return { + type: subItem.type, + path: subItem.path, + metadata: localizedSubMetadata, + lastUpdated: await this.getLastModifiedDate(subPath), + } + }), + ) + item.items = subcomponents.filter((sub): sub is NonNullable => sub !== null) } + + // Also scan directory for unlisted subcomponents + await this.scanPackageSubcomponents(componentDir, item) + } + items.push(item) + // Skip recursion if this is a package directory + if (this.isPackageMetadata(localizedMetadata)) { + continue } } // Recursively scan subdirectories only if not in a package - if (!metadata?.["en"] || !this.isPackageMetadata(metadata["en"])) { + if (!metadata || !this.isPackageMetadata(localizedMetadata)) { const subItems = await this.scanDirectory(componentDir, repoUrl, sourceName) items.push(...subItems) } @@ -84,6 +107,28 @@ export class MetadataScanner { return items } + /** + * Gets localized metadata with fallback + * @param metadata The localized metadata object + * @returns The metadata in the user's locale or fallback locale, or null if neither is available + */ + private getLocalizedMetadata(metadata: LocalizedMetadata): ComponentMetadata | null { + const { userLocale, fallbackLocale } = this.localizationOptions + + // First try user's locale + if (metadata[userLocale]) { + return metadata[userLocale] + } + + // Fall back to fallbackLocale (typically English) + if (metadata[fallbackLocale]) { + return metadata[fallbackLocale] + } + + // No suitable metadata found + return null + } + /** * Loads metadata for a component * @param componentDir The component directory @@ -226,22 +271,27 @@ export class MetadataScanner { // Try to load metadata directly const subMetadata = await this.loadComponentMetadata(subPath) - console.log(`Metadata for ${entry.name}:`, subMetadata?.["en"]) - if (subMetadata?.["en"]) { - const isListed = packageItem.items?.some((i) => i.path === relativePath) - console.log(`${entry.name} is ${isListed ? "already listed" : "not listed"}`) + if (subMetadata) { + // Get localized metadata with fallback + const localizedSubMetadata = this.getLocalizedMetadata(subMetadata) + if (localizedSubMetadata) { + console.log(`Metadata for ${entry.name}:`, localizedSubMetadata) - if (!isListed) { - const subItem = { - type: subMetadata["en"].type, - path: relativePath, - metadata: subMetadata["en"], - lastUpdated: await this.getLastModifiedDate(subPath), + const isListed = packageItem.items?.some((i) => i.path === relativePath) + console.log(`${entry.name} is ${isListed ? "already listed" : "not listed"}`) + + if (!isListed) { + const subItem = { + type: localizedSubMetadata.type, + path: relativePath, + metadata: localizedSubMetadata, + lastUpdated: await this.getLastModifiedDate(subPath), + } + packageItem.items = packageItem.items || [] + packageItem.items.push(subItem) + console.log(`Added ${entry.name} to items`) } - packageItem.items = packageItem.items || [] - packageItem.items.push(subItem) - console.log(`Added ${entry.name} to items`) } } diff --git a/src/services/package-manager/PackageManagerManager.ts b/src/services/package-manager/PackageManagerManager.ts index 9021716b6f..31802786eb 100644 --- a/src/services/package-manager/PackageManagerManager.ts +++ b/src/services/package-manager/PackageManagerManager.ts @@ -8,7 +8,9 @@ import { PackageManagerSource, ComponentType, ComponentMetadata, + LocalizationOptions, } from "./types" +import { getUserLocale } from "./utils" /** * Service for managing package manager data @@ -23,7 +25,11 @@ export class PackageManagerManager { private cache: Map = new Map() constructor(private readonly context: vscode.ExtensionContext) { - this.gitFetcher = new GitFetcher(context) + const localizationOptions: LocalizationOptions = { + userLocale: getUserLocale(), + fallbackLocale: "en", + } + this.gitFetcher = new GitFetcher(context, localizationOptions) } /** @@ -430,16 +436,6 @@ export class PackageManagerManager { this.clearCache() } - /** - * Helper method to check if an item matches the given filters - */ - /** - * Helper method to check if an item matches the given filters - */ - /** - * Helper method to check if an item matches the given filters - */ - /** * Helper method to get the sort value for an item */ diff --git a/src/services/package-manager/__tests__/GetLocalizedMetadata.test.ts b/src/services/package-manager/__tests__/GetLocalizedMetadata.test.ts new file mode 100644 index 0000000000..1843148da1 --- /dev/null +++ b/src/services/package-manager/__tests__/GetLocalizedMetadata.test.ts @@ -0,0 +1,79 @@ +import { MetadataScanner } from "../MetadataScanner" +import { ComponentMetadata, LocalizationOptions, LocalizedMetadata } from "../types" + +describe("getLocalizedMetadata", () => { + let metadataScanner: MetadataScanner + + beforeEach(() => { + // Initialize with French locale + const localizationOptions: LocalizationOptions = { + userLocale: "fr", + fallbackLocale: "en", + } + metadataScanner = new MetadataScanner(undefined, localizationOptions) + }) + + test("should use user locale when available", () => { + // Create mock metadata with both user locale and English + const metadata: LocalizedMetadata = { + en: { + name: "English Name", + description: "English Description", + version: "1.0.0", + type: "mode", + }, + fr: { + name: "Nom Français", + description: "Description Française", + version: "1.0.0", + type: "mode", + }, + } + + // Call getLocalizedMetadata + const result = (metadataScanner as any).getLocalizedMetadata(metadata) + + // Expect French metadata to be used + expect(result).toBeDefined() + expect(result.name).toBe("Nom Français") + expect(result.description).toBe("Description Française") + }) + + test("should fall back to English when user locale not available", () => { + // Create mock metadata with only English + const metadata: LocalizedMetadata = { + en: { + name: "English Name", + description: "English Description", + version: "1.0.0", + type: "mode", + }, + } + + // Call getLocalizedMetadata + const result = (metadataScanner as any).getLocalizedMetadata(metadata) + + // Expect English metadata to be used as fallback + expect(result).toBeDefined() + expect(result.name).toBe("English Name") + expect(result.description).toBe("English Description") + }) + + test("should return null when neither user locale nor fallback locale is available", () => { + // Create mock metadata with neither user locale nor English + const metadata: LocalizedMetadata = { + de: { + name: "Deutscher Name", + description: "Deutsche Beschreibung", + version: "1.0.0", + type: "mode", + }, + } + + // Call getLocalizedMetadata + const result = (metadataScanner as any).getLocalizedMetadata(metadata) + + // Expect null result + expect(result).toBeNull() + }) +}) diff --git a/src/services/package-manager/__tests__/LocalizationFallback.test.ts b/src/services/package-manager/__tests__/LocalizationFallback.test.ts new file mode 100644 index 0000000000..98e46999d6 --- /dev/null +++ b/src/services/package-manager/__tests__/LocalizationFallback.test.ts @@ -0,0 +1,9 @@ +mockFs.readdir.mockImplementation((dir, options) => { + console.log("Mock readdir called with:", dir) + const result = [ + { name: "metadata.en.yml", isFile: () => true, isDirectory: () => false }, + { name: "metadata.fr.yml", isFile: () => true, isDirectory: () => false }, + ] as any + console.log("Mock readdir returning:", result) + return Promise.resolve(result) +}) diff --git a/src/services/package-manager/constants.ts b/src/services/package-manager/constants.ts index 333ae61807..f4ea02cdc3 100644 --- a/src/services/package-manager/constants.ts +++ b/src/services/package-manager/constants.ts @@ -5,8 +5,7 @@ /** * Default package manager repository URL */ -export const DEFAULT_PACKAGE_MANAGER_REPO_URL = - "https://github.com/RooVetGit/Roo-Code/tree/main/package-manager-template" +export const DEFAULT_PACKAGE_MANAGER_REPO_URL = "https://github.com/RooVetGit/Roo-Code-Packages" /** * Default package manager repository name diff --git a/src/services/package-manager/types.ts b/src/services/package-manager/types.ts index 86c0d11a72..e9f5529c7e 100644 --- a/src/services/package-manager/types.ts +++ b/src/services/package-manager/types.ts @@ -110,3 +110,11 @@ export interface PackageManagerRepository { export type LocalizedMetadata = { [locale: string]: T } + +/** + * Options for localization handling + */ +export interface LocalizationOptions { + userLocale: string + fallbackLocale: string +} diff --git a/src/services/package-manager/utils.ts b/src/services/package-manager/utils.ts new file mode 100644 index 0000000000..2ada345e55 --- /dev/null +++ b/src/services/package-manager/utils.ts @@ -0,0 +1,13 @@ +import * as vscode from "vscode" + +/** + * Gets the user's locale from VS Code environment + * @returns The user's locale code (e.g., 'en', 'fr') + */ +export function getUserLocale(): string { + // Get from VS Code API + const vscodeLocale = vscode.env.language + + // Extract just the language part (e.g., "en-US" -> "en") + return vscodeLocale.split("-")[0].toLowerCase() +}