documentation start

This commit is contained in:
Smartsheet-JB-Brown 2025-04-14 12:37:27 -07:00
parent 2ca7a85c20
commit 71d30b835b
15 changed files with 6150 additions and 0 deletions

View file

@ -0,0 +1,22 @@
# Package Manager Documentation
This directory contains comprehensive documentation for the Roo Code Package Manager feature, including both user guides and implementation documentation.
## Table of Contents
### User Guide
1. [Introduction to Package Manager](./user-guide/01-introduction.md)
2. [Browsing Packages](./user-guide/02-browsing-packages.md)
3. [Searching and Filtering](./user-guide/03-searching-and-filtering.md)
4. [Working with Package Details](./user-guide/04-working-with-details.md)
5. [Adding Packages](./user-guide/05-adding-packages.md)
6. [Adding Custom Package Sources](./user-guide/06-adding-custom-sources.md)
### Implementation Documentation
1. [Package Manager Architecture](./implementation/01-architecture.md)
2. [Core Components](./implementation/02-core-components.md)
3. [Data Structures](./implementation/03-data-structures.md)
4. [Search and Filter Implementation](./implementation/04-search-and-filter.md)
5. [UI Component Design](./implementation/05-ui-components.md)
6. [Testing Strategy](./implementation/06-testing-strategy.md)
7. [Extending the Package Manager](./implementation/07-extending.md)

View file

@ -0,0 +1,388 @@
# Package Manager Architecture
This document provides a comprehensive overview of the Package Manager's architecture, including its components, interactions, and data flow.
## System Overview
The Package Manager is built on a modular architecture that separates concerns between data management, UI rendering, and user interactions. The system consists of several key components that work together to provide a seamless experience for discovering, browsing, and managing packages.
### High-Level Architecture
```mermaid
graph TD
User[User] -->|Interacts with| UI[Package Manager UI]
UI -->|Sends messages| MH[Message Handler]
MH -->|Processes requests| PM[PackageManagerManager]
PM -->|Loads data from| MS[MetadataScanner]
MS -->|Reads| FS[File System / Git Repositories]
PM -->|Returns filtered data| MH
MH -->|Updates state| UI
UI -->|Displays| User
```
The architecture follows a message-based pattern where:
1. The UI sends messages to the backend through a message handler
2. The backend processes these messages and returns results
3. The UI updates based on the returned data
4. Components are loosely coupled through message passing
## Component Interactions
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
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
3. **Source Management**:
- UI sends source management commands
- PackageManagerManager updates source configurations
- MetadataScanner reloads data from updated sources
## Data Flow Diagram
The following diagram illustrates the data flow through the Package Manager system:
```mermaid
graph LR
subgraph Sources
GR[Git Repositories]
FS[File System]
end
subgraph Backend
MS[MetadataScanner]
PM[PackageManagerManager]
MH[Message Handler]
end
subgraph Frontend
UI[UI Components]
State[State Management]
end
GR -->|Raw Data| MS
FS -->|Template Data| MS
MS -->|Parsed Metadata| PM
PM -->|Stored Items| PM
UI -->|User Actions| MH
MH -->|Messages| PM
PM -->|Filtered Data| MH
MH -->|Updates| State
State -->|Renders| UI
```
## Sequence Diagrams
### Package Loading Sequence
The following sequence diagram shows how packages are loaded from sources:
```mermaid
sequenceDiagram
participant User
participant UI as UI Components
participant MH as Message Handler
participant PM as PackageManagerManager
participant MS as MetadataScanner
participant FS as File System/Git
User->>UI: Open Package Manager
UI->>MH: Send init message
MH->>PM: Initialize
PM->>MS: Request metadata scan
MS->>FS: Read repository data
FS-->>MS: Return raw data
MS-->>PM: Return parsed metadata
PM-->>MH: Return initial items
MH-->>UI: Update with items
UI-->>User: Display packages
```
### Search and Filter Sequence
This sequence diagram illustrates the search and filter process:
```mermaid
sequenceDiagram
participant User
participant UI as UI Components
participant MH as Message Handler
participant PM as PackageManagerManager
User->>UI: Enter search term
UI->>MH: Send search message
MH->>PM: Apply search filter
PM->>PM: Filter items
PM-->>MH: Return filtered items
MH-->>UI: Update with filtered items
UI-->>User: Display filtered results
User->>UI: Select type filter
UI->>MH: Send type filter message
MH->>PM: Apply type filter
PM->>PM: Filter by type
PM-->>MH: Return type-filtered items
MH-->>UI: Update with type-filtered items
UI-->>User: Display type-filtered results
```
## Class Diagrams
### Core Classes
The following class diagram shows the main classes in the Package Manager system:
```mermaid
classDiagram
class PackageManagerManager {
-currentItems: PackageManagerItem[]
-sources: PackageManagerSource[]
+getItems(): PackageManagerItem[]
+filterItems(filters): PackageManagerItem[]
+addSource(url): void
+removeSource(url): void
+refreshSources(): void
}
class MetadataScanner {
+scanDirectory(path): PackageManagerItem[]
+scanRepository(url): PackageManagerItem[]
-parseMetadata(file): any
-buildComponentHierarchy(items): PackageManagerItem[]
}
class PackageManagerMessageHandler {
+handleMessage(message): any
-handleSearchMessage(message): any
-handleFilterMessage(message): any
-handleSourceMessage(message): any
}
class PackageManagerItem {
+name: string
+description: string
+type: string
+items: any[]
+tags: string[]
+matchInfo: MatchInfo
}
class PackageManagerSource {
+url: string
+name: string
+enabled: boolean
}
PackageManagerManager --> PackageManagerItem: manages
PackageManagerManager --> PackageManagerSource: configures
PackageManagerManager --> MetadataScanner: uses
PackageManagerMessageHandler --> PackageManagerManager: calls
```
### UI Component Classes
This class diagram shows the main UI components:
```mermaid
classDiagram
class PackageManagerView {
-items: PackageManagerItem[]
-filters: Filters
-activeTab: string
+render(): JSX
+handleFilterChange(): void
+handleSearch(): void
}
class PackageManagerItemCard {
-item: PackageManagerItem
-filters: Filters
+render(): JSX
+handleTagClick(): void
}
class ExpandableSection {
-title: string
-isExpanded: boolean
+toggle(): void
+render(): JSX
}
class TypeGroup {
-type: string
-items: any[]
-searchTerm: string
+render(): JSX
}
PackageManagerView --> PackageManagerItemCard: contains
PackageManagerItemCard --> ExpandableSection: contains
ExpandableSection --> TypeGroup: contains
```
## Component Responsibilities
### Backend Components
1. **MetadataScanner**
- 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
3. **packageManagerMessageHandler**
- 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
2. **PackageManagerItemCard**
- 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
4. **TypeGroup**
- Groups and displays components by type
- Formats item lists
- Highlights search matches
- Provides consistent styling
## Data Flow Patterns
### Message-Based Communication
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
}
```
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
3. **Response Structure**:
```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
2. **Frontend State**:
- Current filters and search terms
- UI state (active tab, expanded sections)
- Display preferences
3. **Persistent State**:
- 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
2. **Efficient Filtering**:
- 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
## 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
2. **Parsing Errors**:
- 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
## 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
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
3. **Custom Sources**:
- 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)

View file

@ -0,0 +1,533 @@
# Core Components
This document provides detailed information about the core components of the Package Manager system, their responsibilities, implementation details, and interactions.
## MetadataScanner
The MetadataScanner is responsible for reading and parsing package metadata from various sources, including local file systems and remote Git repositories.
### Responsibilities
- Scanning directories for package metadata files
- Parsing YAML metadata into structured objects
- Building component hierarchies
- Handling file system and Git operations
- Supporting localized metadata
### Implementation Details
```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<PackageManagerItem[]> {
// 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<PackageManagerItem[]> {
// Implementation details
}
/**
* Parses a YAML metadata file
* @param filePath Path to the metadata file
* @returns Parsed metadata object
*/
private async parseMetadataFile(filePath: string): Promise<any> {
// 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
}
}
```
### Key Algorithms
#### Directory Scanning
The directory scanning algorithm recursively traverses directories looking for metadata files:
1. Start at the root directory
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
5. Build the component hierarchy
#### Metadata Parsing
The metadata parsing process handles multiple formats and localizations:
1. Read the YAML file content
2. Parse the YAML into a JavaScript object
3. Extract the locale from the filename (e.g., `en` from `metadata.en.yml`)
4. Validate required fields (name, description, version)
5. Process optional fields (tags, author, etc.)
6. Return a structured metadata object
### Error Handling
The MetadataScanner includes robust error handling:
- Invalid YAML files are reported with specific parsing errors
- Missing required fields trigger validation errors
- File system access issues are caught and reported
- Network errors during Git operations are handled gracefully
- Partial results are returned when possible, with error flags
## PackageManagerManager
The PackageManagerManager is the central component that manages package items, applies filters, and handles package operations.
### Responsibilities
- Storing and managing package items
- Applying filters and search criteria
- Managing package sources
- Handling package operations
- Maintaining state between sessions
### Implementation Details
```typescript
class PackageManagerManager {
private currentItems: PackageManagerItem[] = [];
private sources: PackageManagerSource[] = [];
/**
* 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;
}
/**
* 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<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<PackageManagerItem[]> {
// Implementation details
}
/**
* Save state to persistent storage
*/
private saveState(): void {
// Implementation details
}
}
```
### Key Algorithms
#### Item Filtering
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
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
4. If tag filters are specified:
- Keep only items that have at least one of the specified tags
5. Return the filtered items with match information
#### Source Management
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
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
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
### State Persistence
The PackageManagerManager maintains state between sessions:
- Source configurations are stored in extension global state
- User preferences are persisted
- Cached metadata can be stored for performance
- State is loaded during initialization
- State is saved after significant changes
## packageManagerMessageHandler
The packageManagerMessageHandler is responsible for routing messages between the UI and the backend components.
### Responsibilities
- Processing messages from the UI
- Calling appropriate PackageManagerManager methods
- Returning results to the UI
- Handling errors and status updates
- Managing asynchronous operations
### Implementation Details
```typescript
/**
* Handle package manager messages
* @param message The message to handle
* @param packageManager The package manager instance
* @returns Response object
*/
export async function handlePackageManagerMessages(
message: any,
packageManager: PackageManagerManager
): Promise<any> {
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 "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...
default:
return {
type: "error",
error: `Unknown message type: ${message.type}`
};
}
}
```
### Message Types
The message handler processes several types of messages:
#### Input Messages
1. **getItems**: Request all package items
```typescript
{ type: "getItems" }
```
2. **search**: Apply search and filter criteria
```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"
}
```
4. **removeSource**: Remove a package source
```typescript
{
type: "removeSource",
url: "https://github.com/username/repo.git"
}
```
5. **refreshSources**: Refresh all sources
```typescript
{ type: "refreshSources" }
```
#### Output Messages
1. **items**: Response with all items
```typescript
{
type: "items",
data: [/* package items */]
}
```
2. **searchResults**: Response with filtered items
```typescript
{
type: "searchResults",
data: [/* filtered items */]
}
```
3. **sourceAdded**: Response after adding a source
```typescript
{
type: "sourceAdded",
data: { success: true }
}
```
4. **error**: Error response
```typescript
{
type: "error",
error: "Error message"
}
```
### Asynchronous Processing
The message handler manages asynchronous operations:
1. Asynchronous methods return promises
2. Errors are caught and returned as error messages
3. Long-running operations can provide progress updates
4. The UI can display loading indicators during processing
## UI Components
The Package Manager includes several key UI components that render the interface and handle user interactions.
### PackageManagerView
The main container component that manages the overall UI:
```tsx
const PackageManagerView: React.FC = () => {
const [items, setItems] = useState<PackageManagerItem[]>([]);
const [filters, setFilters] = useState({ type: "", search: "", tags: [] });
const [activeTab, setActiveTab] = useState<"browse" | "sources">("browse");
// Implementation details...
return (
<div className="package-manager-container">
<div className="tabs">
<button
className={activeTab === "browse" ? "active" : ""}
onClick={() => setActiveTab("browse")}
>
Browse
</button>
<button
className={activeTab === "sources" ? "active" : ""}
onClick={() => setActiveTab("sources")}
>
Sources
</button>
</div>
{activeTab === "browse" ? (
<div className="browse-container">
<FilterPanel
filters={filters}
setFilters={setFilters}
/>
<div className="results-area">
{items.map(item => (
<PackageManagerItemCard
key={item.name}
item={item}
filters={filters}
setFilters={setFilters}
activeTab={activeTab}
setActiveTab={setActiveTab}
/>
))}
</div>
</div>
) : (
<SourcesPanel />
)}
</div>
);
};
```
### Component Interactions
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
2. **State Management**:
- 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
### 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
2. **Screen Reader Support**:
- 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
## Component Integration
The core components work together to provide a complete package management experience:
### Initialization Flow
1. The Package Manager is activated
2. The PackageManagerManager loads stored state
3. The UI sends an initial "getItems" message
4. The message handler calls PackageManagerManager.getItems()
5. The UI receives and displays the items
### Search and Filter Flow
1. The user enters a search term or selects filters
2. The UI sends a "search" message with the criteria
3. The message handler calls PackageManagerManager.filterItems()
4. The PackageManagerManager applies the filters
5. The UI receives and displays the filtered items
### Source Management Flow
1. The user adds a new source
2. The UI sends an "addSource" message
3. The message handler calls PackageManagerManager.addSource()
4. The PackageManagerManager adds the source and scans for items
5. The UI receives confirmation and updates the display
## Performance Optimizations
The core components include several performance optimizations:
1. **Lazy Loading**:
- 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
3. **Efficient Filtering**:
- 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
---
**Previous**: [Package Manager Architecture](./01-architecture.md) | **Next**: [Data Structures](./03-data-structures.md)

