// CLI History Component // Displays execution history with filtering, search, and delete // Supports native session linking and full conversation parsing // ========== CLI History State ========== let cliExecutionHistory = []; let cliHistoryFilter = null; // Filter by tool let cliHistorySearch = ''; // Search query let cliHistoryLimit = 50; let showNativeOnly = false; // Filter to show only native-linked executions // ========== Data Loading ========== async function loadCliHistory(options = {}) { try { const { limit = cliHistoryLimit, tool = cliHistoryFilter, status = null } = options; // Use history-native endpoint to get native session info // Use recursiveQueryEnabled setting (from cli-status.js) to control recursive query const recursive = typeof recursiveQueryEnabled !== 'undefined' ? recursiveQueryEnabled : true; let url = `/api/cli/history-native?path=${encodeURIComponent(projectPath)}&limit=${limit}&recursive=${recursive}`; if (tool) url += `&tool=${tool}`; if (status) url += `&status=${status}`; if (cliHistorySearch) url += `&search=${encodeURIComponent(cliHistorySearch)}`; const response = await fetch(url); if (!response.ok) throw new Error('Failed to load CLI history'); const data = await response.json(); cliExecutionHistory = data.executions || []; return data; } catch (err) { console.error('Failed to load CLI history:', err); return { executions: [], total: 0, count: 0 }; } } // Load native session content for a specific execution async function loadNativeSessionContent(executionId, sourceDir) { try { // If sourceDir provided, use it to build the correct path // Check if sourceDir is absolute path (contains : or starts with /) let basePath = projectPath; if (sourceDir && sourceDir !== '.') { const isAbsolute = sourceDir.includes(':') || sourceDir.startsWith('/'); basePath = isAbsolute ? sourceDir : projectPath + '/' + sourceDir; } const url = `/api/cli/native-session?path=${encodeURIComponent(basePath)}&id=${encodeURIComponent(executionId)}`; const response = await fetch(url); if (!response.ok) return null; return await response.json(); } catch (err) { console.error('Failed to load native session:', err); return null; } } // Load enriched conversation (CCW + Native merged) async function loadEnrichedConversation(executionId) { try { const url = `/api/cli/enriched?path=${encodeURIComponent(projectPath)}&id=${encodeURIComponent(executionId)}`; const response = await fetch(url); if (!response.ok) return null; return await response.json(); } catch (err) { console.error('Failed to load enriched conversation:', err); return null; } } async function loadExecutionDetail(executionId, sourceDir) { try { // If sourceDir provided, use it to build the correct path // Check if sourceDir is absolute path (contains : or starts with /) let basePath = projectPath; if (sourceDir && sourceDir !== '.') { const isAbsolute = sourceDir.includes(':') || sourceDir.startsWith('/'); basePath = isAbsolute ? sourceDir : projectPath + '/' + sourceDir; } const url = `/api/cli/execution?path=${encodeURIComponent(basePath)}&id=${encodeURIComponent(executionId)}`; const response = await fetch(url); if (!response.ok) throw new Error('Execution not found'); return await response.json(); } catch (err) { console.error('Failed to load execution detail:', err); return null; } } // ========== Rendering ========== 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 = `

Execution History

${renderHistorySearch()} ${renderToolFilter()}

No executions yet

`; if (window.lucide) lucide.createIcons(); return; } const historyHtml = filteredHistory.length === 0 ? `

No matching results

` : 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.updated_at || exec.timestamp)); const turnBadge = exec.turn_count && exec.turn_count > 1 ? `${exec.turn_count} turns` : ''; // Native session indicator const hasNative = exec.hasNativeSession || exec.nativeSessionId; const nativeBadge = hasNative ? ` ` : ''; // Normalize and escape sourceDir for use in onclick // Convert backslashes to forward slashes to prevent JS escape issues in onclick const sourceDirEscaped = exec.sourceDir ? exec.sourceDir.replace(/\\/g, '/').replace(/'/g, "\\'") : ''; return `
${exec.tool.toUpperCase()} ${exec.mode || 'analysis'} ${exec.status} ${nativeBadge}
${escapeHtml(exec.prompt_preview)}
${timeAgo} ${duration} ${exec.id.substring(0, 13)}...${exec.id.split('-').pop()} ${turnBadge}
${hasNative ? ` ` : ''}
`; }).join(''); container.innerHTML = `

Execution History

