feat: Enhance CLI tools and history management

- Added CLI Manager and CLI History views to the navigation.
- Implemented rendering for CLI tools with detailed status and actions.
- Introduced a new CLI History view to display execution history with search and filter capabilities.
- Added hooks for managing and displaying available SKILLs in the Hook Manager.
- Created modals for Hook Wizards and Template View for better user interaction.
- Implemented semantic search dependency checks and installation functions in CodexLens.
- Updated dashboard layout to accommodate new features and improve user experience.
This commit is contained in:
catlog22
2025-12-12 16:26:49 +08:00
parent a393601ec5
commit dfa8dbc52a
13 changed files with 2081 additions and 2161 deletions

View File

@@ -4,6 +4,7 @@
// ========== CLI State ==========
let cliToolStatus = { gemini: {}, qwen: {}, codex: {} };
let codexLensStatus = { ready: false };
let semanticStatus = { available: false };
let defaultCliTool = 'gemini';
// ========== Initialization ==========
@@ -41,6 +42,11 @@ async function loadCodexLensStatus() {
// Update CodexLens badge
updateCodexLensBadge();
// If CodexLens is ready, also check semantic status
if (data.ready) {
await loadSemanticStatus();
}
return data;
} catch (err) {
console.error('Failed to load CodexLens status:', err);
@@ -48,6 +54,19 @@ async function loadCodexLensStatus() {
}
}
async function loadSemanticStatus() {
try {
const response = await fetch('/api/codexlens/semantic/status');
if (!response.ok) throw new Error('Failed to load semantic status');
const data = await response.json();
semanticStatus = data;
return data;
} catch (err) {
console.error('Failed to load semantic status:', err);
return null;
}
}
// ========== Badge Update ==========
function updateCliBadge() {
const badge = document.getElementById('badgeCliTools');
@@ -75,6 +94,18 @@ function renderCliStatus() {
const container = document.getElementById('cli-status-panel');
if (!container) return;
const toolDescriptions = {
gemini: 'Google AI for code analysis',
qwen: 'Alibaba AI assistant',
codex: 'OpenAI code generation'
};
const toolIcons = {
gemini: 'sparkle',
qwen: 'bot',
codex: 'code-2'
};
const tools = ['gemini', 'qwen', 'codex'];
const toolsHtml = tools.map(tool => {
@@ -89,21 +120,28 @@ function renderCliStatus() {
<span class="cli-tool-name">${tool.charAt(0).toUpperCase() + tool.slice(1)}</span>
${isDefault ? '<span class="cli-tool-badge">Default</span>' : ''}
</div>
<div class="cli-tool-info">
<div class="cli-tool-desc text-xs text-muted-foreground mt-1">
${toolDescriptions[tool]}
</div>
<div class="cli-tool-info mt-2">
${isAvailable
? `<span class="text-success">Ready</span>`
: `<span class="text-muted-foreground">Not Installed</span>`
? `<span class="text-success flex items-center gap-1"><i data-lucide="check-circle" class="w-3 h-3"></i> Ready</span>`
: `<span class="text-muted-foreground flex items-center gap-1"><i data-lucide="circle-dashed" class="w-3 h-3"></i> Not Installed</span>`
}
</div>
<div class="cli-tool-actions mt-3">
${isAvailable && !isDefault
? `<button class="btn-sm btn-outline flex items-center gap-1" onclick="setDefaultCliTool('${tool}')">
<i data-lucide="star" class="w-3 h-3"></i> Set Default
</button>`
: ''
}
</div>
${isAvailable && !isDefault
? `<button class="btn-sm btn-outline" onclick="setDefaultCliTool('${tool}')">Set Default</button>`
: ''
}
</div>
`;
}).join('');
// CodexLens card
// CodexLens card with semantic search info
const codexLensHtml = `
<div class="cli-tool-card tool-codexlens ${codexLensStatus.ready ? 'available' : 'unavailable'}">
<div class="cli-tool-header">
@@ -111,21 +149,64 @@ function renderCliStatus() {
<span class="cli-tool-name">CodexLens</span>
<span class="badge px-1.5 py-0.5 text-xs rounded bg-muted text-muted-foreground">Index</span>
</div>
<div class="cli-tool-info">
<div class="cli-tool-desc text-xs text-muted-foreground mt-1">
${codexLensStatus.ready ? 'Code indexing & FTS search' : 'Full-text code search engine'}
</div>
<div class="cli-tool-info mt-2">
${codexLensStatus.ready
? `<span class="text-success">v${codexLensStatus.version || 'installed'}</span>`
: `<span class="text-muted-foreground">Not Installed</span>`
? `<span class="text-success flex items-center gap-1"><i data-lucide="check-circle" class="w-3 h-3"></i> v${codexLensStatus.version || 'installed'}</span>`
: `<span class="text-muted-foreground flex items-center gap-1"><i data-lucide="circle-dashed" class="w-3 h-3"></i> Not Installed</span>`
}
</div>
<div class="cli-tool-actions flex gap-2 mt-2">
<div class="cli-tool-actions flex gap-2 mt-3">
${!codexLensStatus.ready
? `<button class="btn-sm btn-primary" onclick="installCodexLens()">Install</button>`
: `<button class="btn-sm btn-outline" onclick="initCodexLensIndex()">Init Index</button>`
? `<button class="btn-sm btn-primary flex items-center gap-1" onclick="installCodexLens()">
<i data-lucide="download" class="w-3 h-3"></i> Install
</button>`
: `<button class="btn-sm btn-outline flex items-center gap-1" onclick="initCodexLensIndex()">
<i data-lucide="database" class="w-3 h-3"></i> Init Index
</button>`
}
</div>
</div>
`;
// Semantic Search card (only show if CodexLens is installed)
const semanticHtml = codexLensStatus.ready ? `
<div class="cli-tool-card tool-semantic ${semanticStatus.available ? 'available' : 'unavailable'}">
<div class="cli-tool-header">
<span class="cli-tool-status ${semanticStatus.available ? 'status-available' : 'status-unavailable'}"></span>
<span class="cli-tool-name">Semantic Search</span>
<span class="badge px-1.5 py-0.5 text-xs rounded ${semanticStatus.available ? 'bg-primary/20 text-primary' : 'bg-muted text-muted-foreground'}">AI</span>
</div>
<div class="cli-tool-desc text-xs text-muted-foreground mt-1">
${semanticStatus.available ? 'AI-powered code understanding' : 'Natural language code search'}
</div>
<div class="cli-tool-info mt-2">
${semanticStatus.available
? `<span class="text-success flex items-center gap-1"><i data-lucide="sparkles" class="w-3 h-3"></i> ${semanticStatus.backend || 'Ready'}</span>`
: `<span class="text-muted-foreground flex items-center gap-1"><i data-lucide="circle-dashed" class="w-3 h-3"></i> Not Installed</span>`
}
</div>
<div class="cli-tool-actions flex flex-col gap-2 mt-3">
${!semanticStatus.available ? `
<button class="btn-sm btn-primary w-full flex items-center justify-center gap-1" onclick="openSemanticInstallWizard()">
<i data-lucide="brain" class="w-3 h-3"></i> Install AI Model
</button>
<div class="flex items-center justify-center gap-1 text-xs text-muted-foreground">
<i data-lucide="hard-drive" class="w-3 h-3"></i>
<span>~500MB download</span>
</div>
` : `
<div class="flex items-center gap-1 text-xs text-muted-foreground">
<i data-lucide="cpu" class="w-3 h-3"></i>
<span>bge-small-en-v1.5</span>
</div>
`}
</div>
</div>
` : '';
container.innerHTML = `
<div class="cli-status-header">
<h3><i data-lucide="terminal" class="w-4 h-4"></i> CLI Tools</h3>
@@ -136,6 +217,7 @@ function renderCliStatus() {
<div class="cli-tools-grid">
${toolsHtml}
${codexLensHtml}
${semanticHtml}
</div>
`;
@@ -203,3 +285,152 @@ async function initCodexLensIndex() {
showRefreshToast(`Init error: ${err.message}`, 'error');
}
}
// ========== Semantic Search Installation Wizard ==========
function openSemanticInstallWizard() {
const modal = document.createElement('div');
modal.id = 'semanticInstallModal';
modal.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50';
modal.innerHTML = `
<div class="bg-card rounded-lg shadow-xl w-full max-w-md mx-4 overflow-hidden">
<div class="p-6">
<div class="flex items-center gap-3 mb-4">
<div class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
<i data-lucide="brain" class="w-5 h-5 text-primary"></i>
</div>
<div>
<h3 class="text-lg font-semibold">Install Semantic Search</h3>
<p class="text-sm text-muted-foreground">AI-powered code understanding</p>
</div>
</div>
<div class="space-y-4">
<div class="bg-muted/50 rounded-lg p-4">
<h4 class="font-medium mb-2">What will be installed:</h4>
<ul class="text-sm space-y-2 text-muted-foreground">
<li class="flex items-start gap-2">
<i data-lucide="check" class="w-4 h-4 text-success mt-0.5"></i>
<span><strong>sentence-transformers</strong> - ML framework</span>
</li>
<li class="flex items-start gap-2">
<i data-lucide="check" class="w-4 h-4 text-success mt-0.5"></i>
<span><strong>bge-small-en-v1.5</strong> - Embedding model (~130MB)</span>
</li>
<li class="flex items-start gap-2">
<i data-lucide="check" class="w-4 h-4 text-success mt-0.5"></i>
<span><strong>PyTorch</strong> - Deep learning backend (~300MB)</span>
</li>
</ul>
</div>
<div class="bg-warning/10 border border-warning/20 rounded-lg p-3">
<div class="flex items-start gap-2">
<i data-lucide="alert-triangle" class="w-4 h-4 text-warning mt-0.5"></i>
<div class="text-sm">
<p class="font-medium text-warning">Large Download</p>
<p class="text-muted-foreground">Total size: ~500MB. First-time model loading may take a few minutes.</p>
</div>
</div>
</div>
<div id="semanticInstallProgress" class="hidden">
<div class="flex items-center gap-3">
<div class="animate-spin w-5 h-5 border-2 border-primary border-t-transparent rounded-full"></div>
<span class="text-sm" id="semanticInstallStatus">Installing dependencies...</span>
</div>
<div class="mt-2 h-2 bg-muted rounded-full overflow-hidden">
<div id="semanticProgressBar" class="h-full bg-primary transition-all duration-300" style="width: 0%"></div>
</div>
</div>
</div>
</div>
<div class="border-t border-border p-4 flex justify-end gap-3 bg-muted/30">
<button class="btn-outline px-4 py-2" onclick="closeSemanticInstallWizard()">Cancel</button>
<button id="semanticInstallBtn" class="btn-primary px-4 py-2" onclick="startSemanticInstall()">
<i data-lucide="download" class="w-4 h-4 mr-2"></i>
Install Now
</button>
</div>
</div>
`;
document.body.appendChild(modal);
// Initialize Lucide icons in modal
if (window.lucide) {
lucide.createIcons();
}
}
function closeSemanticInstallWizard() {
const modal = document.getElementById('semanticInstallModal');
if (modal) {
modal.remove();
}
}
async function startSemanticInstall() {
const progressDiv = document.getElementById('semanticInstallProgress');
const installBtn = document.getElementById('semanticInstallBtn');
const statusText = document.getElementById('semanticInstallStatus');
const progressBar = document.getElementById('semanticProgressBar');
// Show progress, disable button
progressDiv.classList.remove('hidden');
installBtn.disabled = true;
installBtn.innerHTML = '<span class="animate-pulse">Installing...</span>';
// Simulate progress stages
const stages = [
{ progress: 10, text: 'Installing numpy...' },
{ progress: 30, text: 'Installing sentence-transformers...' },
{ progress: 50, text: 'Installing PyTorch dependencies...' },
{ progress: 70, text: 'Downloading embedding model...' },
{ progress: 90, text: 'Finalizing installation...' }
];
let currentStage = 0;
const progressInterval = setInterval(() => {
if (currentStage < stages.length) {
statusText.textContent = stages[currentStage].text;
progressBar.style.width = `${stages[currentStage].progress}%`;
currentStage++;
}
}, 2000);
try {
const response = await fetch('/api/codexlens/semantic/install', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
});
clearInterval(progressInterval);
const result = await response.json();
if (result.success) {
progressBar.style.width = '100%';
statusText.textContent = 'Installation complete!';
setTimeout(() => {
closeSemanticInstallWizard();
showRefreshToast('Semantic search installed successfully!', 'success');
loadSemanticStatus().then(() => renderCliStatus());
}, 1000);
} else {
statusText.textContent = `Error: ${result.error}`;
progressBar.classList.add('bg-destructive');
installBtn.disabled = false;
installBtn.innerHTML = '<i data-lucide="refresh-cw" class="w-4 h-4 mr-2"></i> Retry';
if (window.lucide) lucide.createIcons();
}
} catch (err) {
clearInterval(progressInterval);
statusText.textContent = `Error: ${err.message}`;
progressBar.classList.add('bg-destructive');
installBtn.disabled = false;
installBtn.innerHTML = '<i data-lucide="refresh-cw" class="w-4 h-4 mr-2"></i> Retry';
if (window.lucide) lucide.createIcons();
}
}

View File

@@ -74,6 +74,29 @@ const HOOK_TEMPLATES = {
tool: { type: 'select', options: ['gemini', 'qwen', 'codex'], default: 'gemini', label: 'CLI Tool' },
interval: { type: 'number', default: 300, min: 60, max: 3600, label: 'Interval (seconds)', step: 60 }
}
},
// SKILL Context Loader templates
'skill-context-keyword': {
event: 'UserPromptSubmit',
matcher: '',
command: 'bash',
args: ['-c', 'ccw tool exec skill_context_loader \'{"keywords":"$SKILL_KEYWORDS","skills":"$SKILL_NAMES","prompt":"$CLAUDE_PROMPT"}\''],
description: 'Load SKILL context based on keyword matching in user prompt',
category: 'skill',
configurable: true,
config: {
keywords: { type: 'text', default: '', label: 'Keywords (comma-separated)', placeholder: 'react,workflow,api' },
skills: { type: 'text', default: '', label: 'SKILL Names (comma-separated)', placeholder: 'prompt-enhancer,command-guide' }
}
},
'skill-context-auto': {
event: 'UserPromptSubmit',
matcher: '',
command: 'bash',
args: ['-c', 'ccw tool exec skill_context_loader \'{"mode":"auto","prompt":"$CLAUDE_PROMPT"}\''],
description: 'Auto-detect and load SKILL based on skill name in prompt',
category: 'skill',
configurable: false
}
};
@@ -102,6 +125,28 @@ const WIZARD_TEMPLATES = {
{ key: 'interval', type: 'number', label: 'Interval (seconds)', default: 300, min: 60, max: 3600, step: 60, showFor: ['periodic'], description: 'Time between updates' },
{ key: 'strategy', type: 'select', label: 'Update Strategy', options: ['related', 'single-layer'], default: 'related', description: 'Related: changed modules, Single-layer: current directory' }
]
},
'skill-context': {
name: 'SKILL Context Loader',
description: 'Automatically load SKILL packages based on keywords in user prompts',
icon: 'sparkles',
options: [
{
id: 'keyword',
name: 'Keyword Matching',
description: 'Load specific SKILLs when keywords are detected in prompt',
templateId: 'skill-context-keyword'
},
{
id: 'auto',
name: 'Auto Detection',
description: 'Automatically detect and load SKILLs by name in prompt',
templateId: 'skill-context-auto'
}
],
configFields: [],
requiresSkillDiscovery: true,
customRenderer: 'renderSkillContextConfig'
}
};
@@ -327,7 +372,8 @@ function getHookEventDescription(event) {
'PreToolUse': 'Runs before a tool is executed',
'PostToolUse': 'Runs after a tool completes',
'Notification': 'Runs when a notification is triggered',
'Stop': 'Runs when the agent stops'
'Stop': 'Runs when the agent stops',
'UserPromptSubmit': 'Runs when user submits a prompt'
};
return descriptions[event] || event;
}
@@ -337,7 +383,8 @@ function getHookEventIcon(event) {
'PreToolUse': '⏳',
'PostToolUse': '✅',
'Notification': '🔔',
'Stop': '🛑'
'Stop': '🛑',
'UserPromptSubmit': '💬'
};
return icons[event] || '🪝';
}
@@ -347,7 +394,468 @@ function getHookEventIconLucide(event) {
'PreToolUse': '<i data-lucide="clock" class="w-5 h-5"></i>',
'PostToolUse': '<i data-lucide="check-circle" class="w-5 h-5"></i>',
'Notification': '<i data-lucide="bell" class="w-5 h-5"></i>',
'Stop': '<i data-lucide="octagon-x" class="w-5 h-5"></i>'
'Stop': '<i data-lucide="octagon-x" class="w-5 h-5"></i>',
'UserPromptSubmit': '<i data-lucide="message-square" class="w-5 h-5"></i>'
};
return icons[event] || '<i data-lucide="webhook" class="w-5 h-5"></i>';
}
// ========== Wizard Modal Functions ==========
let currentWizardTemplate = null;
let wizardConfig = {};
function openHookWizardModal(wizardId) {
const wizard = WIZARD_TEMPLATES[wizardId];
if (!wizard) {
showRefreshToast('Wizard template not found', 'error');
return;
}
currentWizardTemplate = { id: wizardId, ...wizard };
wizardConfig = {};
// Set defaults
wizard.configFields.forEach(field => {
wizardConfig[field.key] = field.default;
});
const modal = document.getElementById('hookWizardModal');
if (modal) {
renderWizardModalContent();
modal.classList.remove('hidden');
}
}
function closeHookWizardModal() {
const modal = document.getElementById('hookWizardModal');
if (modal) {
modal.classList.add('hidden');
currentWizardTemplate = null;
wizardConfig = {};
}
}
function renderWizardModalContent() {
const container = document.getElementById('wizardModalContent');
if (!container || !currentWizardTemplate) return;
const wizard = currentWizardTemplate;
const selectedOption = wizardConfig.triggerType || wizard.options[0].id;
container.innerHTML = `
<div class="space-y-6">
<!-- Wizard Header -->
<div class="flex items-center gap-3 pb-4 border-b border-border">
<div class="p-2 bg-primary/10 rounded-lg">
<i data-lucide="${wizard.icon}" class="w-6 h-6 text-primary"></i>
</div>
<div>
<h3 class="text-lg font-semibold text-foreground">${escapeHtml(wizard.name)}</h3>
<p class="text-sm text-muted-foreground">${escapeHtml(wizard.description)}</p>
</div>
</div>
<!-- Trigger Type Selection -->
<div class="space-y-3">
<label class="block text-sm font-medium text-foreground">When to Trigger</label>
<div class="grid grid-cols-1 gap-3">
${wizard.options.map(opt => `
<label class="flex items-start gap-3 p-3 border rounded-lg cursor-pointer transition-all ${selectedOption === opt.id ? 'border-primary bg-primary/5' : 'border-border hover:border-muted-foreground'}">
<input type="radio" name="wizardTrigger" value="${opt.id}"
${selectedOption === opt.id ? 'checked' : ''}
onchange="updateWizardTrigger('${opt.id}')"
class="mt-1">
<div class="flex-1">
<span class="font-medium text-foreground">${escapeHtml(opt.name)}</span>
<p class="text-sm text-muted-foreground">${escapeHtml(opt.description)}</p>
</div>
</label>
`).join('')}
</div>
</div>
<!-- Configuration Fields -->
<div class="space-y-4">
<label class="block text-sm font-medium text-foreground">Configuration</label>
${wizard.customRenderer ? window[wizard.customRenderer]() : wizard.configFields.map(field => {
// Check if field should be shown for current trigger type
const shouldShow = !field.showFor || field.showFor.includes(selectedOption);
if (!shouldShow) return '';
const value = wizardConfig[field.key] ?? field.default;
if (field.type === 'select') {
return `
<div class="space-y-1">
<label class="block text-sm text-muted-foreground">${escapeHtml(field.label)}</label>
<select id="wizard_${field.key}"
onchange="updateWizardConfig('${field.key}', this.value)"
class="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground focus:outline-none focus:ring-2 focus:ring-primary">
${field.options.map(opt => `
<option value="${opt}" ${value === opt ? 'selected' : ''}>${opt}</option>
`).join('')}
</select>
${field.description ? `<p class="text-xs text-muted-foreground">${escapeHtml(field.description)}</p>` : ''}
</div>
`;
} else if (field.type === 'number') {
return `
<div class="space-y-1">
<label class="block text-sm text-muted-foreground">${escapeHtml(field.label)}</label>
<div class="flex items-center gap-2">
<input type="number" id="wizard_${field.key}"
value="${value}"
min="${field.min || 0}"
max="${field.max || 9999}"
step="${field.step || 1}"
onchange="updateWizardConfig('${field.key}', parseInt(this.value))"
class="flex-1 px-3 py-2 bg-background border border-border rounded-lg text-foreground focus:outline-none focus:ring-2 focus:ring-primary">
<span class="text-sm text-muted-foreground">${formatIntervalDisplay(value)}</span>
</div>
${field.description ? `<p class="text-xs text-muted-foreground">${escapeHtml(field.description)}</p>` : ''}
</div>
`;
}
return '';
}).join('')}
</div>
<!-- Preview -->
<div class="space-y-2">
<label class="block text-sm font-medium text-foreground">Generated Command Preview</label>
<div class="bg-muted/50 rounded-lg p-3 font-mono text-xs overflow-x-auto">
<pre id="wizardCommandPreview" class="whitespace-pre-wrap text-muted-foreground">${escapeHtml(generateWizardCommand())}</pre>
</div>
</div>
<!-- Scope Selection -->
<div class="space-y-3">
<label class="block text-sm font-medium text-foreground">Install To</label>
<div class="flex gap-4">
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" name="wizardScope" value="project" checked>
<span class="text-sm text-foreground">Project</span>
<span class="text-xs text-muted-foreground">(.claude/settings.json)</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" name="wizardScope" value="global">
<span class="text-sm text-foreground">Global</span>
<span class="text-xs text-muted-foreground">(~/.claude/settings.json)</span>
</label>
</div>
</div>
</div>
`;
// Initialize Lucide icons
if (typeof lucide !== 'undefined') lucide.createIcons();
}
function updateWizardTrigger(triggerId) {
wizardConfig.triggerType = triggerId;
renderWizardModalContent();
}
function updateWizardConfig(key, value) {
wizardConfig[key] = value;
// Update command preview
const preview = document.getElementById('wizardCommandPreview');
if (preview) {
preview.textContent = generateWizardCommand();
}
// Re-render if interval changed (to update display)
if (key === 'interval') {
const displaySpan = document.querySelector(`#wizard_${key}`)?.parentElement?.querySelector('.text-muted-foreground:last-child');
if (displaySpan) {
displaySpan.textContent = formatIntervalDisplay(value);
}
}
}
function formatIntervalDisplay(seconds) {
if (seconds < 60) return `${seconds}s`;
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
if (secs === 0) return `${mins}min`;
return `${mins}min ${secs}s`;
}
// ========== SKILL Context Wizard Custom Functions ==========
function renderSkillContextConfig() {
const selectedOption = wizardConfig.triggerType || 'keyword';
const skillConfigs = wizardConfig.skillConfigs || [];
const availableSkills = window.availableSkills || [];
if (selectedOption === 'auto') {
return `
<div class="bg-muted/30 rounded-lg p-4 text-sm text-muted-foreground">
<div class="flex items-center gap-2 mb-2">
<i data-lucide="info" class="w-4 h-4"></i>
<span class="font-medium">Auto Detection Mode</span>
</div>
<p>SKILLs will be automatically loaded when their name appears in your prompt.</p>
<p class="mt-2">Available SKILLs: ${availableSkills.map(s => \`<span class="px-1.5 py-0.5 bg-emerald-500/10 text-emerald-500 rounded text-xs">${escapeHtml(s.name)}</span>\`).join(' ')}</p>
</div>
`;
}
return `
<div class="space-y-4">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-foreground">Configure SKILLs</span>
<button type="button" onclick="addSkillConfig()"
class="px-3 py-1.5 text-xs bg-primary text-primary-foreground rounded-lg hover:opacity-90 flex items-center gap-1">
<i data-lucide="plus" class="w-3 h-3"></i> Add SKILL
</button>
</div>
<div id="skillConfigsList" class="space-y-3">
${skillConfigs.length === 0 ? \`
<div class="text-center py-6 text-muted-foreground text-sm border border-dashed border-border rounded-lg">
<i data-lucide="package" class="w-8 h-8 mx-auto mb-2 opacity-50"></i>
<p>No SKILLs configured yet</p>
<p class="text-xs mt-1">Click "Add SKILL" to configure keyword triggers</p>
</div>
\` : skillConfigs.map((config, idx) => \`
<div class="border border-border rounded-lg p-3 bg-card">
<div class="flex items-center justify-between mb-2">
<select onchange="updateSkillConfig(${idx}, 'skill', this.value)"
class="px-2 py-1 text-sm bg-background border border-border rounded text-foreground">
<option value="">Select SKILL...</option>
${availableSkills.map(s => \`
<option value="${s.id}" ${config.skill === s.id ? 'selected' : ''}>${escapeHtml(s.name)}</option>
\`).join('')}
</select>
<button onclick="removeSkillConfig(${idx})"
class="p-1 text-muted-foreground hover:text-destructive rounded">
<i data-lucide="trash-2" class="w-4 h-4"></i>
</button>
</div>
<div class="space-y-1">
<label class="text-xs text-muted-foreground">Trigger Keywords (comma-separated)</label>
<input type="text"
value="${config.keywords || ''}"
onchange="updateSkillConfig(${idx}, 'keywords', this.value)"
placeholder="e.g., react, hooks, component"
class="w-full px-2 py-1.5 text-sm bg-background border border-border rounded text-foreground">
</div>
</div>
\`).join('')}
</div>
${availableSkills.length === 0 ? \`
<div class="text-xs text-amber-500 flex items-center gap-1">
<i data-lucide="alert-triangle" class="w-3 h-3"></i>
No SKILLs found. Create SKILL packages in .claude/skills/
</div>
\` : ''}
</div>
`;
}
function addSkillConfig() {
if (!wizardConfig.skillConfigs) {
wizardConfig.skillConfigs = [];
}
wizardConfig.skillConfigs.push({ skill: '', keywords: '' });
renderWizardModalContent();
}
function removeSkillConfig(index) {
if (wizardConfig.skillConfigs) {
wizardConfig.skillConfigs.splice(index, 1);
renderWizardModalContent();
}
}
function updateSkillConfig(index, key, value) {
if (wizardConfig.skillConfigs && wizardConfig.skillConfigs[index]) {
wizardConfig.skillConfigs[index][key] = value;
const preview = document.getElementById('wizardCommandPreview');
if (preview) {
preview.textContent = generateWizardCommand();
}
}
}
function generateWizardCommand() {
if (!currentWizardTemplate) return '';
const wizard = currentWizardTemplate;
const wizardId = wizard.id;
const triggerType = wizardConfig.triggerType || wizard.options[0].id;
const selectedOption = wizard.options.find(o => o.id === triggerType);
if (!selectedOption) return '';
const baseTemplate = HOOK_TEMPLATES[selectedOption.templateId];
if (!baseTemplate) return '';
// Handle skill-context wizard
if (wizardId === 'skill-context') {
const keywords = wizardConfig.keywords || '';
const skills = wizardConfig.skills || '';
if (triggerType === 'keyword') {
const params = JSON.stringify({ keywords, skills, prompt: '$CLAUDE_PROMPT' });
return `ccw tool exec skill_context_loader '${params}'`;
} else {
// auto mode
const params = JSON.stringify({ mode: 'auto', prompt: '$CLAUDE_PROMPT' });
return `ccw tool exec skill_context_loader '${params}'`;
}
}
// Handle memory-update wizard (default)
const tool = wizardConfig.tool || 'gemini';
const strategy = wizardConfig.strategy || 'related';
const interval = wizardConfig.interval || 300;
// Build the ccw tool command based on configuration
const params = JSON.stringify({ strategy, tool });
if (triggerType === 'periodic') {
return `INTERVAL=${interval}; LAST_FILE=~/.claude/.last_memory_update; NOW=$(date +%s); LAST=0; [ -f "$LAST_FILE" ] && LAST=$(cat "$LAST_FILE"); if [ $((NOW - LAST)) -ge $INTERVAL ]; then echo $NOW > "$LAST_FILE"; ccw tool exec update_module_claude '${params}' & fi`;
} else {
return `ccw tool exec update_module_claude '${params}'`;
}
}
async function submitHookWizard() {
if (!currentWizardTemplate) return;
const wizard = currentWizardTemplate;
const triggerType = wizardConfig.triggerType || wizard.options[0].id;
const selectedOption = wizard.options.find(o => o.id === triggerType);
if (!selectedOption) return;
const baseTemplate = HOOK_TEMPLATES[selectedOption.templateId];
if (!baseTemplate) return;
const scope = document.querySelector('input[name="wizardScope"]:checked')?.value || 'project';
const command = generateWizardCommand();
const hookData = {
command: 'bash',
args: ['-c', command]
};
if (baseTemplate.matcher) {
hookData.matcher = baseTemplate.matcher;
}
await saveHook(scope, baseTemplate.event, hookData);
closeHookWizardModal();
}
// ========== Template View/Copy Functions ==========
function viewTemplateDetails(templateId) {
const template = HOOK_TEMPLATES[templateId];
if (!template) return;
const modal = document.getElementById('templateViewModal');
const content = document.getElementById('templateViewContent');
if (modal && content) {
const args = template.args || [];
content.innerHTML = `
<div class="space-y-4">
<div class="flex items-center gap-3 pb-3 border-b border-border">
<i data-lucide="webhook" class="w-5 h-5 text-primary"></i>
<div>
<h4 class="font-semibold text-foreground">${escapeHtml(templateId)}</h4>
<p class="text-sm text-muted-foreground">${escapeHtml(template.description || 'No description')}</p>
</div>
</div>
<div class="space-y-3 text-sm">
<div class="flex items-start gap-2">
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded shrink-0 w-16">Event</span>
<span class="font-medium text-foreground">${escapeHtml(template.event)}</span>
</div>
<div class="flex items-start gap-2">
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded shrink-0 w-16">Matcher</span>
<span class="text-muted-foreground">${escapeHtml(template.matcher || 'All tools')}</span>
</div>
<div class="flex items-start gap-2">
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded shrink-0 w-16">Command</span>
<code class="font-mono text-xs text-foreground">${escapeHtml(template.command)}</code>
</div>
${args.length > 0 ? `
<div class="flex items-start gap-2">
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded shrink-0 w-16">Args</span>
<div class="flex-1">
<pre class="font-mono text-xs text-muted-foreground bg-muted/50 rounded p-2 overflow-x-auto whitespace-pre-wrap">${escapeHtml(args.join('\n'))}</pre>
</div>
</div>
` : ''}
${template.category ? `
<div class="flex items-start gap-2">
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded shrink-0 w-16">Category</span>
<span class="px-2 py-0.5 text-xs rounded-full bg-primary/10 text-primary">${escapeHtml(template.category)}</span>
</div>
` : ''}
</div>
<div class="flex gap-2 pt-3 border-t border-border">
<button class="flex-1 px-3 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity"
onclick="copyTemplateToClipboard('${templateId}')">
<i data-lucide="copy" class="w-4 h-4 inline mr-1"></i> Copy JSON
</button>
<button class="flex-1 px-3 py-2 text-sm bg-muted text-foreground rounded-lg hover:bg-hover transition-colors"
onclick="editTemplateAsNew('${templateId}')">
<i data-lucide="pencil" class="w-4 h-4 inline mr-1"></i> Edit as New
</button>
</div>
</div>
`;
modal.classList.remove('hidden');
if (typeof lucide !== 'undefined') lucide.createIcons();
}
}
function closeTemplateViewModal() {
const modal = document.getElementById('templateViewModal');
if (modal) {
modal.classList.add('hidden');
}
}
function copyTemplateToClipboard(templateId) {
const template = HOOK_TEMPLATES[templateId];
if (!template) return;
const hookJson = {
matcher: template.matcher || undefined,
command: template.command,
args: template.args
};
// Clean up undefined values
Object.keys(hookJson).forEach(key => {
if (hookJson[key] === undefined || hookJson[key] === '') {
delete hookJson[key];
}
});
navigator.clipboard.writeText(JSON.stringify(hookJson, null, 2))
.then(() => showRefreshToast('Template copied to clipboard', 'success'))
.catch(() => showRefreshToast('Failed to copy', 'error'));
}
function editTemplateAsNew(templateId) {
const template = HOOK_TEMPLATES[templateId];
if (!template) return;
closeTemplateViewModal();
// Open create modal with template data
openHookCreateModal({
event: template.event,
matcher: template.matcher || '',
command: template.command,
args: template.args || []
});
}

View File

@@ -98,6 +98,12 @@ function initNavigation() {
renderProjectOverview();
} else if (currentView === 'explorer') {
renderExplorer();
} else if (currentView === 'cli-manager') {
renderCliManager();
} else if (currentView === 'cli-history') {
renderCliHistoryView();
} else if (currentView === 'hook-manager') {
renderHookManager();
}
});
});
@@ -118,6 +124,8 @@ function updateContentTitle() {
titleEl.textContent = 'File Explorer';
} else if (currentView === 'cli-manager') {
titleEl.textContent = 'CLI Tools & CCW';
} else if (currentView === 'cli-history') {
titleEl.textContent = 'CLI Execution History';
} else if (currentView === 'liteTasks') {
const names = { 'lite-plan': 'Lite Plan Sessions', 'lite-fix': 'Lite Fix Sessions' };
titleEl.textContent = names[currentLiteType] || 'Lite Tasks';