\${escapeHtml(prompt.description || 'No description available')}
@@ -943,7 +1014,7 @@ export class PromptCardsWebviewProvider implements vscode.WebviewViewProvider {
@@ -952,7 +1023,7 @@ export class PromptCardsWebviewProvider implements vscode.WebviewViewProvider {
function getTypeIcon(type) {
switch(type) {
- case 'chatmode': return '💬';
+ case 'agents': return '💬';
case 'instructions': return '📖';
case 'prompts': return '⚡';
default: return '📄';
@@ -1010,10 +1081,12 @@ export class PromptCardsWebviewProvider implements vscode.WebviewViewProvider {
}
function togglePrompt(promptPath) {
+ console.log('[Promptitude WebView] togglePrompt called with:', promptPath);
vscode.postMessage({
command: 'togglePrompt',
promptPath: promptPath
});
+ console.log('[Promptitude WebView] Message posted to extension');
}
function viewPrompt(promptPath) {
@@ -1030,6 +1103,12 @@ export class PromptCardsWebviewProvider implements vscode.WebviewViewProvider {
});
}
+ function openLocalFolder() {
+ vscode.postMessage({
+ command: 'openLocalFolder'
+ });
+ }
+
// Request initial data
vscode.postMessage({ command: 'refresh' });
diff --git a/src/ui/promptCommands.ts b/src/ui/promptCommands.ts
index 3193748..d57964c 100644
--- a/src/ui/promptCommands.ts
+++ b/src/ui/promptCommands.ts
@@ -26,8 +26,8 @@ export class PromptCommandManager {
this.refreshPrompts();
});
- const toggleSelectionCommand = vscode.commands.registerCommand('prompts.toggleSelection', (item: PromptTreeItem | PromptInfo) => {
- this.toggleSelection(item);
+ const toggleSelectionCommand = vscode.commands.registerCommand('prompts.toggleSelection', async (item: PromptTreeItem | PromptInfo) => {
+ await this.toggleSelection(item);
});
const selectAllCommand = vscode.commands.registerCommand('prompts.selectAll', () => {
@@ -39,20 +39,20 @@ export class PromptCommandManager {
});
// Prompt action commands
- const editPromptCommand = vscode.commands.registerCommand('prompts.editPrompt', (item: PromptTreeItem | PromptInfo) => {
- this.editPrompt(item);
+ const editPromptCommand = vscode.commands.registerCommand('prompts.editPrompt', async (item: PromptTreeItem | PromptInfo) => {
+ await this.editPrompt(item);
});
- const viewPromptCommand = vscode.commands.registerCommand('prompts.viewPrompt', (item: PromptTreeItem | PromptInfo) => {
- this.viewPrompt(item);
+ const viewPromptCommand = vscode.commands.registerCommand('prompts.viewPrompt', async (item: PromptTreeItem | PromptInfo) => {
+ await this.viewPrompt(item);
});
- const deletePromptCommand = vscode.commands.registerCommand('prompts.deletePrompt', (item: PromptTreeItem | PromptInfo) => {
- this.deletePrompt(item);
+ const deletePromptCommand = vscode.commands.registerCommand('prompts.deletePrompt', async (item: PromptTreeItem | PromptInfo) => {
+ await this.deletePrompt(item);
});
- const duplicatePromptCommand = vscode.commands.registerCommand('prompts.duplicatePrompt', (item: PromptTreeItem | PromptInfo) => {
- this.duplicatePrompt(item);
+ const duplicatePromptCommand = vscode.commands.registerCommand('prompts.duplicatePrompt', async (item: PromptTreeItem | PromptInfo) => {
+ await this.duplicatePrompt(item);
});
// Settings command
@@ -92,42 +92,88 @@ export class PromptCommandManager {
}
this.logger.info(`Toggling selection for: ${promptInfo.name}, current active: ${promptInfo.active}, repositoryUrl: ${promptInfo.repositoryUrl || 'none'}`);
+ this.logger.debug(`Prompt info details - path: ${promptInfo.path}, workspaceName: ${promptInfo.workspaceName || 'none'}`);
- // Toggle the selection in the tree provider first
+ // Store the current state before toggling
+ const wasActive = promptInfo.active;
+ this.logger.debug(`State before toggle: wasActive = ${wasActive}`);
+
+ // Toggle the selection in the tree provider first (optimistic update)
this.treeProvider.toggleSelection(promptInfo);
- this.webviewProvider.updateSelectionStatus(promptInfo);
-
+ this.logger.debug(`State after toggle: promptInfo.active = ${promptInfo.active}`);
+
// Handle symlink creation/removal if SyncManager is available
if (this.syncManager) {
+ this.logger.debug(`SyncManager is available, proceeding with file operations`);
if (promptInfo.active) {
// Prompt was activated - create symlink
const repositoryUrl = promptInfo.repositoryUrl;
if (repositoryUrl) {
this.logger.info(`Activating prompt: ${promptInfo.name} with URL: ${repositoryUrl}`);
- await this.syncManager.activatePrompt(promptInfo.name, repositoryUrl);
- this.logger.info(`Successfully created symlink for activated prompt: ${promptInfo.name}`);
- vscode.window.showInformationMessage(`✅ Activated prompt: ${promptInfo.name}`);
+
+ // Show immediate feedback
+ const activatingMsg = vscode.window.setStatusBarMessage(`$(sync~spin) Activating prompt: ${promptInfo.name}...`);
+
+ try {
+ const actualWorkspaceName = await this.syncManager.activatePrompt(promptInfo.name, repositoryUrl);
+ // Update the workspace name in the prompt info
+ if (actualWorkspaceName !== promptInfo.name) {
+ promptInfo.workspaceName = actualWorkspaceName;
+ // Update the path to reflect the actual workspace filename
+ const promptsDir = this.config.getPromptsDirectory();
+ promptInfo.path = this.fileSystem.joinPath(promptsDir, actualWorkspaceName);
+ }
+ this.logger.info(`Successfully created symlink/copy for activated prompt: ${promptInfo.name} as ${actualWorkspaceName}`);
+ activatingMsg.dispose();
+
+ // File operation succeeded - refresh tree and update webview
+ this.treeProvider.refresh();
+ this.webviewProvider.updateSelectionStatus(promptInfo);
+ vscode.window.showInformationMessage(`✅ Activated prompt: ${promptInfo.name}`);
+ } catch (activationError) {
+ activatingMsg.dispose();
+ // Revert the toggle since activation failed
+ this.treeProvider.toggleSelection(promptInfo);
+ this.webviewProvider.updateSelectionStatus(promptInfo);
+ throw activationError;
+ }
} else {
const errorMsg = `No repository URL found for prompt: ${promptInfo.name}. Cannot create symlink.`;
this.logger.error(errorMsg);
vscode.window.showErrorMessage(errorMsg);
// Revert the toggle since we couldn't create the symlink
this.treeProvider.toggleSelection(promptInfo);
+ this.webviewProvider.updateSelectionStatus(promptInfo);
}
} else {
// Prompt was deactivated - remove symlink
- this.logger.info(`Deactivating prompt: ${promptInfo.name}`);
- await this.syncManager.deactivatePrompt(promptInfo.name);
- this.logger.info(`Successfully removed symlink for deactivated prompt: ${promptInfo.name}`);
- vscode.window.showInformationMessage(`✅ Deactivated prompt: ${promptInfo.name}`);
+ // Use workspaceName if available, otherwise fall back to name
+ const nameToDeactivate = promptInfo.workspaceName || promptInfo.name;
+ this.logger.info(`Deactivating prompt: ${nameToDeactivate}`);
+
+ try {
+ await this.syncManager.deactivatePrompt(nameToDeactivate);
+ this.logger.info(`Successfully removed symlink/copy for deactivated prompt: ${nameToDeactivate}`);
+
+ // File operation succeeded - refresh tree and update webview
+ this.treeProvider.refresh();
+ this.webviewProvider.updateSelectionStatus(promptInfo);
+ vscode.window.showInformationMessage(`✅ Deactivated prompt: ${promptInfo.name}`);
+ } catch (deactivationError) {
+ // Revert the toggle since deactivation failed
+ this.treeProvider.toggleSelection(promptInfo);
+ this.webviewProvider.updateSelectionStatus(promptInfo);
+ throw deactivationError;
+ }
}
} else {
this.logger.error('SyncManager not available - cannot create/remove symlinks');
vscode.window.showErrorMessage('SyncManager not available');
// Revert the toggle
this.treeProvider.toggleSelection(promptInfo);
+ this.webviewProvider.updateSelectionStatus(promptInfo);
}
-
+
const status = promptInfo.active ? 'activated' : 'deactivated';
this.logger.info(`Prompt ${promptInfo.name} ${status}`);
} catch (error) {
@@ -146,12 +192,75 @@ export class PromptCommandManager {
}
}
- private selectAll(): void {
+ private async selectAll(): Promise
{
try {
- this.treeProvider.selectAll();
- const count = this.treeProvider.getSelectedPrompts().length;
- vscode.window.showInformationMessage(`Activated ${count} prompts`);
- this.logger.debug(`Activated all prompts (${count} total)`);
+ if (!this.syncManager) {
+ vscode.window.showErrorMessage('SyncManager not available');
+ return;
+ }
+
+ // Get all prompts that are currently inactive
+ const allPrompts = this.treeProvider.getAllPrompts();
+ const inactivePrompts = allPrompts.filter(p => !p.active);
+
+ if (inactivePrompts.length === 0) {
+ vscode.window.showInformationMessage('All prompts are already activated');
+ return;
+ }
+
+ // Use withProgress for proper loading indicator
+ await vscode.window.withProgress({
+ location: vscode.ProgressLocation.Notification,
+ title: `Activating ${inactivePrompts.length} prompts`,
+ cancellable: false
+ }, async (progress) => {
+ if (!this.syncManager) {
+ throw new Error('SyncManager not available');
+ }
+
+ // Activate all inactive prompts by creating symlinks
+ let successCount = 0;
+ const errors: string[] = [];
+
+ for (let i = 0; i < inactivePrompts.length; i++) {
+ const prompt = inactivePrompts[i];
+ progress.report({
+ increment: (100 / inactivePrompts.length),
+ message: `${i + 1}/${inactivePrompts.length}: ${prompt.name}`
+ });
+
+ try {
+ if (!prompt.repositoryUrl) {
+ errors.push(`${prompt.name}: No repository URL`);
+ continue;
+ }
+
+ const workspaceName = await this.syncManager.activatePrompt(prompt.name, prompt.repositoryUrl);
+ prompt.workspaceName = workspaceName;
+ prompt.active = true;
+ successCount++;
+
+ // Update details view if this prompt is currently being viewed
+ this.webviewProvider.updateSelectionStatus(prompt);
+ } catch (error) {
+ const errorMsg = error instanceof Error ? error.message : String(error);
+ errors.push(`${prompt.name}: ${errorMsg}`);
+ this.logger.warn(`Failed to activate ${prompt.name}: ${errorMsg}`);
+ }
+ }
+
+ this.treeProvider.refresh();
+
+ if (successCount === inactivePrompts.length) {
+ vscode.window.showInformationMessage(`✅ Activated all ${successCount} prompts`);
+ } else if (successCount > 0) {
+ vscode.window.showWarningMessage(`⚠️ Activated ${successCount}/${inactivePrompts.length} prompts. ${errors.length} failed.`);
+ } else {
+ vscode.window.showErrorMessage(`❌ Failed to activate prompts: ${errors.join(', ')}`);
+ }
+
+ this.logger.debug(`Activated ${successCount}/${inactivePrompts.length} prompts`);
+ });
} catch (error) {
this.logger.error(`Failed to activate all prompts: ${error}`);
vscode.window.showErrorMessage(`Failed to activate all prompts: ${error}`);
@@ -160,16 +269,67 @@ export class PromptCommandManager {
private async deselectAll(): Promise {
try {
- // Get all currently selected prompts before deselecting
- const selected = [...this.treeProvider.getSelectedPrompts()];
-
- // Deactivate each prompt using toggleSelection to ensure symlinks are removed
- for (const prompt of selected) {
- await this.toggleSelection(prompt);
+ if (!this.syncManager) {
+ vscode.window.showErrorMessage('SyncManager not available');
+ return;
+ }
+
+ // Get all currently selected prompts
+ const activePrompts = this.treeProvider.getSelectedPrompts();
+
+ if (activePrompts.length === 0) {
+ vscode.window.showInformationMessage('No prompts are currently activated');
+ return;
}
-
- vscode.window.showInformationMessage('All prompts deactivated');
- this.logger.debug('Deactivated all prompts');
+
+ // Use withProgress for proper loading indicator
+ await vscode.window.withProgress({
+ location: vscode.ProgressLocation.Notification,
+ title: `Deactivating ${activePrompts.length} prompts`,
+ cancellable: false
+ }, async (progress) => {
+ if (!this.syncManager) {
+ throw new Error('SyncManager not available');
+ }
+
+ // Deactivate all prompts efficiently by removing symlinks directly
+ let successCount = 0;
+ const errors: string[] = [];
+
+ for (let i = 0; i < activePrompts.length; i++) {
+ const prompt = activePrompts[i];
+ progress.report({
+ increment: (100 / activePrompts.length),
+ message: `${i + 1}/${activePrompts.length}: ${prompt.name}`
+ });
+
+ try {
+ const nameToDeactivate = prompt.workspaceName || prompt.name;
+ await this.syncManager.deactivatePrompt(nameToDeactivate);
+ prompt.active = false;
+ successCount++;
+
+ // Update details view if this prompt is currently being viewed
+ this.webviewProvider.updateSelectionStatus(prompt);
+ } catch (error) {
+ const errorMsg = error instanceof Error ? error.message : String(error);
+ errors.push(`${prompt.name}: ${errorMsg}`);
+ this.logger.warn(`Failed to deactivate ${prompt.name}: ${errorMsg}`);
+ }
+ }
+
+ this.treeProvider.refresh();
+
+ if (successCount === activePrompts.length) {
+ vscode.window.showInformationMessage(`✅ Deactivated all ${successCount} prompts`);
+ } else if (successCount > 0) {
+ vscode.window.showWarningMessage(`⚠️ Deactivated ${successCount}/${activePrompts.length} prompts. ${errors.length} failed.`);
+ } else {
+ vscode.window.showErrorMessage(`❌ Failed to deactivate prompts: ${errors.join(', ')}`);
+ }
+
+ this.logger.debug(`Deactivated ${successCount}/${activePrompts.length} prompts`);
+ });
} catch (error) {
this.logger.error(`Failed to deactivate all prompts: ${error}`);
vscode.window.showErrorMessage(`Failed to deactivate all prompts: ${error}`);
@@ -227,7 +387,7 @@ export class PromptCommandManager {
await vscode.workspace.fs.delete(vscode.Uri.file(promptInfo.path));
this.treeProvider.refresh();
this.webviewProvider.clearPrompt();
-
+
vscode.window.showInformationMessage(`Prompt "${promptInfo.name}" deleted successfully`);
this.logger.debug(`Deleted prompt: ${promptInfo.name}`);
}
@@ -249,7 +409,7 @@ export class PromptCommandManager {
const baseName = promptInfo.name.replace(/\.[^/.]+$/, '');
const extension = promptInfo.name.substring(baseName.length);
let newName = `${baseName}_copy${extension}`;
-
+
// Ensure unique filename
const promptsDir = this.config.getPromptsDirectory();
let counter = 1;
@@ -259,14 +419,14 @@ export class PromptCommandManager {
}
const newPath = this.fileSystem.joinPath(promptsDir, newName);
-
+
// Copy content
const content = await this.fileSystem.readFileContent(promptInfo.path);
await this.fileSystem.writeFileContent(newPath, content);
-
+
// Refresh tree view
this.treeProvider.refresh();
-
+
vscode.window.showInformationMessage(`Prompt duplicated as "${newName}"`);
this.logger.debug(`Duplicated prompt ${promptInfo.name} as ${newName}`);
} catch (error) {
@@ -289,11 +449,11 @@ export class PromptCommandManager {
if ('promptInfo' in item) {
return item.promptInfo;
}
-
+
if ('name' in item && 'path' in item && 'type' in item) {
return item as PromptInfo;
}
-
+
return undefined;
}
@@ -301,7 +461,7 @@ export class PromptCommandManager {
async deleteSelectedPrompts(): Promise {
try {
const selectedPrompts = this.treeProvider.getSelectedPrompts();
-
+
if (selectedPrompts.length === 0) {
vscode.window.showInformationMessage('No active prompts to delete');
return;
@@ -349,7 +509,7 @@ export class PromptCommandManager {
async exportSelectedPrompts(): Promise {
try {
const selectedPrompts = this.treeProvider.getSelectedPrompts();
-
+
if (selectedPrompts.length === 0) {
vscode.window.showInformationMessage('No active prompts to export');
return;
diff --git a/src/ui/promptDetailsWebview.ts b/src/ui/promptDetailsWebview.ts
index c7a0d60..6ee2f9e 100644
--- a/src/ui/promptDetailsWebview.ts
+++ b/src/ui/promptDetailsWebview.ts
@@ -4,7 +4,7 @@ import { PromptInfo } from './promptTreeProvider';
import { FileSystemManager } from '../utils/fileSystem';
import { Logger } from '../utils/logger';
import { ConfigManager } from '../configManager';
-import { encodeRepositorySlug } from '../storage/repositoryStorage';
+import { encodeRepositorySlug, getRepositoryStorageDirectory } from '../storage/repositoryStorage';
export class PromptDetailsWebviewProvider implements vscode.WebviewViewProvider {
public static readonly viewType = 'promptitude.details';
@@ -13,13 +13,16 @@ export class PromptDetailsWebviewProvider implements vscode.WebviewViewProvider
private _currentPrompt?: PromptInfo;
private fileSystem: FileSystemManager;
private logger: Logger;
+ private context?: vscode.ExtensionContext;
constructor(
private readonly _extensionUri: vscode.Uri,
- private readonly config: ConfigManager
+ private readonly config: ConfigManager,
+ context?: vscode.ExtensionContext
) {
this.fileSystem = new FileSystemManager();
this.logger = Logger.get('PromptDetailsWebviewProvider');
+ this.context = context;
}
public resolveWebviewView(
@@ -76,9 +79,11 @@ export class PromptDetailsWebviewProvider implements vscode.WebviewViewProvider
}
try {
+ this.logger.debug(`showPrompt called for: ${prompt.name}, active: ${prompt.active}, repositoryUrl: ${prompt.repositoryUrl}`);
+
// Compute the actual file path
const actualPath = this.getActualFilePath(prompt);
- this.logger.debug(`Reading prompt from: ${actualPath}`);
+ this.logger.debug(`Actual path resolved to: ${actualPath}`);
const content = await this.fileSystem.readFileContent(actualPath);
const metadata = await this.getPromptMetadata(prompt);
@@ -105,34 +110,49 @@ export class PromptDetailsWebviewProvider implements vscode.WebviewViewProvider
private getActualFilePath(prompt: PromptInfo): string {
const fs = require('fs');
- this.logger.debug(`Getting actual path - active: ${prompt.active}, repositoryUrl: ${prompt.repositoryUrl}, path: ${prompt.path}`);
+ this.logger.debug(`Getting actual path - active: ${prompt.active}, repositoryUrl: ${prompt.repositoryUrl}, path: ${prompt.path}, name: ${prompt.name}`);
- // First, check if the workspace path exists (for active prompts)
- if (fs.existsSync(prompt.path)) {
- this.logger.debug(`File exists at workspace path: ${prompt.path}`);
- return prompt.path;
- }
-
- // If workspace path doesn't exist and we have a repository URL, try repository storage
- if (prompt.repositoryUrl) {
- // Get prompts directory and compute repository storage directory
- const promptsDir = this.config.getPromptsDirectory();
- const repoStorageDir = path.join(path.dirname(promptsDir), 'repos');
+ // For inactive prompts with a repository URL, go directly to repository storage
+ if (!prompt.active && prompt.repositoryUrl) {
+ // Get repository storage directory using the helper function
+ const repoStorageDir = getRepositoryStorageDirectory(this.context);
// Encode repository URL using the same logic as SyncManager
const encodedUrl = encodeRepositorySlug(prompt.repositoryUrl);
- // Build path to repository storage
+ // Build path to repository storage using the original filename
const repoStoragePath = path.join(repoStorageDir, encodedUrl, prompt.name);
+ this.logger.debug(`Repo storage dir: ${repoStorageDir}`);
+ this.logger.debug(`Encoded URL: ${encodedUrl}`);
+ this.logger.debug(`Looking for inactive prompt at repository storage: ${repoStoragePath}`);
+
if (fs.existsSync(repoStoragePath)) {
- this.logger.debug(`File exists at repository storage: ${repoStoragePath}`);
+ this.logger.debug(`File found at repository storage: ${repoStoragePath}`);
return repoStoragePath;
} else {
this.logger.warn(`File not found at repository storage: ${repoStoragePath}`);
+ // Try to list files in the encoded directory to help debug
+ const encodedDir = path.join(repoStorageDir, encodedUrl);
+ if (fs.existsSync(encodedDir)) {
+ try {
+ const files = fs.readdirSync(encodedDir);
+ this.logger.debug(`Files in repository directory: ${files.join(', ')}`);
+ } catch (err) {
+ this.logger.debug(`Could not list files in repository directory: ${err}`);
+ }
+ } else {
+ this.logger.warn(`Repository directory does not exist: ${encodedDir}`);
+ }
}
}
+ // For active prompts or when repository lookup failed, check workspace path
+ if (fs.existsSync(prompt.path)) {
+ this.logger.debug(`File exists at workspace path: ${prompt.path}`);
+ return prompt.path;
+ }
+
// Fallback to the original path (will likely fail, but at least we tried)
this.logger.warn(`File not found anywhere, returning original path: ${prompt.path}`);
return prompt.path;
@@ -446,7 +466,7 @@ export class PromptDetailsWebviewProvider implements vscode.WebviewViewProvider
// Update source if from repository
if (data.prompt.repositoryUrl) {
const repoName = extractRepositoryName(data.prompt.repositoryUrl);
- promptSource.innerHTML = \`\${repoName} 🔗\`;
+ promptSource.innerHTML = \`\${repoName}\`;
// Add click handler for the link
const repoLink = promptSource.querySelector('.repo-link');
diff --git a/src/ui/promptTreeProvider.ts b/src/ui/promptTreeProvider.ts
index f32d87f..1ec7cc1 100644
--- a/src/ui/promptTreeProvider.ts
+++ b/src/ui/promptTreeProvider.ts
@@ -4,18 +4,19 @@ import * as fs from 'fs';
import { ConfigManager } from '../configManager';
import { FileSystemManager } from '../utils/fileSystem';
import { Logger } from '../utils/logger';
-import { decodeRepositorySlug } from '../storage/repositoryStorage';
+import { decodeRepositorySlug, encodeRepositorySlug, getRepositoryStorageDirectory } from '../storage/repositoryStorage';
export interface PromptInfo {
- name: string;
+ name: string; // Original filename from repository
path: string;
- type: 'chatmode' | 'instructions' | 'prompts';
+ type: 'agents' | 'instructions' | 'prompts';
size: number;
lastModified: Date;
lineCount: number;
active: boolean;
repositoryUrl?: string; // The repository URL this prompt came from
description?: string; // Extracted description from prompt content
+ workspaceName?: string; // Unique name used in workspace (may differ from name if there are conflicts)
}
export class PromptTreeItem extends vscode.TreeItem {
@@ -55,7 +56,7 @@ export class PromptTreeItem extends vscode.TreeItem {
private getTypeIcon(): vscode.ThemeIcon {
switch (this.promptInfo.type) {
- case 'chatmode':
+ case 'agents':
return new vscode.ThemeIcon('comment-discussion');
case 'instructions':
return new vscode.ThemeIcon('book');
@@ -91,7 +92,7 @@ export class CategoryTreeItem extends vscode.TreeItem {
private getIcon(): vscode.ThemeIcon {
switch (this.category.toLowerCase()) {
- case 'chatmode':
+ case 'agents':
return new vscode.ThemeIcon('comment-discussion');
case 'instructions':
return new vscode.ThemeIcon('book');
@@ -136,7 +137,7 @@ export class PromptTreeDataProvider implements vscode.TreeDataProvider p.active).length;
categories.push(new CategoryTreeItem(
@@ -196,7 +197,7 @@ export class PromptTreeDataProvider implements vscode.TreeDataProvider a.name.localeCompare(b.name))
.map(prompt => new PromptTreeItem(prompt, vscode.TreeItemCollapsibleState.None));
@@ -204,7 +205,7 @@ export class PromptTreeDataProvider implements vscode.TreeDataProvider {
try {
- const promptsDir = this.config.getPromptsDirectory();
- // Store repositories in a sibling directory to prompts
- // This ensures profile-specific storage when using VS Code profiles
- const parentDir = path.dirname(promptsDir);
- const repoStorageDir = path.join(parentDir, 'repos');
-
+ // Get repository storage directory from globalStorage
+ const repoStorageDir = getRepositoryStorageDirectory();
+
if (!await this.fileSystem.directoryExists(repoStorageDir)) {
- this.logger.debug('Repository storage directory does not exist');
+ this.logger.debug(`Repository storage directory does not exist: ${repoStorageDir}`);
return;
}
@@ -303,20 +300,20 @@ export class PromptTreeDataProvider implements vscode.TreeDataProvider
+ const found = prompts.find(p =>
p.name === fileName && p.repositoryUrl === repositoryUrl
);
if (found) {
@@ -382,26 +380,59 @@ export class PromptTreeDataProvider implements vscode.TreeDataProvider {
+ // Check if this filename exists in multiple repositories
+ const allPrompts = Array.from(this.prompts.values()).flat();
+ const promptsWithSameName = allPrompts.filter(p => p.name === fileName);
+
+ // If this filename only appears once across all repos, use original name
+ if (promptsWithSameName.length <= 1) {
+ // Check repository storage to see if file exists in other repos
+ const repoStorageDir = getRepositoryStorageDirectory();
+
+ if (await this.fileSystem.directoryExists(repoStorageDir)) {
+ const repoDirs = await this.fileSystem.readDirectory(repoStorageDir);
+ let repoCount = 0;
+
+ for (const repoDir of repoDirs) {
+ const fullRepoPath = path.join(repoStorageDir, repoDir);
+ if (await this.fileSystem.directoryExists(fullRepoPath)) {
+ const filePath = path.join(fullRepoPath, fileName);
+ if (await this.fileSystem.fileExists(filePath)) {
+ repoCount++;
+ if (repoCount > 1) {
+ break;
+ }
+ }
+ }
+ }
+
+ if (repoCount <= 1) {
+ return fileName;
+ }
+ } else {
+ return fileName;
+ }
+ }
+
+ // File exists in multiple repos - need to make it unique
+ const repoIdentifier = this.getRepositoryIdentifier(repositoryUrl);
+
+ // Insert identifier before the file extension
+ const lastDotIndex = fileName.lastIndexOf('.');
+ if (lastDotIndex > 0) {
+ const baseName = fileName.substring(0, lastDotIndex);
+ const extension = fileName.substring(lastDotIndex);
+ return `${baseName}@${repoIdentifier}${extension}`;
+ } else {
+ return `${fileName}@${repoIdentifier}`;
+ }
+ }
+
+ /**
+ * Extract a short, readable identifier from a repository URL
+ * This mirrors the logic in SyncManager.getRepositoryIdentifier
+ */
+ private getRepositoryIdentifier(repositoryUrl: string): string {
+ try {
+ // Remove protocol and common prefixes
+ let identifier = repositoryUrl
+ .replace(/^https?:\/\//, '')
+ .replace(/^www\./, '')
+ .replace(/\.git$/, '');
+
+ // For GitHub URLs: github.com/org/repo -> org-repo
+ if (identifier.includes('github.com/')) {
+ const parts = identifier.split('github.com/')[1].split('/');
+ if (parts.length >= 2) {
+ return `${parts[0]}-${parts[1]}`;
+ }
+ }
+
+ // For Azure DevOps: dev.azure.com/org/project/_git/repo -> org-project-repo
+ if (identifier.includes('dev.azure.com/')) {
+ const parts = identifier.split('/').filter(p => p && p !== '_git');
+ if (parts.length >= 4) {
+ return `${parts[1]}-${parts[2]}-${parts[3]}`;
+ }
+ }
+
+ // Fallback: use last 2 path segments separated by dash
+ const pathParts = identifier.split('/').filter(p => p);
+ if (pathParts.length >= 2) {
+ return `${pathParts[pathParts.length - 2]}-${pathParts[pathParts.length - 1]}`;
+ }
+
+ // Last resort: use the last path segment
+ return pathParts[pathParts.length - 1] || 'repo';
+ } catch (error) {
+ this.logger.warn(`Failed to extract repository identifier from ${repositoryUrl}, using fallback`);
+ return 'repo';
+ }
+ }
+
private isPromptFile(fileName: string): boolean {
// Filter out directories, hidden files, and non-prompt files
- return !fileName.startsWith('.') &&
- !fileName.startsWith('_') &&
- (fileName.endsWith('.md') || fileName.endsWith('.txt'));
+ return !fileName.startsWith('.') &&
+ !fileName.startsWith('_') &&
+ (fileName.endsWith('.md') || fileName.endsWith('.txt'));
}
private async createPromptInfo(promptsDir: string, fileName: string): Promise {
@@ -423,28 +549,65 @@ export class PromptTreeDataProvider implements vscode.TreeDataProvider ${repoConfig.url}`);
+ break;
+ }
+ }
+
+ // Additional check: if no repository match found but file exists in workspace prompts directory
+ // it might be a copied file that was just activated
+ if (!isActiveFileCopy) {
+ const promptsDir = this.config.getPromptsDirectory();
+ const normalizedPromptsDir = promptsDir.replace(/\\/g, '/');
+ const normalizedFilePath = filePath.replace(/\\/g, '/');
+
+ // If the file is in the prompts directory, check all repo storage for a matching file
+ if (normalizedFilePath.startsWith(normalizedPromptsDir)) {
+ this.logger.debug(`Windows: File ${fileName} found in workspace prompts directory, checking against repository storage`);
+ // Note: This prompt is in the workspace directory
+ // It will be marked as active if we can find it in any repository storage
+ // The repository URL will remain undefined if not found
+ }
+ }
}
} catch (error) {
// If lstat fails, it's likely not a symlink
this.logger.debug(`Not a symlink or failed to check: ${fileName}`);
}
-
+
+ // Active if it's a symlink OR a file copy on Windows that exists in repo storage
+ const isActive = isSymlink || isActiveFileCopy;
+
const promptInfo: PromptInfo = {
name: fileName,
path: filePath,
@@ -452,13 +615,14 @@ export class PromptTreeDataProvider implements vscode.TreeDataProvider line.trim())
.filter(line => line && !line.startsWith('#') && !line.startsWith('//') && !line.startsWith('/*'));
-
+
if (lines.length > 0) {
const firstLine = lines[0];
return firstLine.length > 100 ? firstLine.substring(0, 100) + '...' : firstLine;
}
-
+
return 'No description available';
}
@@ -585,22 +750,32 @@ export class PromptTreeDataProvider implements vscode.TreeDataProvider part === 'repos');
-
+
if (reposIndex !== -1 && reposIndex + 1 < pathParts.length) {
const encodedRepoUrl = pathParts[reposIndex + 1];
// Decode the repository URL
return this.decodeRepositoryUrl(encodedRepoUrl);
}
-
+
return undefined;
} catch (error) {
this.logger.warn(`Failed to extract repository URL from path: ${targetPath}: ${error}`);