${renderHistorySearch()} ${renderToolFilter()}
${historyHtml}
`; if (window.lucide) lucide.createIcons(); } function renderHistorySearch() { return ` `; } function renderToolFilter() { const tools = ['all', 'gemini', 'qwen', 'codex']; return ` `; } // ========== Execution Detail Modal ========== async function showExecutionDetail(executionId, sourceDir) { const conversation = await loadExecutionDetail(executionId, sourceDir); if (!conversation) { showRefreshToast('Conversation not found', 'error'); return; } // Handle both old (single execution) and new (conversation) formats const isConversation = conversation.turns && Array.isArray(conversation.turns); const turnCount = isConversation ? conversation.turn_count : 1; const totalDuration = isConversation ? conversation.total_duration_ms : conversation.duration_ms; const latestStatus = isConversation ? conversation.latest_status : conversation.status; const createdAt = isConversation ? conversation.created_at : conversation.timestamp; // Build turns HTML with improved multi-turn display let turnsHtml = ''; if (isConversation && conversation.turns.length > 0) { turnsHtml = conversation.turns.map((turn, idx) => { const isFirst = idx === 0; const isLast = idx === conversation.turns.length - 1; const turnTime = new Date(turn.timestamp).toLocaleTimeString(); const statusIcon = turn.status === 'success' ? 'check-circle' : turn.status === 'timeout' ? 'clock' : 'x-circle'; return `
${isFirst ? '▶' : '↳'} Turn ${turn.turn} ${isLast ? 'Latest' : ''}
${turnTime} ${turn.status} ${formatDuration(turn.duration_ms)}

User Prompt

${escapeHtml(turn.prompt)}
${turn.output.stdout ? `

Assistant Response

${escapeHtml(turn.output.stdout)}
` : ''} ${turn.output.stderr ? `

Errors

${escapeHtml(turn.output.stderr)}
` : ''} ${turn.output.truncated ? `

Output was truncated due to size.

` : ''}
`; }).join('
'); } else { // Legacy single execution format const detail = conversation; turnsHtml = `

User Prompt

