diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index ba69db5..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(npm run compile:*)", - "Bash(grep:*)", - "Bash(sed:*)", - "Bash(rg:*)", - "Bash(npx tsc:*)" - ], - "deny": [] - }, - "enableAllProjectMcpServers": false -} \ No newline at end of file diff --git a/.gitignore b/.gitignore index a20aa9e..82cac34 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ dist node_modules .vscode-test/ *.vsix -backup \ No newline at end of file +backup +.claude/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index ef57185..e61ab08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,45 @@ All notable changes to the "claude-code-chat" extension will be documented in th Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. +## [1.2.1] - 2026-01-05 + +### 🚀 Features Added + +#### **User Commands Discovery** +- Commands now discovered from both global (`~/.claude/commands/`) and project (`.claude/commands/`) folders +- Different icons distinguish command sources: 📂 for project commands, 👤 for global commands +- Project commands override global commands with the same name +- Duplicate detection with visual warning showing which commands exist in both locations + +#### **Sync .claude Folder** +- New "Sync Now" button in Settings to refresh user commands +- Instantly re-scans both global and project `.claude/commands/` folders +- Visual feedback with success/error status messages + +#### **AskUserQuestion Tool Support** +- Full UI support for Claude's `AskUserQuestion` tool +- Purple-themed question cards with multiple choice options +- Support for single and multi-select questions +- "Other" option with custom text input +- Smooth animations and visual feedback + +#### **Plan Mode Improvements** +- Fixed plan mode state synchronization when ExitPlanMode is allowed +- UI toggle now correctly syncs to OFF when plan mode exits + +#### **Version Badge** +- Extension version now displayed in header +- Easy visibility of current installed version + +### 🐛 Bug Fixes +- **Critical**: Fixed `tool_use_id` field naming mismatch in control_response messages (was sending `toolUseID` instead of `tool_use_id`) +- Fixed plan mode toggle not syncing when Claude exits plan mode via ExitPlanMode tool + +### 🔧 Technical Improvements +- Renamed "Custom Commands" section to "User Commands" to avoid confusion with user-created snippets +- Added cross-platform build script (`build.js`) with version prompting +- Improved command discovery with proper deduplication logic + ## [1.1.0] - 2025-12-06 ### 🚀 Features Added diff --git a/build.js b/build.js new file mode 100644 index 0000000..245cb89 --- /dev/null +++ b/build.js @@ -0,0 +1,159 @@ +#!/usr/bin/env node + +/** + * Build script for Claude Code Chat VS Code Extension + * Cross-platform (Windows/Mac/Linux) + * Prompts for version bump before building + */ + +const { execSync } = require('child_process'); +const readline = require('readline'); +const fs = require('fs'); +const path = require('path'); + +// Colors for console output +const colors = { + reset: '\x1b[0m', + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + cyan: '\x1b[36m' +}; + +function log(message, color = 'reset') { + console.log(`${colors[color]}${message}${colors.reset}`); +} + +function exec(command, options = {}) { + try { + return execSync(command, { + stdio: options.silent ? 'pipe' : 'inherit', + encoding: 'utf8', + ...options + }); + } catch (error) { + if (!options.ignoreError) { + log(`Error executing: ${command}`, 'red'); + process.exit(1); + } + return null; + } +} + +function getPackageVersion() { + const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); + return pkg.version; +} + +function prompt(question) { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + return new Promise(resolve => { + rl.question(question, answer => { + rl.close(); + resolve(answer.trim()); + }); + }); +} + +function calculateNewVersion(current, type) { + const [major, minor, patch] = current.split('.').map(Number); + switch (type) { + case 'major': return `${major + 1}.0.0`; + case 'minor': return `${major}.${minor + 1}.0`; + case 'patch': return `${major}.${minor}.${patch + 1}`; + default: return current; + } +} + +async function main() { + console.log(''); + log('================================', 'blue'); + log(' Claude Code Chat Build Tool', 'blue'); + log('================================', 'blue'); + console.log(''); + + const currentVersion = getPackageVersion(); + log(`Current version: ${currentVersion}`, 'green'); + console.log(''); + + // Show version bump options + console.log('How would you like to bump the version?'); + console.log(''); + log(` 1) patch (${currentVersion} -> ${calculateNewVersion(currentVersion, 'patch')}) - Bug fixes`, 'yellow'); + log(` 2) minor (${currentVersion} -> ${calculateNewVersion(currentVersion, 'minor')}) - New features`, 'yellow'); + log(` 3) major (${currentVersion} -> ${calculateNewVersion(currentVersion, 'major')}) - Breaking changes`, 'yellow'); + log(` 4) skip - Keep current version`, 'yellow'); + log(` 5) custom - Enter version manually`, 'yellow'); + console.log(''); + + const choice = await prompt('Select option [1-5]: '); + + let newVersion = currentVersion; + + switch (choice) { + case '1': + log('Bumping patch version...', 'yellow'); + exec('npm version patch --no-git-tag-version'); + newVersion = getPackageVersion(); + break; + case '2': + log('Bumping minor version...', 'yellow'); + exec('npm version minor --no-git-tag-version'); + newVersion = getPackageVersion(); + break; + case '3': + log('Bumping major version...', 'yellow'); + exec('npm version major --no-git-tag-version'); + newVersion = getPackageVersion(); + break; + case '4': + log('Keeping current version...', 'yellow'); + break; + case '5': + const customVersion = await prompt('Enter custom version (e.g., 1.2.3): '); + if (!/^\d+\.\d+\.\d+$/.test(customVersion)) { + log('Invalid version format. Use semantic versioning (e.g., 1.2.3)', 'red'); + process.exit(1); + } + log(`Setting version to ${customVersion}...`, 'yellow'); + exec(`npm version ${customVersion} --no-git-tag-version`); + newVersion = customVersion; + break; + default: + log('Invalid option. Exiting.', 'red'); + process.exit(1); + } + + console.log(''); + log(`Building version: ${newVersion}`, 'green'); + console.log(''); + + // Compile TypeScript + log('Compiling TypeScript...', 'yellow'); + exec('npm run compile'); + + // Build the VSIX + log('Packaging VSIX...', 'yellow'); + exec('npx vsce package'); + + const outputName = `claude-code-chat-${newVersion}.vsix`; + + console.log(''); + log('================================', 'green'); + log(' Build Complete!', 'green'); + log('================================', 'green'); + console.log(''); + log(`Output: ${outputName}`, 'blue'); + log(`Version: ${newVersion}`, 'green'); + console.log(''); +} + +main().catch(err => { + log(`Build failed: ${err.message}`, 'red'); + process.exit(1); +}); diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..9d3e759 --- /dev/null +++ b/build.sh @@ -0,0 +1,98 @@ +#!/bin/bash + +# Build script for Claude Code Chat VS Code Extension +# Prompts for version bump before building + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Get current version from package.json +CURRENT_VERSION=$(node -p "require('./package.json').version") + +echo -e "${BLUE}================================${NC}" +echo -e "${BLUE} Claude Code Chat Build Tool${NC}" +echo -e "${BLUE}================================${NC}" +echo "" +echo -e "Current version: ${GREEN}${CURRENT_VERSION}${NC}" +echo "" + +# Prompt for version bump +echo "How would you like to bump the version?" +echo "" +echo -e " ${YELLOW}1)${NC} patch (${CURRENT_VERSION} -> x.x.+1) - Bug fixes" +echo -e " ${YELLOW}2)${NC} minor (${CURRENT_VERSION} -> x.+1.0) - New features" +echo -e " ${YELLOW}3)${NC} major (${CURRENT_VERSION} -> +1.0.0) - Breaking changes" +echo -e " ${YELLOW}4)${NC} skip - Keep current version" +echo -e " ${YELLOW}5)${NC} custom - Enter version manually" +echo "" +read -p "Select option [1-5]: " choice + +case $choice in + 1) + echo -e "${YELLOW}Bumping patch version...${NC}" + npm version patch --no-git-tag-version + ;; + 2) + echo -e "${YELLOW}Bumping minor version...${NC}" + npm version minor --no-git-tag-version + ;; + 3) + echo -e "${YELLOW}Bumping major version...${NC}" + npm version major --no-git-tag-version + ;; + 4) + echo -e "${YELLOW}Keeping current version...${NC}" + ;; + 5) + read -p "Enter custom version (e.g., 1.2.3): " custom_version + if [[ ! $custom_version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo -e "${RED}Invalid version format. Use semantic versioning (e.g., 1.2.3)${NC}" + exit 1 + fi + echo -e "${YELLOW}Setting version to ${custom_version}...${NC}" + npm version "$custom_version" --no-git-tag-version + ;; + *) + echo -e "${RED}Invalid option. Exiting.${NC}" + exit 1 + ;; +esac + +# Get the new version +NEW_VERSION=$(node -p "require('./package.json').version") +OUTPUT_NAME="claude-code-chat-${NEW_VERSION}.vsix" + +echo "" +echo -e "Building version: ${GREEN}${NEW_VERSION}${NC}" +echo "" + +# Compile TypeScript +echo -e "${YELLOW}Compiling TypeScript...${NC}" +npm run compile + +# Build the VSIX +echo -e "${YELLOW}Packaging VSIX...${NC}" +npx vsce package + +echo "" +echo -e "${GREEN}================================${NC}" +echo -e "${GREEN} Build Complete!${NC}" +echo -e "${GREEN}================================${NC}" +echo "" +echo -e "Output: ${BLUE}${OUTPUT_NAME}${NC}" +echo -e "Version: ${GREEN}${NEW_VERSION}${NC}" +echo "" + +# Ask if user wants to build Open VSIX version too +read -p "Also build Open VSIX version? [y/N]: " build_open_vsix +if [[ $build_open_vsix =~ ^[Yy]$ ]]; then + echo "" + echo -e "${YELLOW}Building Open VSIX version...${NC}" + bash build/open-vsix/build.sh +fi diff --git a/build/open-vsix/build.sh b/build/open-vsix/build.sh index a673553..6331e87 100755 --- a/build/open-vsix/build.sh +++ b/build/open-vsix/build.sh @@ -6,7 +6,10 @@ set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -VERSION="1.1.0" +PROJECT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)" + +# Read version from package.json (no hardcoding!) +VERSION=$(node -p "require('${PROJECT_DIR}/package.json').version") OUTPUT_NAME="vsix-claude-code-chat-${VERSION}.vsix" echo "Building Open VSIX version ${VERSION}..." diff --git a/package-lock.json b/package-lock.json index 82a87fc..d0ef97d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "claude-code-chat", - "version": "1.0.0", + "version": "1.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claude-code-chat", - "version": "1.0.0", + "version": "1.2.1", "license": "SEE LICENSE IN LICENSE", "devDependencies": { "@types/mocha": "^10.0.10", @@ -1020,6 +1020,7 @@ "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.32.1", "@typescript-eslint/types": "8.32.1", @@ -1519,6 +1520,7 @@ "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2652,6 +2654,7 @@ "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -6146,6 +6149,7 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 05b3db5..6c4fe8d 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "claude-code-chat", "displayName": "Chat for Claude Code", "description": "Beautiful Claude Code Chat Interface for VS Code", - "version": "1.1.0", + "version": "1.2.1", "publisher": "AndrePimenta", "author": "Andre Pimenta", "repository": { @@ -195,7 +195,8 @@ "watch": "tsc -watch -p ./", "pretest": "npm run compile && npm run lint", "lint": "eslint src", - "test": "vscode-test" + "test": "vscode-test", + "build": "node build.js" }, "devDependencies": { "@types/mocha": "^10.0.10", diff --git a/src/extension.ts b/src/extension.ts index 6d62328..9467d53 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -2,6 +2,7 @@ import * as vscode from 'vscode'; import * as cp from 'child_process'; import * as util from 'util'; import * as path from 'path'; +import * as os from 'os'; import getHtml from './ui'; const exec = util.promisify(cp.exec); @@ -134,6 +135,17 @@ class ClaudeChatProvider { suggestions?: any[]; toolUseId: string; }> = new Map(); + // Pending AskUserQuestion requests from stdio control_request messages + private _pendingQuestionRequests: Map; + }>; + toolUseId: string; + }> = new Map(); private _currentConversation: Array<{ timestamp: string, messageType: string, data: any }> = []; private _conversationStartTime: string | undefined; private _conversationIndex: Array<{ @@ -153,6 +165,7 @@ class ClaudeChatProvider { private _selectedModel: string = 'default'; // Default model private _isProcessing: boolean | undefined; private _draftMessage: string = ''; + private _planModeEnabled: boolean = false; // Track plan mode state from webview constructor( private readonly _extensionUri: vscode.Uri, @@ -254,6 +267,14 @@ class ClaudeChatProvider { data: this._isProcessing ? 'Claude is working...' : 'Ready to chat with Claude Code! Type your message below.' }); + // Send extension version to webview + this._postMessage({ + type: 'versionInfo', + data: { + extensionVersion: this._context.extension.packageJSON.version + } + }); + // Send current model to webview this._postMessage({ type: 'modelSelected', @@ -374,6 +395,12 @@ class ClaudeChatProvider { case 'getCustomSnippets': this._sendCustomSnippets(); return; + case 'getProjectCommands': + this._discoverProjectCommands(); + return; + case 'syncClaudeFolder': + this._syncClaudeFolder(); + return; case 'saveCustomSnippet': this._saveCustomSnippet(message.snippet); return; @@ -386,6 +413,16 @@ class ClaudeChatProvider { case 'saveInputText': this._saveInputText(message.text); return; + case 'planModeChanged': + this._planModeEnabled = message.enabled; + return; + case 'questionResponse': + this._handleQuestionResponse(message.id, message.answers); + return; + case 'triggerTestQuestion': + // Debug: trigger test AskUserQuestion UI + this._postMessage({ type: 'testAskUserQuestion' }); + return; } } @@ -1501,6 +1538,26 @@ class ClaudeChatProvider { console.log(`Permission request for tool: ${toolName}, requestId: ${requestId}`); + // Handle AskUserQuestion tool specially - show question UI instead of permission dialog + if (toolName === 'AskUserQuestion') { + this._handleAskUserQuestion(requestId, input.questions || [], toolUseId); + return; + } + + // Auto-deny EnterPlanMode if Plan First toggle is OFF + // This prevents Claude from entering plan mode on its own when user hasn't enabled it + if (toolName === 'EnterPlanMode' && !this._planModeEnabled) { + console.log('Auto-denying EnterPlanMode because Plan First toggle is OFF'); + this._sendPermissionResponse(requestId, false, { + requestId, + toolName, + input, + suggestions, + toolUseId + }, false); + return; + } + // Check if this tool is pre-approved const isPreApproved = await this._isToolPreApproved(toolName, input); @@ -1580,7 +1637,7 @@ class ClaudeChatProvider { updatedInput: pendingRequest.input, // Pass back suggestions if user chose "always allow" updatedPermissions: alwaysAllow ? pendingRequest.suggestions : undefined, - toolUseID: pendingRequest.toolUseId + tool_use_id: pendingRequest.toolUseId } } }; @@ -1594,7 +1651,7 @@ class ClaudeChatProvider { behavior: 'deny', message: 'User denied permission', interrupt: true, - toolUseID: pendingRequest.toolUseId + tool_use_id: pendingRequest.toolUseId } } }; @@ -1628,6 +1685,14 @@ class ClaudeChatProvider { // Send the response to Claude via stdin this._sendPermissionResponse(id, approved, pendingRequest, alwaysAllow); + // Sync plan mode state when ExitPlanMode is allowed + // This ensures the UI toggle turns OFF when Claude exits plan mode + if (pendingRequest.toolName === 'ExitPlanMode' && approved) { + console.log('ExitPlanMode allowed - syncing plan mode state to OFF'); + this._planModeEnabled = false; + this._postMessage({ type: 'planModeExited' }); + } + // Update the permission request status in UI this._postMessage({ type: 'updatePermissionStatus', @@ -1644,9 +1709,113 @@ class ClaudeChatProvider { } /** - * Cancel all pending permission requests (called when process ends) + * Handle AskUserQuestion tool requests from Claude CLI + * Shows interactive question UI to user and sends response back + */ + private _handleAskUserQuestion( + requestId: string, + questions: Array<{ + question: string; + header: string; + multiSelect: boolean; + options: Array<{ label: string; description: string }>; + }>, + toolUseId: string + ): void { + console.log(`AskUserQuestion request: ${requestId}, ${questions.length} questions`); + + // Store the pending request + this._pendingQuestionRequests.set(requestId, { + requestId, + questions, + toolUseId + }); + + // Send to webview for display + this._sendAndSaveMessage({ + type: 'askUserQuestion', + data: { + id: requestId, + questions: questions, + status: 'pending' + } + }); + } + + /** + * Handle user's answer to AskUserQuestion from webview + */ + private _handleQuestionResponse(id: string, answers: Record): void { + const pendingRequest = this._pendingQuestionRequests.get(id); + if (!pendingRequest) { + console.error('No pending question request found for id:', id); + return; + } + + // Remove from pending + this._pendingQuestionRequests.delete(id); + + // Send response to Claude CLI via stdin + this._sendQuestionResponse(id, answers, pendingRequest); + + // Update UI to show answered state + this._postMessage({ + type: 'updateQuestionStatus', + data: { + id: id, + status: 'answered', + answers: answers + } + }); + } + + /** + * Send AskUserQuestion response back to Claude CLI via stdin + */ + private _sendQuestionResponse( + requestId: string, + answers: Record, + pendingRequest: { + requestId: string; + questions: Array<{ + question: string; + header: string; + multiSelect: boolean; + options: Array<{ label: string; description: string }>; + }>; + toolUseId: string; + } + ): void { + if (!this._currentClaudeProcess?.stdin || this._currentClaudeProcess.stdin.destroyed) { + console.error('Cannot send question response: stdin not available'); + return; + } + + const response = { + type: 'control_response', + response: { + subtype: 'success', + request_id: requestId, + response: { + behavior: 'allow', + updatedInput: { + answers: answers + }, + tool_use_id: pendingRequest.toolUseId + } + } + }; + + const responseJson = JSON.stringify(response) + '\n'; + console.log('Sending question response:', responseJson); + this._currentClaudeProcess.stdin.write(responseJson); + } + + /** + * Cancel all pending permission and question requests (called when process ends) */ private _cancelPendingPermissionRequests(): void { + // Cancel permission requests for (const [id, _request] of this._pendingPermissionRequests) { this._postMessage({ type: 'updatePermissionStatus', @@ -1657,6 +1826,18 @@ class ClaudeChatProvider { }); } this._pendingPermissionRequests.clear(); + + // Cancel question requests + for (const [id, _request] of this._pendingQuestionRequests) { + this._postMessage({ + type: 'updateQuestionStatus', + data: { + id: id, + status: 'cancelled' + } + }); + } + this._pendingQuestionRequests.clear(); } /** @@ -2084,6 +2265,148 @@ class ClaudeChatProvider { } } + /** + * Discover project commands from .claude/commands/ folder + * Parses markdown files with YAML frontmatter for descriptions + * Project commands override global commands with the same name + */ + private async _discoverProjectCommands(): Promise { + try { + // 1. Scan user home ~/.claude/commands/ + const homeDir = os.homedir(); + const userCommandsUri = vscode.Uri.file(path.join(homeDir, '.claude', 'commands')); + const userCommands = await this._scanCommandsFolder(userCommandsUri, 'user'); + + // 2. Scan workspace .claude/commands/ + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + let projectCommands: Array<{ name: string; description: string; filePath: string; source: string }> = []; + if (workspaceFolder) { + const projectCommandsUri = vscode.Uri.joinPath(workspaceFolder.uri, '.claude', 'commands'); + projectCommands = await this._scanCommandsFolder(projectCommandsUri, 'project'); + } + + // 3. Detect duplicates (project commands override user commands with same name) + const userCommandNames = new Set(userCommands.map(cmd => cmd.name)); + const projectCommandNames = new Set(projectCommands.map(cmd => cmd.name)); + const duplicates = [...userCommandNames].filter(name => projectCommandNames.has(name)); + + // 4. Build final command list (project overrides user) + const commandMap = new Map(); + for (const cmd of userCommands) { + commandMap.set(cmd.name, cmd); + } + for (const cmd of projectCommands) { + commandMap.set(cmd.name, cmd); // Project overrides user + } + + const commands = Array.from(commandMap.values()); + commands.sort((a, b) => a.name.localeCompare(b.name)); + + // Log duplicates if any + if (duplicates.length > 0) { + console.log(`Found ${duplicates.length} duplicate command(s): ${duplicates.join(', ')} (project version used)`); + } + + console.log(`Discovered ${commands.length} user commands (${userCommands.length} global, ${projectCommands.length} project, ${duplicates.length} duplicates)`); + this._postMessage({ + type: 'projectCommandsData', + data: commands, + duplicates: duplicates + }); + } catch (error) { + console.error('Error discovering project commands:', error); + this._postMessage({ type: 'projectCommandsData', data: [], duplicates: [] }); + } + } + + /** + * Scan a commands folder and return array of command objects + */ + private async _scanCommandsFolder( + commandsUri: vscode.Uri, + source: 'project' | 'user' + ): Promise> { + const commands: Array<{ name: string; description: string; filePath: string; source: string }> = []; + + let files: [string, vscode.FileType][]; + try { + files = await vscode.workspace.fs.readDirectory(commandsUri); + } catch { + // Directory doesn't exist - no commands to discover + return commands; + } + + for (const [fileName, fileType] of files) { + if (fileType !== vscode.FileType.File || !fileName.endsWith('.md')) { + continue; + } + + const fileUri = vscode.Uri.joinPath(commandsUri, fileName); + try { + const contentBytes = await vscode.workspace.fs.readFile(fileUri); + const content = Buffer.from(contentBytes).toString('utf8'); + + // Parse YAML frontmatter + const frontmatter = this._parseCommandFrontmatter(content); + const name = fileName.replace(/\.md$/, ''); + + commands.push({ + name, + description: frontmatter?.description || `Run /${name} command`, + filePath: fileUri.fsPath, + source + }); + } catch (error) { + console.error(`Error reading command file ${fileName}:`, error); + } + } + + return commands; + } + + /** + * Parse YAML frontmatter from markdown file content + */ + private _parseCommandFrontmatter(content: string): { description: string } | null { + const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); + if (!match) { + return null; + } + + const frontmatter = match[1]; + const descMatch = frontmatter.match(/description:\s*(.+)/); + + return { + description: descMatch ? descMatch[1].trim() : '' + }; + } + + /** + * Sync .claude folder - refresh user commands and other discoverable content + */ + private async _syncClaudeFolder(): Promise { + try { + console.log('Syncing .claude folder...'); + + // Re-discover project commands from both user home and workspace + await this._discoverProjectCommands(); + + // Send success response + this._postMessage({ + type: 'syncComplete', + data: { message: 'Successfully synced .claude folder' } + }); + + console.log('.claude folder sync complete'); + } catch (error) { + console.error('Error syncing .claude folder:', error); + this._postMessage({ + type: 'syncError', + data: { message: error instanceof Error ? error.message : 'Unknown error' } + }); + } + } + private async _saveCustomSnippet(snippet: any): Promise { try { const customSnippets = this._context.globalState.get<{ [key: string]: any }>('customPromptSnippets', {}); diff --git a/src/script.ts b/src/script.ts index 7029415..483aa78 100644 --- a/src/script.ts +++ b/src/script.ts @@ -858,6 +858,11 @@ const getScript = (isTelemetryEnabled: boolean) => `