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

@@ -1,26 +1,11 @@
// CLI Manager View
// Main view combining CLI status, CCW installations, and history panels
// Main view combining CLI status and CCW installations panels (two-column layout)
// ========== CLI Manager State ==========
var currentCliExecution = null;
var cliExecutionOutput = '';
var ccwInstallations = [];
// ========== Initialization ==========
function initCliManager() {
document.querySelectorAll('.nav-item[data-view="cli-manager"]').forEach(function(item) {
item.addEventListener('click', function() {
setActiveNavItem(item);
currentView = 'cli-manager';
currentFilter = null;
currentLiteType = null;
currentSessionDetailKey = null;
updateContentTitle();
renderCliManager();
});
});
}
// ========== CCW Installations ==========
async function loadCcwInstallations() {
try {
@@ -50,27 +35,192 @@ async function renderCliManager() {
// Load data
await Promise.all([
loadCliToolStatus(),
loadCliHistory(),
loadCcwInstallations()
]);
container.innerHTML = '<div class="cli-manager-container">' +
'<div class="cli-manager-grid">' +
'<div class="cli-panel"><div id="cli-status-panel"></div></div>' +
'<div class="cli-panel"><div id="ccw-install-panel"></div></div>' +
container.innerHTML = '<div class="status-manager">' +
'<div class="status-two-column">' +
'<div class="status-section" id="tools-section"></div>' +
'<div class="status-section" id="ccw-section"></div>' +
'</div>' +
'<div class="cli-panel cli-panel-full"><div id="cli-history-panel"></div></div>' +
'</div>';
// Render sub-panels
renderCliStatus();
renderCcwInstallPanel();
renderCliHistory();
renderToolsSection();
renderCcwSection();
// Initialize Lucide icons
if (window.lucide) lucide.createIcons();
}
// ========== Tools Section (Left Column) ==========
function renderToolsSection() {
var container = document.getElementById('tools-section');
if (!container) return;
var toolDescriptions = {
gemini: 'Google AI for code analysis',
qwen: 'Alibaba AI assistant',
codex: 'OpenAI code generation'
};
var tools = ['gemini', 'qwen', 'codex'];
var available = Object.values(cliToolStatus).filter(function(t) { return t.available; }).length;
var toolsHtml = tools.map(function(tool) {
var status = cliToolStatus[tool] || {};
var isAvailable = status.available;
var isDefault = defaultCliTool === tool;
return '<div class="tool-item ' + (isAvailable ? 'available' : 'unavailable') + '">' +
'<div class="tool-item-left">' +
'<span class="tool-status-dot ' + (isAvailable ? 'status-available' : 'status-unavailable') + '"></span>' +
'<div class="tool-item-info">' +
'<div class="tool-item-name">' + tool.charAt(0).toUpperCase() + tool.slice(1) +
(isDefault ? '<span class="tool-default-badge">Default</span>' : '') +
'</div>' +
'<div class="tool-item-desc">' + toolDescriptions[tool] + '</div>' +
'</div>' +
'</div>' +
'<div class="tool-item-right">' +
(isAvailable
? '<span class="tool-status-text success"><i data-lucide="check-circle" class="w-3.5 h-3.5"></i> Ready</span>'
: '<span class="tool-status-text muted"><i data-lucide="circle-dashed" class="w-3.5 h-3.5"></i> Not Installed</span>') +
(isAvailable && !isDefault
? '<button class="btn-sm btn-outline" onclick="setDefaultCliTool(\'' + tool + '\')"><i data-lucide="star" class="w-3 h-3"></i> Set Default</button>'
: '') +
'</div>' +
'</div>';
}).join('');
// CodexLens item
var codexLensHtml = '<div class="tool-item ' + (codexLensStatus.ready ? 'available' : 'unavailable') + '">' +
'<div class="tool-item-left">' +
'<span class="tool-status-dot ' + (codexLensStatus.ready ? 'status-available' : 'status-unavailable') + '"></span>' +
'<div class="tool-item-info">' +
'<div class="tool-item-name">CodexLens <span class="tool-type-badge">Index</span></div>' +
'<div class="tool-item-desc">' + (codexLensStatus.ready ? 'Code indexing & FTS search' : 'Full-text code search engine') + '</div>' +
'</div>' +
'</div>' +
'<div class="tool-item-right">' +
(codexLensStatus.ready
? '<span class="tool-status-text success"><i data-lucide="check-circle" class="w-3.5 h-3.5"></i> v' + (codexLensStatus.version || 'installed') + '</span>' +
'<button class="btn-sm btn-outline" onclick="initCodexLensIndex()"><i data-lucide="database" class="w-3 h-3"></i> Init Index</button>'
: '<span class="tool-status-text muted"><i data-lucide="circle-dashed" class="w-3.5 h-3.5"></i> Not Installed</span>' +
'<button class="btn-sm btn-primary" onclick="installCodexLens()"><i data-lucide="download" class="w-3 h-3"></i> Install</button>') +
'</div>' +
'</div>';
// Semantic Search item (only show if CodexLens is installed)
var semanticHtml = '';
if (codexLensStatus.ready) {
semanticHtml = '<div class="tool-item ' + (semanticStatus.available ? 'available' : 'unavailable') + '">' +
'<div class="tool-item-left">' +
'<span class="tool-status-dot ' + (semanticStatus.available ? 'status-available' : 'status-unavailable') + '"></span>' +
'<div class="tool-item-info">' +
'<div class="tool-item-name">Semantic Search <span class="tool-type-badge ai">AI</span></div>' +
'<div class="tool-item-desc">' + (semanticStatus.available ? 'AI-powered code understanding' : 'Natural language code search') + '</div>' +
'</div>' +
'</div>' +
'<div class="tool-item-right">' +
(semanticStatus.available
? '<span class="tool-status-text success"><i data-lucide="sparkles" class="w-3.5 h-3.5"></i> ' + (semanticStatus.backend || 'Ready') + '</span>'
: '<span class="tool-status-text muted"><i data-lucide="circle-dashed" class="w-3.5 h-3.5"></i> Not Installed</span>' +
'<button class="btn-sm btn-primary" onclick="openSemanticInstallWizard()"><i data-lucide="brain" class="w-3 h-3"></i> Install</button>') +
'</div>' +
'</div>';
}
container.innerHTML = '<div class="section-header">' +
'<div class="section-header-left">' +
'<h3><i data-lucide="terminal" class="w-4 h-4"></i> CLI Tools</h3>' +
'<span class="section-count">' + available + '/' + tools.length + ' available</span>' +
'</div>' +
'<button class="btn-icon" onclick="refreshAllCliStatus()" title="Refresh Status">' +
'<i data-lucide="refresh-cw" class="w-4 h-4"></i>' +
'</button>' +
'</div>' +
'<div class="tools-list">' +
toolsHtml +
codexLensHtml +
semanticHtml +
'</div>';
if (window.lucide) lucide.createIcons();
}
// ========== CCW Section (Right Column) ==========
function renderCcwSection() {
var container = document.getElementById('ccw-section');
if (!container) return;
var installationsHtml = '';
if (ccwInstallations.length === 0) {
installationsHtml = '<div class="ccw-empty-state">' +
'<i data-lucide="package-x" class="w-8 h-8"></i>' +
'<p>No installations found</p>' +
'<button class="btn btn-sm btn-primary" onclick="showCcwInstallModal()">' +
'<i data-lucide="download" class="w-3 h-3"></i> Install CCW</button>' +
'</div>';
} else {
installationsHtml = '<div class="ccw-list">';
for (var i = 0; i < ccwInstallations.length; i++) {
var inst = ccwInstallations[i];
var isGlobal = inst.installation_mode === 'Global';
var modeIcon = isGlobal ? 'home' : 'folder';
var version = inst.application_version || 'unknown';
var installDate = new Date(inst.installation_date).toLocaleDateString();
installationsHtml += '<div class="ccw-item">' +
'<div class="ccw-item-left">' +
'<div class="ccw-item-mode ' + (isGlobal ? 'global' : 'path') + '">' +
'<i data-lucide="' + modeIcon + '" class="w-4 h-4"></i>' +
'</div>' +
'<div class="ccw-item-info">' +
'<div class="ccw-item-header">' +
'<span class="ccw-item-name">' + inst.installation_mode + '</span>' +
'<span class="ccw-version-tag">v' + version + '</span>' +
'</div>' +
'<div class="ccw-item-path" title="' + inst.installation_path + '">' + escapeHtml(inst.installation_path) + '</div>' +
'<div class="ccw-item-meta">' +
'<span><i data-lucide="calendar" class="w-3 h-3"></i> ' + installDate + '</span>' +
'<span><i data-lucide="file" class="w-3 h-3"></i> ' + (inst.files_count || 0) + ' files</span>' +
'</div>' +
'</div>' +
'</div>' +
'<div class="ccw-item-actions">' +
'<button class="btn-icon btn-icon-sm" onclick="runCcwUpgrade()" title="Upgrade">' +
'<i data-lucide="arrow-up-circle" class="w-4 h-4"></i>' +
'</button>' +
'<button class="btn-icon btn-icon-sm btn-danger" onclick="confirmCcwUninstall(\'' + escapeHtml(inst.installation_path) + '\')" title="Uninstall">' +
'<i data-lucide="trash-2" class="w-4 h-4"></i>' +
'</button>' +
'</div>' +
'</div>';
}
installationsHtml += '</div>';
}
container.innerHTML = '<div class="section-header">' +
'<div class="section-header-left">' +
'<h3><i data-lucide="package" class="w-4 h-4"></i> CCW Install</h3>' +
'<span class="section-count">' + ccwInstallations.length + ' installation' + (ccwInstallations.length !== 1 ? 's' : '') + '</span>' +
'</div>' +
'<div class="section-header-actions">' +
'<button class="btn-icon" onclick="showCcwInstallModal()" title="Add Installation">' +
'<i data-lucide="plus" class="w-4 h-4"></i>' +
'</button>' +
'<button class="btn-icon" onclick="loadCcwInstallations().then(function() { renderCcwSection(); if (window.lucide) lucide.createIcons(); })" title="Refresh">' +
'<i data-lucide="refresh-cw" class="w-4 h-4"></i>' +
'</button>' +
'</div>' +
'</div>' +
installationsHtml;
if (window.lucide) lucide.createIcons();
}
// CCW Install Carousel State
var ccwCarouselIndex = 0;

View File

@@ -0,0 +1,132 @@
// CLI History View
// Standalone view for CLI execution history
// ========== Rendering ==========
async function renderCliHistoryView() {
var container = document.getElementById('mainContent');
if (!container) return;
// Hide stats grid and search for History view
var statsGrid = document.getElementById('statsGrid');
var searchInput = document.getElementById('searchInput');
if (statsGrid) statsGrid.style.display = 'none';
if (searchInput) searchInput.parentElement.style.display = 'none';
// Load history data
await loadCliHistory();
// Filter by search query
var filteredHistory = cliHistorySearch
? cliExecutionHistory.filter(function(exec) {
return exec.prompt_preview.toLowerCase().includes(cliHistorySearch.toLowerCase()) ||
exec.tool.toLowerCase().includes(cliHistorySearch.toLowerCase());
})
: cliExecutionHistory;
var historyHtml = '';
if (cliExecutionHistory.length === 0) {
historyHtml = '<div class="history-empty-state">' +
'<i data-lucide="terminal" class="w-12 h-12"></i>' +
'<h3>No executions yet</h3>' +
'<p>CLI execution history will appear here</p>' +
'</div>';
} else if (filteredHistory.length === 0) {
historyHtml = '<div class="history-empty-state">' +
'<i data-lucide="search-x" class="w-10 h-10"></i>' +
'<h3>No matching results</h3>' +
'<p>Try adjusting your search or filter</p>' +
'</div>';
} else {
historyHtml = '<div class="history-list">';
for (var i = 0; i < filteredHistory.length; i++) {
var exec = filteredHistory[i];
var statusIcon = exec.status === 'success' ? 'check-circle' :
exec.status === 'timeout' ? 'clock' : 'x-circle';
var statusClass = exec.status === 'success' ? 'success' :
exec.status === 'timeout' ? 'warning' : 'error';
var duration = formatDuration(exec.duration_ms);
var timeAgo = getTimeAgo(new Date(exec.timestamp));
historyHtml += '<div class="history-item" onclick="showExecutionDetail(\'' + exec.id + '\')">' +
'<div class="history-item-main">' +
'<div class="history-item-header">' +
'<span class="history-tool-tag tool-' + exec.tool + '">' + exec.tool + '</span>' +
'<span class="history-mode-tag">' + (exec.mode || 'analysis') + '</span>' +
'<span class="history-status ' + statusClass + '">' +
'<i data-lucide="' + statusIcon + '" class="w-3.5 h-3.5"></i>' +
exec.status +
'</span>' +
'</div>' +
'<div class="history-item-prompt">' + escapeHtml(exec.prompt_preview) + '</div>' +
'<div class="history-item-meta">' +
'<span class="history-time"><i data-lucide="clock" class="w-3 h-3"></i> ' + timeAgo + '</span>' +
'<span class="history-duration"><i data-lucide="timer" class="w-3 h-3"></i> ' + duration + '</span>' +
'</div>' +
'</div>' +
'<div class="history-item-actions">' +
'<button class="btn-icon" onclick="event.stopPropagation(); showExecutionDetail(\'' + exec.id + '\')" title="View Details">' +
'<i data-lucide="eye" class="w-4 h-4"></i>' +
'</button>' +
'<button class="btn-icon btn-danger" onclick="event.stopPropagation(); confirmDeleteExecution(\'' + exec.id + '\')" title="Delete">' +
'<i data-lucide="trash-2" class="w-4 h-4"></i>' +
'</button>' +
'</div>' +
'</div>';
}
historyHtml += '</div>';
}
container.innerHTML = '<div class="history-view">' +
'<div class="history-header">' +
'<div class="history-header-left">' +
'<span class="history-count">' + cliExecutionHistory.length + ' execution' + (cliExecutionHistory.length !== 1 ? 's' : '') + '</span>' +
'</div>' +
'<div class="history-header-right">' +
'<div class="history-search-wrapper">' +
'<i data-lucide="search" class="w-4 h-4"></i>' +
'<input type="text" class="history-search-input" placeholder="Search executions..." ' +
'value="' + escapeHtml(cliHistorySearch) + '" ' +
'onkeyup="searchCliHistoryView(this.value)" oninput="searchCliHistoryView(this.value)">' +
'</div>' +
'<select class="history-filter-select" onchange="filterCliHistoryView(this.value)">' +
'<option value=""' + (cliHistoryFilter === null ? ' selected' : '') + '>All Tools</option>' +
'<option value="gemini"' + (cliHistoryFilter === 'gemini' ? ' selected' : '') + '>Gemini</option>' +
'<option value="qwen"' + (cliHistoryFilter === 'qwen' ? ' selected' : '') + '>Qwen</option>' +
'<option value="codex"' + (cliHistoryFilter === 'codex' ? ' selected' : '') + '>Codex</option>' +
'</select>' +
'<button class="btn-icon" onclick="refreshCliHistoryView()" title="Refresh">' +
'<i data-lucide="refresh-cw" class="w-4 h-4"></i>' +
'</button>' +
'</div>' +
'</div>' +
historyHtml +
'</div>';
// Initialize Lucide icons
if (window.lucide) lucide.createIcons();
}
// ========== Actions ==========
async function filterCliHistoryView(tool) {
cliHistoryFilter = tool || null;
await loadCliHistory();
renderCliHistoryView();
}
function searchCliHistoryView(query) {
cliHistorySearch = query;
renderCliHistoryView();
// Preserve focus and cursor position
var searchInput = document.querySelector('.history-search-input');
if (searchInput) {
searchInput.focus();
searchInput.setSelectionRange(query.length, query.length);
}
}
async function refreshCliHistoryView() {
await loadCliHistory();
renderCliHistoryView();
showRefreshToast('History refreshed', 'success');
}

View File

@@ -74,6 +74,22 @@ async function renderHookManager() {
`}
</div>
<!-- Hook Wizards -->
<div class="hook-section mb-6">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-3">
<h3 class="text-lg font-semibold text-foreground">Hook Wizards</h3>
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-success/20 text-success">Guided Setup</span>
</div>
<span class="text-sm text-muted-foreground">Configure complex hooks with guided wizards</span>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
${renderWizardCard('memory-update')}
${renderWizardCard('skill-context')}
</div>
</div>
<!-- Quick Install Templates -->
<div class="hook-section">
<div class="flex items-center justify-between mb-4">
@@ -134,11 +150,112 @@ async function renderHookManager() {
// Attach event listeners
attachHookEventListeners();
// Initialize Lucide icons
if (typeof lucide !== 'undefined') lucide.createIcons();
}
// Load available SKILLs for skill-context wizard
async function loadAvailableSkills() {
try {
const response = await fetch(`/api/skills?path=${encodeURIComponent(projectPath)}`);
if (!response.ok) throw new Error('Failed to load skills');
const data = await response.json();
const container = document.getElementById('skill-discovery-skill-context');
if (container && data.skills) {
if (data.skills.length === 0) {
container.innerHTML = `
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">Available SKILLs:</span>
<span class="text-muted-foreground ml-2">No SKILLs found in .claude/skills/</span>
`;
} else {
const skillBadges = data.skills.map(skill => `
<span class="px-2 py-0.5 bg-emerald-500/10 text-emerald-500 rounded" title="${escapeHtml(skill.description)}">${escapeHtml(skill.name)}</span>
`).join('');
container.innerHTML = `
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">Available SKILLs:</span>
<div class="flex flex-wrap gap-1 mt-1">${skillBadges}</div>
`;
}
}
// Store skills for wizard use
window.availableSkills = data.skills || [];
} catch (err) {
console.error('Failed to load skills:', err);
const container = document.getElementById('skill-discovery-skill-context');
if (container) {
container.innerHTML = `
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">Available SKILLs:</span>
<span class="text-destructive ml-2">Error loading skills</span>
`;
}
}
}
// Call loadAvailableSkills after rendering hook manager
const originalRenderHookManager = typeof renderHookManager === 'function' ? renderHookManager : null;
function renderWizardCard(wizardId) {
const wizard = WIZARD_TEMPLATES[wizardId];
if (!wizard) return '';
// Determine what to show in the tools/skills section
const toolsSection = wizard.requiresSkillDiscovery
? `
<div class="flex items-center gap-2 text-xs text-muted-foreground mb-4">
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">Event:</span>
<span class="px-2 py-0.5 bg-amber-500/10 text-amber-500 rounded">UserPromptSubmit</span>
</div>
<div id="skill-discovery-${wizardId}" class="text-xs text-muted-foreground mb-4">
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">Available SKILLs:</span>
<span class="text-muted-foreground ml-2">Loading...</span>
</div>
`
: `
<div class="flex items-center gap-2 text-xs text-muted-foreground mb-4">
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">CLI Tools:</span>
<span class="px-2 py-0.5 bg-blue-500/10 text-blue-500 rounded">gemini</span>
<span class="px-2 py-0.5 bg-purple-500/10 text-purple-500 rounded">qwen</span>
<span class="px-2 py-0.5 bg-green-500/10 text-green-500 rounded">codex</span>
</div>
`;
return `
<div class="hook-wizard-card bg-gradient-to-br from-primary/5 to-primary/10 border border-primary/20 rounded-lg p-5 hover:shadow-lg transition-all">
<div class="flex items-start justify-between mb-4">
<div class="flex items-center gap-3">
<div class="p-2.5 bg-primary/10 rounded-lg">
<i data-lucide="${wizard.icon}" class="w-6 h-6 text-primary"></i>
</div>
<div>
<h4 class="font-semibold text-foreground">${escapeHtml(wizard.name)}</h4>
<p class="text-sm text-muted-foreground">${escapeHtml(wizard.description)}</p>
</div>
</div>
</div>
<div class="space-y-2 mb-4">
${wizard.options.map(opt => `
<div class="flex items-center gap-2 text-sm text-muted-foreground">
<i data-lucide="check" class="w-4 h-4 text-success"></i>
<span>${escapeHtml(opt.name)}: ${escapeHtml(opt.description)}</span>
</div>
`).join('')}
</div>
${toolsSection}
<button class="w-full px-4 py-2.5 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center justify-center gap-2"
onclick="openHookWizardModal('${wizardId}')">
<i data-lucide="wand-2" class="w-4 h-4"></i>
Open Wizard
</button>
</div>
`;
}
function countHooks(hooks) {
let count = 0;
for (const event of Object.keys(hooks)) {
@@ -214,6 +331,8 @@ function renderHooksByEvent(hooks, scope) {
function renderQuickInstallCard(templateId, title, description, event, matcher) {
const isInstalled = isHookTemplateInstalled(templateId);
const template = HOOK_TEMPLATES[templateId];
const category = template?.category || 'general';
return `
<div class="hook-template-card bg-card border border-border rounded-lg p-4 hover:shadow-md transition-all ${isInstalled ? 'border-success bg-success-light/30' : ''}">
@@ -225,6 +344,11 @@ function renderQuickInstallCard(templateId, title, description, event, matcher)
<p class="text-xs text-muted-foreground">${escapeHtml(description)}</p>
</div>
</div>
<button class="p-1.5 text-muted-foreground hover:text-foreground hover:bg-hover rounded transition-colors"
onclick="viewTemplateDetails('${templateId}')"
title="View template details">
<i data-lucide="eye" class="w-4 h-4"></i>
</button>
</div>
<div class="hook-template-meta text-xs text-muted-foreground mb-3 flex items-center gap-3">
@@ -234,6 +358,7 @@ function renderQuickInstallCard(templateId, title, description, event, matcher)
<span class="flex items-center gap-1">
Matches: <span class="font-medium">${matcher}</span>
</span>
<span class="px-1.5 py-0.5 bg-primary/10 text-primary rounded text-xs">${category}</span>
</div>
<div class="flex items-center gap-2">