${escapeHtml(detail.prompt)}
${detail.output.stdout ? `

Assistant Response

${escapeHtml(detail.output.stdout)}
` : ''} ${detail.output.stderr ? `

Errors

${escapeHtml(detail.output.stderr)}
` : ''} ${detail.output.truncated ? `

Output was truncated due to size.

` : ''}
`; } // Build concatenated prompt view (for multi-turn conversations) let concatenatedPromptHtml = ''; if (isConversation && conversation.turns.length > 1) { concatenatedPromptHtml = ` `; } // Check if native session is available const hasNativeSession = conversation.hasNativeSession || conversation.nativeSessionId; const modalContent = `
${conversation.tool} ${turnCount > 1 ? ` ${turnCount} turns` : ''} ${latestStatus} ${formatDuration(totalDuration)}
${conversation.model || 'default'} ${conversation.mode} ${new Date(createdAt).toLocaleString()} ${executionId.split('-')[0]}
${hasNativeSession ? `
` : ''}
${turnCount > 1 ? `
` : ''}
${turnsHtml}
${concatenatedPromptHtml}
${turnCount > 1 ? ` ` : ''}
`; // Store conversation data for format switching window._currentConversation = conversation; showModal('Conversation Detail', modalContent); } // ========== Actions ========== async function filterCliHistory(tool) { cliHistoryFilter = tool || null; await loadCliHistory(); 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, sourceDir) { if (confirm('Delete this execution record? This action cannot be undone.')) { deleteExecution(executionId, sourceDir); } } async function deleteExecution(executionId, sourceDir) { try { // Build correct path - use sourceDir if provided for recursive items // Check if sourceDir is absolute path (contains : or starts with /) let basePath = projectPath; if (sourceDir && sourceDir !== '.') { const isAbsolute = sourceDir.includes(':') || sourceDir.startsWith('/'); basePath = isAbsolute ? sourceDir : projectPath + '/' + sourceDir; } const response = await fetch(`/api/cli/execution?path=${encodeURIComponent(basePath)}&id=${encodeURIComponent(executionId)}`, { method: 'DELETE' }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to delete'); } // Reload fresh data from server and re-render await loadCliHistory(); // Render appropriate view based on current view if (typeof currentView !== 'undefined' && (currentView === 'history' || currentView === 'cli-history')) { renderCliHistoryView(); } else { renderCliHistory(); } showRefreshToast('Execution deleted', 'success'); } catch (err) { console.error('Failed to delete execution:', err); showRefreshToast('Delete failed: ' + err.message, 'error'); } } // ========== Copy Functions ========== async function copyCliExecutionId(executionId) { if (navigator.clipboard) { try { await navigator.clipboard.writeText(executionId); showRefreshToast('ID copied: ' + executionId, 'success'); } catch (err) { console.error('Failed to copy ID:', err); showRefreshToast('Failed to copy ID', 'error'); } } } 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'); } } } async function copyConversationId(conversationId) { if (navigator.clipboard) { try { await navigator.clipboard.writeText(conversationId); showRefreshToast('ID copied to clipboard', 'success'); } catch (err) { showRefreshToast('Failed to copy', 'error'); } } } // ========== Concatenated Prompt Functions ========== /** * Build concatenated prompt from conversation turns * Formats: plain, yaml, json */ function buildConcatenatedPrompt(conversation, format) { if (!conversation || !conversation.turns || conversation.turns.length === 0) { return ''; } const turns = conversation.turns; switch (format) { case 'yaml': return buildYamlPrompt(conversation); case 'json': return buildJsonPrompt(conversation); case 'plain': default: return buildPlainPrompt(conversation); } } function buildPlainPrompt(conversation) { const parts = []; parts.push('=== CONVERSATION HISTORY ==='); parts.push(''); for (const turn of conversation.turns) { parts.push('--- Turn ' + turn.turn + ' ---'); parts.push('USER:'); parts.push(turn.prompt); parts.push(''); parts.push('ASSISTANT:'); parts.push(turn.output.stdout || '[No output]'); parts.push(''); } parts.push('=== NEW REQUEST ==='); parts.push(''); parts.push('[Your next prompt here]'); return parts.join('\n'); } function buildYamlPrompt(conversation) { const lines = []; lines.push('context:'); lines.push(' tool: ' + conversation.tool); lines.push(' model: ' + (conversation.model || 'default')); lines.push(' mode: ' + conversation.mode); lines.push(''); lines.push('conversation:'); for (const turn of conversation.turns) { lines.push(' - turn: ' + turn.turn); lines.push(' timestamp: ' + turn.timestamp); lines.push(' status: ' + turn.status); lines.push(' user: |'); turn.prompt.split('\n').forEach(function(line) { lines.push(' ' + line); }); lines.push(' assistant: |'); (turn.output.stdout || '[No output]').split('\n').forEach(function(line) { lines.push(' ' + line); }); lines.push(''); } lines.push('new_request: |'); lines.push(' [Your next prompt here]'); return lines.join('\n'); } function buildJsonPrompt(conversation) { const data = { context: { tool: conversation.tool, model: conversation.model || 'default', mode: conversation.mode }, conversation: conversation.turns.map(function(turn) { return { turn: turn.turn, timestamp: turn.timestamp, status: turn.status, user: turn.prompt, assistant: turn.output.stdout || '[No output]' }; }), new_request: '[Your next prompt here]' }; return JSON.stringify(data, null, 2); } /** * Toggle between per-turn and concatenated views */ function toggleConversationView(view) { var turnsContainer = document.getElementById('turnsContainer'); var concatSection = document.getElementById('concatPromptSection'); var buttons = document.querySelectorAll('.cli-view-toggle button'); if (view === 'concat') { if (turnsContainer) turnsContainer.style.display = 'none'; if (concatSection) concatSection.style.display = 'block'; buttons.forEach(function(btn, idx) { btn.classList.toggle('active', idx === 1); }); } else { if (turnsContainer) turnsContainer.style.display = 'block'; if (concatSection) concatSection.style.display = 'none'; buttons.forEach(function(btn, idx) { btn.classList.toggle('active', idx === 0); }); } if (window.lucide) lucide.createIcons(); } /** * Switch concatenation format (plain/yaml/json) */ function switchConcatFormat(format, executionId) { var conversation = window._currentConversation; if (!conversation) return; var output = document.getElementById('concatPromptOutput'); if (output) { output.textContent = buildConcatenatedPrompt(conversation, format); } // Update button states var buttons = document.querySelectorAll('.cli-concat-format-selector button'); buttons.forEach(function(btn) { var btnFormat = btn.textContent.toLowerCase(); btn.className = 'btn btn-xs ' + (btnFormat === format ? 'btn-primary' : 'btn-outline'); }); } /** * Copy concatenated prompt to clipboard */ async function copyConcatenatedPrompt(executionId) { var conversation = window._currentConversation; if (!conversation) { showRefreshToast('Conversation not found', 'error'); return; } var prompt = buildConcatenatedPrompt(conversation, 'plain'); if (navigator.clipboard) { try { await navigator.clipboard.writeText(prompt); showRefreshToast('Full prompt copied to clipboard', 'success'); } catch (err) { showRefreshToast('Failed to copy', 'error'); } } } // ========== Native Session Detail ========== /** * Show native session detail modal with full conversation content */ async function showNativeSessionDetail(executionId, sourceDir) { // Load native session content const nativeSession = await loadNativeSessionContent(executionId, sourceDir); if (!nativeSession) { showRefreshToast('Native session not found', 'error'); return; } // Build turns HTML from native session const turnsHtml = nativeSession.turns && nativeSession.turns.length > 0 ? nativeSession.turns.map((turn, idx) => { const isLast = idx === nativeSession.turns.length - 1; const roleIcon = turn.role === 'user' ? 'user' : 'bot'; const roleClass = turn.role === 'user' ? 'user' : 'assistant'; // Token info const tokenInfo = turn.tokens ? ` ${turn.tokens.total || 0} tokens (in: ${turn.tokens.input || 0}, out: ${turn.tokens.output || 0}${turn.tokens.cached ? `, cached: ${turn.tokens.cached}` : ''}) ` : ''; // Thoughts section (collapsible) const thoughtsHtml = turn.thoughts && turn.thoughts.length > 0 ? `
💭 Thinking Process (${turn.thoughts.length} thoughts)
    ${turn.thoughts.map(t => `
  • ${escapeHtml(t)}
  • `).join('')}
