diff --git a/SETUP_GUIDE.md b/SETUP_GUIDE.md index 8bf6bcd..bea5f2c 100644 --- a/SETUP_GUIDE.md +++ b/SETUP_GUIDE.md @@ -10,7 +10,7 @@ This guide will walk you through everything you need to know to set up and use t 2. [Understanding the Interface](#understanding-the-interface) 3. [Configuration Options](#configuration-options) 4. [Setting Up Your First Catalog](#setting-up-your-first-catalog) -5. [Working with Hats (Presets)](#working-with-hats-presets) +5. [Working with Presets](#working-with-presets) 6. [Team Collaboration Workflow](#team-collaboration-workflow) 7. [Advanced Configuration](#advanced-configuration) 8. [Troubleshooting](#troubleshooting) @@ -54,7 +54,7 @@ The main interface showing your catalog structure: - **✓ Active**: Resource is copied to runtime directory and ready to use - **⚠ Modified**: Active resource has been changed and differs from catalog version - **📁 Available**: Resource exists in catalog but isn't activated -- **🔍 Missing**: Referenced in Hat but not found in catalog +- **🔍 Missing**: Referenced in Preset but not found in catalog #### Context Menu (Right-click) - **Activate**: Copy resource to runtime directory @@ -65,11 +65,11 @@ The main interface showing your catalog structure: ### Title Bar Commands -#### Hats Menu 🎩 -- **Apply Hat (Preset)**: Activate a saved preset -- **Save Hat from Active (Workspace)**: Save current active resources as a workspace preset -- **Save Hat from Active (User)**: Save current active resources as a user preset -- **Delete Hat**: Remove a saved preset +#### Presets Menu 🎩 +- **Apply Preset**: Activate a saved preset +- **Save Preset from Active (Workspace)**: Save current active resources as a workspace preset +- **Save Preset from Active (User)**: Save current active resources as a user preset +- **Delete Preset**: Remove a saved preset #### Dev Menu 🔧 - **Create Template Catalog**: Generate sample catalog structure @@ -110,7 +110,7 @@ Access settings via: **File** → **Preferences** → **Settings** → Search "C ├── prompts/ # Reusable prompt templates ├── tasks/ # VS Code task definitions ├── mcp/ # Model Context Protocol configs - └── hats/ # Preset collections + └── presets/ # Preset collections ``` ### Method 2: Manual Catalog Creation @@ -119,7 +119,7 @@ Access settings via: **File** → **Preferences** → **Settings** → Search "C ``` mkdir copilot_catalog cd copilot_catalog - mkdir chatmodes instructions prompts tasks mcp hats + mkdir chatmodes instructions prompts tasks mcp presets ``` 2. **Add your first resource** (example instruction): @@ -161,23 +161,23 @@ Example configuration: } ``` -## Working with Hats (Presets) +## Working with Presets -Hats let you save and apply collections of resources with one click. +Presets let you save and apply collections of resources with one click. -### Creating Your First Hat +### Creating Your First Preset 1. **Activate some resources** (right-click → Activate) -2. **Save as Hat**: Click **Hats** → **Save Hat from Active (Workspace)** +2. **Save as Preset**: Click **Presets** → **Save Preset from Active (Workspace)** 3. **Name it**: e.g., "Code Review Setup" 4. **Add description** (optional): "Resources for thorough code reviews" -### Hat File Structure +### Preset File Structure -Workspace hats are saved to `.vscode/copilot-hats.json`: +Workspace presets are saved to `.vscode/copilot-presets.json`: ```json { - "hats": [ + "presets": [ { "name": "Code Review Setup", "description": "Resources for thorough code reviews", @@ -191,27 +191,27 @@ Workspace hats are saved to `.vscode/copilot-hats.json`: } ``` -### Applying Hats +### Applying Presets -1. **Click Hats → Apply Hat (Preset)** -2. **Choose your hat** from the list +1. **Click Presets → Apply Preset** +2. **Choose your preset** from the list 3. **Select mode**: - - **Additive**: Add hat resources to currently active ones - - **Exclusive**: Deactivate everything else, activate only hat resources + - **Additive**: Add preset resources to currently active ones + - **Exclusive**: Deactivate everything else, activate only preset resources -### Hat Types +### Preset Types -#### Workspace Hats (.vscode/copilot-hats.json) +#### Workspace Presets (.vscode/copilot-presets.json) - Shared with your team via Git - Perfect for role-based setups ("Frontend Dev", "Backend Dev", "QA") - Project-specific configurations -#### User Hats (Global) +#### User Presets (Global) - Personal to your machine - Cross-project personal preferences - Not shared with team -#### Catalog Hats (copilot_catalog/hats/*.json) +#### Catalog Presets (copilot_catalog/presets/*.json) - Stored in the catalog itself - Can be shared across multiple repositories - Version-controlled with the catalog @@ -228,17 +228,17 @@ Workspace hats are saved to `.vscode/copilot-hats.json`: git add copilot_catalog/ git commit -m "Add team catalog" - # Create team hats in VS Code and commit - git add .vscode/copilot-hats.json + # Create team presets in VS Code and commit + git add .vscode/copilot-presets.json git commit -m "Add team presets" git push ``` 2. **Team Members**: ```bash - git pull # Get latest catalog and hats + git pull # Get latest catalog and presets # Open VS Code, go to ContextShare view - # Click Hats → Apply Hat → Choose team preset + # Click Presets → Apply Preset → Choose team preset ``` ### Best Practices for Teams @@ -253,16 +253,16 @@ copilot_catalog/ ├── chatmodes/ │ ├── roles/ # Role-based chat modes │ └── tasks/ # Task-specific modes -└── hats/ +└── presets/ ├── frontend-dev.json # Frontend developer preset ├── backend-dev.json # Backend developer preset └── code-review.json # Code reviewer preset ``` -#### 2. Hat Strategy -- **Role-based hats**: "Frontend Dev", "Backend Dev", "DevOps", "QA" -- **Task-based hats**: "Code Review", "Bug Fixing", "Feature Development" -- **Project-phase hats**: "Initial Development", "Maintenance", "Refactoring" +#### 2. Preset Strategy +- **Role-based presets**: "Frontend Dev", "Backend Dev", "DevOps", "QA" +- **Task-based presets**: "Code Review", "Bug Fixing", "Feature Development" +- **Project-phase presets**: "Initial Development", "Maintenance", "Refactoring" #### 3. Naming Conventions - Use consistent prefixes: `TEAM.`, `PROJECT.`, `ROLE.` @@ -379,15 +379,15 @@ Tip: Do not hand-edit the manifest; scripts keep it correct based on package.jso 3. **Check file conflicts**: Look for existing files that might block activation 4. **View diagnostics**: Use Dev → Diagnostics for detailed error info -#### Hat Application Fails +#### Preset Application Fails -**Problem**: Applying a hat doesn't activate expected resources +**Problem**: Applying a preset doesn't activate expected resources **Solutions**: -1. **Check resource paths**: Ensure hat references valid catalog paths +1. **Check resource paths**: Ensure preset references valid catalog paths 2. **Refresh catalog**: Resource might have been moved or renamed 3. **Check exclusive mode**: In exclusive mode, other resources are deactivated -4. **Verify catalog source**: Hat might reference resources from different catalog +4. **Verify catalog source**: Preset might reference resources from different catalog #### Performance Issues @@ -436,8 +436,8 @@ Tip: Do not hand-edit the manifest; scripts keep it correct based on package.jso ## Next Steps 🎯 **Try it out**: Create a simple catalog and experiment with activating resources -🎩 **Make your first Hat**: Save a useful combination as a preset -👥 **Share with team**: Commit your catalog and hats to Git +🎩 **Make your first Preset**: Save a useful combination as a preset +👥 **Share with team**: Commit your catalog and presets to Git ⚡ **Iterate**: Refine your setup based on what works for your workflow **Happy cataloging!** 🚀 diff --git a/SOFTWARE_ARCHITECTURE_SPECIFICATION.md b/SOFTWARE_ARCHITECTURE_SPECIFICATION.md index e222671..3c23881 100644 --- a/SOFTWARE_ARCHITECTURE_SPECIFICATION.md +++ b/SOFTWARE_ARCHITECTURE_SPECIFICATION.md @@ -42,7 +42,7 @@ The extension operates within the VS Code ecosystem as a client-side tool that: - Discovers AI resources from multiple local and remote catalogs - Manages resource activation/deactivation states with advanced merging capabilities - Provides specialized tree-based UI views for different resource categories -- Supports preset configurations ("Hats") for resource groups +- Supports preset configurations for resource groups - Handles user-created resources with enable/disable functionality - Ensures secure handling of remote content with comprehensive validation - Integrates with VS Code's native task and MCP systems @@ -194,7 +194,7 @@ graph TB #### 4.2.1 Service Layer Pattern - **Purpose**: Encapsulate business logic in reusable services -- **Implementation**: ResourceService, HatService, FileService +- **Implementation**: ResourceService, PresetService, FileService - **Benefits**: Testability, separation of concerns, reusability #### 4.2.2 Repository Pattern @@ -237,7 +237,7 @@ graph LR subgraph "Services" RS[ResourceService] - HS[HatService] + PS[PresetService] FS[FileService] end @@ -287,7 +287,7 @@ async function loadResources() **Dependencies:** - ResourceService -- HatService +- PresetService - FileService - OverviewTreeProvider - CategoryTreeProvider (multiple instances) @@ -329,26 +329,26 @@ clearRemoteCache() - Resource state computation with specialized MCP/task handling - User resource enable/disable state tracking -#### 5.2.3 Hat Service (`services/hatService.ts`) +#### 5.2.3 Preset Service (`services/presetService.ts`) **Responsibilities:** -- Hat (preset) discovery from multiple sources -- Hat application (bulk resource activation/deactivation) -- Hat creation from current active resources -- Hat persistence to workspace and user storage +- Preset discovery from multiple sources +- Preset application (bulk resource activation/deactivation) +- Preset creation from current active resources +- Preset persistence to workspace and user storage **Key Functions:** ```typescript -async discoverHats(repo: Repository): Promise -async applyHat(repo: Repository, resources: Resource[], hat: Hat): Promise -async createHatFromActive(name: string, resources: Resource[], source: HatSource): Promise -async deleteHat(hat: Hat, repo?: Repository): Promise +async discoverPresets(repo: Repository): Promise +async applyPreset(repo: Repository, resources: Resource[], preset: Preset): Promise +async createPresetFromActive(name: string, resources: Resource[], source: PresetSource): Promise +async deletePreset(preset: Preset, repo?: Repository): Promise ``` **Storage Locations:** -- Catalog: `{catalog}/hats/*.json` -- Workspace: `.vscode/copilot-hats.json` -- User: Global storage `hats.json` +- Catalog: `{catalog}/presets/*.json` +- Workspace: `.vscode/copilot-presets.json` +- User: Global storage `presets.json` #### 5.2.4 File Service (`services/fileService.ts`) @@ -645,7 +645,7 @@ private remoteCache: Map>Ext: activate() Ext->>Services: Initialize FileService Ext->>Services: Initialize ResourceService - Ext->>Services: Initialize HatService + Ext->>Services: Initialize PresetService Ext->>UI: Initialize CatalogTreeProvider Ext->>VSCode: Register commands Ext->>VSCode: Register tree data provider @@ -1103,7 +1103,7 @@ graph TD subgraph "Test Files" RS[resourceService.test.ts] - HS[hats.test.ts] + PS[presets.test.ts] SEC[security.test.ts] DIS[display.test.ts] NAM[naming.test.ts] @@ -1139,7 +1139,7 @@ graph TD #### 11.1.2 Test Categories **Unit Tests:** -- Service logic validation (ResourceService, HatService) +- Service logic validation (ResourceService, PresetService) - Utility function testing (display, security, naming) - Model validation and state management - Error handling verification @@ -1225,7 +1225,7 @@ export class MockFileService implements IFileService { ```json { "scripts": { - "test": "node dist/test/resourceService.test.js && node dist/test/treeIcons.test.js && node dist/test/naming.test.js && node dist/test/mcpMerge.test.js && node dist/test/hats.test.js && node dist/test/commandsRegistered.test.js && node dist/test/display.test.js && node dist/test/catalogDisplayName.test.js && node dist/test/repositoryDiscovery.test.js && node dist/test/targetPath.test.js && node dist/test/workspaceConfiguration.test.js && node dist/test/resourceActivation.test.js", + "test": "node dist/test/resourceService.test.js && node dist/test/treeIcons.test.js && node dist/test/naming.test.js && node dist/test/mcpMerge.test.js && node dist/test/presets.test.js && node dist/test/commandsRegistered.test.js && node dist/test/display.test.js && node dist/test/catalogDisplayName.test.js && node dist/test/repositoryDiscovery.test.js && node dist/test/targetPath.test.js && node dist/test/workspaceConfiguration.test.js && node dist/test/resourceActivation.test.js", "test:all": "npm run test", "test:edge-cases": "node dist/test/repositoryDiscovery.test.js && node dist/test/targetPath.test.js && node dist/test/workspaceConfiguration.test.js && node dist/test/resourceActivation.test.js", "test:watch": "npm run build && npm run test" @@ -1549,7 +1549,7 @@ export function validateResourcePath(path: string): boolean { | Term | Definition | |------|------------| | **Catalog** | Directory containing AI resource templates | -| **Hat** | Named preset of resources for bulk activation | +| **Preset** | Named collection of resources for bulk activation | | **Resource** | Individual AI asset (chatmode, instruction, prompt, task, MCP) | | **Runtime** | Directory where active resources are copied for use | | **Activation** | Process of copying catalog resource to runtime directory | @@ -1574,7 +1574,7 @@ workspace/ │ │ └── build.task.json │ ├── mcp/ │ │ └── servers.mcp.json -│ └── hats/ +│ └── presets/ │ └── preset.json ├── .github/ # Runtime directory │ ├── chatmodes/ @@ -1584,7 +1584,7 @@ workspace/ │ └── mcp/ └── .vscode/ ├── settings.json - └── copilot-hats.json # Workspace hats + └── copilot-presets.json # Workspace presets ``` #### 15.2.2 Naming Conventions @@ -1597,10 +1597,10 @@ workspace/ - `user.{original}` (first variant) - `user.{N}.{original}` (subsequent variants) -**Hat Files:** -- Catalog: `{catalog}/hats/{name}.json` -- Workspace: `.vscode/copilot-hats.json` -- User: `{globalStorage}/hats.json` +**Preset Files:** +- Catalog: `{catalog}/presets/{name}.json` +- Workspace: `.vscode/copilot-presets.json` +- User: `{globalStorage}/presets.json` ### 15.3 Configuration Examples @@ -1740,7 +1740,7 @@ Enable `copilotCatalog.enableFileLogging` for persistent debug logs. 1. Check `.vscode/settings.json` for configuration 2. Verify catalog directory structure 3. Examine runtime directory contents -4. Review hat definitions in `.vscode/copilot-hats.json` +4. Review preset definitions in `.vscode/copilot-presets.json` --- diff --git a/example-catalog/hats/Copilot-Catalog-Setup.json b/example-catalog/presets/Copilot-Catalog-Setup.json similarity index 100% rename from example-catalog/hats/Copilot-Catalog-Setup.json rename to example-catalog/presets/Copilot-Catalog-Setup.json diff --git a/package-lock.json b/package-lock.json index dde59cf..9dc687c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "contextshare", - "version": "0.3.5", + "version": "0.3.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "contextshare", - "version": "0.3.5", + "version": "0.3.6", "license": "MIT", "devDependencies": { "@types/glob": "^8.1.0", diff --git a/package.json b/package.json index bd7f946..fe16f23 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,8 @@ "contributes": { "submenus": [ { - "id": "copilotCatalog.hatsMenu", - "label": "Hats", + "id": "copilotCatalog.presetsMenu", + "label": "Presets", "icon": "$(kebab-vertical)" }, { @@ -228,7 +228,7 @@ }, { "command": "copilotCatalog.hats.apply", - "title": "ContextShare: Apply Hat (Preset)", + "title": "ContextShare: Apply Preset", "icon": { "light": "resources/icons/hat-light.svg", "dark": "resources/icons/hat-dark.svg" @@ -236,17 +236,17 @@ }, { "command": "copilotCatalog.hats.createWorkspace", - "title": "ContextShare: Save Hat from Active (Workspace)", + "title": "ContextShare: Save Preset from Active (Workspace)", "icon": "$(save)" }, { "command": "copilotCatalog.hats.createUser", - "title": "ContextShare: Save Hat from Active (User)", + "title": "ContextShare: Save Preset from Active (User)", "icon": "$(account)" }, { "command": "copilotCatalog.hats.delete", - "title": "ContextShare: Delete Hat (Workspace/User)", + "title": "ContextShare: Delete Preset (Workspace/User)", "icon": "$(trash)" }, { @@ -343,7 +343,7 @@ "group": "navigation" }, { - "submenu": "copilotCatalog.hatsMenu", + "submenu": "copilotCatalog.presetsMenu", "when": "view == copilotCatalogOverview || view == copilotCatalogChatmodes", "group": "navigation@b" }, @@ -353,7 +353,7 @@ "group": "navigation@c" } ], - "copilotCatalog.hatsMenu": [ + "copilotCatalog.presetsMenu": [ { "command": "copilotCatalog.hats.apply", "when": "view == copilotCatalogOverview || view == copilotCatalogChatmodes", diff --git a/src/extension.ts b/src/extension.ts index 6f8354a..4d8ff1a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -8,7 +8,7 @@ import * as path from 'path'; import * as vscode from 'vscode'; import { Repository, Resource, ResourceCategory, ResourceState } from './models'; import { FileService } from './services/fileService'; -import { HatService } from './services/hatService'; +import { PresetService } from './services/presetService'; import { ResourceService } from './services/resourceService'; import { CategoryTreeProvider } from './tree/categoryTreeProvider'; import { OptionsTreeProvider } from './tree/optionsTreeProvider'; @@ -139,7 +139,7 @@ export async function activate(context: vscode.ExtensionContext) { const tasksTree = new CategoryTreeProvider(ResourceCategory.TASKS); const mcpTree = new CategoryTreeProvider(ResourceCategory.MCP); const optionsTree = new OptionsTreeProvider(); - const hatService = new HatService(fileService, resourceService, context.globalStorageUri.fsPath); + const presetService = new PresetService(fileService, resourceService, context.globalStorageUri.fsPath); // Track whether we've warned user about read-only catalog views let shownReadonlyNotice = false; @@ -879,63 +879,63 @@ export async function activate(context: vscode.ExtensionContext) { await refresh(); }) , - // --- Hats (Presets) --- + // --- Presets (formerly Hats) --- vscode.commands.registerCommand('copilotCatalog.hats.apply', async () => { if(!currentRepo){ vscode.window.showWarningMessage('No repository available.'); return; } - const hats = await hatService.discoverHats(currentRepo); - if(hats.length===0){ vscode.window.showInformationMessage('No hats found (check catalog hats/, workspace .vscode/copilot-hats.json, or user hats).'); return; } - const pick = await vscode.window.showQuickPick(hats.map(h=> ({ label: h.name, description: h.description || h.source, detail: `${h.resources.length} items`, hat: h })), { placeHolder: 'Select a Hat to apply' }); + const presets = await presetService.discoverPresets(currentRepo); + if(presets.length===0){ vscode.window.showInformationMessage('No presets found (check catalog presets/, workspace .vscode/copilot-presets.json, or user presets).'); return; } + const pick = await vscode.window.showQuickPick(presets.map(h=> ({ label: h.name, description: h.description || h.source, detail: `${h.resources.length} items`, preset: h })), { placeHolder: 'Select a Preset to apply' }); if(!pick) return; - // Ask whether to enforce exclusivity (deactivate non-hat resources) + // Ask whether to enforce exclusivity (deactivate non-preset resources) const mode = await vscode.window.showQuickPick([ { label: 'Apply (keep others active)', value: 'nonExclusive' }, { label: 'Apply Exclusively (deactivate others)', value: 'exclusive' } - ], { placeHolder: 'How should the Hat be applied?' }); + ], { placeHolder: 'How should the Preset be applied?' }); if(!mode) return; const exclusive = mode.value === 'exclusive'; const currentResources = catalogFilter ? allResources.filter(r => r.catalogName === catalogFilter) : allResources; - const res = await hatService.applyHat(currentRepo, currentResources, pick.hat, { exclusive }); - await logger.info(`Applied hat ${pick.hat.name}: activated=${res.activated} deactivated=${res.deactivated} missing=${res.missing.length} errors=${res.errors.length}`); - if(res.errors.length){ vscode.window.showWarningMessage(`Hat applied with errors. Activated ${res.activated}, Deactivated ${res.deactivated}. Missing: ${res.missing.length}.`); } else { vscode.window.showInformationMessage(`Hat applied. Activated ${res.activated}, Deactivated ${res.deactivated}. Missing: ${res.missing.length}.`); } + const res = await presetService.applyPreset(currentRepo, currentResources, pick.preset, { exclusive }); + await logger.info(`Applied preset ${pick.preset.name}: activated=${res.activated} deactivated=${res.deactivated} missing=${res.missing.length} errors=${res.errors.length}`); + if(res.errors.length){ vscode.window.showWarningMessage(`Preset applied with errors. Activated ${res.activated}, Deactivated ${res.deactivated}. Missing: ${res.missing.length}.`); } else { vscode.window.showInformationMessage(`Preset applied. Activated ${res.activated}, Deactivated ${res.deactivated}. Missing: ${res.missing.length}.`); } await loadResources(); updateStatus(); }), vscode.commands.registerCommand('copilotCatalog.hats.createWorkspace', async () => { if(!currentRepo){ vscode.window.showWarningMessage('No repository available.'); return; } - const name = await vscode.window.showInputBox({ prompt: 'Name for the workspace Hat', placeHolder: 'My Hat' }); + const name = await vscode.window.showInputBox({ prompt: 'Name for the workspace Preset', placeHolder: 'My Preset' }); if(!name) return; const desc = await vscode.window.showInputBox({ prompt: 'Optional description' }); const currentResources = catalogFilter ? allResources.filter(r => r.catalogName === catalogFilter) : allResources; - const hat = await hatService.createHatFromActive(name, desc, currentResources, 'workspace', currentRepo); - await logger.info(`Created workspace hat ${hat.name} with ${hat.resources.length} resources`); - vscode.window.showInformationMessage(`Saved Hat "${hat.name}" to workspace (.vscode/copilot-hats.json).`); + const preset = await presetService.createPresetFromActive(name, desc, currentResources, 'workspace', currentRepo); + await logger.info(`Created workspace preset ${preset.name} with ${preset.resources.length} resources`); + vscode.window.showInformationMessage(`Saved Preset "${preset.name}" to workspace (.vscode/copilot-presets.json).`); }), vscode.commands.registerCommand('copilotCatalog.hats.createUser', async () => { - const name = await vscode.window.showInputBox({ prompt: 'Name for the user Hat', placeHolder: 'My Hat' }); + const name = await vscode.window.showInputBox({ prompt: 'Name for the user Preset', placeHolder: 'My Preset' }); if(!name) return; const desc = await vscode.window.showInputBox({ prompt: 'Optional description' }); const currentResources = catalogFilter ? allResources.filter(r => r.catalogName === catalogFilter) : allResources; - const hat = await hatService.createHatFromActive(name, desc, currentResources, 'user'); - await logger.info(`Created user hat ${hat.name} with ${hat.resources.length} resources`); - vscode.window.showInformationMessage(`Saved Hat "${hat.name}" to user settings (global storage).`); + const preset = await presetService.createPresetFromActive(name, desc, currentResources, 'user'); + await logger.info(`Created user preset ${preset.name} with ${preset.resources.length} resources`); + vscode.window.showInformationMessage(`Saved Preset "${preset.name}" to user settings (global storage).`); }), vscode.commands.registerCommand('copilotCatalog.hats.delete', async () => { if(!currentRepo){ vscode.window.showWarningMessage('No repository available.'); return; } - const hats = await hatService.discoverHats(currentRepo); - const deletable = hats.filter(h=> h.source === 'workspace' || h.source === 'user'); - if(deletable.length === 0){ vscode.window.showInformationMessage('No workspace/user hats to delete.'); return; } - const pick = await vscode.window.showQuickPick(deletable.map(h=> ({ label: h.name, description: h.description || h.source, detail: `${h.source} hat`, hat: h })), { placeHolder: 'Select a Hat to delete' }); + const presets = await presetService.discoverPresets(currentRepo); + const deletable = presets.filter(h=> h.source === 'workspace' || h.source === 'user'); + if(deletable.length === 0){ vscode.window.showInformationMessage('No workspace/user presets to delete.'); return; } + const pick = await vscode.window.showQuickPick(deletable.map(h=> ({ label: h.name, description: h.description || h.source, detail: `${h.source} preset`, preset: h })), { placeHolder: 'Select a Preset to delete' }); if(!pick) return; - const confirm = await vscode.window.showWarningMessage(`Delete Hat "${pick.hat.name}" from ${pick.hat.source}?`, { modal: true }, 'Delete'); + const confirm = await vscode.window.showWarningMessage(`Delete Preset "${pick.preset.name}" from ${pick.preset.source}?`, { modal: true }, 'Delete'); if(confirm !== 'Delete') return; - const ok = await hatService.deleteHat(pick.hat, currentRepo); - if(ok) vscode.window.showInformationMessage(`Deleted Hat "${pick.hat.name}" (${pick.hat.source}).`); - else vscode.window.showWarningMessage('Hat not found or could not be deleted.'); + const ok = await presetService.deletePreset(pick.preset, currentRepo); + if(ok) vscode.window.showInformationMessage(`Deleted Preset "${pick.preset.name}" (${pick.preset.source}).`); + else vscode.window.showWarningMessage('Preset not found or could not be deleted.'); }) , // User resource enable/disable diff --git a/src/models.ts b/src/models.ts index b213deb..b2bd301 100644 --- a/src/models.ts +++ b/src/models.ts @@ -15,7 +15,7 @@ export enum ResourceCategory { export enum ResourceState { INACTIVE = 0, ACTIVE = 1, MODIFIED = 2 } export type ResourceOrigin = 'catalog' | 'user' | 'remote'; -export type HatSource = 'catalog' | 'workspace' | 'user'; +export type PresetSource = 'catalog' | 'workspace' | 'user'; // Configuration for multiple catalog sources export interface CatalogSource { @@ -56,14 +56,14 @@ export interface OperationResult { success: boolean; resource: Resource; message export interface ActivateOptions { merge?: boolean } -// Hat (preset) definition: a named set of catalog resource relative paths -export interface Hat { +// Preset definition: a named set of catalog resource relative paths +export interface Preset { id: string; // unique id for tree/commands name: string; // display name description?: string; // optional description resources: string[]; // list of resource.relativePath entries to activate - source: HatSource; // where it came from - definitionPath?: string; // absolute file path where the hat is defined (for catalog/workspace), when applicable + source: PresetSource; // where it came from + definitionPath?: string; // absolute file path where the preset is defined (for catalog/workspace), when applicable } export interface IFileService { diff --git a/src/services/hatService.ts b/src/services/presetService.ts similarity index 56% rename from src/services/hatService.ts rename to src/services/presetService.ts index bcaa519..ea78c1a 100644 --- a/src/services/hatService.ts +++ b/src/services/presetService.ts @@ -1,36 +1,36 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. import * as path from 'path'; -import { Hat, HatSource, IFileService, Repository, Resource } from '../models'; +import { Preset, PresetSource, IFileService, Repository, Resource } from '../models'; import { ResourceService } from './resourceService'; import { logger } from '../utils/logger'; -export class HatService { +export class PresetService { constructor(private fileService: IFileService, private resourceService: ResourceService, private userStorageRoot: string){ } - // Discover hats from catalog (repo.catalogPath/hats/*.json), workspace (.vscode/copilot-hats.json), and user storage (/hats.json) - async discoverHats(repo: Repository): Promise{ - const hats: Hat[] = []; - // Catalog hats directory - hats.push(...await this.readHatsFromDirectory(path.join(repo.catalogPath, 'hats'), 'catalog')); - // Workspace hats file - const workspaceFile = path.join(repo.rootPath, '.vscode', 'copilot-hats.json'); - hats.push(...await this.readHatsFromFile(workspaceFile, 'workspace')); - // User hats file - const userFile = path.join(this.userStorageRoot, 'hats.json'); - hats.push(...await this.readHatsFromFile(userFile, 'user')); + // Discover presets from catalog (repo.catalogPath/presets/*.json), workspace (.vscode/copilot-presets.json), and user storage (/presets.json) + async discoverPresets(repo: Repository): Promise{ + const presets: Preset[] = []; + // Catalog presets directory + presets.push(...await this.readPresetsFromDirectory(path.join(repo.catalogPath, 'presets'), 'catalog')); + // Workspace presets file + const workspaceFile = path.join(repo.rootPath, '.vscode', 'copilot-presets.json'); + presets.push(...await this.readPresetsFromFile(workspaceFile, 'workspace')); + // User presets file + const userFile = path.join(this.userStorageRoot, 'presets.json'); + presets.push(...await this.readPresetsFromFile(userFile, 'user')); // Ensure unique ids: prefix by source and filename when available const seen = new Set(); - for(const h of hats){ + for(const h of presets){ if(!h.id){ h.id = `${h.source}:${h.name}`; } let id = h.id; let i=1; while(seen.has(id)){ id = `${h.id}-${i++}`; } h.id = id; seen.add(id); } - return hats; + return presets; } - async applyHat(repo: Repository, resources: Resource[], hat: Hat, options?: { exclusive?: boolean }): Promise<{success:boolean; activated:number; deactivated:number; missing:string[]; errors:string[]}>{ + async applyPreset(repo: Repository, resources: Resource[], preset: Preset, options?: { exclusive?: boolean }): Promise<{success:boolean; activated:number; deactivated:number; missing:string[]; errors:string[]}>{ const missing: string[] = []; const errors: string[] = []; let activated = 0; @@ -39,7 +39,7 @@ export class HatService { const byRel = new Map(); for(const r of resources){ byRel.set(toSlash(r.relativePath), r); } // Resolve desired resource relative paths to Resource objects - const desiredSet = new Set(hat.resources.map(r=> toSlash(r))); + const desiredSet = new Set(preset.resources.map(r=> toSlash(r))); if(options?.exclusive){ // Deactivate any currently active (non-user) resource not in desired set for(const r of resources){ @@ -52,7 +52,7 @@ export class HatService { } } } - for(const rel of hat.resources){ + for(const rel of preset.resources){ const key = toSlash(rel); let target = byRel.get(key); if(!target){ @@ -69,44 +69,44 @@ export class HatService { return { success: errors.length===0, activated, deactivated, missing, errors }; } - async createHatFromActive(name: string, description: string|undefined, resources: Resource[], source: HatSource, repo?: Repository): Promise{ + async createPresetFromActive(name: string, description: string|undefined, resources: Resource[], source: PresetSource, repo?: Repository): Promise{ const rels = resources.filter(r=> r.state === 1 /* ACTIVE */ || (r as any).origin === 'user').map(r=> r.relativePath); - const hat: Hat = { id: `${source}:${name}`, name, description, resources: Array.from(new Set(rels)), source }; + const preset: Preset = { id: `${source}:${name}`, name, description, resources: Array.from(new Set(rels)), source }; if(source === 'workspace' && repo){ - await this.saveHatToWorkspace(repo, hat); + await this.savePresetToWorkspace(repo, preset); } else if(source === 'user'){ - await this.saveHatToUser(hat); + await this.savePresetToUser(preset); } - return hat; + return preset; } - async listWorkspaceHats(repo: Repository): Promise{ - const wsFile = path.join(repo.rootPath, '.vscode', 'copilot-hats.json'); - return this.readHatsFromFile(wsFile, 'workspace'); + async listWorkspacePresets(repo: Repository): Promise{ + const wsFile = path.join(repo.rootPath, '.vscode', 'copilot-presets.json'); + return this.readPresetsFromFile(wsFile, 'workspace'); } - async listUserHats(): Promise{ - const userFile = path.join(this.userStorageRoot, 'hats.json'); - return this.readHatsFromFile(userFile, 'user'); + async listUserPresets(): Promise{ + const userFile = path.join(this.userStorageRoot, 'presets.json'); + return this.readPresetsFromFile(userFile, 'user'); } - async deleteHat(hat: Hat, repo?: Repository): Promise{ - if(hat.source === 'workspace'){ + async deletePreset(preset: Preset, repo?: Repository): Promise{ + if(preset.source === 'workspace'){ if(!repo) return false; const wsDir = path.join(repo.rootPath, '.vscode'); - const wsFile = path.join(wsDir, 'copilot-hats.json'); - const list = await this.readHatArrayFile(wsFile); + const wsFile = path.join(wsDir, 'copilot-presets.json'); + const list = await this.readPresetArrayFile(wsFile); const before = list.length; - const filtered = list.filter(h=> (h?.name||'') !== hat.name); + const filtered = list.filter(h=> (h?.name||'') !== preset.name); if(filtered.length === before) return false; await this.fileService.ensureDirectory(wsDir); await this.fileService.writeFile(wsFile, JSON.stringify(filtered, null, 2)); return true; } - if(hat.source === 'user'){ - const userFile = path.join(this.userStorageRoot, 'hats.json'); - const list = await this.readHatArrayFile(userFile); + if(preset.source === 'user'){ + const userFile = path.join(this.userStorageRoot, 'presets.json'); + const list = await this.readPresetArrayFile(userFile); const before = list.length; - const filtered = list.filter(h=> (h?.name||'') !== hat.name); + const filtered = list.filter(h=> (h?.name||'') !== preset.name); if(filtered.length === before) return false; await this.fileService.ensureDirectory(this.userStorageRoot); await this.fileService.writeFile(userFile, JSON.stringify(filtered, null, 2)); @@ -115,27 +115,27 @@ export class HatService { return false; } - async saveHatToUser(hat: Hat): Promise{ - const userFile = path.join(this.userStorageRoot, 'hats.json'); - const list = await this.readHatArrayFile(userFile); - const filtered = list.filter(h=> h.name !== hat.name); - filtered.push({ name: hat.name, description: hat.description, resources: hat.resources }); + async savePresetToUser(preset: Preset): Promise{ + const userFile = path.join(this.userStorageRoot, 'presets.json'); + const list = await this.readPresetArrayFile(userFile); + const filtered = list.filter(h=> h.name !== preset.name); + filtered.push({ name: preset.name, description: preset.description, resources: preset.resources }); await this.fileService.ensureDirectory(this.userStorageRoot); await this.fileService.writeFile(userFile, JSON.stringify(filtered, null, 2)); } - async saveHatToWorkspace(repo: Repository, hat: Hat): Promise{ + async savePresetToWorkspace(repo: Repository, preset: Preset): Promise{ const wsDir = path.join(repo.rootPath, '.vscode'); - const wsFile = path.join(wsDir, 'copilot-hats.json'); - const list = await this.readHatArrayFile(wsFile); - const filtered = list.filter(h=> h.name !== hat.name); - filtered.push({ name: hat.name, description: hat.description, resources: hat.resources }); + const wsFile = path.join(wsDir, 'copilot-presets.json'); + const list = await this.readPresetArrayFile(wsFile); + const filtered = list.filter(h=> h.name !== preset.name); + filtered.push({ name: preset.name, description: preset.description, resources: preset.resources }); await this.fileService.ensureDirectory(wsDir); await this.fileService.writeFile(wsFile, JSON.stringify(filtered, null, 2)); } - private async readHatsFromDirectory(dir: string, source: HatSource): Promise{ - const out: Hat[] = []; + private async readPresetsFromDirectory(dir: string, source: PresetSource): Promise{ + const out: Preset[] = []; try{ const entries = await this.fileService.listDirectory(dir); for(const name of entries){ @@ -143,19 +143,19 @@ export class HatService { const st = await this.fileService.stat(full).catch(()=> 'missing'); if(st !== 'file') continue; if(!name.toLowerCase().endsWith('.json')) continue; - const parsed = await this.tryParseHatFile(full); + const parsed = await this.tryParsePresetFile(full); if(parsed){ out.push({ ...parsed, source, id: `${source}:${parsed.name}`, definitionPath: full }); } } }catch{ /* no dir */ } return out; } - private async readHatsFromFile(file: string, source: HatSource): Promise{ - const list = await this.readHatArrayFile(file); + private async readPresetsFromFile(file: string, source: PresetSource): Promise{ + const list = await this.readPresetArrayFile(file); return list.map(h=> ({ id: `${source}:${h.name}` , name: h.name, description: h.description, resources: h.resources||[], source, definitionPath: file })); } - private async tryParseHatFile(file: string): Promise<{name:string; description?:string; resources:string[]} | undefined>{ + private async tryParsePresetFile(file: string): Promise<{name:string; description?:string; resources:string[]} | undefined>{ try{ const raw = await this.fileService.readFile(file); const obj = JSON.parse(raw); @@ -165,12 +165,12 @@ export class HatService { return { name: obj.name, description, resources }; } }catch(error) { - await logger.warn(`[HatService] Failed to parse hat file: ${error}`); + await logger.warn(`[PresetService] Failed to parse preset file: ${error}`); } return undefined; } - private async readHatArrayFile(file: string): Promise>{ + private async readPresetArrayFile(file: string): Promise>{ try{ const raw = await this.fileService.readFile(file); const arr = JSON.parse(raw); diff --git a/test/hats.test.ts b/test/hats.test.ts deleted file mode 100644 index 0d36bc5..0000000 --- a/test/hats.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -import { MockFileService } from './fileService.mock'; -import { ResourceService } from '../src/services/resourceService'; -import { HatService } from '../src/services/hatService'; -import { Repository, ResourceCategory } from '../src/models'; -import * as path from 'path'; - -function repoFor(root: string): Repository { - return { id: 'r', name: 'r', rootPath: root, catalogPath: path.join(root,'copilot_catalog'), runtimePath: path.join(root,'.github'), isActive: true }; -} - -(async function run(){ - const root = path.resolve('/ws'); - const files: Record = { - [path.join(root, 'copilot_catalog', 'chatmodes', 'a.chatmode.md')]: 'A', - [path.join(root, 'copilot_catalog', 'instructions', 'b.instructions.md')]: 'B', - [path.join(root, 'copilot_catalog', 'hats', 'ab.json')]: JSON.stringify({ name: 'AB', resources: [ 'chatmodes/a.chatmode.md', 'instructions/b.instructions.md' ]}, null, 2) - }; - const fs = new MockFileService(files); - const rs = new ResourceService(fs); - const hats = new HatService(fs, rs, path.join(root, '.user')); - const repo = repoFor(root); - const resources = await rs.discoverResources(repo); - const discovered = await hats.discoverHats(repo); - if(discovered.length !== 1 || discovered[0].name !== 'AB') throw new Error('Hat discovery failed'); - const applyRes = await hats.applyHat(repo, resources, discovered[0]); - if(!applyRes.success || applyRes.activated !== 2){ - console.error('applyRes', applyRes); - throw new Error('Hat apply failed'); - } - // Exclusive apply should deactivate any active not in hat (none at this moment because only two are active); still fine to call - const applyExclusive = await hats.applyHat(repo, resources, discovered[0], { exclusive: true }); - if(!applyExclusive.success){ throw new Error('Exclusive hat apply failed'); } - // Save from active to workspace - const created = await hats.createHatFromActive('WS', 'desc', resources, 'workspace', repo); - if(created.name !== 'WS') throw new Error('Create hat failed'); - const wsFile = path.join(root, '.vscode', 'copilot-hats.json'); - const wsExists = await fs.pathExists(wsFile); - if(!wsExists) throw new Error('Workspace hats file not written'); - // Create a user hat as well - await hats.createHatFromActive('USR', undefined, resources, 'user'); - const userFile = path.join(root, '.user', 'hats.json'); - const userExists = await fs.pathExists(userFile); - if(!userExists) throw new Error('User hats file not written'); - // Delete both - const wsHats = await hats.listWorkspaceHats(repo); - const usrHats = await hats.listUserHats(); - const delWs = await hats.deleteHat(wsHats.find(h=> h.name==='WS')!, repo); - const delUsr = await hats.deleteHat(usrHats.find(h=> h.name==='USR')!); - if(!delWs || !delUsr) throw new Error('Delete hat failed'); - console.log('hats.test PASS'); -})().catch(e=>{ console.error('hats.test FAIL', e); process.exit(1); }); diff --git a/test/presets.test.ts b/test/presets.test.ts new file mode 100644 index 0000000..62abc24 --- /dev/null +++ b/test/presets.test.ts @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +import { MockFileService } from './fileService.mock'; +import { ResourceService } from '../src/services/resourceService'; +import { PresetService } from '../src/services/presetService'; +import { Repository, ResourceCategory } from '../src/models'; +import * as path from 'path'; + +function repoFor(root: string): Repository { + return { id: 'r', name: 'r', rootPath: root, catalogPath: path.join(root,'copilot_catalog'), runtimePath: path.join(root,'.github'), isActive: true }; +} + +(async function run(){ + const root = path.resolve('/ws'); + const files: Record = { + [path.join(root, 'copilot_catalog', 'chatmodes', 'a.chatmode.md')]: 'A', + [path.join(root, 'copilot_catalog', 'instructions', 'b.instructions.md')]: 'B', + [path.join(root, 'copilot_catalog', 'presets', 'ab.json')]: JSON.stringify({ name: 'AB', resources: [ 'chatmodes/a.chatmode.md', 'instructions/b.instructions.md' ]}, null, 2) + }; + const fs = new MockFileService(files); + const rs = new ResourceService(fs); + const presets = new PresetService(fs, rs, path.join(root, '.user')); + const repo = repoFor(root); + const resources = await rs.discoverResources(repo); + const discovered = await presets.discoverPresets(repo); + if(discovered.length !== 1 || discovered[0].name !== 'AB') throw new Error('Preset discovery failed'); + const applyRes = await presets.applyPreset(repo, resources, discovered[0]); + if(!applyRes.success || applyRes.activated !== 2){ + console.error('applyRes', applyRes); + throw new Error('Preset apply failed'); + } + // Exclusive apply should deactivate any active not in preset (none at this moment because only two are active); still fine to call + const applyExclusive = await presets.applyPreset(repo, resources, discovered[0], { exclusive: true }); + if(!applyExclusive.success){ throw new Error('Exclusive preset apply failed'); } + // Save from active to workspace + const created = await presets.createPresetFromActive('WS', 'desc', resources, 'workspace', repo); + if(created.name !== 'WS') throw new Error('Create preset failed'); + const wsFile = path.join(root, '.vscode', 'copilot-presets.json'); + const wsExists = await fs.pathExists(wsFile); + if(!wsExists) throw new Error('Workspace presets file not written'); + // Create a user preset as well + await presets.createPresetFromActive('USR', undefined, resources, 'user'); + const userFile = path.join(root, '.user', 'presets.json'); + const userExists = await fs.pathExists(userFile); + if(!userExists) throw new Error('User presets file not written'); + // Delete both + const wsPresets = await presets.listWorkspacePresets(repo); + const usrPresets = await presets.listUserPresets(); + const delWs = await presets.deletePreset(wsPresets.find(h=> h.name==='WS')!, repo); + const delUsr = await presets.deletePreset(usrPresets.find(h=> h.name==='USR')!); + if(!delWs || !delUsr) throw new Error('Delete preset failed'); + console.log('presets.test PASS'); +})().catch(e=>{ console.error('presets.test FAIL', e); process.exit(1); }); diff --git a/tsconfig.json b/tsconfig.json index 7e4fcc6..b0dc9e2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1 +1,19 @@ -{"compilerOptions": {"allowJs": true,"module": "commonjs","target": "es2022","outDir": "dist","lib": ["es2022", "dom"],"sourceMap": true,"rootDir": ".","strict": true,"esModuleInterop": true,"forceConsistentCasingInFileNames": true,"skipLibCheck": true},"include": ["src","test"],"exclude": ["node_modules", ".vscode-test"]} +{ + "compilerOptions": { + "allowJs": true, + "module": "commonjs", + "target": "es2022", + "outDir": "dist", + "lib": ["es2022", "dom"], + "sourceMap": true, + "rootDir": ".", + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "moduleResolution": "node", + "types": ["node", "vscode"] + }, + "include": ["src", "test"], + "exclude": ["node_modules", ".vscode-test"] +}