mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-12 02:37:45 +08:00
feat: Enhance global notifications with localStorage persistence and clear functionality
feat: Implement generic modal functions for better UI consistency feat: Update navigation titles for CLI Tools view feat: Add JSON formatting for notification details in CLI execution feat: Introduce localStorage handling for global notification queue feat: Expand CLI Manager view to include CCW installations with carousel feat: Add CCW installation management with modal for user interaction fix: Improve event delegation for explorer tree item interactions refactor: Clean up CLI Tools section in dashboard HTML feat: Add functionality to delete CLI execution history by ID
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
// CLI History Component
|
||||
// Displays execution history with filtering and search
|
||||
// Displays execution history with filtering, search, and delete
|
||||
|
||||
// ========== CLI History State ==========
|
||||
let cliExecutionHistory = [];
|
||||
let cliHistoryFilter = null; // Filter by tool
|
||||
let cliHistorySearch = ''; // Search query
|
||||
let cliHistoryLimit = 50;
|
||||
|
||||
// ========== Data Loading ==========
|
||||
@@ -44,19 +45,28 @@ function renderCliHistory() {
|
||||
const container = document.getElementById('cli-history-panel');
|
||||
if (!container) return;
|
||||
|
||||
// Filter by search query
|
||||
const filteredHistory = cliHistorySearch
|
||||
? cliExecutionHistory.filter(exec =>
|
||||
exec.prompt_preview.toLowerCase().includes(cliHistorySearch.toLowerCase()) ||
|
||||
exec.tool.toLowerCase().includes(cliHistorySearch.toLowerCase())
|
||||
)
|
||||
: cliExecutionHistory;
|
||||
|
||||
if (cliExecutionHistory.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="cli-history-header">
|
||||
<h3>Execution History</h3>
|
||||
<div class="cli-history-controls">
|
||||
${renderHistorySearch()}
|
||||
${renderToolFilter()}
|
||||
<button class="btn-icon" onclick="refreshCliHistory()" title="Refresh">
|
||||
<i data-lucide="refresh-cw"></i>
|
||||
<i data-lucide="refresh-cw" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="empty-state">
|
||||
<i data-lucide="terminal"></i>
|
||||
<i data-lucide="terminal" class="w-8 h-8"></i>
|
||||
<p>No executions yet</p>
|
||||
</div>
|
||||
`;
|
||||
@@ -65,36 +75,53 @@ function renderCliHistory() {
|
||||
return;
|
||||
}
|
||||
|
||||
const historyHtml = cliExecutionHistory.map(exec => {
|
||||
const statusIcon = exec.status === 'success' ? 'check-circle' :
|
||||
exec.status === 'timeout' ? 'clock' : 'x-circle';
|
||||
const statusClass = exec.status === 'success' ? 'text-success' :
|
||||
exec.status === 'timeout' ? 'text-warning' : 'text-destructive';
|
||||
const duration = formatDuration(exec.duration_ms);
|
||||
const timeAgo = getTimeAgo(new Date(exec.timestamp));
|
||||
const historyHtml = filteredHistory.length === 0
|
||||
? `<div class="empty-state">
|
||||
<i data-lucide="search-x" class="w-6 h-6"></i>
|
||||
<p>No matching results</p>
|
||||
</div>`
|
||||
: filteredHistory.map(exec => {
|
||||
const statusIcon = exec.status === 'success' ? 'check-circle' :
|
||||
exec.status === 'timeout' ? 'clock' : 'x-circle';
|
||||
const statusClass = exec.status === 'success' ? 'text-success' :
|
||||
exec.status === 'timeout' ? 'text-warning' : 'text-destructive';
|
||||
const duration = formatDuration(exec.duration_ms);
|
||||
const timeAgo = getTimeAgo(new Date(exec.timestamp));
|
||||
|
||||
return `
|
||||
<div class="cli-history-item" onclick="showExecutionDetail('${exec.id}')">
|
||||
<div class="cli-history-item-header">
|
||||
<span class="cli-tool-tag cli-tool-${exec.tool}">${exec.tool}</span>
|
||||
<span class="cli-history-time">${timeAgo}</span>
|
||||
<i data-lucide="${statusIcon}" class="${statusClass}"></i>
|
||||
</div>
|
||||
<div class="cli-history-prompt">${escapeHtml(exec.prompt_preview)}</div>
|
||||
<div class="cli-history-meta">
|
||||
<span class="text-muted-foreground">${duration}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
return `
|
||||
<div class="cli-history-item">
|
||||
<div class="cli-history-item-content" onclick="showExecutionDetail('${exec.id}')">
|
||||
<div class="cli-history-item-header">
|
||||
<span class="cli-tool-tag cli-tool-${exec.tool}">${exec.tool}</span>
|
||||
<span class="cli-history-time">${timeAgo}</span>
|
||||
<i data-lucide="${statusIcon}" class="w-3.5 h-3.5 ${statusClass}"></i>
|
||||
</div>
|
||||
<div class="cli-history-prompt">${escapeHtml(exec.prompt_preview)}</div>
|
||||
<div class="cli-history-meta">
|
||||
<span>${duration}</span>
|
||||
<span>${exec.mode || 'analysis'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cli-history-actions">
|
||||
<button class="btn-icon" onclick="event.stopPropagation(); showExecutionDetail('${exec.id}')" title="View Details">
|
||||
<i data-lucide="eye" class="w-3.5 h-3.5"></i>
|
||||
</button>
|
||||
<button class="btn-icon btn-danger" onclick="event.stopPropagation(); confirmDeleteExecution('${exec.id}')" title="Delete">
|
||||
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="cli-history-header">
|
||||
<h3>Execution History</h3>
|
||||
<div class="cli-history-controls">
|
||||
${renderHistorySearch()}
|
||||
${renderToolFilter()}
|
||||
<button class="btn-icon" onclick="refreshCliHistory()" title="Refresh">
|
||||
<i data-lucide="refresh-cw"></i>
|
||||
<i data-lucide="refresh-cw" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -106,6 +133,17 @@ function renderCliHistory() {
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
function renderHistorySearch() {
|
||||
return `
|
||||
<input type="text"
|
||||
class="cli-history-search"
|
||||
placeholder="Search history..."
|
||||
value="${escapeHtml(cliHistorySearch)}"
|
||||
onkeyup="searchCliHistory(this.value)"
|
||||
oninput="searchCliHistory(this.value)">
|
||||
`;
|
||||
}
|
||||
|
||||
function renderToolFilter() {
|
||||
const tools = ['all', 'gemini', 'qwen', 'codex'];
|
||||
return `
|
||||
@@ -135,30 +173,41 @@ async function showExecutionDetail(executionId) {
|
||||
<span class="text-muted-foreground">${formatDuration(detail.duration_ms)}</span>
|
||||
</div>
|
||||
<div class="cli-detail-meta">
|
||||
<span class="text-muted-foreground">Model: ${detail.model || 'default'}</span>
|
||||
<span class="text-muted-foreground">Mode: ${detail.mode}</span>
|
||||
<span class="text-muted-foreground">${new Date(detail.timestamp).toLocaleString()}</span>
|
||||
<span><i data-lucide="cpu" class="w-3 h-3"></i> ${detail.model || 'default'}</span>
|
||||
<span><i data-lucide="toggle-right" class="w-3 h-3"></i> ${detail.mode}</span>
|
||||
<span><i data-lucide="calendar" class="w-3 h-3"></i> ${new Date(detail.timestamp).toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cli-detail-section">
|
||||
<h4>Prompt</h4>
|
||||
<h4><i data-lucide="message-square"></i> Prompt</h4>
|
||||
<pre class="cli-detail-prompt">${escapeHtml(detail.prompt)}</pre>
|
||||
</div>
|
||||
${detail.output.stdout ? `
|
||||
<div class="cli-detail-section">
|
||||
<h4>Output</h4>
|
||||
<h4><i data-lucide="terminal"></i> Output</h4>
|
||||
<pre class="cli-detail-output">${escapeHtml(detail.output.stdout)}</pre>
|
||||
</div>
|
||||
` : ''}
|
||||
${detail.output.stderr ? `
|
||||
<div class="cli-detail-section">
|
||||
<h4>Errors</h4>
|
||||
<h4><i data-lucide="alert-triangle"></i> Errors</h4>
|
||||
<pre class="cli-detail-error">${escapeHtml(detail.output.stderr)}</pre>
|
||||
</div>
|
||||
` : ''}
|
||||
${detail.output.truncated ? `
|
||||
<p class="text-warning">Output was truncated due to size.</p>
|
||||
<p class="text-warning" style="font-size: 0.75rem; margin-top: 0.5rem;">
|
||||
<i data-lucide="info" class="w-3 h-3" style="display: inline;"></i>
|
||||
Output was truncated due to size.
|
||||
</p>
|
||||
` : ''}
|
||||
<div class="cli-detail-actions">
|
||||
<button class="btn btn-sm btn-outline" onclick="copyExecutionPrompt('${executionId}')">
|
||||
<i data-lucide="copy" class="w-3.5 h-3.5"></i> Copy Prompt
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline btn-danger" onclick="confirmDeleteExecution('${executionId}'); closeModal();">
|
||||
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i> Delete
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
showModal('Execution Detail', modalContent);
|
||||
@@ -171,12 +220,69 @@ async function filterCliHistory(tool) {
|
||||
renderCliHistory();
|
||||
}
|
||||
|
||||
function searchCliHistory(query) {
|
||||
cliHistorySearch = query;
|
||||
renderCliHistory();
|
||||
// Preserve focus and cursor position
|
||||
const searchInput = document.querySelector('.cli-history-search');
|
||||
if (searchInput) {
|
||||
searchInput.focus();
|
||||
searchInput.setSelectionRange(query.length, query.length);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshCliHistory() {
|
||||
await loadCliHistory();
|
||||
renderCliHistory();
|
||||
showRefreshToast('History refreshed', 'success');
|
||||
}
|
||||
|
||||
// ========== Delete Execution ==========
|
||||
function confirmDeleteExecution(executionId) {
|
||||
if (confirm('Delete this execution record? This action cannot be undone.')) {
|
||||
deleteExecution(executionId);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteExecution(executionId) {
|
||||
try {
|
||||
const response = await fetch(`/api/cli/execution?path=${encodeURIComponent(projectPath)}&id=${encodeURIComponent(executionId)}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to delete');
|
||||
}
|
||||
|
||||
// Remove from local state
|
||||
cliExecutionHistory = cliExecutionHistory.filter(exec => exec.id !== executionId);
|
||||
renderCliHistory();
|
||||
showRefreshToast('Execution deleted', 'success');
|
||||
} catch (err) {
|
||||
console.error('Failed to delete execution:', err);
|
||||
showRefreshToast('Delete failed: ' + err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Copy Prompt ==========
|
||||
async function copyExecutionPrompt(executionId) {
|
||||
const detail = await loadExecutionDetail(executionId);
|
||||
if (!detail) {
|
||||
showRefreshToast('Execution not found', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (navigator.clipboard) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(detail.prompt);
|
||||
showRefreshToast('Prompt copied to clipboard', 'success');
|
||||
} catch (err) {
|
||||
showRefreshToast('Failed to copy', 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Helpers ==========
|
||||
function formatDuration(ms) {
|
||||
if (ms >= 60000) {
|
||||
|
||||
@@ -79,17 +79,22 @@ function addGlobalNotification(type, message, details = null, source = null) {
|
||||
timestamp: new Date().toISOString(),
|
||||
read: false
|
||||
};
|
||||
|
||||
|
||||
globalNotificationQueue.unshift(notification);
|
||||
|
||||
|
||||
// Keep only last 100 notifications
|
||||
if (globalNotificationQueue.length > 100) {
|
||||
globalNotificationQueue = globalNotificationQueue.slice(0, 100);
|
||||
}
|
||||
|
||||
|
||||
// Persist to localStorage
|
||||
if (typeof saveNotificationsToStorage === 'function') {
|
||||
saveNotificationsToStorage();
|
||||
}
|
||||
|
||||
renderGlobalNotifications();
|
||||
updateGlobalNotifBadge();
|
||||
|
||||
|
||||
// Show toast for important notifications
|
||||
if (type === 'error' || type === 'success') {
|
||||
showNotificationToast(notification);
|
||||
@@ -204,6 +209,12 @@ function updateGlobalNotifBadge() {
|
||||
*/
|
||||
function clearGlobalNotifications() {
|
||||
globalNotificationQueue = [];
|
||||
|
||||
// Clear from localStorage
|
||||
if (typeof saveNotificationsToStorage === 'function') {
|
||||
saveNotificationsToStorage();
|
||||
}
|
||||
|
||||
renderGlobalNotifications();
|
||||
updateGlobalNotifBadge();
|
||||
}
|
||||
@@ -213,6 +224,12 @@ function clearGlobalNotifications() {
|
||||
*/
|
||||
function markAllNotificationsRead() {
|
||||
globalNotificationQueue.forEach(n => n.read = true);
|
||||
|
||||
// Save to localStorage
|
||||
if (typeof saveNotificationsToStorage === 'function') {
|
||||
saveNotificationsToStorage();
|
||||
}
|
||||
|
||||
renderGlobalNotifications();
|
||||
updateGlobalNotifBadge();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,56 @@
|
||||
// MODAL DIALOGS
|
||||
// ==========================================
|
||||
|
||||
// Generic Modal Functions
|
||||
function showModal(title, content, options = {}) {
|
||||
// Remove existing modal if any
|
||||
closeModal();
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'generic-modal-overlay';
|
||||
overlay.innerHTML = `
|
||||
<div class="generic-modal ${options.size || ''}">
|
||||
<div class="generic-modal-header">
|
||||
<h3 class="generic-modal-title">${escapeHtml(title)}</h3>
|
||||
<button class="generic-modal-close" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
<div class="generic-modal-body">
|
||||
${content}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// Trigger animation
|
||||
requestAnimationFrame(() => overlay.classList.add('active'));
|
||||
|
||||
// Initialize Lucide icons in modal
|
||||
if (window.lucide) lucide.createIcons();
|
||||
|
||||
// Close on overlay click
|
||||
overlay.addEventListener('click', (e) => {
|
||||
if (e.target === overlay) closeModal();
|
||||
});
|
||||
|
||||
// Close on Escape key
|
||||
const escHandler = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
closeModal();
|
||||
document.removeEventListener('keydown', escHandler);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', escHandler);
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
const overlay = document.querySelector('.generic-modal-overlay');
|
||||
if (overlay) {
|
||||
overlay.classList.remove('active');
|
||||
setTimeout(() => overlay.remove(), 200);
|
||||
}
|
||||
}
|
||||
|
||||
// SVG Icons
|
||||
const icons = {
|
||||
folder: '<svg viewBox="0 0 24 24"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>',
|
||||
|
||||
@@ -116,6 +116,8 @@ function updateContentTitle() {
|
||||
titleEl.textContent = 'MCP Server Management';
|
||||
} else if (currentView === 'explorer') {
|
||||
titleEl.textContent = 'File Explorer';
|
||||
} else if (currentView === 'cli-manager') {
|
||||
titleEl.textContent = 'CLI Tools & CCW';
|
||||
} else if (currentView === 'liteTasks') {
|
||||
const names = { 'lite-plan': 'Lite Plan Sessions', 'lite-fix': 'Lite Fix Sessions' };
|
||||
titleEl.textContent = names[currentLiteType] || 'Lite Tasks';
|
||||
|
||||
@@ -3,6 +3,42 @@
|
||||
// ==========================================
|
||||
// Real-time silent refresh (no notification bubbles)
|
||||
|
||||
/**
|
||||
* Format JSON object for display in notifications
|
||||
* @param {Object} obj - Object to format
|
||||
* @param {number} maxLen - Max string length
|
||||
* @returns {string} Formatted string
|
||||
*/
|
||||
function formatJsonDetails(obj, maxLen = 150) {
|
||||
if (!obj || typeof obj !== 'object') return String(obj);
|
||||
|
||||
// Try pretty format first
|
||||
try {
|
||||
const formatted = JSON.stringify(obj, null, 2);
|
||||
if (formatted.length <= maxLen) {
|
||||
return formatted;
|
||||
}
|
||||
|
||||
// For longer content, show key-value pairs on separate lines
|
||||
const entries = Object.entries(obj);
|
||||
if (entries.length === 0) return '{}';
|
||||
|
||||
const lines = entries.slice(0, 5).map(([key, val]) => {
|
||||
let valStr = typeof val === 'object' ? JSON.stringify(val) : String(val);
|
||||
if (valStr.length > 50) valStr = valStr.substring(0, 47) + '...';
|
||||
return `${key}: ${valStr}`;
|
||||
});
|
||||
|
||||
if (entries.length > 5) {
|
||||
lines.push(`... +${entries.length - 5} more`);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
} catch (e) {
|
||||
return JSON.stringify(obj).substring(0, maxLen) + '...';
|
||||
}
|
||||
}
|
||||
|
||||
let wsConnection = null;
|
||||
let autoRefreshInterval = null;
|
||||
let lastDataHash = null;
|
||||
@@ -132,9 +168,7 @@ function handleToolExecutionNotification(payload) {
|
||||
notifType = 'info';
|
||||
message = `Executing ${toolName}...`;
|
||||
if (params) {
|
||||
// Show truncated params
|
||||
const paramStr = JSON.stringify(params);
|
||||
details = paramStr.length > 100 ? paramStr.substring(0, 100) + '...' : paramStr;
|
||||
details = formatJsonDetails(params, 150);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -142,12 +176,10 @@ function handleToolExecutionNotification(payload) {
|
||||
notifType = 'success';
|
||||
message = `${toolName} completed`;
|
||||
if (result) {
|
||||
// Show truncated result
|
||||
if (result._truncated) {
|
||||
details = result.preview;
|
||||
} else {
|
||||
const resultStr = JSON.stringify(result);
|
||||
details = resultStr.length > 150 ? resultStr.substring(0, 150) + '...' : resultStr;
|
||||
details = formatJsonDetails(result, 200);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user