custom commands init

This commit is contained in:
andrepimenta
2025-07-14 21:57:33 +01:00
parent 42a5ebf763
commit 2aa7db86e7
3 changed files with 571 additions and 7 deletions

View File

@@ -296,6 +296,15 @@ class ClaudeChatProvider {
case 'deleteMCPServer':
this._deleteMCPServer(message.name);
return;
case 'getCustomSnippets':
this._sendCustomSnippets();
return;
case 'saveCustomSnippet':
this._saveCustomSnippet(message.snippet);
return;
case 'deleteCustomSnippet':
this._deleteCustomSnippet(message.snippetId);
return;
}
}
@@ -1615,6 +1624,73 @@ class ClaudeChatProvider {
}
}
private async _sendCustomSnippets(): Promise<void> {
try {
const customSnippets = this._context.globalState.get<{[key: string]: any}>('customPromptSnippets', {});
this._postMessage({
type: 'customSnippetsData',
data: customSnippets
});
} catch (error) {
console.error('Error loading custom snippets:', error);
this._postMessage({
type: 'customSnippetsData',
data: {}
});
}
}
private async _saveCustomSnippet(snippet: any): Promise<void> {
try {
const customSnippets = this._context.globalState.get<{[key: string]: any}>('customPromptSnippets', {});
customSnippets[snippet.id] = snippet;
await this._context.globalState.update('customPromptSnippets', customSnippets);
this._postMessage({
type: 'customSnippetSaved',
data: { snippet }
});
console.log('Saved custom snippet:', snippet.name);
} catch (error) {
console.error('Error saving custom snippet:', error);
this._postMessage({
type: 'error',
data: 'Failed to save custom snippet'
});
}
}
private async _deleteCustomSnippet(snippetId: string): Promise<void> {
try {
const customSnippets = this._context.globalState.get<{[key: string]: any}>('customPromptSnippets', {});
if (customSnippets[snippetId]) {
delete customSnippets[snippetId];
await this._context.globalState.update('customPromptSnippets', customSnippets);
this._postMessage({
type: 'customSnippetDeleted',
data: { snippetId }
});
console.log('Deleted custom snippet:', snippetId);
} else {
this._postMessage({
type: 'error',
data: 'Snippet not found'
});
}
} catch (error) {
console.error('Error deleting custom snippet:', error);
this._postMessage({
type: 'error',
data: 'Failed to delete custom snippet'
});
}
}
private convertToWSLPath(windowsPath: string): string {
const config = vscode.workspace.getConfiguration('claudeCodeChat');
const wslEnabled = config.get<boolean>('wsl.enabled', false);

View File

@@ -1905,12 +1905,108 @@ const styles = `
}
/* Slash commands modal */
.slash-commands-search {
padding: 16px 20px;
border-bottom: 1px solid var(--vscode-panel-border);
}
.search-input-wrapper {
display: flex;
align-items: center;
border: 1px solid var(--vscode-input-border);
border-radius: 6px;
background-color: var(--vscode-input-background);
transition: all 0.2s ease;
position: relative;
}
.search-input-wrapper:focus-within {
border-color: var(--vscode-focusBorder);
box-shadow: 0 0 0 1px var(--vscode-focusBorder);
}
.search-prefix {
display: flex;
align-items: center;
justify-content: center;
min-width: 32px;
height: 32px;
background-color: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
font-size: 13px;
font-weight: 600;
border-radius: 4px 0 0 4px;
border-right: 1px solid var(--vscode-input-border);
}
.slash-commands-search input {
flex: 1;
padding: 8px 12px;
border: none !important;
background: transparent;
color: var(--vscode-input-foreground);
font-size: 13px;
outline: none !important;
box-shadow: none !important;
}
.slash-commands-search input:focus {
border: none !important;
outline: none !important;
box-shadow: none !important;
}
.slash-commands-search input::placeholder {
color: var(--vscode-input-placeholderForeground);
}
.command-input-wrapper {
display: flex;
align-items: center;
border: 1px solid var(--vscode-input-border);
border-radius: 6px;
background-color: var(--vscode-input-background);
transition: all 0.2s ease;
width: 100%;
position: relative;
}
.command-input-wrapper:focus-within {
border-color: var(--vscode-focusBorder);
box-shadow: 0 0 0 1px var(--vscode-focusBorder);
}
.command-prefix {
display: flex;
align-items: center;
justify-content: center;
min-width: 32px;
height: 32px;
background-color: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
font-size: 12px;
font-weight: 600;
border-radius: 4px 0 0 4px;
border-right: 1px solid var(--vscode-input-border);
}
.slash-commands-section {
margin-bottom: 24px;
}
.slash-commands-section h3 {
margin: 16px 20px 12px 20px;
font-size: 14px;
font-weight: 600;
color: var(--vscode-foreground);
}
.slash-commands-info {
padding: 12px 16px;
padding: 12px 20px;
background-color: rgba(255, 149, 0, 0.1);
border: 1px solid rgba(255, 149, 0, 0.2);
border-radius: 4px;
margin-bottom: 16px;
margin: 0 20px 16px 20px;
}
.slash-commands-info p {
@@ -1921,11 +2017,141 @@ const styles = `
opacity: 0.9;
}
.prompt-snippet-item {
border-left: 3px solid var(--vscode-charts-blue);
background-color: rgba(0, 122, 204, 0.05);
}
.prompt-snippet-item:hover {
background-color: rgba(0, 122, 204, 0.1);
border-left-color: var(--vscode-charts-blue);
}
.add-snippet-item {
border-left: 3px solid var(--vscode-charts-green);
background-color: rgba(0, 200, 83, 0.05);
border: 1px dashed var(--vscode-panel-border);
}
.add-snippet-item:hover {
background-color: rgba(0, 200, 83, 0.1);
border-left-color: var(--vscode-charts-green);
border-style: solid;
}
.add-snippet-form {
background-color: var(--vscode-editor-background);
border: 1px solid var(--vscode-panel-border);
border-radius: 6px;
padding: 16px;
margin: 8px 0;
animation: slideDown 0.2s ease;
}
.add-snippet-form .form-group {
margin-bottom: 12px;
}
.add-snippet-form label {
display: block;
margin-bottom: 4px;
font-weight: 500;
font-size: 12px;
color: var(--vscode-foreground);
}
.add-snippet-form textarea {
width: 100%;
padding: 6px 8px;
border: 1px solid var(--vscode-input-border);
border-radius: 3px;
background-color: var(--vscode-input-background);
color: var(--vscode-input-foreground);
font-size: 12px;
font-family: var(--vscode-font-family);
box-sizing: border-box;
}
.add-snippet-form .command-input-wrapper input {
flex: 1;
padding: 6px 8px;
border: none !important;
background: transparent;
color: var(--vscode-input-foreground);
font-size: 12px;
font-family: var(--vscode-font-family);
outline: none !important;
box-shadow: none !important;
}
.add-snippet-form .command-input-wrapper input:focus {
border: none !important;
outline: none !important;
box-shadow: none !important;
}
.add-snippet-form textarea:focus {
outline: none;
border-color: var(--vscode-focusBorder);
}
.add-snippet-form input::placeholder,
.add-snippet-form textarea::placeholder {
color: var(--vscode-input-placeholderForeground);
}
.add-snippet-form textarea {
resize: vertical;
min-height: 60px;
}
.add-snippet-form .form-buttons {
display: flex;
gap: 8px;
justify-content: flex-end;
margin-top: 12px;
}
.custom-snippet-item {
position: relative;
}
.snippet-actions {
display: flex;
align-items: center;
opacity: 0;
transition: opacity 0.2s ease;
margin-left: 8px;
}
.custom-snippet-item:hover .snippet-actions {
opacity: 1;
}
.snippet-delete-btn {
background: none;
border: none;
color: var(--vscode-descriptionForeground);
cursor: pointer;
padding: 4px;
border-radius: 3px;
font-size: 12px;
transition: all 0.2s ease;
opacity: 0.7;
}
.snippet-delete-btn:hover {
background-color: rgba(231, 76, 60, 0.1);
color: var(--vscode-errorForeground);
opacity: 1;
}
.slash-commands-list {
display: grid;
gap: 8px;
max-height: 400px;
overflow-y: auto;
padding: 0 20px;
}
.slash-command-item {

272
src/ui.ts
View File

@@ -438,13 +438,120 @@ const html = `<!DOCTYPE html>
<div id="slashCommandsModal" class="tools-modal" style="display: none;">
<div class="tools-modal-content">
<div class="tools-modal-header">
<span>Claude Code Commands</span>
<span>Commands & Prompt Snippets</span>
<button class="tools-close-btn" onclick="hideSlashCommandsModal()">✕</button>
</div>
<div class="slash-commands-info">
<p>These commands require the Claude CLI and will open in VS Code terminal. Return here after completion.</p>
<!-- Search box -->
<div class="slash-commands-search">
<div class="search-input-wrapper">
<span class="search-prefix">/</span>
<input type="text" id="slashCommandsSearch" placeholder="Search commands and snippets..." oninput="filterSlashCommands()">
</div>
</div>
<div class="slash-commands-list">
<!-- Custom Commands Section -->
<div class="slash-commands-section">
<h3>Custom Commands</h3>
<div class="slash-commands-info">
<p>Custom slash commands for quick prompt access. Click to use directly in chat.</p>
</div>
<div class="slash-commands-list" id="promptSnippetsList">
<!-- Add Custom Snippet Button -->
<div class="slash-command-item add-snippet-item" onclick="showAddSnippetForm()">
<div class="slash-command-icon"></div>
<div class="slash-command-content">
<div class="slash-command-title">Add Custom Command</div>
<div class="slash-command-description">Create your own slash command</div>
</div>
</div>
<!-- Add Custom Command Form (initially hidden) -->
<div class="add-snippet-form" id="addSnippetForm" style="display: none;">
<div class="form-group">
<label for="snippetName">Command name:</label>
<div class="command-input-wrapper">
<span class="command-prefix">/</span>
<input type="text" id="snippetName" placeholder="e.g., fix-bug" maxlength="50">
</div>
</div>
<div class="form-group">
<label for="snippetPrompt">Prompt Text:</label>
<textarea id="snippetPrompt" placeholder="e.g., Help me fix this bug in my code..." rows="3" maxlength="500"></textarea>
</div>
<div class="form-buttons">
<button class="btn" onclick="saveCustomSnippet()">Save Command</button>
<button class="btn outlined" onclick="hideAddSnippetForm()">Cancel</button>
</div>
</div>
<!-- Built-in Snippets -->
<div class="slash-command-item prompt-snippet-item" onclick="usePromptSnippet('performance-analysis')">
<div class="slash-command-icon">⚡</div>
<div class="slash-command-content">
<div class="slash-command-title">/performance-analysis</div>
<div class="slash-command-description">Analyze this code for performance issues and suggest optimizations</div>
</div>
</div>
<div class="slash-command-item prompt-snippet-item" onclick="usePromptSnippet('security-review')">
<div class="slash-command-icon">🔒</div>
<div class="slash-command-content">
<div class="slash-command-title">/security-review</div>
<div class="slash-command-description">Review this code for security vulnerabilities</div>
</div>
</div>
<div class="slash-command-item prompt-snippet-item" onclick="usePromptSnippet('implementation-review')">
<div class="slash-command-icon">🔍</div>
<div class="slash-command-content">
<div class="slash-command-title">/implementation-review</div>
<div class="slash-command-description">Review the implementation in this code</div>
</div>
</div>
<div class="slash-command-item prompt-snippet-item" onclick="usePromptSnippet('code-explanation')">
<div class="slash-command-icon">📖</div>
<div class="slash-command-content">
<div class="slash-command-title">/code-explanation</div>
<div class="slash-command-description">Explain how this code works in detail</div>
</div>
</div>
<div class="slash-command-item prompt-snippet-item" onclick="usePromptSnippet('bug-fix')">
<div class="slash-command-icon">🐛</div>
<div class="slash-command-content">
<div class="slash-command-title">/bug-fix</div>
<div class="slash-command-description">Help me fix this bug in my code</div>
</div>
</div>
<div class="slash-command-item prompt-snippet-item" onclick="usePromptSnippet('refactor')">
<div class="slash-command-icon">🔄</div>
<div class="slash-command-content">
<div class="slash-command-title">/refactor</div>
<div class="slash-command-description">Refactor this code to improve readability and maintainability</div>
</div>
</div>
<div class="slash-command-item prompt-snippet-item" onclick="usePromptSnippet('test-generation')">
<div class="slash-command-icon">🧪</div>
<div class="slash-command-content">
<div class="slash-command-title">/test-generation</div>
<div class="slash-command-description">Generate comprehensive tests for this code</div>
</div>
</div>
<div class="slash-command-item prompt-snippet-item" onclick="usePromptSnippet('documentation')">
<div class="slash-command-icon">📝</div>
<div class="slash-command-content">
<div class="slash-command-title">/documentation</div>
<div class="slash-command-description">Generate documentation for this code</div>
</div>
</div>
</div>
</div>
<!-- Native Slash Commands Section -->
<div class="slash-commands-section">
<h3>Native Slash Commands</h3>
<div class="slash-commands-info">
<p>These commands require the Claude CLI and will open in VS Code terminal. Return here after completion.</p>
</div>
<div class="slash-commands-list" id="nativeCommandsList">
<div class="slash-command-item" onclick="executeSlashCommand('bug')">
<div class="slash-command-icon">🐛</div>
<div class="slash-command-content">
@@ -598,6 +705,7 @@ const html = `<!DOCTYPE html>
</div>
</div>
</div>
</div>
<script>
const vscode = acquireVsCodeApi();
@@ -1975,6 +2083,10 @@ const html = `<!DOCTYPE html>
// Slash commands modal functions
function showSlashCommandsModal() {
document.getElementById('slashCommandsModal').style.display = 'flex';
// Auto-focus the search input
setTimeout(() => {
document.getElementById('slashCommandsSearch').focus();
}, 100);
}
function hideSlashCommandsModal() {
@@ -2109,6 +2221,136 @@ const html = `<!DOCTYPE html>
}
}
// Store custom snippets data globally
let customSnippetsData = {};
function usePromptSnippet(snippetType) {
const builtInSnippets = {
'performance-analysis': 'Analyze this code for performance issues and suggest optimizations',
'security-review': 'Review this code for security vulnerabilities',
'implementation-review': 'Review the implementation in this code',
'code-explanation': 'Explain how this code works in detail',
'bug-fix': 'Help me fix this bug in my code',
'refactor': 'Refactor this code to improve readability and maintainability',
'test-generation': 'Generate comprehensive tests for this code',
'documentation': 'Generate documentation for this code'
};
// Check built-in snippets first
let promptText = builtInSnippets[snippetType];
// If not found in built-in, check custom snippets
if (!promptText && customSnippetsData[snippetType]) {
promptText = customSnippetsData[snippetType].prompt;
}
if (promptText) {
// Hide the modal
hideSlashCommandsModal();
// Insert the prompt into the message input
messageInput.value = promptText;
messageInput.focus();
// Auto-resize the textarea
autoResizeTextarea();
}
}
function showAddSnippetForm() {
document.getElementById('addSnippetForm').style.display = 'block';
document.getElementById('snippetName').focus();
}
function hideAddSnippetForm() {
document.getElementById('addSnippetForm').style.display = 'none';
// Clear form fields
document.getElementById('snippetName').value = '';
document.getElementById('snippetPrompt').value = '';
}
function saveCustomSnippet() {
const name = document.getElementById('snippetName').value.trim();
const prompt = document.getElementById('snippetPrompt').value.trim();
if (!name || !prompt) {
alert('Please fill in both name and prompt text.');
return;
}
// Generate a unique ID for the snippet
const snippetId = 'custom-' + Date.now();
// Save the snippet using VS Code global storage
const snippetData = {
name: name,
prompt: prompt,
id: snippetId
};
vscode.postMessage({
type: 'saveCustomSnippet',
snippet: snippetData
});
// Hide the form
hideAddSnippetForm();
}
function loadCustomSnippets(snippetsData = {}) {
const snippetsList = document.getElementById('promptSnippetsList');
// Remove existing custom snippets
const existingCustom = snippetsList.querySelectorAll('.custom-snippet-item');
existingCustom.forEach(item => item.remove());
// Add custom snippets after the add button and form
const addForm = document.getElementById('addSnippetForm');
Object.values(snippetsData).forEach(snippet => {
const snippetElement = document.createElement('div');
snippetElement.className = 'slash-command-item prompt-snippet-item custom-snippet-item';
snippetElement.onclick = () => usePromptSnippet(snippet.id);
snippetElement.innerHTML = \`
<div class="slash-command-icon">📝</div>
<div class="slash-command-content">
<div class="slash-command-title">/\${snippet.name}</div>
<div class="slash-command-description">\${snippet.prompt}</div>
</div>
<div class="snippet-actions">
<button class="snippet-delete-btn" onclick="event.stopPropagation(); deleteCustomSnippet('\${snippet.id}')" title="Delete snippet">🗑️</button>
</div>
\`;
// Insert after the form
addForm.parentNode.insertBefore(snippetElement, addForm.nextSibling);
});
}
function deleteCustomSnippet(snippetId) {
vscode.postMessage({
type: 'deleteCustomSnippet',
snippetId: snippetId
});
}
function filterSlashCommands() {
const searchTerm = document.getElementById('slashCommandsSearch').value.toLowerCase();
const allItems = document.querySelectorAll('.slash-command-item');
allItems.forEach(item => {
const title = item.querySelector('.slash-command-title').textContent.toLowerCase();
const description = item.querySelector('.slash-command-description').textContent.toLowerCase();
if (title.includes(searchTerm) || description.includes(searchTerm)) {
item.style.display = 'flex';
} else {
item.style.display = 'none';
}
});
}
function openModelTerminal() {
vscode.postMessage({
type: 'openModelTerminal'
@@ -3239,6 +3481,11 @@ const html = `<!DOCTYPE html>
}
});
// Request custom snippets from VS Code on page load
vscode.postMessage({
type: 'getCustomSnippets'
});
// Detect slash commands input
messageInput.addEventListener('input', (e) => {
const value = messageInput.value;
@@ -3253,7 +3500,22 @@ const html = `<!DOCTYPE html>
window.addEventListener('message', event => {
const message = event.data;
if (message.type === 'settingsData') {
if (message.type === 'customSnippetsData') {
// Update global custom snippets data
customSnippetsData = message.data || {};
// Refresh the snippets display
loadCustomSnippets(customSnippetsData);
} else if (message.type === 'customSnippetSaved') {
// Refresh snippets after saving
vscode.postMessage({
type: 'getCustomSnippets'
});
} else if (message.type === 'customSnippetDeleted') {
// Refresh snippets after deletion
vscode.postMessage({
type: 'getCustomSnippets'
});
} else if (message.type === 'settingsData') {
// Update UI with current settings
const thinkingIntensity = message.data['thinking.intensity'] || 'think';
const intensityValues = ['think', 'think-hard', 'think-harder', 'ultrathink'];