View file

@ -0,0 +1,670 @@
# Data Structures
This document details the key data structures used in the Package Manager, including their definitions, relationships, and usage patterns.
## Package and Component Types
The Package Manager uses a type system to categorize different kinds of components:
### ComponentType Enumeration
```typescript
/**
* Supported component types
*/
export type ComponentType = "mode" | "prompt" | "package" | "mcp server";
```
These types represent the different kinds of components that can be managed by the Package Manager:
1. **mode**: AI assistant personalities with specialized capabilities
2. **prompt**: Pre-configured instructions for specific tasks
3. **package**: Collections of related components
4. **mcp server**: Model Context Protocol servers that provide additional functionality
The type system is extensible, allowing for new component types to be added in the future.
## Metadata Interfaces
The Package Manager uses a set of interfaces to define the structure of metadata for different components:
### BaseMetadata
```typescript
/**
* Base metadata interface
*/
export interface BaseMetadata {
name: string;
description: string;
version: string;
tags?: string[];
}
```
This interface defines the common properties shared by all metadata types:
- **name**: The display name of the component
- **description**: A detailed explanation of the component's purpose
- **version**: The semantic version number
- **tags**: Optional array of relevant keywords
### RepositoryMetadata
```typescript
/**
* Repository root metadata
*/
export interface RepositoryMetadata extends BaseMetadata {}
```
This interface represents the metadata for a package source repository. It currently inherits all properties from BaseMetadata without adding additional fields, but is defined separately to allow for future repository-specific extensions.
### ComponentMetadata
```typescript
/**
* Component metadata with type
*/
export interface ComponentMetadata extends BaseMetadata {
type: ComponentType;
}
```
This interface extends BaseMetadata to include a type field, which specifies the component type.
### PackageMetadata
```typescript
/**
* Package metadata with optional subcomponents
*/
export interface PackageMetadata extends ComponentMetadata {
type: "package";
items?: {
type: ComponentType;
path: string;
metadata?: ComponentMetadata;
}[];
}
```
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
### SubcomponentMetadata
```typescript
/**
* Subcomponent metadata with parent reference
*/
export interface SubcomponentMetadata extends ComponentMetadata {
parentPackage: {
name: string;
path: string;
};
}
```
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
## Item Structures
The Package Manager uses several interfaces to represent items in the UI:
### MatchInfo
```typescript
/**
* Information about why an item matched search/filter criteria
*/
export interface MatchInfo {
matched: boolean;
matchReason?: {
nameMatch?: boolean;
descriptionMatch?: boolean;
tagMatch?: boolean;
hasMatchingSubcomponents?: boolean;
};
}
```
This interface provides information about why an item matched search or filter criteria:
- **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
### PackageManagerItem
```typescript
/**
* 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;
}
```
This interface represents a complete package manager item as displayed in the UI:
- **name**: The display name of the item
- **description**: A detailed explanation of the item's purpose
- **type**: The component type
- **url**: The URL to the item's source
- **repoUrl**: The URL to the repository containing the item
- **sourceName**: Optional name of the source repository
- **author**: Optional author name
- **tags**: Optional array of relevant keywords
- **version**: Optional semantic version number
- **lastUpdated**: Optional date of last update
- **sourceUrl**: Optional URL to additional documentation
- **items**: Optional array of subcomponents
- **matchInfo**: Optional information about search/filter matches
### PackageManagerSource
```typescript
/**
* Represents a Git repository source for package manager items
*/
export interface PackageManagerSource {
url: string;
name?: string;
enabled: boolean;
}
```
This interface represents a package source repository:
- **url**: The URL to the Git repository
- **name**: Optional display name for the source
- **enabled**: Boolean indicating if the source is active
### PackageManagerRepository
```typescript
/**
* Represents a repository with its metadata and items
*/
export interface PackageManagerRepository {
metadata: RepositoryMetadata;
items: PackageManagerItem[];
url: string;
error?: string;
}
```
This interface represents a complete repository with its metadata and items:
- **metadata**: The repository metadata
- **items**: Array of items in the repository
- **url**: The URL to the repository
- **error**: Optional error message if there was a problem loading the repository
### LocalizedMetadata
```typescript
/**
* Utility type for metadata files with locale
*/
export type LocalizedMetadata<T> = {
[locale: string]: T;
};
```
This utility type represents metadata that can be localized to different languages:
- **[locale: string]**: Keys are locale identifiers (e.g., "en", "fr")
- **T**: The type of metadata being localized
## UI Component Props
The Package Manager UI components use several prop interfaces:
### PackageManagerItemCardProps
```typescript
interface PackageManagerItemCardProps {
item: PackageManagerItem;
filters: { type: string; search: string; tags: string[] };
setFilters: React.Dispatch<React.SetStateAction<{ type: string; search: string; tags: string[] }>>;
activeTab: "browse" | "sources";
setActiveTab: React.Dispatch<React.SetStateAction<"browse" | "sources">>;
}
```
This interface defines the props for the PackageManagerItemCard component:
- **item**: The package item to display
- **filters**: The current filter state
- **setFilters**: Function to update filters
- **activeTab**: The currently active tab
- **setActiveTab**: Function to change the active tab
### ExpandableSectionProps
```typescript
interface ExpandableSectionProps {
title: string;
children: React.ReactNode;
className?: string;
defaultExpanded?: boolean;
badge?: string;
}
```
This interface defines the props for the ExpandableSection component:
- **title**: The section header text
- **children**: The content to display when expanded
- **className**: Optional CSS class name
- **defaultExpanded**: Optional flag to set initial expanded state
- **badge**: Optional badge text to display
### TypeGroupProps
```typescript
interface TypeGroupProps {
type: string;
items: Array<{
name: string;
description?: string;
metadata?: any;
path?: string;
}>;
className?: string;
searchTerm?: string;
}
```
This interface defines the props for the TypeGroup component:
- **type**: The component type to display
- **items**: Array of items of this type
- **className**: Optional CSS class name
- **searchTerm**: Optional search term for highlighting matches
## Grouped Items Structure
The Package Manager uses a specialized structure for grouping items by type:
### GroupedItems
```typescript
export interface GroupedItems {
[type: string]: {
type: string;
items: Array<{
name: string;
description?: string;
metadata?: any;
path?: string;
}>;
};
}
```
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
## Filter and Sort Structures
The Package Manager uses several structures for filtering and sorting:
### Filters
```typescript
interface Filters {
type: string;
search: string;
tags: string[];
}
```
This interface represents the filter criteria:
- **type**: The component type filter
- **search**: The search term
- **tags**: Array of tag filters
### SortConfig
```typescript
interface SortConfig {
by: string;
order: "asc" | "desc";
}
```
This interface represents the sort configuration:
- **by**: The field to sort by (e.g., "name", "author")
- **order**: The sort order ("asc" for ascending, "desc" for descending)
## Message Structures
The Package Manager uses a message-based architecture for communication:
### Input Messages
```typescript
// Get all items
{ type: "getItems" }
// Apply search and filter criteria
{
type: "search",
search: string,
typeFilter: string,
tagFilters: string[]
}
// Add a new package source
{
type: "addSource",
url: string,
name?: string
}
// Remove a package source
{
type: "removeSource",
url: string
}
// Refresh all sources
{ type: "refreshSources" }
```
### Output Messages
```typescript
// Response with all items
{
type: "items",
data: PackageManagerItem[]
}
// Response with filtered items
{
type: "searchResults",
data: PackageManagerItem[]
}
// Response after adding a source
{
type: "sourceAdded",
data: { success: boolean }
}
// Error response
{
type: "error",
error: string
}
```
## Template Structure
The Package Manager uses a specific directory structure for templates:
### Basic Template Structure
```
package-manager-template/
├── metadata.en.yml # Repository metadata
├── README.md # Repository documentation
├── packages/ # Directory for package components
│ └── data-platform/ # Example package
│ └── metadata.en.yml # Package metadata
├── modes/ # Directory for mode components
│ └── developer-mode/ # Example mode
│ └── metadata.en.yml # Mode metadata
├── mcp servers/ # Directory for MCP server components
│ ├── example-server/ # Example server
│ │ └── metadata.en.yml # Server metadata
│ └── file-analyzer/ # Another example server
│ └── metadata.en.yml # Server metadata
└── groups/ # Directory for grouping components
└── data-engineering/ # Example group
└── metadata.en.yml # Group metadata
```
### Metadata File Structure
```yaml
# Repository metadata (metadata.en.yml)
name: "Package Manager Template"
description: "A template repository for creating package manager sources"
version: "1.0.0"
# Component metadata (e.g., modes/developer-mode/metadata.en.yml)
name: "Developer Mode"
description: "A specialized mode for software development tasks"
version: "1.0.0"
type: "mode"
tags:
- development
- coding
- software
```
## Data Flow and Transformations
The Package Manager transforms data through several stages:
### From File System to Metadata
1. Raw YAML files are read from the file system
2. YAML is parsed into JavaScript objects
3. Objects are validated against metadata interfaces
4. Localized metadata is combined into a single structure
### From Metadata to Items
1. Metadata objects are transformed into PackageManagerItem objects
2. File paths are converted to URLs
3. Parent-child relationships are established
4. Additional information is added (e.g., lastUpdated)
### From Items to UI
1. Items are filtered based on user criteria
2. Match information is added to items
3. Items are sorted according to user preferences
4. Items are grouped by type for display
## Data Validation
The Package Manager includes validation at several levels:
### Metadata Validation
```typescript
function validateMetadata(metadata: any): boolean {
// 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;
}
// Additional validation...
return true;
}
```
### URL Validation
```typescript
function isValidUrl(urlString: string): boolean {
try {
new URL(urlString);
return true;
} catch (e) {
return false;
}
}
```
### Tag Validation
```typescript
function validateTags(tags: any[]): string[] {
if (!Array.isArray(tags)) {
return [];
}
return tags
.filter(tag => typeof tag === "string" && tag.trim().length > 0)
.map(tag => tag.trim());
}
```
## Data Relationships
The Package Manager maintains several important relationships between data structures:
### Parent-Child Relationships
Packages can contain subcomponents, creating a hierarchical structure:
```
Package
├── Mode
├── MCP Server
├── Prompt
└── Nested Package
├── Mode
└── MCP Server
```
This relationship is represented in the data structures:
- Packages have an `items` array containing subcomponents
- Subcomponents have a `parentPackage` reference
### Source-Item Relationships
Items are associated with their source repositories:
- Each item has a `repoUrl` field pointing to its source
- Sources have a list of items they provide
- When a source is disabled, its items are hidden
### Type-Group Relationships
Items are grouped by their type for display:
- The `GroupedItems` interface organizes items by type
- Each type group contains items of that type
- The UI displays these groups separately
## Serialization and Persistence
The Package Manager serializes data for persistence:
### Source Persistence
```typescript
// Save sources to extension state
private saveState(): void {
this.context.globalState.update("packageManagerSources", this.sources);
}
// Load sources from extension state
private loadState(): void {
const savedSources = this.context.globalState.get<PackageManagerSource[]>("packageManagerSources", []);
this.sources = savedSources;
}
```
### Metadata Caching
```typescript
// Cache metadata to improve performance
private cacheMetadata(url: string, metadata: any): void {
const cacheKey = `metadata_${url}`;
this.context.globalState.update(cacheKey, {
timestamp: Date.now(),
data: metadata
});
}
// Retrieve cached metadata
private getCachedMetadata(url: string): any | null {
const cacheKey = `metadata_${url}`;
const cached = this.context.globalState.get(cacheKey);
if (!cached || Date.now() - cached.timestamp > CACHE_TTL) {
return null;
}
return cached.data;
}
```
## Data Structure Evolution
The Package Manager's data structures are designed for evolution:
### Versioning Strategy
- Interfaces include version fields
- New fields are added as optional
- Breaking changes are avoided when possible
- Migration code handles legacy data formats
### Extensibility Points
- The ComponentType can be extended with new types
- Metadata interfaces can be extended with new fields
- The message system can handle new message types
- The UI can adapt to display new data formats
---
**Previous**: [Core Components](./02-core-components.md) | **Next**: [Search and Filter Implementation](./04-search-and-filter.md)

