// ========================================== // ISSUE MANAGER VIEW // Manages issues, solutions, and execution queue // ========================================== // ========== Issue State ========== var issueData = { issues: [], historyIssues: [], // Archived/completed issues from history queue: { tasks: [], solutions: [], conflicts: [], execution_groups: [], grouped_items: {} }, selectedIssue: null, selectedSolution: null, selectedSolutionIssueId: null, statusFilter: 'all', searchQuery: '', viewMode: 'issues', // 'issues' | 'queue' // Search suggestions state searchSuggestions: [], showSuggestions: false, selectedSuggestion: -1 }; var issueLoading = false; var issueDragState = { dragging: null, groupId: null }; // Multi-queue state var queueData = { queues: [], // All queue index entries activeQueueId: null, // Currently active queue expandedQueueId: null // Queue showing execution groups }; // ========== Main Render Function ========== async function renderIssueManager() { const container = document.getElementById('mainContent'); if (!container) return; // Hide stats grid and search hideStatsAndCarousel(); // Show loading state container.innerHTML = '
' + '
' + '

' + t('common.loading') + '

' + '
'; // Load data await Promise.all([loadIssueData(), loadQueueData(), loadAllQueues()]); // Render the main view renderIssueView(); } // ========== Data Loading ========== async function loadIssueData() { issueLoading = true; try { const response = await fetch('/api/issues?path=' + encodeURIComponent(projectPath)); if (!response.ok) throw new Error('Failed to load issues'); const data = await response.json(); issueData.issues = data.issues || []; updateIssueBadge(); } catch (err) { console.error('Failed to load issues:', err); issueData.issues = []; } finally { issueLoading = false; } } async function loadIssueHistory() { try { const response = await fetch('/api/issues/history?path=' + encodeURIComponent(projectPath)); if (!response.ok) throw new Error('Failed to load issue history'); const data = await response.json(); issueData.historyIssues = data.issues || []; } catch (err) { console.error('Failed to load issue history:', err); issueData.historyIssues = []; } } async function loadQueueData() { try { const response = await fetch('/api/queue?path=' + encodeURIComponent(projectPath)); if (!response.ok) throw new Error('Failed to load queue'); issueData.queue = await response.json(); } catch (err) { console.error('Failed to load queue:', err); issueData.queue = { tasks: [], solutions: [], conflicts: [], execution_groups: [], grouped_items: {} }; } } async function loadAllQueues() { try { const response = await fetch('/api/queue/history?path=' + encodeURIComponent(projectPath)); if (!response.ok) throw new Error('Failed to load queue history'); const data = await response.json(); queueData.queues = data.queues || []; queueData.activeQueueId = data.active_queue_id; } catch (err) { console.error('Failed to load all queues:', err); queueData.queues = []; queueData.activeQueueId = null; } } async function loadIssueDetail(issueId) { try { const response = await fetch('/api/issues/' + encodeURIComponent(issueId) + '?path=' + encodeURIComponent(projectPath)); if (!response.ok) throw new Error('Failed to load issue detail'); return await response.json(); } catch (err) { console.error('Failed to load issue detail:', err); return null; } } function updateIssueBadge() { const badge = document.getElementById('badgeIssues'); if (badge) { badge.textContent = issueData.issues.length; } } // ========== Main View Render ========== function renderIssueView() { const container = document.getElementById('mainContent'); if (!container) return; const issues = issueData.issues || []; const historyIssues = issueData.historyIssues || []; // Apply both status and search filters let filteredIssues; if (issueData.statusFilter === 'all') { filteredIssues = issues; } else if (issueData.statusFilter === 'completed') { // For 'completed' filter, include both current completed issues and archived history issues const currentCompleted = issues.filter(i => i.status === 'completed'); // Mark history issues as archived for visual distinction const archivedIssues = historyIssues.map(i => ({ ...i, _isArchived: true })); filteredIssues = [...currentCompleted, ...archivedIssues]; } else { filteredIssues = issues.filter(i => i.status === issueData.statusFilter); } if (issueData.searchQuery) { const query = issueData.searchQuery.toLowerCase(); filteredIssues = filteredIssues.filter(i => { // Basic field search const basicMatch = i.id.toLowerCase().includes(query) || (i.title && i.title.toLowerCase().includes(query)) || (i.context && i.context.toLowerCase().includes(query)); if (basicMatch) return true; // Search in solutions if (i.solutions && i.solutions.length > 0) { return i.solutions.some(sol => (sol.description && sol.description.toLowerCase().includes(query)) || (sol.approach && sol.approach.toLowerCase().includes(query)) ); } return false; }); } container.innerHTML = `

${t('issues.title') || 'Issue Manager'}

${t('issues.description') || 'Manage issues, solutions, and execution queue'}

${issueData.viewMode === 'issues' ? renderIssueListSection(filteredIssues) : renderQueueSection()}
`; lucide.createIcons(); // Initialize drag-drop if in queue view if (issueData.viewMode === 'queue') { initQueueDragDrop(); } } function switchIssueView(mode) { issueData.viewMode = mode; renderIssueView(); } // ========== Issue List Section ========== function renderIssueListSection(issues) { const statuses = ['all', 'registered', 'planning', 'planned', 'queued', 'executing', 'completed', 'failed']; const totalIssues = issueData.issues?.length || 0; return `
${t('issues.filterStatus') || 'Status'}: ${statuses.map(status => ` `).join('')}
${t('issues.showing') || 'Showing'} ${issues.length} ${t('issues.of') || 'of'} ${totalIssues} ${t('issues.issues') || 'issues'}
${issues.length === 0 ? `

${t('issues.noIssues') || 'No issues found'}

${issueData.searchQuery || issueData.statusFilter !== 'all' ? (t('issues.tryDifferentFilter') || 'Try adjusting your search or filters') : (t('issues.createHint') || 'Click "Create" to add your first issue')}

${!issueData.searchQuery && issueData.statusFilter === 'all' ? ` ` : ''}
` : issues.map(issue => renderIssueCard(issue)).join('')}
`; } function renderIssueCard(issue) { const statusColors = { registered: 'registered', planning: 'planning', planned: 'planned', queued: 'queued', executing: 'executing', completed: 'completed', failed: 'failed' }; const isArchived = issue._isArchived; return `
${highlightMatch(issue.id, issueData.searchQuery)} ${issue.status || 'unknown'} ${isArchived ? '' + (t('issues.archived') || 'Archived') + '' : ''}
${renderPriorityStars(issue.priority || 3)}

