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:
catlog22
2025-12-11 21:18:28 +08:00
parent e350e0c7bb
commit 766a8d2145
13 changed files with 1404 additions and 320 deletions

View File

@@ -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) {