View file

@ -0,0 +1,754 @@
# Search and Filter Implementation
This document details the implementation of search and filtering functionality in the Package Manager, including algorithms, optimization techniques, and performance considerations.
## Search Algorithm
The Package Manager implements a comprehensive search algorithm that matches user queries against multiple fields and supports hierarchical component structures.
### Search Term Matching
The core of the search functionality is the `containsSearchTerm` function, which checks if a string contains a search term:
```typescript
/**
* Checks if a string contains a search term (case insensitive)
* @param text The text to search in
* @param searchTerm The term to search for
* @returns True if the text contains the search term
*/
export function containsSearchTerm(text: string | undefined, searchTerm: string): boolean {
if (!text || !searchTerm) {
return false;
}
return text.toLowerCase().includes(searchTerm.toLowerCase());
}
```
This function:
- Handles undefined inputs gracefully
- Performs case-insensitive matching
- Uses JavaScript's native `includes` method for performance
### Item Search Implementation
The main search function applies the search term to multiple fields:
```typescript
/**
* Checks if an item matches a search term
* @param item The item to check
* @param searchTerm The search term
* @returns Match information
*/
function itemMatchesSearch(item: PackageManagerItem, searchTerm: string): MatchInfo {
if (!searchTerm) {
return { matched: true };
}
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 subcomponents
let hasMatchingSubcomponents = false;
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);
subItem.matchInfo = {
matched: subNameMatch || subDescMatch,
matchReason: subNameMatch || subDescMatch ? {
nameMatch: subNameMatch,
descriptionMatch: subDescMatch
} : undefined
};
});
}
const matched = nameMatch || descriptionMatch || authorMatch || hasMatchingSubcomponents;
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
- Returns detailed match information
### Search Optimization Techniques
The search implementation includes several optimizations:
1. **Early Termination**:
- 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
3. **Match Caching**:
- 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
## Filter Logic
The Package Manager implements multiple filter types that can be combined to narrow down results.
### Type Filtering
Type filtering restricts results to components of a specific type:
```typescript
/**
* Filters items by type
* @param items Items to filter
* @param type Type to filter by
* @returns Filtered items
*/
function filterByType(items: PackageManagerItem[], type: string): PackageManagerItem[] {
if (!type) {
return items;
}
return items.filter(item => item.type === type);
}
```
### Tag Filtering
Tag filtering shows only items with specific tags:
```typescript
/**
* Filters items by tags
* @param items Items to filter
* @param tags Tags to filter by
* @returns Filtered items
*/
function filterByTags(items: PackageManagerItem[], tags: string[]): PackageManagerItem[] {
if (!tags.length) {
return items;
}
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));
});
}
```
### Combined Filtering
The main filter function combines all filter types:
```typescript
/**
* Filters items based on criteria
* @param items Items to filter
* @param filters Filter criteria
* @returns Filtered items
*/
export function filterItems(
items: PackageManagerItem[],
filters: { type?: string; search?: string; tags?: string[] }
): PackageManagerItem[] {
if (!isFilterActive(filters)) {
return items;
}
let result = items;
// 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 tag filter
if (filters.tags?.length) {
result = filterByTags(result, filters.tags);
}
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
- Returns a new array with filtered items
### Filter Optimization Techniques
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
2. **Short-Circuit Evaluation**:
- 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
4. **Selective Processing**:
- Only processes necessary fields for each filter
- Avoids redundant calculations
## Selector Functions
The Package Manager uses selector functions to extract and transform data for the UI:
### Filter Status Selector
```typescript
/**
* Checks if any filters are active
* @param filters Filter criteria
* @returns True if any filters are active
*/
export const isFilterActive = (filters: Filters): boolean => {
return !!(filters.type || filters.search || filters.tags.length > 0);
};
```
### Display Items Selector
```typescript
/**
* Gets items for display based on filters and sort config
* @param items All items
* @param filters Filter criteria
* @param sortConfig Sort configuration
* @returns Filtered and sorted items
*/
export const getDisplayedItems = (
items: PackageManagerItem[],
filters: Filters,
sortConfig: SortConfig,
): PackageManagerItem[] => {
const filteredItems = filterItems(items, filters);
return sortItems(filteredItems, sortConfig);
};
```
### Sort Function
```typescript
/**
* Sorts items based on configuration
* @param items Items to sort
* @param config Sort configuration
* @returns Sorted items
*/
export const sortItems = (items: PackageManagerItem[], config: SortConfig): PackageManagerItem[] => {
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);
}
return config.order === "asc" ? comparison : -comparison;
});
};
```
## Grouping Implementation
The Package Manager includes functionality to group items by type:
### Group By Type Function
```typescript
/**
* Groups package items by their type
* @param items Array of items to group
* @returns Object with items grouped by type
*/
export function groupItemsByType(items: PackageManagerItem["items"] = []): GroupedItems {
if (!items?.length) {
return {};
}
return items.reduce((groups: GroupedItems, item) => {
if (!item.type) {
return groups;
}
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,
});
return groups;
}, {});
}
```
### Helper Functions
```typescript
/**
* Gets the total number of items across all groups
* @param groups Grouped items object
* @returns Total number of items
*/
export function getTotalItemCount(groups: GroupedItems): number {
return Object.values(groups).reduce((total, group) => total + group.items.length, 0);
}
/**
* Gets an array of unique types from the grouped items
* @param groups Grouped items object
* @returns Array of type strings
*/
export function getUniqueTypes(groups: GroupedItems): string[] {
return Object.keys(groups).sort();
}
```
## UI Integration
The search and filter functionality is integrated with the UI through several components:
### Search Input Component
```tsx
const SearchInput: React.FC<{
value: string;
onChange: (value: string) => void;
}> = ({ value, onChange }) => {
// Debounce search input to avoid excessive filtering
const debouncedOnChange = useDebounce(onChange, 300);
return (
<div className="search-container">
<span className="codicon codicon-search"></span>
<input
type="text"
value={value}
onChange={(e) => debouncedOnChange(e.target.value)}
placeholder="Search packages..."
className="search-input"
aria-label="Search packages"
/>
{value && (
<button
className="clear-button"
onClick={() => onChange("")}
aria-label="Clear search"
>
<span className="codicon codicon-close"></span>
</button>
)}
</div>
);
};
```
### Type Filter Component
```tsx
const TypeFilter: React.FC<{
value: string;
onChange: (value: string) => void;
types: string[];
}> = ({ value, onChange, types }) => {
return (
<div className="type-filter">
<h3>Filter by Type</h3>
<div className="filter-options">
<label className="filter-option">
<input
type="radio"
name="type-filter"
value=""
checked={value === ""}
onChange={() => onChange("")}
/>
<span>All Types</span>
</label>
{types.map((type) => (
<label key={type} className="filter-option">
<input
type="radio"
name="type-filter"
value={type}
checked={value === type}
onChange={() => onChange(type)}
/>
<span>{getTypeLabel(type)}</span>
</label>
))}
</div>
</div>
);
};
```
### Tag Filter Component
```tsx
const TagFilter: React.FC<{
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]);
}
};
return (
<div className="tag-filter">
<h3>Filter by Tags</h3>
<div className="tag-cloud">
{availableTags.map((tag) => (
<button
key={tag}
className={`tag ${selectedTags.includes(tag) ? "selected" : ""}`}
onClick={() => toggleTag(tag)}
aria-pressed={selectedTags.includes(tag)}
>
{tag}
</button>
))}
</div>
</div>
);
};
```
## Performance Considerations
The search and filter implementation includes several performance optimizations:
### Large Dataset Handling
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
2. **Progressive 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
### Search Optimizations
For efficient searching:
1. **Debounced Input**:
```typescript
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return debouncedValue;
}
```
2. **Incremental Matching**:
- 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
### 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
2. **Filter Caching**:
- 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
## Edge Cases and Error Handling
The search and filter implementation handles several edge cases:
### Empty Results
When no items match the filters:
```tsx
const NoResults: React.FC<{
filters: Filters;
clearFilters: () => void;
}> = ({ filters, clearFilters }) => {
return (
<div className="no-results">
<span className="codicon codicon-info"></span>
<h3>No matching packages found</h3>
<p>
No packages match your current filters.
{isFilterActive(filters) && (
<>
<br />
<button onClick={clearFilters} className="clear-filters-button">
Clear all filters
</button>
</>
)}
</p>
</div>
);
};
```
### Invalid Search Terms
The system handles invalid search terms:
- Empty searches show all items
- Special characters are escaped
- Very long search terms are truncated
- Malformed regex patterns are handled safely
### Filter Conflicts
When filters conflict:
- Shows a warning when appropriate
- Provides suggestions to resolve conflicts
- Falls back to reasonable defaults
- Preserves user intent when possible
## Testing Strategy
The search and filter functionality includes comprehensive tests:
### Unit Tests
```typescript
describe("Search Utils", () => {
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 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"]
}
];
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 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");
});
});
});
```
### Integration Tests
```typescript
describe("Package Manager Search Integration", () => {
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");
});
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);
// Set up manager with template data
manager["currentItems"] = [...templateItems];
});
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");
});
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);
// 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)

View file

@ -0,0 +1,878 @@
# UI Component Design
This document details the design and implementation of the Package Manager's UI components, including their structure, styling, interactions, and accessibility features.
## PackageManagerItemCard
The PackageManagerItemCard is the primary component for displaying package information in the UI.
### Component Structure
```tsx
export const PackageManagerItemCard: React.FC<PackageManagerItemCardProps> = ({
item,
filters,
setFilters,
activeTab,
setActiveTab,
}) => {
// 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";
}
};
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,
});
};
// Group items by type
const groupedItems = useMemo(() => {
if (!item.items?.length) {
return null;
}
return groupItemsByType(item.items);
}, [item.items]) as GroupedItems | null;
return (
<div className="border border-vscode-panel-border rounded-md p-4 bg-vscode-panel-background">
{/* Header section with name, author, and type badge */}
<div className="flex justify-between items-start">
<div>
<h3 className="text-lg font-semibold text-vscode-foreground">{item.name}</h3>
{item.author && <p className="text-sm text-vscode-descriptionForeground">{`by ${item.author}`}</p>}
</div>
<span className={`px-2 py-1 text-xs text-white rounded-full ${getTypeColor(item.type)}`}>
{getTypeLabel(item.type)}
</span>
</div>
{/* Description */}
<p className="my-2 text-vscode-foreground">{item.description}</p>
{/* Tags section */}
{item.tags && item.tags.length > 0 && (
<div className="flex flex-wrap gap-1 my-2">
{item.tags.map((tag) => (
<button
key={tag}
className={`px-2 py-1 text-xs rounded-full hover:bg-vscode-button-secondaryBackground ${
filters.tags.includes(tag)
? "bg-vscode-button-background text-vscode-button-foreground"
: "bg-vscode-badge-background text-vscode-badge-foreground"
}`}
onClick={() => {
if (filters.tags.includes(tag)) {
setFilters({
...filters,
tags: filters.tags.filter((t) => t !== tag),
});
} else {
setFilters({
...filters,
tags: [...filters.tags, tag],
});
if (activeTab !== "browse") {
setActiveTab("browse");
}
}
}}
title={filters.tags.includes(tag) ? `Remove tag filter: ${tag}` : `Filter by tag: ${tag}`}>
{tag}
</button>
))}
</div>
)}
{/* Footer section with metadata and action button */}
<div className="flex justify-between items-center mt-4">
<div className="flex items-center gap-4 text-sm text-vscode-descriptionForeground">
{item.version && (
<span className="flex items-center">
<span className="codicon codicon-tag mr-1"></span>
{item.version}
</span>
)}
{item.lastUpdated && (
<span className="flex items-center">
<span className="codicon codicon-calendar mr-1"></span>
{new Date(item.lastUpdated).toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
})}
</span>
)}
</div>
<Button onClick={handleOpenUrl}>
<span className="codicon codicon-link-external mr-2"></span>
{item.sourceUrl ? "View" : item.sourceName || "Source"}
</Button>
</div>
{/* Details section with subcomponents */}
{groupedItems && (
<ExpandableSection
title="Component Details"
badge={
filters.search
? (() => {
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)
}>
<div className="space-y-4">
{Object.entries(groupedItems).map(([type, group]) => (
<TypeGroup key={type} type={type} items={group.items} searchTerm={filters.search} />
))}
</div>
</ExpandableSection>
)}
</div>
);
};
```
### 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
2. **Interactive Elements**:
- 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
4. **VSCode Integration**:
- Uses VSCode theme variables for colors
- Matches VSCode UI patterns
- Integrates with VSCode messaging system
## ExpandableSection
The ExpandableSection component provides a collapsible container for content that doesn't need to be visible at all times.
### Component Structure
```tsx
export const ExpandableSection: React.FC<ExpandableSectionProps> = ({
title,
children,
className,
defaultExpanded = false,
badge,
}) => {
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
return (
<div className={cn("border-t border-vscode-panel-border mt-4", className)}>
<button
className="w-full flex items-center justify-between py-2 text-sm text-vscode-foreground hover:text-vscode-textLink"
onClick={() => setIsExpanded(!isExpanded)}
aria-expanded={isExpanded}
aria-controls="details-content">
<span className="font-medium flex items-center">
<span className="codicon codicon-list-unordered mr-1"></span>
{title}
</span>
<div className="flex items-center">
{badge && (
<span className="mr-2 text-xs bg-vscode-badge-background text-vscode-badge-foreground px-1 py-0.5 rounded">
{badge}
</span>
)}
<span
className={cn(
"codicon",
isExpanded ? "codicon-chevron-down" : "codicon-chevron-right",
"transition-transform duration-200",
)}
/>
</div>
</button>
<div
id="details-content"
className={cn(
"overflow-hidden transition-[max-height,opacity] duration-200 ease-in-out",
isExpanded ? "max-h-[500px] opacity-100" : "max-h-0 opacity-0",
)}
role="region"
aria-labelledby="details-button">
<div className="py-2 px-1 bg-vscode-panel-background rounded-sm">{children}</div>
</div>
</div>
);
};
```
### Design Considerations
1. **Animation**:
- 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
3. **Flexibility**:
- 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
## TypeGroup
The TypeGroup component displays a collection of items of the same type, with special handling for search matches.
### Component Structure
```tsx
export const TypeGroup: React.FC<TypeGroupProps> = ({ 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`;
}
};
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);
};
return (
<div className={cn("mb-4", className)}>
<h4 className="text-sm font-medium text-vscode-foreground mb-2">{getTypeLabel(type)}</h4>
<ol className="list-decimal list-inside space-y-1">
{items.map((item, index) => {
const matches = itemMatchesSearch(item);
return (
<li
key={`${item.path || index}`}
className={cn(
"text-sm pl-1",
matches ? "text-vscode-foreground font-medium" : "text-vscode-foreground",
)}
title={item.path}>
<span className={cn("font-medium", matches ? "text-vscode-textLink" : "")}>
{item.name}
</span>
{item.description && (
<span className="text-vscode-descriptionForeground"> - {item.description}</span>
)}
{matches && (
<span className="ml-2 text-xs bg-vscode-badge-background text-vscode-badge-foreground px-1 py-0.5 rounded">
match
</span>
)}
</li>
);
})}
</ol>
</div>
);
};
```
### Design Considerations
1. **List Presentation**:
- 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
3. **Information Display**:
- 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
## Filter Components
The Package Manager includes several components for filtering and searching.
### SearchInput
```tsx
const SearchInput: React.FC<{
value: string;
onChange: (value: string) => void;
}> = ({ value, onChange }) => {
// Debounce search input to avoid excessive filtering
const debouncedOnChange = useDebounce(onChange, 300);
return (
<div className="search-container">
<span className="codicon codicon-search"></span>
<input
type="text"
value={value}
onChange={(e) => debouncedOnChange(e.target.value)}
placeholder="Search packages..."
className="search-input"
aria-label="Search packages"
/>
{value && (
<button
className="clear-button"
onClick={() => onChange("")}
aria-label="Clear search"
>
<span className="codicon codicon-close"></span>
</button>
)}
</div>
);
};
```
### TypeFilterGroup
```tsx
const TypeFilterGroup: React.FC<{
selectedType: string;
onChange: (type: string) => void;
availableTypes: string[];
}> = ({ selectedType, onChange, availableTypes }) => {
return (
<div className="filter-group">
<h3 className="filter-heading">Filter by Type</h3>
<div className="filter-options">
<label className="filter-option">
<input
type="radio"
name="type-filter"
value=""
checked={selectedType === ""}
onChange={() => onChange("")}
/>
<span>All Types</span>
</label>
{availableTypes.map((type) => (
<label key={type} className="filter-option">
<input
type="radio"
name="type-filter"
value={type}
checked={selectedType === type}
onChange={() => onChange(type)}
/>
<span>{getTypeLabel(type)}</span>
</label>
))}
</div>
</div>
);
};
```
### TagFilterGroup
```tsx
const TagFilterGroup: React.FC<{
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]);
}
};
return (
<div className="filter-group">
<h3 className="filter-heading">Filter by Tags</h3>
<div className="tag-cloud">
{availableTags.map((tag) => (
<button
key={tag}
className={`tag ${selectedTags.includes(tag) ? "selected" : ""}`}
onClick={() => toggleTag(tag)}
aria-pressed={selectedTags.includes(tag)}
>
{tag}
</button>
))}
</div>
</div>
);
};
```
## Styling Approach
The Package Manager UI uses a combination of Tailwind CSS and VSCode theme variables for styling.
### VSCode Theme Integration
The components use VSCode theme variables to ensure they match the user's selected theme:
```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);
}
.package-description {
color: var(--vscode-descriptionForeground);
}
.package-link {
color: var(--vscode-textLink-foreground);
}
.package-link:hover {
color: var(--vscode-textLink-activeForeground);
}
```
### Tailwind CSS Usage
Tailwind CSS is used for utility-based styling:
```tsx
// Example of Tailwind CSS usage
<div className="flex justify-between items-center p-4 border rounded-md">
<h3 className="text-lg font-semibold">{item.name}</h3>
<span className="px-2 py-1 text-xs text-white rounded-full bg-blue-600">
{getTypeLabel(item.type)}
</span>
</div>
```
### Custom Utility Functions
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";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
```
## Responsive Design
The Package Manager UI is designed to work across different viewport sizes:
### Layout Adjustments
```tsx
// Example of responsive layout
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{items.map(item => (
<PackageManagerItemCard key={item.name} item={item} />
))}
</div>
```
### Mobile Considerations
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
2. **Touch Optimization**:
- 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
## Accessibility Features
The Package Manager UI includes several accessibility features:
### Keyboard Navigation
```tsx
// Example of keyboard navigation support
<button
className="filter-button"
onClick={handleClick}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleClick();
}
}}
tabIndex={0}
role="checkbox"
aria-checked={isSelected}
>
{label}
</button>
```
### Screen Reader Support
```tsx
// Example of screen reader support
<div
role="region"
aria-label="Package details"
aria-expanded={isExpanded}
>
<button
aria-controls="details-content"
aria-expanded={isExpanded}
onClick={toggleExpanded}
>
{isExpanded ? "Hide details" : "Show details"}
</button>
<div id="details-content" hidden={!isExpanded}>
{/* Details content */}
</div>
</div>
```
### Focus Management
```tsx
// Example of focus management
const buttonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (isOpen && buttonRef.current) {
buttonRef.current.focus();
}
}, [isOpen]);
return (
<button ref={buttonRef} className="focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
{label}
</button>
);
```
### Color Contrast
The UI ensures sufficient color contrast for all text:
- Text uses VSCode theme variables that maintain proper contrast
- Interactive elements have clear focus states
- Color is not the only means of conveying information
## Animation and Transitions
The Package Manager UI uses subtle animations to enhance the user experience:
### Expand/Collapse Animation
```tsx
// Example of expand/collapse animation
<div
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}
</div>
```
### Hover Effects
```tsx
// Example of hover effects
<button className="px-2 py-1 rounded-md transition-colors duration-150 hover:bg-vscode-button-hoverBackground">
{label}
</button>
```
### Loading States
```tsx
// Example of loading state animation
<div className="loading-indicator">
<div className="spinner animate-spin h-5 w-5 border-2 border-t-transparent rounded-full"></div>
<span>Loading packages...</span>
</div>
```
## Error Handling in UI
The Package Manager UI includes graceful error handling:
### Error States
```tsx
// Example of error state display
const ErrorDisplay: React.FC<{ error: string; retry: () => void }> = ({ error, retry }) => {
return (
<div className="error-container p-4 border border-red-500 rounded-md bg-red-50 text-red-700">
<div className="flex items-center">
<span className="codicon codicon-error mr-2"></span>
<h3 className="font-medium">Error loading packages</h3>
</div>
<p className="mt-2 mb-4">{error}</p>
<button
className="px-3 py-1 bg-red-600 text-white rounded-md hover:bg-red-700"
onClick={retry}
>
Retry
</button>
</div>
);
};
```
### Empty States
```tsx
// Example of empty state display
const EmptyState: React.FC<{ message: string }> = ({ message }) => {
return (
<div className="empty-state p-8 text-center text-vscode-descriptionForeground">
<div className="codicon codicon-info text-4xl mb-2"></div>
<p>{message}</p>
</div>
);
};
```
### Loading States
```tsx
// Example of loading state with skeleton
const PackageCardSkeleton: React.FC = () => {
return (
<div className="border border-vscode-panel-border rounded-md p-4 bg-vscode-panel-background animate-pulse">
<div className="flex justify-between items-start">
<div className="w-2/3">
<div className="h-6 bg-vscode-panel-border rounded"></div>
<div className="h-4 w-1/3 bg-vscode-panel-border rounded mt-2"></div>
</div>
<div className="h-6 w-16 bg-vscode-panel-border rounded-full"></div>
</div>
<div className="h-4 bg-vscode-panel-border rounded mt-4"></div>
<div className="h-4 bg-vscode-panel-border rounded mt-2 w-5/6"></div>
<div className="flex gap-2 mt-4">
<div className="h-6 w-16 bg-vscode-panel-border rounded-full"></div>
<div className="h-6 w-16 bg-vscode-panel-border rounded-full"></div>
</div>
</div>
);
};
```
## Component Testing
The Package Manager UI components include comprehensive tests:
### Unit Tests
```typescript
// Example of component unit test
describe("PackageManagerItemCard", () => {
const mockItem: 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",
lastUpdated: "2025-04-01"
};
const mockFilters = { type: "", search: "", tags: [] };
const mockSetFilters = jest.fn();
const mockSetActiveTab = jest.fn();
it("renders correctly", () => {
render(
<PackageManagerItemCard
item={mockItem}
filters={mockFilters}
setFilters={mockSetFilters}
activeTab="browse"
setActiveTab={mockSetActiveTab}
/>
);
expect(screen.getByText("Test Package")).toBeInTheDocument();
expect(screen.getByText("A test package")).toBeInTheDocument();
expect(screen.getByText("Package")).toBeInTheDocument();
});
it("handles tag clicks", () => {
render(
<PackageManagerItemCard
item={mockItem}
filters={mockFilters}
setFilters={mockSetFilters}
activeTab="browse"
setActiveTab={mockSetActiveTab}
/>
);
fireEvent.click(screen.getByText("test"));
expect(mockSetFilters).toHaveBeenCalledWith({
type: "",
search: "",
tags: ["test"]
});
});
});
```
### Snapshot Tests
```typescript
// Example of snapshot test
it("matches snapshot", () => {
const { container } = render(
<PackageManagerItemCard
item={mockItem}
filters={mockFilters}
setFilters={mockSetFilters}
activeTab="browse"
setActiveTab={mockSetActiveTab}
/>
);
expect(container).toMatchSnapshot();
});
```
### Accessibility Tests
```typescript
// Example of accessibility test
it("meets accessibility requirements", async () => {
const { container } = render(
<PackageManagerItemCard
item={mockItem}
filters={mockFilters}
setFilters={mockSetFilters}
activeTab="browse"
setActiveTab={mockSetActiveTab}
/>
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
```
---
**Previous**: [Search and Filter Implementation](./04-search-and-filter.md) | **Next**: [Testing Strategy](./06-testing-strategy.md)

View file

@ -0,0 +1,885 @@
# Testing Strategy
This document outlines the comprehensive testing strategy for the Package Manager, including unit tests, integration tests, and test data management.
## Testing Philosophy
The Package Manager follows a multi-layered testing approach to ensure reliability and maintainability:
1. **Unit Testing**: Testing individual components in isolation
2. **Integration Testing**: Testing interactions between components
3. **End-to-End Testing**: Testing complete user workflows
4. **Test-Driven Development**: Writing tests before implementation when appropriate
5. **Continuous Testing**: Running tests automatically on code changes
## Unit Tests
Unit tests focus on testing individual functions, classes, and components in isolation.
### Backend Unit Tests
Backend unit tests verify the functionality of core services and utilities:
#### MetadataScanner Tests
```typescript
describe("MetadataScanner", () => {
let scanner: 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(`
name: "Test Package"
description: "A test package"
version: "1.0.0"
type: "package"
`));
});
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"
});
});
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();
});
});
describe("scanDirectory", () => {
// Tests for directory scanning
});
});
```
#### PackageManagerManager Tests
```typescript
describe("PackageManagerManager", () => {
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;
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[];
const result = manager.filterItems({ type: "mode" });
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[];
const result = manager.filterItems({ search: "alpha" });
expect(result).toHaveLength(1);
expect(result[0].name).toBe("Alpha Item");
});
// More filter tests...
});
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);
});
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);
});
});
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
}
});
});
// More search matching tests...
});
});
```
### Frontend Unit Tests
Frontend unit tests verify the functionality of UI components:
#### PackageManagerItemCard Tests
```typescript
describe("PackageManagerItemCard", () => {
const mockItem: 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",
lastUpdated: "2025-04-01"
};
const mockFilters = { type: "", search: "", tags: [] };
const mockSetFilters = jest.fn();
const mockSetActiveTab = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
});
it("renders correctly", () => {
render(
<PackageManagerItemCard
item={mockItem}
filters={mockFilters}
setFilters={mockSetFilters}
activeTab="browse"
setActiveTab={mockSetActiveTab}
/>
);
expect(screen.getByText("Test Package")).toBeInTheDocument();
expect(screen.getByText("A test package")).toBeInTheDocument();
expect(screen.getByText("Package")).toBeInTheDocument();
});
it("handles tag clicks", () => {
render(
<PackageManagerItemCard
item={mockItem}
filters={mockFilters}
setFilters={mockSetFilters}
activeTab="browse"
setActiveTab={mockSetActiveTab}
/>
);
fireEvent.click(screen.getByText("test"));
expect(mockSetFilters).toHaveBeenCalledWith({
type: "",
search: "",
tags: ["test"]
});
});
// More component tests...
});
```
#### ExpandableSection Tests
```typescript
describe("ExpandableSection", () => {
it("renders collapsed by default", () => {
render(
<ExpandableSection title="Test Section">
<div>Test Content</div>
</ExpandableSection>
);
expect(screen.getByText("Test Section")).toBeInTheDocument();
expect(screen.queryByText("Test Content")).not.toBeVisible();
});
it("expands when clicked", () => {
render(
<ExpandableSection title="Test Section">
<div>Test Content</div>
</ExpandableSection>
);
fireEvent.click(screen.getByText("Test Section"));
expect(screen.getByText("Test Content")).toBeVisible();
});
it("can be expanded by default", () => {
render(
<ExpandableSection title="Test Section" defaultExpanded={true}>
<div>Test Content</div>
</ExpandableSection>
);
expect(screen.getByText("Test Content")).toBeVisible();
});
// More component tests...
});
```
#### TypeGroup Tests
```typescript
describe("TypeGroup", () => {
const mockItems = [
{ name: "Item 1", description: "Description 1" },
{ name: "Item 2", description: "Description 2" }
];
it("renders type heading and items", () => {
render(<TypeGroup type="mode" items={mockItems} />);
expect(screen.getByText("Modes")).toBeInTheDocument();
expect(screen.getByText("Item 1")).toBeInTheDocument();
expect(screen.getByText("Item 2")).toBeInTheDocument();
});
it("highlights items matching search term", () => {
render(<TypeGroup type="mode" items={mockItems} searchTerm="item 1" />);
const item1 = screen.getByText("Item 1");
const item2 = screen.getByText("Item 2");
expect(item1.className).toContain("text-vscode-textLink");
expect(item2.className).not.toContain("text-vscode-textLink");
expect(screen.getByText("match")).toBeInTheDocument();
});
// More component tests...
});
```
## Integration Tests
Integration tests verify that different components work together correctly.
### Backend Integration Tests
```typescript
describe("Package Manager Integration", () => {
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");
});
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);
// 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: []
};
const result = await handlePackageManagerMessages(message, manager);
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: []
};
const result = await handlePackageManagerMessages(message, manager);
expect(result.type).toBe("searchResults");
expect(result.data.every(item => item.type === "mode")).toBe(true);
});
// 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: []
};
const result = await 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);
});
// More end-to-end flow tests...
});
});
```
### Frontend Integration Tests
```typescript
describe("Package Manager UI Integration", () => {
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"],
items: [
{
type: "mode",
path: "/test/path",
metadata: {
name: "Test Mode",
description: "A test mode",
type: "mode"
}
}
]
},
{
name: "Another Package",
description: "Another test package",
type: "mode",
url: "https://example.com",
repoUrl: "https://github.com/example/repo",
tags: ["example"]
}
];
beforeEach(() => {
// Mock VSCode API
(vscode.postMessage as jest.Mock).mockClear();
});
it("should filter items when search is entered", async () => {
render(<PackageManagerView initialItems={mockItems} />);
// Both packages should be visible initially
expect(screen.getByText("Test Package")).toBeInTheDocument();
expect(screen.getByText("Another Package")).toBeInTheDocument();
// Enter search term
const searchInput = screen.getByPlaceholderText("Search packages...");
fireEvent.change(searchInput, { target: { value: "another" } });
// Wait for debounce
await waitFor(() => {
expect(screen.queryByText("Test Package")).not.toBeInTheDocument();
expect(screen.getByText("Another Package")).toBeInTheDocument();
});
});
it("should expand details when search matches subcomponents", async () => {
render(<PackageManagerView initialItems={mockItems} />);
// Enter search term that matches a subcomponent
const searchInput = screen.getByPlaceholderText("Search packages...");
fireEvent.change(searchInput, { target: { value: "test mode" } });
// Wait for debounce and expansion
await waitFor(() => {
expect(screen.getByText("Test Mode")).toBeInTheDocument();
expect(screen.getByText("A test mode")).toBeInTheDocument();
});
// Check that the match is highlighted
const modeElement = screen.getByText("Test Mode");
expect(modeElement.className).toContain("text-vscode-textLink");
});
// More UI integration tests...
});
```
## Test Data Management
The Package Manager uses several approaches to manage test data:
### Mock Data
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...
];
```
### Test Fixtures
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"
},
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"
}
}
]
}
};
```
### Template Data
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");
});
```
### Test Data Generators
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"];
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
};
});
}
function generateSubcomponents(count: number): PackageManagerItem["items"] {
const types: ComponentType[] = ["mode", "mcp server", "prompt"];
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
}
};
});
}
```
## Test Organization
The Package Manager tests are organized by functionality rather than by file structure:
### Consolidated Test Files
```
src/services/package-manager/__tests__/
├── PackageManager.consolidated.test.ts # Combined tests
├── searchUtils.test.ts # Search utility tests
└── PackageSubcomponents.test.ts # Subcomponent tests
```
### Test Structure
Tests are organized into logical groups:
```typescript
describe("Package Manager", () => {
// Shared setup
describe("Direct Filtering", () => {
// Tests for filtering functionality
});
describe("Message Handler Integration", () => {
// Tests for message handling
});
describe("Sorting", () => {
// Tests for sorting functionality
});
});
```
## Test Coverage
The Package Manager maintains high test coverage:
### Coverage Goals
- **Backend Logic**: 90%+ coverage
- **UI Components**: 80%+ coverage
- **Integration Points**: 85%+ coverage
### Coverage Reporting
```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
}
}
};
```
### Critical Path Testing
Critical paths have additional test coverage:
1. **Search and Filter**: Comprehensive tests for all filter combinations
2. **Message Handling**: Tests for all message types and error conditions
3. **UI Interactions**: Tests for all user interaction flows
## Test Performance
The Package Manager tests are optimized for performance:
### Fast Unit Tests
```typescript
// Fast unit tests with minimal dependencies
describe("containsSearchTerm", () => {
it("should return true for exact matches", () => {
expect(containsSearchTerm("hello world", "hello")).toBe(true);
});
// More tests...
});
```
### Optimized Integration Tests
```typescript
// Optimized integration tests
describe("Package Manager Integration", () => {
// 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];
});
// Tests...
});
```
### Parallel Test Execution
```typescript
// jest.config.js
module.exports = {
// ...other config
maxWorkers: "50%", // Use 50% of available cores
maxConcurrency: 5 // Run up to 5 tests concurrently
};
```
## Continuous Integration
The Package Manager tests are integrated into the CI/CD pipeline:
### GitHub Actions Workflow
```yaml
# .github/workflows/test.yml
name: Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Upload coverage
uses: codecov/codecov-action@v2
with:
file: ./coverage/lcov.info
```
### Pre-commit Hooks
```json
// package.json
{
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"jest --findRelatedTests"
]
}
}
```
## Test Debugging
The Package Manager includes tools for debugging tests:
### Debug Logging
```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:*";
// Test logic...
// Restore debug setting
process.env.DEBUG = originalDebug;
});
});
```
### Visual Debugging
```typescript
// Visual debugging for UI tests
describe("UI component test", () => {
it("should render correctly", async () => {
const { container } = render(<PackageManagerItemCard item={mockItem} />);
// Save screenshot for visual debugging
if (process.env.SAVE_SCREENSHOTS) {
const screenshot = await page.screenshot();
fs.writeFileSync("./screenshots/item-card.png", screenshot);
}
// Test assertions...
});
});
```
## Test Documentation
The Package Manager tests include comprehensive documentation:
### Test Comments
```typescript
/**
* Tests the search functionality with various edge cases
*
* Edge cases covered:
* - Empty search term
* - Case sensitivity
* - Special characters
* - Very long search terms
* - Matching in subcomponents
*/
describe("Search functionality", () => {
// 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...
});
});
```
---
**Previous**: [UI Component Design](./05-ui-components.md) | **Next**: [Extending the Package Manager](./07-extending.md)

View file

@ -0,0 +1,956 @@
# Extending the Package Manager
This document provides guidance on extending the Package Manager with new features, component types, and customizations.
## Adding New Component Types
The Package Manager is designed to be extensible, allowing for the addition of new component types beyond the default ones (mode, mcp server, prompt, package).
### Extending the ComponentType
To add a new component type:
1. **Update the ComponentType Type**:
```typescript
/**
* Supported component types
*/
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";
}
};
```
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";
}
};
```
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`;
}
};
```
### Directory Structure for New Types
When adding a new component type, follow this directory structure in your package source repository:
```
repository-root/
├── metadata.en.yml
├── your-new-type/ # Directory for your new component type
│ ├── component-1/
│ │ └── metadata.en.yml
│ └── component-2/
│ └── metadata.en.yml
└── ...
```
### Metadata for New Types
The metadata for your new component type should follow the standard format:
```yaml
name: "Your Component Name"
description: "Description of your component"
version: "1.0.0"
type: "your-new-type"
tags:
- relevant-tag-1
- relevant-tag-2
```
### UI Considerations for New Types
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
2. **Type-Specific Rendering**:
- 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
```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";
}
};
```
## Creating Custom Templates
You can create custom templates to provide a starting point for users creating new components.
### Template Structure
A custom template should follow this structure:
```
custom-template/
├── metadata.en.yml
├── README.md
└── [component-specific files]
```
### Template Metadata
The template metadata should include:
```yaml
name: "Your Template Name"
description: "Description of your template"
version: "1.0.0"
type: "your-component-type"
template: true
templateFor: "your-component-type"
```
### Template Registration
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);
};
```
### Template Usage
Users can create new components from your template:
```typescript
// In the UI
const createFromTemplate = (templateName: string) => {
vscode.postMessage({
type: "createFromTemplate",
templateName
});
};
```
## Implementing New Features
The Package Manager is designed to be extended with new features. Here's how to implement common types of features:
### Adding a New Filter Type
To add a new filter type (beyond type, search, and tags):
1. **Update the Filters Interface**:
```typescript
interface Filters {
type: string;
search: string;
tags: string[];
yourNewFilter: string; // Add your new filter
}
```
2. **Update the Filter Function**:
```typescript
export function filterItems(
items: PackageManagerItem[],
filters: {
type?: string;
search?: string;
tags?: string[];
yourNewFilter?: string; // Add your new filter
}
): PackageManagerItem[] {
// Existing filter logic...
// Add your new filter logic
if (filters.yourNewFilter) {
result = result.filter(item => {
// Your filter implementation
return yourFilterLogic(item, filters.yourNewFilter);
});
}
return result;
}
```
3. **Add UI Controls**:
```tsx
const YourNewFilterControl: React.FC<{
value: string;
onChange: (value: string) => void;
}> = ({ value, onChange }) => {
return (
<div className="filter-group">
<h3 className="filter-heading">Your New Filter</h3>
{/* Your filter UI controls */}
</div>
);
};
```
4. **Integrate with the Main UI**:
```tsx
<FilterPanel>
<TypeFilterGroup
selectedType={filters.type}
onChange={handleTypeChange}
availableTypes={availableTypes}
/>
<SearchInput
value={filters.search}
onChange={handleSearchChange}
/>
<TagFilterGroup
selectedTags={filters.tags}
onChange={handleTagsChange}
availableTags={availableTags}
/>
<YourNewFilterControl
value={filters.yourNewFilter}
onChange={handleYourNewFilterChange}
/>
</FilterPanel>
```
### Adding a New View Mode
To add a new view mode (beyond the card view):
1. **Add a View Mode State**:
```typescript
type ViewMode = "card" | "list" | "yourNewView";
const [viewMode, setViewMode] = useState<ViewMode>("card");
```
2. **Create the View Component**:
```tsx
const YourNewView: React.FC<{
items: PackageManagerItem[];
filters: Filters;
setFilters: (filters: Filters) => void;
}> = ({ items, filters, setFilters }) => {
return (
<div className="your-new-view">
{/* Your view implementation */}
</div>
);
};
```
3. **Add View Switching Controls**:
```tsx
const ViewModeSelector: React.FC<{
viewMode: ViewMode;
setViewMode: (mode: ViewMode) => void;
}> = ({ viewMode, setViewMode }) => {
return (
<div className="view-mode-selector">
<button
className={`view-mode-button ${viewMode === "card" ? "active" : ""}`}
onClick={() => setViewMode("card")}
aria-pressed={viewMode === "card"}
title="Card View"
>
<span className="codicon codicon-preview"></span>
</button>
<button
className={`view-mode-button ${viewMode === "list" ? "active" : ""}`}
onClick={() => setViewMode("list")}
aria-pressed={viewMode === "list"}
title="List View"
>
<span className="codicon codicon-list-flat"></span>
</button>
<button
className={`view-mode-button ${viewMode === "yourNewView" ? "active" : ""}`}
onClick={() => setViewMode("yourNewView")}
aria-pressed={viewMode === "yourNewView"}
title="Your New View"
>
<span className="codicon codicon-your-icon"></span>
</button>
</div>
);
};
```
4. **Integrate with the Main UI**:
```tsx
<div className="package-manager-container">
<div className="toolbar">
<ViewModeSelector viewMode={viewMode} setViewMode={setViewMode} />
{/* Other toolbar items */}
</div>
<div className="content">
{viewMode === "card" && (
<CardView items={items} filters={filters} setFilters={setFilters} />
)}
{viewMode === "list" && (
<ListView items={items} filters={filters} setFilters={setFilters} />
)}
{viewMode === "yourNewView" && (
<YourNewView items={items} filters={filters} setFilters={setFilters} />
)}
</div>
</div>
```
### Adding Custom Actions
To add custom actions for package items:
1. **Create an Action Handler**:
```typescript
const handleCustomAction = (item: PackageManagerItem) => {
vscode.postMessage({
type: "customAction",
item: item.name,
itemType: item.type
});
};
```
2. **Add Action Button to the UI**:
```tsx
<Button
onClick={() => handleCustomAction(item)}
className="custom-action-button"
>
<span className="codicon codicon-your-icon mr-2"></span>
Your Custom Action
</Button>
```
3. **Handle the Action in the Message Handler**:
```typescript
case "customAction":
// Handle the custom action
const { item, itemType } = message;
// Your custom action implementation
return {
type: "customActionResult",
success: true,
data: { /* result data */ }
};
```
## Customizing the UI
The Package Manager UI can be customized in several ways:
### Custom Styling
To customize the styling:
1. **Add Custom CSS Variables**:
```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;
}
```
2. **Use Custom Classes**:
```tsx
<div className="your-custom-component">
<div className="your-custom-header">
{/* Your custom UI */}
</div>
</div>
```
3. **Add Custom Themes**:
```typescript
type Theme = "default" | "compact" | "detailed" | "yourCustomTheme";
const [theme, setTheme] = useState<Theme>("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";
}
};
```
### Custom Components
To replace or extend existing components:
1. **Create a Custom Component**:
```tsx
const CustomPackageCard: React.FC<PackageManagerItemCardProps> = (props) => {
// Your custom implementation
return (
<div className="custom-package-card">
{/* Your custom UI */}
<h3>{props.item.name}</h3>
{/* Additional custom elements */}
<div className="custom-footer">
{/* Custom footer content */}
</div>
</div>
);
};
```
2. **Use Component Injection**:
```tsx
interface ComponentOverrides {
PackageCard?: React.ComponentType<PackageManagerItemCardProps>;
ExpandableSection?: React.ComponentType<ExpandableSectionProps>;
TypeGroup?: React.ComponentType<TypeGroupProps>;
}
const PackageManagerView: React.FC<{
initialItems: PackageManagerItem[];
componentOverrides?: ComponentOverrides;
}> = ({ initialItems, componentOverrides = {} }) => {
// Component selection logic
const PackageCard = componentOverrides.PackageCard || PackageManagerItemCard;
return (
<div className="package-manager">
{items.map(item => (
<PackageCard
key={item.name}
item={item}
filters={filters}
setFilters={setFilters}
activeTab={activeTab}
setActiveTab={setActiveTab}
/>
))}
</div>
);
};
```
### Custom Layouts
To implement custom layouts:
1. **Create a Layout Component**:
```tsx
const CustomLayout: React.FC<{
sidebar: React.ReactNode;
content: React.ReactNode;
footer?: React.ReactNode;
}> = ({ sidebar, content, footer }) => {
return (
<div className="custom-layout">
<div className="custom-sidebar">{sidebar}</div>
<div className="custom-content">{content}</div>
{footer && <div className="custom-footer">{footer}</div>}
</div>
);
};
```
2. **Use the Layout in the Main UI**:
```tsx
<CustomLayout
sidebar={
<FilterPanel
filters={filters}
setFilters={setFilters}
availableTypes={availableTypes}
availableTags={availableTags}
/>
}
content={
<div className="results-area">
{filteredItems.map(item => (
<PackageManagerItemCard
key={item.name}
item={item}
filters={filters}
setFilters={setFilters}
activeTab={activeTab}
setActiveTab={setActiveTab}
/>
))}
</div>
}
footer={
<div className="status-bar">
{`Showing ${filteredItems.length} of ${items.length} packages`}
</div>
}
/>
```
## Extending Backend Functionality
The Package Manager backend can be extended with new functionality:
### Custom Source Providers
To add support for new source types:
1. **Create a Source Provider Interface**:
```typescript
interface SourceProvider {
type: string;
canHandle(url: string): boolean;
fetchItems(url: string): Promise<PackageManagerItem[]>;
}
```
2. **Implement a Custom Provider**:
```typescript
class CustomSourceProvider implements SourceProvider {
type = "custom";
canHandle(url: string): boolean {
return url.startsWith("custom://");
}
async fetchItems(url: string): Promise<PackageManagerItem[]> {
// Your custom implementation
// Fetch items from your custom source
return items;
}
}
```
3. **Register the Provider**:
```typescript
// In your extension code
const registerSourceProviders = (packageManager: PackageManagerManager) => {
packageManager.registerSourceProvider(new CustomSourceProvider());
};
```
### Custom Metadata Processors
To add support for custom metadata formats:
1. **Create a Metadata Processor Interface**:
```typescript
interface MetadataProcessor {
canProcess(filePath: string): boolean;
process(filePath: string, content: string): Promise<any>;
}
```
2. **Implement a Custom Processor**:
```typescript
class CustomMetadataProcessor implements MetadataProcessor {
canProcess(filePath: string): boolean {
return filePath.endsWith(".custom");
}
async process(filePath: string, content: string): Promise<any> {
// Your custom processing logic
return processedMetadata;
}
}
```
3. **Register the Processor**:
```typescript
// In your extension code
const registerMetadataProcessors = (metadataScanner: MetadataScanner) => {
metadataScanner.registerProcessor(new CustomMetadataProcessor());
};
```
### Custom Message Handlers
To add support for custom messages:
1. **Extend the Message Handler**:
```typescript
// In your extension code
const extendMessageHandler = () => {
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 */ }
};
}
// Fall back to the original handler
return originalHandler(message, packageManager);
};
};
```
2. **Register the Extended Handler**:
```typescript
// In your extension code
const customMessageHandler = extendMessageHandler();
context.subscriptions.push(
vscode.commands.registerCommand("packageManager.handleMessage", (message) => {
return customMessageHandler(message, packageManager);
})
);
```
## Integration with Other Systems
The Package Manager can be integrated with other systems:
### Integration with External APIs
To integrate with external APIs:
1. **Create an API Client**:
```typescript
class ExternalApiClient {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
async fetchPackages(): Promise<PackageManagerItem[]> {
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
}));
}
}
```
2. **Create a Source Provider for the API**:
```typescript
class ApiSourceProvider implements SourceProvider {
private apiClient: ExternalApiClient;
constructor(apiUrl: string) {
this.apiClient = new ExternalApiClient(apiUrl);
}
type = "api";
canHandle(url: string): boolean {
return url.startsWith("api://");
}
async fetchItems(url: string): Promise<PackageManagerItem[]> {
return this.apiClient.fetchPackages();
}
}
```
3. **Register the API Provider**:
```typescript
// In your extension code
const registerApiProvider = (packageManager: PackageManagerManager) => {
packageManager.registerSourceProvider(
new ApiSourceProvider("https://your-api.example.com")
);
};
```
### Integration with Authentication Systems
To integrate with authentication systems:
1. **Create an Authentication Provider**:
```typescript
class AuthProvider {
private token: string | null = null;
async login(): Promise<boolean> {
// Your authentication logic
this.token = "your-auth-token";
return true;
}
async getToken(): Promise<string | null> {
if (!this.token) {
await this.login();
}
return this.token;
}
isAuthenticated(): boolean {
return !!this.token;
}
}
```
2. **Use Authentication in API Requests**:
```typescript
class AuthenticatedApiClient extends ExternalApiClient {
private authProvider: AuthProvider;
constructor(baseUrl: string, authProvider: AuthProvider) {
super(baseUrl);
this.authProvider = authProvider;
}
async fetchPackages(): Promise<PackageManagerItem[]> {
const token = await this.authProvider.getToken();
if (!token) {
throw new Error("Authentication required");
}
const response = await fetch(`${this.baseUrl}/packages`, {
headers: {
Authorization: `Bearer ${token}`
}
});
// Process response as before
}
}
```
### Integration with Local Development Tools
To integrate with local development tools:
1. **Create a Local Development Provider**:
```typescript
class LocalDevProvider {
private workspacePath: string;
constructor(workspacePath: string) {
this.workspacePath = workspacePath;
}
async createLocalPackage(template: string, name: string): Promise<string> {
const targetPath = path.join(this.workspacePath, name);
// Create directory
await fs.promises.mkdir(targetPath, { recursive: true });
// Copy template files
// Your implementation
return targetPath;
}
async buildLocalPackage(packagePath: string): Promise<boolean> {
// Your build implementation
return true;
}
async testLocalPackage(packagePath: string): Promise<boolean> {
// Your test implementation
return true;
}
}
```
2. **Integrate with the Package Manager**:
```typescript
// In your extension code
const registerLocalDevTools = (context: vscode.ExtensionContext) => {
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders) {
return;
}
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);
}),
vscode.commands.registerCommand("packageManager.buildLocal", async (packagePath) => {
return localDevProvider.buildLocalPackage(packagePath);
}),
vscode.commands.registerCommand("packageManager.testLocal", async (packagePath) => {
return localDevProvider.testLocalPackage(packagePath);
})
);
};
```
## Best Practices for Extensions
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
2. **Document Your Extensions**:
- 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
### Performance Considerations
1. **Lazy Loading**:
- 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
3. **UI Responsiveness**:
- 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
2. **Cross-Platform Support**:
- 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
---
**Previous**: [Testing Strategy](./06-testing-strategy.md)

View file

@ -0,0 +1,397 @@
# 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<PackageManagerItem[]> {
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>): 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<typeof sub> => 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<void> {
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
<Select
value={currentLocale}
onChange={(e) => {
setCurrentLocale(e.target.value);
// Trigger refresh with new locale
}}
>
<option value="en">English</option>
<option value="fr">Français</option>
<option value="es">Español</option>
{/* Add more languages as needed */}
</Select>
```
## Documentation Updates
Update the documentation to reflect the correct localization behavior:
```markdown
### Localization Support
You can provide metadata in multiple languages by using locale-specific files:
- `metadata.en.yml` - English metadata (required as fallback)
- `metadata.es.yml` - Spanish metadata
- `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