${highlightMatch(issue.title || issue.id, issueData.searchQuery)}

${issue.task_count || 0} ${t('issues.tasks') || 'tasks'} ${issue.solution_count || 0} ${t('issues.solutions') || 'solutions'} ${issue.bound_solution_id ? ` ${t('issues.boundSolution') || 'Bound'} ` : ''} ${issue.github_url ? ` ${issue.github_number ? `#${issue.github_number}` : 'GitHub'} ` : ''}
`; } function renderPriorityStars(priority) { const maxStars = 5; let stars = ''; for (let i = 1; i <= maxStars; i++) { stars += ``; } return stars; } async function filterIssuesByStatus(status) { issueData.statusFilter = status; // Load history data when filtering by 'completed' status if (status === 'completed' && issueData.historyIssues.length === 0) { await loadIssueHistory(); } renderIssueView(); } // ========== Queue Section ========== function renderQueueSection() { const queues = queueData.queues || []; const activeQueueId = queueData.activeQueueId; const expandedQueueId = queueData.expandedQueueId; // If a queue is expanded, show loading then load detail if (expandedQueueId) { // Show loading state first, then load async setTimeout(() => loadAndRenderExpandedQueue(expandedQueueId), 0); return `

${escapeHtml(expandedQueueId)}

${t('common.loading') || 'Loading...'}
`; } // Show multi-queue cards view return `

${t('issues.executionQueues') || 'Execution Queues'}

${queues.length} ${t('issues.queues') || 'queues'}
${queues.length === 0 ? `

${t('issues.noQueues') || 'No queues found'}

${t('issues.queueEmptyHint') || 'Generate execution queue from bound solutions'}

` : `
${queues.map(q => renderQueueCard(q, q.id === activeQueueId)).join('')}
`} `; } function renderQueueCard(queue, isActive) { const itemCount = queue.total_solutions || queue.total_tasks || 0; const completedCount = queue.completed_solutions || queue.completed_tasks || 0; const progressPercent = itemCount > 0 ? Math.round((completedCount / itemCount) * 100) : 0; const issueCount = queue.issue_ids?.length || 0; const statusClass = queue.status === 'merged' ? 'merged' : queue.status || ''; const safeQueueId = escapeHtml(queue.id || ''); return `
${safeQueueId}
${isActive ? 'Active' : ''} ${queue.status || 'unknown'}
${completedCount}/${itemCount} ${queue.total_solutions ? 'solutions' : 'tasks'} ${progressPercent}%
${issueCount} issues ${queue.created_at ? new Date(queue.created_at).toLocaleDateString() : 'N/A'}
${!isActive && queue.status !== 'merged' ? ` ` : ''} ${queue.status !== 'merged' ? ` ` : ''}
`; } function toggleQueueExpand(queueId) { if (queueData.expandedQueueId === queueId) { queueData.expandedQueueId = null; } else { queueData.expandedQueueId = queueId; } renderIssueView(); } async function activateQueue(queueId) { try { const response = await fetch('/api/queue/switch?path=' + encodeURIComponent(projectPath), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ queueId }) }); const result = await response.json(); if (result.success) { showNotification(t('issues.queueActivated') || 'Queue activated: ' + queueId, 'success'); await Promise.all([loadQueueData(), loadAllQueues()]); renderIssueView(); } else { showNotification(result.error || 'Failed to activate queue', 'error'); } } catch (err) { console.error('Failed to activate queue:', err); showNotification('Failed to activate queue', 'error'); } } async function deactivateQueue(queueId) { try { const response = await fetch('/api/queue/deactivate?path=' + encodeURIComponent(projectPath), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ queueId }) }); const result = await response.json(); if (result.success) { showNotification(t('issues.queueDeactivated') || 'Queue deactivated', 'success'); queueData.activeQueueId = null; await Promise.all([loadQueueData(), loadAllQueues()]); renderIssueView(); } else { showNotification(result.error || 'Failed to deactivate queue', 'error'); } } catch (err) { console.error('Failed to deactivate queue:', err); showNotification('Failed to deactivate queue', 'error'); } } async function renderExpandedQueueView(queueId) { const safeQueueId = escapeHtml(queueId || ''); // Fetch queue detail let queue; try { const response = await fetch('/api/queue/' + encodeURIComponent(queueId) + '?path=' + encodeURIComponent(projectPath)); queue = await response.json(); if (queue.error) throw new Error(queue.error); } catch (err) { return `

Failed to load queue: ${escapeHtml(err.message)}

`; } const queueItems = queue.solutions || queue.tasks || []; const isSolutionLevel = !!(queue.solutions && queue.solutions.length > 0); const metadata = queue._metadata || {}; const isActive = queueId === queueData.activeQueueId; // Group items by execution_group const groupMap = {}; queueItems.forEach(item => { const groupId = item.execution_group || 'default'; if (!groupMap[groupId]) groupMap[groupId] = []; groupMap[groupId].push(item); }); const groups = queue.execution_groups || Object.keys(groupMap).map(groupId => ({ id: groupId, type: groupId.startsWith('P') ? 'parallel' : 'sequential', solution_count: groupMap[groupId]?.length || 0 })); const groupedItems = queue.grouped_items || groupMap; return `

${escapeHtml(queue.id || queueId)}

${isActive ? 'Active' : ''} ${escapeHtml(queue.status || 'unknown')}
${!isActive && queue.status !== 'merged' ? ` ` : ''} ${isActive ? ` ` : ''}
${isSolutionLevel ? (metadata.total_solutions || queueItems.length) : (metadata.total_tasks || queueItems.length)} ${isSolutionLevel ? 'Solutions' : 'Tasks'}
${metadata.pending_count || queueItems.filter(i => i.status === 'pending').length} Pending
${metadata.executing_count || queueItems.filter(i => i.status === 'executing').length} Executing
${isSolutionLevel ? (metadata.completed_solutions || 0) : (metadata.completed_tasks || queueItems.filter(i => i.status === 'completed').length)} Completed
${metadata.failed_count || queueItems.filter(i => i.status === 'failed').length} Failed

${t('issues.reorderHint') || 'Drag items within a group to reorder. Click item to view details.'}

${groups.map(group => renderQueueGroupWithDelete(group, groupedItems[group.id] || groupMap[group.id] || [], queueId)).join('')}
${queue.conflicts && queue.conflicts.length > 0 ? renderConflictsSection(queue.conflicts) : ''} `; } // Async loader for expanded queue view - renders into DOM container async function loadAndRenderExpandedQueue(queueId) { const wrapper = document.getElementById('queueExpandedWrapper'); if (!wrapper) return; try { const html = await renderExpandedQueueView(queueId); wrapper.innerHTML = html; // Re-init icons and drag-drop after DOM update if (window.lucide) { window.lucide.createIcons(); } // Initialize drag-drop for queue items initQueueDragDrop(); } catch (err) { console.error('Failed to load expanded queue:', err); wrapper.innerHTML = `

Failed to load queue: ${escapeHtml(err.message || 'Unknown error')}

`; if (window.lucide) { window.lucide.createIcons(); } } } function renderQueueGroupWithDelete(group, items, queueId) { const isParallel = group.type === 'parallel'; const itemCount = group.solution_count || group.task_count || items.length; const itemLabel = group.solution_count ? 'solutions' : 'tasks'; return `
${group.id} (${isParallel ? 'Parallel' : 'Sequential'})
${itemCount} ${itemLabel}
${items.map((item, idx) => renderQueueItemWithDelete(item, idx, items.length, queueId)).join('')}
`; } function renderQueueItemWithDelete(item, index, total, queueId) { const statusColors = { pending: '', ready: 'ready', executing: 'executing', completed: 'completed', failed: 'failed', blocked: 'blocked' }; const isSolutionItem = item.task_count !== undefined; const safeItemId = escapeHtml(item.item_id || ''); const safeIssueId = escapeHtml(item.issue_id || ''); const safeQueueId = escapeHtml(queueId || ''); const safeSolutionId = escapeHtml(item.solution_id || ''); const safeTaskId = escapeHtml(item.task_id || '-'); const safeFilesTouched = item.files_touched ? escapeHtml(item.files_touched.join(', ')) : ''; const safeDependsOn = item.depends_on ? escapeHtml(item.depends_on.join(', ')) : ''; return `
${safeItemId} ${safeIssueId} ${isSolutionItem ? ` ${item.task_count} tasks ${item.files_touched && item.files_touched.length > 0 ? ` ${item.files_touched.length} ` : ''} ` : ` ${safeTaskId} `} ${item.depends_on && item.depends_on.length > 0 ? ` ` : ''}
`; } async function deleteQueueItem(queueId, itemId) { if (!confirm('Delete this item from queue?')) return; try { const response = await fetch('/api/queue/' + queueId + '/item/' + encodeURIComponent(itemId) + '?path=' + encodeURIComponent(projectPath), { method: 'DELETE' }); const result = await response.json(); if (result.success) { showNotification('Item deleted from queue', 'success'); await Promise.all([loadQueueData(), loadAllQueues()]); renderIssueView(); } else { showNotification(result.error || 'Failed to delete item', 'error'); } } catch (err) { console.error('Failed to delete queue item:', err); showNotification('Failed to delete item', 'error'); } } async function refreshExpandedQueue(queueId) { await Promise.all([loadQueueData(), loadAllQueues()]); renderIssueView(); } // ========== Queue Merge Modal ========== function showMergeQueueModal(sourceQueueId) { let modal = document.getElementById('mergeQueueModal'); if (!modal) { modal = document.createElement('div'); modal.id = 'mergeQueueModal'; modal.className = 'issue-modal'; document.body.appendChild(modal); } const otherQueues = queueData.queues.filter(q => q.id !== sourceQueueId && q.status !== 'merged' ); const safeSourceId = escapeHtml(sourceQueueId || ''); modal.innerHTML = `

Merge Queue

Merge ${safeSourceId} into another queue:

${otherQueues.length === 0 ? `

No other queues available for merging

` : `

Items from source queue will be appended to target queue. Source queue will be marked as "merged".

`}
`; modal.classList.remove('hidden'); lucide.createIcons(); } function hideMergeQueueModal() { const modal = document.getElementById('mergeQueueModal'); if (modal) { modal.classList.add('hidden'); } } async function executeQueueMerge(sourceQueueId) { const targetQueueId = document.getElementById('targetQueueSelect')?.value; if (!targetQueueId) return; try { const response = await fetch('/api/queue/merge?path=' + encodeURIComponent(projectPath), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sourceQueueId, targetQueueId }) }); const result = await response.json(); if (result.success) { showNotification('Merged ' + result.mergedItemCount + ' items into ' + targetQueueId, 'success'); hideMergeQueueModal(); queueData.expandedQueueId = null; await Promise.all([loadQueueData(), loadAllQueues()]); renderIssueView(); } else { showNotification(result.error || 'Failed to merge queues', 'error'); } } catch (err) { console.error('Failed to merge queues:', err); showNotification('Failed to merge queues', 'error'); } } // ========== Legacy Queue Render (for backward compatibility) ========== function renderLegacyQueueSection() { const queue = issueData.queue; const queueItems = queue.solutions || queue.tasks || []; const isSolutionLevel = !!(queue.solutions && queue.solutions.length > 0); const metadata = queue._metadata || {}; if (queueItems.length === 0) { return `

Queue is empty

`; } const groups = queue.execution_groups || []; let groupedItems = queue.grouped_items || {}; if (groups.length === 0 && queueItems.length > 0) { const groupMap = {}; queueItems.forEach(item => { const groupId = item.execution_group || 'default'; if (!groupMap[groupId]) { groupMap[groupId] = []; } groupMap[groupId].push(item); }); const syntheticGroups = Object.keys(groupMap).map(groupId => ({ id: groupId, type: 'sequential', task_count: groupMap[groupId].length })); return `
${t('issues.queueId') || 'Queue ID'} ${queue.id || 'N/A'}
${t('issues.status') || 'Status'} ${queue.status || 'unknown'}
${t('issues.issues') || 'Issues'} ${(queue.issue_ids || []).join(', ') || 'N/A'}
${isSolutionLevel ? (metadata.total_solutions || queueItems.length) : (metadata.total_tasks || queueItems.length)} ${isSolutionLevel ? (t('issues.totalSolutions') || 'Solutions') : (t('issues.totalTasks') || 'Total')}
${metadata.pending_count || queueItems.filter(i => i.status === 'pending').length} ${t('issues.pending') || 'Pending'}
${metadata.executing_count || queueItems.filter(i => i.status === 'executing').length} ${t('issues.executing') || 'Executing'}
${metadata.completed_count || queueItems.filter(i => i.status === 'completed').length} ${t('issues.completed') || 'Completed'}
${metadata.failed_count || queueItems.filter(i => i.status === 'failed').length} ${t('issues.failed') || 'Failed'}
${syntheticGroups.map(group => renderQueueGroup(group, groupMap[group.id] || [])).join('')}
${queue.conflicts && queue.conflicts.length > 0 ? renderConflictsSection(queue.conflicts) : ''} `; } return `
${groups.length} ${t('issues.executionGroups') || 'groups'} · ${queueItems.length} ${t('issues.totalItems') || 'items'}

${t('issues.reorderHint') || 'Drag items within a group to reorder'}

${groups.map(group => renderQueueGroup(group, groupedItems[group.id] || [])).join('')}
${queue.conflicts && queue.conflicts.length > 0 ? renderConflictsSection(queue.conflicts) : ''} `; } function renderQueueGroup(group, items) { const isParallel = group.type === 'parallel'; // Support both solution-level (solution_count) and task-level (task_count) const itemCount = group.solution_count || group.task_count || items.length; const itemLabel = group.solution_count ? 'solutions' : 'tasks'; return `
${group.id} (${isParallel ? t('issues.parallelGroup') || 'Parallel' : t('issues.sequentialGroup') || 'Sequential'})
${itemCount} ${itemLabel}
${items.map((item, idx) => renderQueueItem(item, idx, items.length)).join('')}
`; } function renderQueueItem(item, index, total) { const statusColors = { pending: '', ready: 'ready', executing: 'executing', completed: 'completed', failed: 'failed', blocked: 'blocked' }; // Check if this is a solution-level item (has task_count) or task-level (has task_id) const isSolutionItem = item.task_count !== undefined; return `
${item.item_id} ${item.issue_id} ${isSolutionItem ? ` ${item.task_count} ${t('issues.tasks') || 'tasks'} ${item.files_touched && item.files_touched.length > 0 ? ` ${item.files_touched.length} ` : ''} ` : ` ${item.task_id || '-'} `} ${item.depends_on && item.depends_on.length > 0 ? ` ` : ''}
`; } function renderConflictsSection(conflicts) { return `

Conflicts (${conflicts.length})

${conflicts.map(c => `
${c.file} ${(c.solutions || c.tasks || []).join(' → ')} ${c.rationale ? ` ` : ''} ${c.resolved || c.resolution ? 'Resolved' : 'Pending'}
`).join('')}
`; } // ========== Drag-Drop for Queue ========== function initQueueDragDrop() { const items = document.querySelectorAll('.queue-item[draggable="true"]'); items.forEach(item => { item.addEventListener('dragstart', handleIssueDragStart); item.addEventListener('dragend', handleIssueDragEnd); item.addEventListener('dragover', handleIssueDragOver); item.addEventListener('drop', handleIssueDrop); }); } function handleIssueDragStart(e) { const item = e.target.closest('.queue-item'); if (!item) return; issueDragState.dragging = item.dataset.itemId; issueDragState.groupId = item.dataset.groupId; item.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', item.dataset.itemId); } function handleIssueDragEnd(e) { const item = e.target.closest('.queue-item'); if (item) { item.classList.remove('dragging'); } issueDragState.dragging = null; issueDragState.groupId = null; // Remove all placeholders document.querySelectorAll('.queue-drop-placeholder').forEach(p => p.remove()); } function handleIssueDragOver(e) { e.preventDefault(); const target = e.target.closest('.queue-item'); if (!target || target.dataset.itemId === issueDragState.dragging) return; // Only allow drag within same group if (target.dataset.groupId !== issueDragState.groupId) { e.dataTransfer.dropEffect = 'none'; return; } e.dataTransfer.dropEffect = 'move'; } function handleIssueDrop(e) { e.preventDefault(); const target = e.target.closest('.queue-item'); if (!target || !issueDragState.dragging) return; // Only allow drop within same group if (target.dataset.groupId !== issueDragState.groupId) return; const container = target.closest('.queue-items'); if (!container) return; // Get new order const items = Array.from(container.querySelectorAll('.queue-item')); const draggedItem = items.find(i => i.dataset.itemId === issueDragState.dragging); const targetIndex = items.indexOf(target); const draggedIndex = items.indexOf(draggedItem); if (draggedIndex === targetIndex) return; // Reorder in DOM if (draggedIndex < targetIndex) { target.after(draggedItem); } else { target.before(draggedItem); } // Get new order and save const newOrder = Array.from(container.querySelectorAll('.queue-item')).map(i => i.dataset.itemId); saveQueueOrder(issueDragState.groupId, newOrder); } async function saveQueueOrder(groupId, newOrder) { try { const response = await fetch('/api/queue/reorder?path=' + encodeURIComponent(projectPath), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ groupId, newOrder }) }); if (!response.ok) { throw new Error('Failed to save queue order'); } const result = await response.json(); if (result.error) { showNotification(result.error, 'error'); } else { showNotification('Queue reordered', 'success'); // Reload queue data await loadQueueData(); } } catch (err) { console.error('Failed to save queue order:', err); showNotification('Failed to save queue order', 'error'); // Reload to restore original order await loadQueueData(); renderIssueView(); } } // ========== Detail Panel ========== async function openIssueDetail(issueId, isArchived = false) { const panel = document.getElementById('issueDetailPanel'); if (!panel) return; panel.innerHTML = '
'; panel.classList.remove('hidden'); lucide.createIcons(); let detail; if (isArchived) { // For archived issues, get detail from historyIssues (already loaded) const historyIssue = issueData.historyIssues.find(i => i.id === issueId); if (historyIssue) { // Mark as archived and provide minimal detail structure detail = { ...historyIssue, _isArchived: true, solutions: historyIssue.solutions || [], tasks: historyIssue.tasks || [] }; } } else { detail = await loadIssueDetail(issueId); } if (!detail) { panel.innerHTML = '
Failed to load issue
'; return; } issueData.selectedIssue = detail; renderIssueDetailPanel(detail); } function renderIssueDetailPanel(issue) { const panel = document.getElementById('issueDetailPanel'); if (!panel) return; const boundSolution = issue.solutions?.find(s => s.is_bound); panel.innerHTML = `

${issue.id}

${issue.status || 'unknown'}
${issue.title || issue.id}
${issue.context || 'No context'}
${(issue.solutions || []).length > 0 ? (issue.solutions || []).map(sol => `
${sol.id} ${sol.is_bound ? '' + (t('issues.bound') || 'Bound') + '' : ''} ${sol.tasks?.length || 0} ${t('issues.tasks') || 'tasks'}
`).join('') : '

' + (t('issues.noSolutions') || 'No solutions') + '

'}
${(issue.tasks || []).length > 0 ? (issue.tasks || []).map(task => `
${task.id}

${task.title || task.description || ''}

`).join('') : '

' + (t('issues.noTasks') || 'No tasks') + '

'}
`; lucide.createIcons(); } function closeIssueDetail() { const panel = document.getElementById('issueDetailPanel'); if (panel) { panel.classList.add('hidden'); } issueData.selectedIssue = null; } function toggleSolutionExpand(solId) { const el = document.getElementById('solution-' + solId); if (el) { el.classList.toggle('hidden'); } } // ========== Solution Detail Modal ========== function openSolutionDetail(issueId, solutionId) { const issue = issueData.selectedIssue || issueData.issues.find(i => i.id === issueId); if (!issue) return; const solution = issue.solutions?.find(s => s.id === solutionId); if (!solution) return; issueData.selectedSolution = solution; issueData.selectedSolutionIssueId = issueId; const modal = document.getElementById('solutionDetailModal'); if (modal) { modal.classList.remove('hidden'); renderSolutionDetail(solution); lucide.createIcons(); } } function closeSolutionDetail() { const modal = document.getElementById('solutionDetailModal'); if (modal) { modal.classList.add('hidden'); } issueData.selectedSolution = null; issueData.selectedSolutionIssueId = null; } function renderSolutionDetail(solution) { const idEl = document.getElementById('solutionDetailId'); const bodyEl = document.getElementById('solutionDetailBody'); const bindBtn = document.getElementById('solutionBindBtn'); if (idEl) { idEl.textContent = solution.id; } // Update bind button state if (bindBtn) { if (solution.is_bound) { bindBtn.innerHTML = `${t('issues.unbind') || 'Unbind'}`; bindBtn.classList.remove('btn-secondary'); bindBtn.classList.add('btn-primary'); } else { bindBtn.innerHTML = `${t('issues.bind') || 'Bind'}`; bindBtn.classList.remove('btn-primary'); bindBtn.classList.add('btn-secondary'); } } if (!bodyEl) return; const tasks = solution.tasks || []; bodyEl.innerHTML = `
${tasks.length} ${t('issues.totalTasks') || 'Total Tasks'}
${solution.is_bound ? '✓' : '—'} ${t('issues.bindStatus') || 'Bind Status'}
${solution.created_at ? new Date(solution.created_at).toLocaleDateString() : '—'} ${t('issues.createdAt') || 'Created'}

${t('issues.taskList') || 'Task List'}

${tasks.length === 0 ? `

${t('issues.noTasks') || 'No tasks in this solution'}

` : tasks.map((task, index) => renderSolutionTask(task, index)).join('')}
`; lucide.createIcons(); } function renderSolutionTask(task, index) { const actionClass = (task.action || 'unknown').toLowerCase(); const modPoints = task.modification_points || []; // Support both old and new field names const implSteps = task.implementation || task.implementation_steps || []; const acceptance = task.acceptance || task.acceptance_criteria || []; const testInfo = task.test || {}; const regression = task.regression || []; const commitInfo = task.commit || {}; const dependsOn = task.depends_on || task.dependencies || []; // Handle acceptance as object or array const acceptanceCriteria = Array.isArray(acceptance) ? acceptance : (acceptance.criteria || []); const acceptanceVerification = acceptance.verification || []; return `
#${index + 1} ${task.id || ''} ${task.action || 'Unknown'}
${task.title || task.description || 'No title'}
`; } function toggleTaskExpand(index) { const details = document.getElementById('taskDetails' + index); const icon = document.getElementById('taskExpandIcon' + index); if (details) { details.classList.toggle('hidden'); } if (icon) { icon.style.transform = details?.classList.contains('hidden') ? '' : 'rotate(180deg)'; } } function toggleSolutionJson() { const content = document.getElementById('solutionJsonContent'); if (content) { content.classList.toggle('hidden'); } } async function toggleSolutionBind() { const solution = issueData.selectedSolution; const issueId = issueData.selectedSolutionIssueId; if (!solution || !issueId) return; const action = solution.is_bound ? 'unbind' : 'bind'; try { const response = await fetch('/api/issues/' + encodeURIComponent(issueId) + '?path=' + encodeURIComponent(projectPath), { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ bound_solution_id: action === 'bind' ? solution.id : null }) }); if (!response.ok) throw new Error('Failed to ' + action); showNotification(action === 'bind' ? (t('issues.solutionBound') || 'Solution bound') : (t('issues.solutionUnbound') || 'Solution unbound'), 'success'); // Refresh data await loadIssueData(); const detail = await loadIssueDetail(issueId); if (detail) { issueData.selectedIssue = detail; // Update solution reference const updatedSolution = detail.solutions?.find(s => s.id === solution.id); if (updatedSolution) { issueData.selectedSolution = updatedSolution; renderSolutionDetail(updatedSolution); } renderIssueDetailPanel(detail); } } catch (err) { console.error('Failed to ' + action + ' solution:', err); showNotification('Failed to ' + action + ' solution', 'error'); } } // Helper: escape HTML function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Helper: escape regex special characters function escapeRegex(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } // Helper: highlight matching text in search results function highlightMatch(text, query) { if (!text || !query) return escapeHtml(text || ''); const escaped = escapeHtml(text); const escapedQuery = escapeRegex(escapeHtml(query)); const regex = new RegExp(`(${escapedQuery})`, 'gi'); return escaped.replace(regex, '$1'); } function openQueueItemDetail(itemId) { // Support both solution-level and task-level queues const items = issueData.queue.solutions || issueData.queue.tasks || []; const item = items.find(q => q.item_id === itemId); if (item) { openIssueDetail(item.issue_id); } } // ========== Edit Functions ========== function startEditField(issueId, field, currentValue) { const container = document.getElementById('issueTitle'); if (!container) return; container.innerHTML = `
`; lucide.createIcons(); document.getElementById('editField')?.focus(); } function startEditContext(issueId) { const container = document.getElementById('issueContext'); const currentValue = issueData.selectedIssue?.context || ''; if (!container) return; container.innerHTML = `
`; lucide.createIcons(); document.getElementById('editContext')?.focus(); } async function saveFieldEdit(issueId, field) { const input = document.getElementById('editField'); if (!input) return; const value = input.value.trim(); if (!value) return; try { const response = await fetch('/api/issues/' + encodeURIComponent(issueId) + '?path=' + encodeURIComponent(projectPath), { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ [field]: value }) }); if (!response.ok) throw new Error('Failed to update'); showNotification('Updated ' + field, 'success'); // Refresh data await loadIssueData(); const detail = await loadIssueDetail(issueId); if (detail) { issueData.selectedIssue = detail; renderIssueDetailPanel(detail); } } catch (err) { showNotification('Failed to update', 'error'); cancelEdit(); } } async function saveContextEdit(issueId) { const textarea = document.getElementById('editContext'); if (!textarea) return; const value = textarea.value; try { const response = await fetch('/api/issues/' + encodeURIComponent(issueId) + '?path=' + encodeURIComponent(projectPath), { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ context: value }) }); if (!response.ok) throw new Error('Failed to update'); showNotification('Context updated', 'success'); // Refresh detail const detail = await loadIssueDetail(issueId); if (detail) { issueData.selectedIssue = detail; renderIssueDetailPanel(detail); } } catch (err) { showNotification('Failed to update context', 'error'); cancelEdit(); } } function cancelEdit() { if (issueData.selectedIssue) { renderIssueDetailPanel(issueData.selectedIssue); } } async function updateTaskStatus(issueId, taskId, status) { try { const response = await fetch('/api/issues/' + encodeURIComponent(issueId) + '/tasks/' + encodeURIComponent(taskId) + '?path=' + encodeURIComponent(projectPath), { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status }) }); if (!response.ok) throw new Error('Failed to update task'); showNotification('Task status updated', 'success'); } catch (err) { showNotification('Failed to update task status', 'error'); } } // ========== Search Functions ========== var searchDebounceTimer = null; function handleIssueSearch(value) { issueData.searchQuery = value; // Update suggestions immediately (no debounce for dropdown) updateSearchSuggestions(value); issueData.showSuggestions = value.length > 0; issueData.selectedSuggestion = -1; updateSuggestionsDropdown(); // Clear previous timer if (searchDebounceTimer) { clearTimeout(searchDebounceTimer); } // 300ms debounce for full re-render to prevent freeze on rapid input searchDebounceTimer = setTimeout(() => { renderIssueView(); // Restore input focus and cursor position const input = document.getElementById('issueSearchInput'); if (input) { input.focus(); input.setSelectionRange(value.length, value.length); } }, 300); } function clearIssueSearch() { if (searchDebounceTimer) { clearTimeout(searchDebounceTimer); } issueData.searchQuery = ''; issueData.showSuggestions = false; issueData.searchSuggestions = []; issueData.selectedSuggestion = -1; renderIssueView(); } // Update search suggestions based on query function updateSearchSuggestions(query) { if (!query || query.length < 1) { issueData.searchSuggestions = []; return; } const q = query.toLowerCase(); const allIssues = [...issueData.issues, ...issueData.historyIssues]; // Find matching issues (max 6) issueData.searchSuggestions = allIssues .filter(issue => { const idMatch = issue.id.toLowerCase().includes(q); const titleMatch = issue.title && issue.title.toLowerCase().includes(q); const contextMatch = issue.context && issue.context.toLowerCase().includes(q); const solutionMatch = issue.solutions && issue.solutions.some(sol => (sol.description && sol.description.toLowerCase().includes(q)) || (sol.approach && sol.approach.toLowerCase().includes(q)) ); return idMatch || titleMatch || contextMatch || solutionMatch; }) .slice(0, 6); } // Render search suggestions dropdown function renderSearchSuggestions() { if (!issueData.searchSuggestions || issueData.searchSuggestions.length === 0) { return ''; } return issueData.searchSuggestions.map((issue, index) => `
${highlightMatch(issue.id, issueData.searchQuery)}
${highlightMatch(issue.title || issue.id, issueData.searchQuery)}
`).join(''); } // Show search suggestions function showSearchSuggestions() { if (issueData.searchQuery) { updateSearchSuggestions(issueData.searchQuery); issueData.showSuggestions = true; updateSuggestionsDropdown(); } } // Hide search suggestions function hideSearchSuggestions() { issueData.showSuggestions = false; issueData.selectedSuggestion = -1; const dropdown = document.getElementById('searchSuggestions'); if (dropdown) { dropdown.classList.remove('show'); } } // Update suggestions dropdown without full re-render function updateSuggestionsDropdown() { const dropdown = document.getElementById('searchSuggestions'); if (dropdown) { dropdown.innerHTML = renderSearchSuggestions(); if (issueData.showSuggestions && issueData.searchSuggestions.length > 0) { dropdown.classList.add('show'); } else { dropdown.classList.remove('show'); } } } // Select a suggestion function selectSuggestion(index) { const issue = issueData.searchSuggestions[index]; if (issue) { hideSearchSuggestions(); openIssueDetail(issue.id, issue._isArchived); } } // Handle keyboard navigation in search function handleSearchKeydown(event) { const suggestions = issueData.searchSuggestions || []; if (!issueData.showSuggestions || suggestions.length === 0) { // If Enter and no suggestions, just search if (event.key === 'Enter') { hideSearchSuggestions(); } return; } switch (event.key) { case 'ArrowDown': event.preventDefault(); issueData.selectedSuggestion = Math.min( issueData.selectedSuggestion + 1, suggestions.length - 1 ); updateSuggestionsDropdown(); break; case 'ArrowUp': event.preventDefault(); issueData.selectedSuggestion = Math.max(issueData.selectedSuggestion - 1, -1); updateSuggestionsDropdown(); break; case 'Enter': event.preventDefault(); if (issueData.selectedSuggestion >= 0) { selectSuggestion(issueData.selectedSuggestion); } else { hideSearchSuggestions(); } break; case 'Escape': hideSearchSuggestions(); break; } } // Close suggestions when clicking outside document.addEventListener('click', function(event) { const searchContainer = document.querySelector('.issue-search'); if (searchContainer && !searchContainer.contains(event.target)) { hideSearchSuggestions(); } }); // ========== Create Issue Modal ========== function generateIssueId() { // Generate unique ID: ISSUE-YYYYMMDD-XXX format const now = new Date(); const dateStr = now.getFullYear().toString() + String(now.getMonth() + 1).padStart(2, '0') + String(now.getDate()).padStart(2, '0'); // Find existing IDs with same date prefix const prefix = 'ISSUE-' + dateStr + '-'; const existingIds = (issueData.issues || []) .map(i => i.id) .filter(id => id.startsWith(prefix)); // Get next sequence number let maxSeq = 0; existingIds.forEach(id => { const seqStr = id.replace(prefix, ''); const seq = parseInt(seqStr, 10); if (!isNaN(seq) && seq > maxSeq) { maxSeq = seq; } }); return prefix + String(maxSeq + 1).padStart(3, '0'); } function showCreateIssueModal() { const modal = document.getElementById('createIssueModal'); if (modal) { modal.classList.remove('hidden'); // Auto-generate issue ID const idInput = document.getElementById('newIssueId'); if (idInput) { idInput.value = generateIssueId(); } lucide.createIcons(); // Focus on title input instead of ID setTimeout(() => { document.getElementById('newIssueTitle')?.focus(); }, 100); } } function regenerateIssueId() { const idInput = document.getElementById('newIssueId'); if (idInput) { idInput.value = generateIssueId(); } } function hideCreateIssueModal() { const modal = document.getElementById('createIssueModal'); if (modal) { modal.classList.add('hidden'); // Clear form const idInput = document.getElementById('newIssueId'); const titleInput = document.getElementById('newIssueTitle'); const contextInput = document.getElementById('newIssueContext'); const prioritySelect = document.getElementById('newIssuePriority'); if (idInput) idInput.value = ''; if (titleInput) titleInput.value = ''; if (contextInput) contextInput.value = ''; if (prioritySelect) prioritySelect.value = '3'; } } async function createIssue() { const idInput = document.getElementById('newIssueId'); const titleInput = document.getElementById('newIssueTitle'); const contextInput = document.getElementById('newIssueContext'); const prioritySelect = document.getElementById('newIssuePriority'); const issueId = idInput?.value?.trim(); const title = titleInput?.value?.trim(); const context = contextInput?.value?.trim(); const priority = parseInt(prioritySelect?.value || '3'); if (!issueId) { showNotification(t('issues.idRequired') || 'Issue ID is required', 'error'); idInput?.focus(); return; } if (!title) { showNotification(t('issues.titleRequired') || 'Title is required', 'error'); titleInput?.focus(); return; } try { const response = await fetch('/api/issues?path=' + encodeURIComponent(projectPath), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: issueId, title: title, context: context, priority: priority, source: 'dashboard' }) }); const result = await response.json(); if (!response.ok || result.error) { showNotification(result.error || 'Failed to create issue', 'error'); return; } showNotification(t('issues.created') || 'Issue created successfully', 'success'); hideCreateIssueModal(); // Reload data and refresh view await loadIssueData(); renderIssueView(); } catch (err) { console.error('Failed to create issue:', err); showNotification('Failed to create issue', 'error'); } } // ========== Delete Issue ========== async function deleteIssue(issueId) { if (!confirm(t('issues.confirmDelete') || 'Are you sure you want to delete this issue?')) { return; } try { const response = await fetch('/api/issues/' + encodeURIComponent(issueId) + '?path=' + encodeURIComponent(projectPath), { method: 'DELETE' }); if (!response.ok) throw new Error('Failed to delete'); showNotification(t('issues.deleted') || 'Issue deleted', 'success'); closeIssueDetail(); // Reload data and refresh view await loadIssueData(); renderIssueView(); } catch (err) { showNotification('Failed to delete issue', 'error'); } } // ========== Queue Operations ========== async function refreshQueue() { try { await loadQueueData(); renderIssueView(); showNotification(t('issues.queueRefreshed') || 'Queue refreshed', 'success'); } catch (err) { showNotification('Failed to refresh queue', 'error'); } } function createExecutionQueue() { showQueueCommandModal(); } function showQueueCommandModal() { // Create modal if not exists let modal = document.getElementById('queueCommandModal'); if (!modal) { modal = document.createElement('div'); modal.id = 'queueCommandModal'; modal.className = 'issue-modal'; document.body.appendChild(modal); } const command = 'claude /issue:queue'; const altCommand = 'ccw issue queue'; modal.innerHTML = `

${t('issues.createQueue') || 'Create Execution Queue'}

${t('issues.queueCommandHint') || 'Run one of the following commands in your terminal to generate the execution queue from bound solutions:'}

${command}
${altCommand}

${t('issues.queueCommandInfo') || 'After running the command, click "Refresh" to see the updated queue.'}

`; modal.classList.remove('hidden'); lucide.createIcons(); } function hideQueueCommandModal() { const modal = document.getElementById('queueCommandModal'); if (modal) { modal.classList.add('hidden'); } } // ========== Queue History Modal ========== async function showQueueHistoryModal() { // Create modal if not exists let modal = document.getElementById('queueHistoryModal'); if (!modal) { modal = document.createElement('div'); modal.id = 'queueHistoryModal'; modal.className = 'issue-modal'; document.body.appendChild(modal); } // Show loading state modal.innerHTML = `

Queue History

Loading...
`; modal.classList.remove('hidden'); lucide.createIcons(); // Fetch queue history try { const response = await fetch(`/api/queue/history?path=${encodeURIComponent(projectPath)}`); const data = await response.json(); const queues = data.queues || []; const activeQueueId = data.active_queue_id; // Render queue list const queueListHtml = queues.length === 0 ? `

No queue history found

` : `
${queues.map(q => `
${q.id} ${q.id === activeQueueId ? 'Active' : ''} ${q.status || 'unknown'}
${q.issue_ids?.length || 0} issues ${q.completed_solutions || q.completed_tasks || 0}/${q.total_solutions || q.total_tasks || 0} ${q.total_solutions ? 'solutions' : 'tasks'} ${q.created_at ? new Date(q.created_at).toLocaleDateString() : 'N/A'}
${q.id !== activeQueueId ? ` ` : ''}
`).join('')}
`; modal.querySelector('.issue-modal-body').innerHTML = queueListHtml; lucide.createIcons(); } catch (err) { console.error('Failed to load queue history:', err); modal.querySelector('.issue-modal-body').innerHTML = `

Failed to load queue history

`; lucide.createIcons(); } } function hideQueueHistoryModal() { const modal = document.getElementById('queueHistoryModal'); if (modal) { modal.classList.add('hidden'); } } async function switchToQueue(queueId) { try { const response = await fetch(`/api/queue/switch?path=${encodeURIComponent(projectPath)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ queueId }) }); const result = await response.json(); if (result.success) { showNotification(t('issues.queueSwitched') || 'Switched to queue: ' + queueId, 'success'); hideQueueHistoryModal(); await loadQueueData(); renderIssueView(); } else { showNotification(result.error || 'Failed to switch queue', 'error'); } } catch (err) { console.error('Failed to switch queue:', err); showNotification('Failed to switch queue', 'error'); } } async function viewQueueDetail(queueId) { const modal = document.getElementById('queueHistoryModal'); if (!modal) return; // Show loading modal.querySelector('.issue-modal-body').innerHTML = `
${t('common.loading') || 'Loading...'}
`; lucide.createIcons(); try { const response = await fetch(`/api/queue/${queueId}?path=${encodeURIComponent(projectPath)}`); const queue = await response.json(); if (queue.error) { throw new Error(queue.error); } // Support both solution-level and task-level queues const items = queue.solutions || queue.queue || queue.tasks || []; const isSolutionLevel = !!(queue.solutions && queue.solutions.length > 0); const metadata = queue._metadata || {}; // Group by execution_group const grouped = {}; items.forEach(item => { const group = item.execution_group || 'ungrouped'; if (!grouped[group]) grouped[group] = []; grouped[group].push(item); }); const itemLabel = isSolutionLevel ? 'solutions' : 'tasks'; const detailHtml = `

${queue.name || queue.id || queueId}

${queue.name ? `${queue.id}` : ''}
${items.length} ${isSolutionLevel ? 'Solutions' : 'Total'}
${items.filter(t => t.status === 'completed').length} Completed
${items.filter(t => t.status === 'pending').length} Pending
${items.filter(t => t.status === 'failed').length} Failed
${Object.entries(grouped).map(([groupId, groupItems]) => `
${groupId} (${groupItems.length} ${itemLabel})
${groupItems.map(item => `
${item.item_id || item.queue_id || item.task_id || 'N/A'} ${isSolutionLevel ? (item.task_count + ' tasks') : (item.title || item.action || 'Untitled')}
${item.issue_id || ''} ${isSolutionLevel && item.files_touched ? `${item.files_touched.length} files` : ''} ${item.status || 'unknown'}
`).join('')}
`).join('')}
`; modal.querySelector('.issue-modal-body').innerHTML = detailHtml; lucide.createIcons(); } catch (err) { console.error('Failed to load queue detail:', err); modal.querySelector('.issue-modal-body').innerHTML = `

Failed to load queue detail

`; lucide.createIcons(); } } function copyCommand(command) { navigator.clipboard.writeText(command).then(() => { showNotification(t('common.copied') || 'Copied to clipboard', 'success'); }).catch(err => { console.error('Failed to copy:', err); // Fallback: select text const textArea = document.createElement('textarea'); textArea.value = command; document.body.appendChild(textArea); textArea.select(); document.execCommand('copy'); document.body.removeChild(textArea); showNotification(t('common.copied') || 'Copied to clipboard', 'success'); }); }