` : ''; // Tool calls section (collapsible for each call) const toolCallsHtml = turn.toolCalls && turn.toolCalls.length > 0 ? `
Tool Calls (${turn.toolCalls.length})
${turn.toolCalls.map((tc, tcIdx) => `
🔧 ${escapeHtml(tc.name)} ${tc.output ? `(${tc.output.length} chars)` : ''}
${tc.input ? `
Input:
${escapeHtml(JSON.stringify(tc.input, null, 2))}
` : ''} ${tc.output ? `
Output:
${escapeHtml(tc.output)}
` : ''}
`).join('')}
` : ''; return `
${turn.role === 'user' ? 'User' : 'Assistant'} Turn ${turn.turnNumber} ${tokenInfo} ${isLast ? 'Latest' : ''}
${escapeHtml(turn.content)}
${thoughtsHtml} ${toolCallsHtml}
`; }).join('') : '

No conversation turns found

'; // Total tokens summary const totalTokensHtml = nativeSession.totalTokens ? `
Total Tokens: ${nativeSession.totalTokens.total || 0} (Input: ${nativeSession.totalTokens.input || 0}, Output: ${nativeSession.totalTokens.output || 0} ${nativeSession.totalTokens.cached ? `, Cached: ${nativeSession.totalTokens.cached}` : ''})
` : ''; const modalContent = `
${nativeSession.tool.toUpperCase()} ${nativeSession.model ? ` ${nativeSession.model}` : ''} ${nativeSession.sessionId}
${new Date(nativeSession.startTime).toLocaleString()} ${nativeSession.workingDir ? ` ${nativeSession.workingDir}` : ''} ${nativeSession.projectHash ? ` ${nativeSession.projectHash.substring(0, 12)}...` : ''}
${totalTokensHtml}
${turnsHtml}
`; // Store for export window._currentNativeSession = nativeSession; showModal('Native Session Detail', modalContent, { size: 'lg' }); } /** * Copy native session ID to clipboard */ async function copyNativeSessionId(sessionId) { if (navigator.clipboard) { try { await navigator.clipboard.writeText(sessionId); showRefreshToast('Session ID copied', 'success'); } catch (err) { showRefreshToast('Failed to copy', 'error'); } } } /** * Copy native session file path */ async function copyNativeSessionPath(executionId) { // Find execution in history const exec = cliExecutionHistory.find(e => e.id === executionId); if (exec && exec.nativeSessionPath) { if (navigator.clipboard) { try { await navigator.clipboard.writeText(exec.nativeSessionPath); showRefreshToast('File path copied', 'success'); } catch (err) { showRefreshToast('Failed to copy', 'error'); } } } else { showRefreshToast('Path not available', 'error'); } } /** * Export native session as JSON file */ function exportNativeSession(executionId) { const session = window._currentNativeSession; if (!session) { showRefreshToast('No session data', 'error'); return; } const blob = new Blob([JSON.stringify(session, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `native-session-${session.sessionId}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showRefreshToast('Session exported', 'success'); } // ========== Helpers ========== function formatDuration(ms) { if (ms >= 60000) { const mins = Math.floor(ms / 60000); const secs = Math.round((ms % 60000) / 1000); return `${mins}m ${secs}s`; } else if (ms >= 1000) { return `${(ms / 1000).toFixed(1)}s`; } return `${ms}ms`; } function getTimeAgo(date) { const seconds = Math.floor((new Date() - date) / 1000); if (seconds < 60) return 'just now'; if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`; if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`; if (seconds < 604800) return `${Math.floor(seconds / 86400)}d ago`; return date.toLocaleDateString(); }