View file

@ -0,0 +1,54 @@
# Introduction to Package Manager
## Overview and Purpose
The Package Manager is a powerful feature in Roo Code that allows you to discover, browse, and utilize various components to enhance your development experience. It serves as a centralized hub for accessing:
- **Modes**: Specialized AI assistants with different capabilities
- **MCP Servers**: Model Context Protocol servers that provide additional functionality
- **Prompts**: Pre-configured instructions for specific tasks
- **Packages**: Collections of related components
The Package Manager simplifies the process of extending Roo Code's capabilities by providing a user-friendly interface to find, filter, and add new components to your environment.
## Key Features and Capabilities
### Component Discovery
- Browse a curated collection of components
- View detailed information about each component
- Explore subcomponents within packages
### Search and Filter
- Search by name and description
- Filter by component type (mode, MCP server, etc.)
- Use tags to find related components
- Combine search and filters for precise results
### Component Details
- View comprehensive information about each component
- See version information
- Access source repositories directly
- Explore subcomponents organized by type
### Package Management
- Add new components to your environment
- Manage custom package sources
- Create and contribute your own packages
## How to Access the Package Manager
The Package Manager can be accessed through the Roo Code extension in VS Code:
1. Open VS Code with the Roo Code extension installed
2. Click on the Roo Code icon in the activity bar
3. Select "Package Manager" from the available options
Alternatively, you can use the Command Palette:
1. Press `Ctrl+Shift+P` (Windows/Linux) or `Cmd+Shift+P` (Mac) to open the Command Palette
2. Type "Roo Code: Open Package Manager"
3. Press Enter to open the Package Manager
---
**Next**: [Browsing Packages](./02-browsing-packages.md)

View file

@ -0,0 +1,138 @@
# Browsing Packages
## Understanding the Package Manager Interface
The Package Manager interface is designed to provide a clean, intuitive experience for discovering and exploring available components. The main interface consists of several key areas:
### Main Sections
1. **Navigation Tabs**
- **Browse**: View all available components
- **Sources**: Manage package sources
2. **Filter Panel**
- Type filters (Modes, MCP Servers, Packages, etc.)
- Search box
- Tag filters
3. **Results Area**
- Package cards displaying component information
- Sorting options
### Interface Layout
```
┌─────────────────────────────────────────────────────────┐
│ [Browse] [Sources] │
├─────────────────────────────────────────────────────────┤
│ FILTERS │
│ Types: □ Mode □ MCP Server □ Package □ Prompt │
│ Search: [ ] │
│ Tags: [Tag cloud] │
├─────────────────────────────────────────────────────────┤
│ PACKAGE CARDS │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Package Name [Type] │ │
│ │ by Author │ │
│ │ │ │
│ │ Description text... │ │
│ │ │ │
│ │ [Tags] [Tags] [Tags] │ │
│ │ │ │
│ │ v1.0.0 Apr 12, 2025 [View] │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Another Package [Type] │ │
│ │ ... │ │
└─────────────────────────────────────────────────────────┘
```
## Package Cards and Information Displayed
Each package in the Package Manager is represented by a card that contains essential information about the component:
### Card Elements
1. **Header Section**
- **Package Name**: The name of the component
- **Author**: The creator or maintainer of the component (if available)
- **Type Badge**: Visual indicator of the component type (Mode, MCP Server, etc.)
2. **Description**
- A brief overview of the component's purpose and functionality
3. **Tags**
- Clickable tags that categorize the component
- Can be used for filtering similar components
4. **Metadata**
- **Version**: The current version of the component (if available)
- **Last Updated**: When the component was last modified (if available)
5. **Actions**
- **View**: Button to access the component's source repository or documentation
6. **Details Section** (expandable)
- Shows subcomponents grouped by type
- Displays additional information when expanded
### Example Card
```
┌─────────────────────────────────────────────────────┐
│ Data Platform Package [Package] │
│ by Roo Team │
│ │
│ A comprehensive data processing and analysis │
│ package with tools for ETL, visualization, and ML. │
│ │
│ [data] [analytics] [machine-learning] │
│ │
│ v2.1.0 Apr 10, 2025 [View] │
│ │
│ ▼ Component Details │
│ MCP Servers: │
│ 1. Data Validator - Validates data formats │
│ 2. ML Predictor - Makes predictions on data │
│ │
│ Modes: │
│ 1. Data Analyst - Helps with data analysis │
│ 2. ETL Engineer - Assists with data pipelines │
└─────────────────────────────────────────────────────┘
```
## Navigating Between Packages
The Package Manager provides several ways to navigate through the available packages:
### Navigation Methods
1. **Scrolling**
- Scroll through the list of package cards to browse all available components
2. **Filtering**
- Use the filter panel to narrow down the displayed packages
- Click on type filters to show only specific component types
- Enter search terms to find packages by name or description
- Click on tags to filter by specific categories
3. **Sorting**
- Sort packages by name or last updated date
- Toggle between ascending and descending order
4. **Tab Navigation**
- Switch between "Browse" and "Sources" tabs to manage package sources
### Keyboard Navigation
For accessibility and efficiency, the Package Manager supports keyboard navigation:
- **Tab**: Move focus between interactive elements
- **Space/Enter**: Activate buttons or toggle filters
- **Arrow Keys**: Navigate between package cards
- **Escape**: Close expanded details or clear filters
---
**Previous**: [Introduction to Package Manager](./01-introduction.md) | **Next**: [Searching and Filtering](./03-searching-and-filtering.md)

View file

@ -0,0 +1,132 @@
# Searching and Filtering
The Package Manager provides powerful search and filtering capabilities to help you quickly find the components you need. This guide explains how to effectively use these features to narrow down your search results.
## Using the Search Functionality
The search box allows you to find components by matching text in various fields:
### What Gets Searched
When you enter a search term, the Package Manager looks for matches in:
1. **Component Name**: The primary identifier of the component
2. **Description**: The detailed explanation of the component's purpose
3. **Subcomponent Names and Descriptions**: Text within nested components
### Search Features
- **Case Insensitive**: Searches ignore letter case for easier matching
- **Whitespace Insensitive**: Extra spaces are normalized in the search
- **Partial Matching**: Finds results that contain your search term anywhere in the text
- **Instant Results**: Results update as you type
- **Match Highlighting**: Matching subcomponents are highlighted and expanded automatically
### Search Implementation
The search uses a simple string contains match that is case and whitespace insensitive. This means:
- "Data" will match "data", "DATA", "Data", etc.
- "machine learning" will match "Machine Learning", "machine-learning", etc.
- Partial words will match: "valid" will match "validation", "validator", etc.
### Search Tips
- Use specific, distinctive terms to narrow results
- Try different variations if you don't find what you're looking for
- Search for technology names or specific functionality
- Look for highlighted "match" indicators in expanded details sections
### Example Searches
| Search Term | Will Find |
|-------------|-----------|
| "data" | Components with "data" in their name, description, or subcomponents |
| "validator" | Components that include validation functionality or have validator subcomponents |
| "machine learning" | Components related to machine learning technology |
## Filtering by Package Type
The type filter allows you to focus on specific categories of components:
### Available Type Filters
- **Mode**: AI assistant personalities with specialized capabilities
- **MCP Server**: Model Context Protocol servers that provide additional functionality
- **Package**: Collections of related components
- **Prompt**: Pre-configured instructions for specific tasks
### Using Type Filters
1. Click on a type checkbox to show only components of that type
2. Select multiple types to show components that match any of the selected types
3. Clear all type filters to show all components again
### Type Filter Behavior
- Type filters apply to the primary component type, not subcomponents
- The type is displayed as a badge on each package card
- Type filtering can be combined with search terms and tag filters
## Using Tags for Filtering
Tags provide a way to filter components by category, technology, or purpose:
### Tag Functionality
- Tags appear as clickable buttons on package cards
- Clicking a tag activates it as a filter
- Active tag filters are highlighted
- Components must have at least one of the selected tags to be displayed
### Finding and Using Tags
1. Browse through package cards to discover available tags
2. Click on a tag to filter for components with that tag
3. Click on additional tags to expand your filter (components with any of the selected tags will be shown)
4. Click on an active tag to deactivate it
### Common Tags
- Technology areas: "data", "web", "security", "ai"
- Programming languages: "python", "javascript", "typescript"
- Functionality: "testing", "documentation", "analysis"
- Domains: "finance", "healthcare", "education"
## Combining Search and Filters
For the most precise results, you can combine search terms, type filters, and tag filters:
### How Combined Filtering Works
1. **AND Logic Between Filter Types**: Components must match the search term AND the selected types AND have at least one of the selected tags
2. **OR Logic Within Tag Filters**: Components must have at least one of the selected tags
### Combined Filter Examples
| Search Term | Type Filter | Tag Filter | Will Find |
|-------------|-------------|------------|-----------|
| "data" | MCP Server | "analytics" | MCP Servers related to data analytics |
| "test" | Mode | "automation", "quality" | Test automation or quality-focused modes |
| "visualization" | Package | "dashboard", "chart" | Packages for creating dashboards or charts |
### Clearing Filters
To reset your search and start over:
1. Clear the search box
2. Uncheck all type filters
3. Deactivate all tag filters by clicking on them
### Filter Status Indicators
The Package Manager provides visual feedback about your current filters:
- Active type filters are checked
- Active tag filters are highlighted
- The search box shows your current search term
- Result counts may be displayed to show how many items match your filters
---
**Previous**: [Browsing Packages](./02-browsing-packages.md) | **Next**: [Working with Package Details](./04-working-with-details.md)

View file

@ -0,0 +1,139 @@
# Working with Package Details
Package Manager items often contain multiple components organized in a hierarchical structure. This guide explains how to work with the details section of package cards to explore and understand the components within each package.
## Expanding Package Details
Most packages in the Package Manager contain subcomponents that are hidden by default to keep the interface clean. You can expand these details to see what's inside each package:
### How to Expand Details
1. Look for the "Component Details" section at the bottom of a package card
2. Click on the section header or the chevron icon (▶) to expand it
3. The section will animate open, revealing the components inside the package
4. Click again to collapse the section when you're done
### Automatic Expansion
The details section will expand automatically when:
- Your search term matches text in a subcomponent
- This is the only condition for automatic expansion
### Details Section Badge
The details section may display a badge with additional information:
- **Match count**: When your search term matches subcomponents, a badge shows how many matches were found (e.g., "3 matches")
- This helps you quickly identify which packages contain relevant subcomponents
## Understanding Component Types
Components within packages are grouped by their type to make them easier to find and understand:
### Common Component Types
1. **Modes**
- 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
3. **Prompts**
- 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
### Type Presentation
Each type section in the details view includes:
- A header with the type name (pluralized, e.g., "MCP Servers")
- A numbered list of components of that type
- Each component's name and description
## Viewing Subcomponents
The details section organizes subcomponents in a clear, structured format:
### Subcomponent List Format
```
Component Details
Type Name:
1. Component Name - Description text goes here
2. Another Component - Its description
Another Type:
1. First Component - Description
2. Second Component - Description
```
### Subcomponent Information
Each subcomponent in the list displays:
1. **Number**: Sequential number within its type group
2. **Name**: The name of the subcomponent
3. **Description**: A brief explanation of the subcomponent's purpose (if available)
4. **Match Indicator**: A "match" badge appears next to items that match your search term
### Navigating Subcomponents
- Scroll within the details section to see all subcomponents
- Components are grouped by type, making it easier to find specific functionality
- Long descriptions may be truncated with an ellipsis (...) to save space (limited to 100 characters)
## Matching Search Terms in Subcomponents
One of the most powerful features of the Package Manager is the ability to search within subcomponents:
### How Subcomponent Matching Works
1. Enter a search term in the search box
2. The Package Manager searches through all subcomponent names and descriptions
3. Packages with matching subcomponents remain visible in the results
4. The details section automatically expands for packages with matches
5. Matching subcomponents are highlighted and marked with a "match" badge
### Visual Indicators for Matches
When a subcomponent matches your search:
- The component name is highlighted in a different color
- A "match" badge appears next to the component
- The details section automatically expands
- A badge on the details section header shows the number of matches
### Search Implementation
The search uses a simple string contains match that is case-insensitive:
- "validator" will match "Data Validator", "Validator Tool", etc.
- "valid" will match "validation", "validator", etc.
- The search will match any part of the name or description
### Example Scenario
If you search for "validator":
1. Packages containing components with "validator" in their name or description remain visible
2. The details section expands automatically for packages with matching subcomponents
3. Components like "Data Validator" or those with "validation" in their description are highlighted
4. A badge might show "2 matches" if two subcomponents match your search term
### Benefits of Subcomponent Matching
- Find functionality buried deep within packages
- Discover relationships between components
- Identify packages that contain specific tools or capabilities
- Locate similar components across different packages
---
**Previous**: [Searching and Filtering](./03-searching-and-filtering.md) | **Next**: [Adding Packages](./05-adding-packages.md)

View file

@ -0,0 +1,6 @@
**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

View file

@ -0,0 +1,198 @@
# Adding Custom Package Sources
The Package Manager allows you to extend its functionality by adding custom package sources. This guide explains how to set up and manage your own package repositories to access additional components beyond the default offerings.
## Setting up a Package Source Repository
A package source repository is a Git repository that contains packages organized in a specific structure. You can create your own repository to host custom packages:
### Repository Requirements
1. **Proper Structure**: The repository must follow the required directory structure
2. **Valid Metadata**: Each package must include properly formatted metadata files
3. **Git Repository**: The source must be a Git repository accessible via HTTPS
### Creating a New Repository
1. Create a new repository on GitHub, GitLab, or another Git hosting service
2. Initialize the repository with a README.md file
3. Clone the repository to your local machine:
```bash
git clone https://github.com/your-username/your-package-repo.git
cd your-package-repo
```
4. Create the basic repository structure:
```bash
mkdir -p packages modes "mcp servers" prompts
touch metadata.en.yml
```
5. Add repository metadata to `metadata.en.yml`:
```yaml
name: "Your Repository Name"
description: "A collection of custom packages for Roo Code"
version: "1.0.0"
```
6. Commit and push the initial structure:
```bash
git add .
git commit -m "Initialize package repository structure"
git push origin main
```
## Required Structure and Metadata
A package source repository must follow a specific structure to be properly recognized by the Package Manager:
### Repository Structure
```
repository-root/
├── metadata.en.yml # Repository metadata
├── README.md # Repository documentation
├── packages/ # Directory for package components
│ ├── package-1/
│ │ ├── metadata.en.yml # Package metadata
│ │ └── README.md
│ └── package-2/
│ ├── metadata.en.yml
│ └── README.md
├── modes/ # Directory for mode components
│ └── custom-mode/
│ └── metadata.en.yml
├── mcp servers/ # Directory for MCP server components
│ └── custom-server/
│ └── metadata.en.yml
└── prompts/ # Directory for prompt components
└── custom-prompt/
└── metadata.en.yml
```
### Repository Metadata
The root `metadata.en.yml` file describes the repository itself:
```yaml
name: "Custom Components Repository"
description: "A collection of specialized components for data science workflows"
version: "1.0.0"
author: "Your Name or Organization"
tags:
- custom
- data-science
```
### Component Organization
- Components should be organized by type in their respective directories
- Each component must have its own directory containing a metadata file
- Components can be nested within packages
- Follow the same structure as described in [Adding Packages](./05-adding-packages.md)
## Adding Sources to Roo Code
Once you have a properly structured package source repository, you can add it to your Roo Code Package Manager:
### Default Package Source
Roo Code comes with a default package source:
- URL: `https://github.com/RooVetGit/Roo-Code-Packages`
- Name: "Roo Code Package Manager Template"
- This source is enabled by default
### Adding a New Source
1. Open VS Code with the Roo Code extension
2. Navigate to the Package Manager
3. Switch to the "Sources" tab
4. Click the "Add Source" button
5. Enter the repository URL:
- Format: `https://github.com/username/repository.git`
- Example: `https://github.com/your-username/your-package-repo.git`
6. Click "Add" to save the source
### Managing Sources
The "Sources" tab provides several options for managing your package sources:
1. **Enable/Disable**: Toggle sources on or off without removing them
2. **Remove**: Delete a source from your configuration
3. **Refresh**: Update the package list from all enabled sources
4. **View Details**: See information about each source
### Source Caching and Refreshing
Package Manager sources are cached to improve performance:
- **Cache Duration**: Sources are cached for 1 hour (3600000 ms)
- **Force Refresh**: To force an immediate refresh of a source:
1. Go to the "Sources" tab
2. Click the "Refresh" button next to the source you want to update
3. This will bypass the cache and fetch the latest data from the repository
### Troubleshooting Sources
If a source isn't loading properly:
1. Check that the repository URL is correct
2. Ensure the repository follows the required structure
3. Look for error messages in the Package Manager interface
4. Try refreshing the sources list
5. Disable and re-enable the source
## Creating Private Sources
For team or organization use, you might want to create private package sources:
### Private Repository Setup
1. Create a private repository on your Git hosting service
2. Follow the same structure requirements as public repositories
3. Set up appropriate access controls for your team members
### Authentication Options
To access private repositories, you may need to:
1. Configure Git credentials on your system
2. Use a personal access token with appropriate permissions
3. Set up SSH keys for authentication
### Organization Best Practices
For teams and organizations:
1. Designate maintainers responsible for the package source
2. Establish quality standards for contributed packages
3. Create a review process for new additions
4. Document usage guidelines for team members
5. Consider implementing versioning for your packages
## Using Multiple Sources
The Package Manager supports multiple package sources simultaneously:
### Benefits of Multiple Sources
- Access components from different providers
- Separate internal and external components
- Test new packages before contributing them to the main repository
- Create specialized sources for different projects or teams
### Source Management Strategy
1. Keep the default source enabled for core components
2. Add specialized sources for specific needs
3. Create a personal source for testing and development
4. Disable sources temporarily when not needed
5. Regularly update sources to get the latest components
---
**Previous**: [Adding Packages](./05-adding-packages.md) | **Next**: [Package Manager Architecture](../implementation/01-architecture.md)