mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
Implements a complete loop execution system with multi-loop parallel support, dashboard monitoring, and comprehensive security validation. Core features: - Loop orchestration engine (loop-manager, loop-state-manager) - Multi-loop parallel execution with independent state management - REST API endpoints for loop control (pause, resume, stop, retry) - WebSocket real-time status updates - Dashboard Loop Monitor view with live updates - Security: path traversal protection and sandboxed JavaScript evaluation Test coverage: - 42 comprehensive tests covering multi-loop, API, WebSocket, security - Security validation for success_condition injection attacks - Edge case handling and end-to-end workflow tests
421 lines
15 KiB
JavaScript
421 lines
15 KiB
JavaScript
// Navigation and Routing
|
||
// Manages navigation events, active state, content title updates, search, and path selector
|
||
|
||
// View lifecycle management
|
||
var currentViewDestroy = null;
|
||
|
||
// Path Selector
|
||
function initPathSelector() {
|
||
const btn = document.getElementById('pathButton');
|
||
const menu = document.getElementById('pathMenu');
|
||
const recentContainer = document.getElementById('recentPaths');
|
||
|
||
// Render recent paths
|
||
if (recentPaths && recentPaths.length > 0) {
|
||
recentPaths.forEach(path => {
|
||
const item = document.createElement('div');
|
||
item.className = 'path-item' + (path === projectPath ? ' active' : '');
|
||
item.dataset.path = path;
|
||
|
||
// Path text
|
||
const pathText = document.createElement('span');
|
||
pathText.className = 'path-text';
|
||
pathText.textContent = path;
|
||
pathText.addEventListener('click', () => selectPath(path));
|
||
item.appendChild(pathText);
|
||
|
||
// Delete button (only for non-current paths)
|
||
if (path !== projectPath) {
|
||
const deleteBtn = document.createElement('button');
|
||
deleteBtn.className = 'path-delete-btn';
|
||
deleteBtn.innerHTML = '×';
|
||
deleteBtn.title = 'Remove from recent';
|
||
deleteBtn.addEventListener('click', async (e) => {
|
||
e.stopPropagation();
|
||
await removeRecentPathFromList(path);
|
||
});
|
||
item.appendChild(deleteBtn);
|
||
}
|
||
|
||
recentContainer.appendChild(item);
|
||
});
|
||
}
|
||
|
||
btn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
menu.classList.toggle('hidden');
|
||
});
|
||
|
||
document.addEventListener('click', () => {
|
||
menu.classList.add('hidden');
|
||
});
|
||
|
||
document.getElementById('browsePath').addEventListener('click', async () => {
|
||
await browseForFolder();
|
||
});
|
||
}
|
||
|
||
// Cleanup function for view transitions
|
||
function cleanupPreviousView() {
|
||
// Call current view's destroy function if exists
|
||
if (currentViewDestroy) {
|
||
currentViewDestroy();
|
||
currentViewDestroy = null;
|
||
}
|
||
// Cleanup graph explorer
|
||
if (currentView === 'graph-explorer' && typeof window.cleanupGraphExplorer === 'function') {
|
||
window.cleanupGraphExplorer();
|
||
}
|
||
// Hide storage card when leaving cli-manager
|
||
var storageCard = document.getElementById('storageCard');
|
||
if (storageCard) {
|
||
storageCard.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// Navigation
|
||
function initNavigation() {
|
||
document.querySelectorAll('.nav-item[data-filter]').forEach(item => {
|
||
item.addEventListener('click', () => {
|
||
cleanupPreviousView();
|
||
|
||
setActiveNavItem(item);
|
||
currentFilter = item.dataset.filter;
|
||
currentLiteType = null;
|
||
currentView = 'sessions';
|
||
currentSessionDetailKey = null;
|
||
updateContentTitle();
|
||
showStatsAndSearch();
|
||
renderSessions();
|
||
});
|
||
});
|
||
|
||
// Lite Tasks Navigation
|
||
document.querySelectorAll('.nav-item[data-lite]').forEach(item => {
|
||
item.addEventListener('click', () => {
|
||
cleanupPreviousView();
|
||
|
||
setActiveNavItem(item);
|
||
currentLiteType = item.dataset.lite;
|
||
currentFilter = null;
|
||
currentView = 'liteTasks';
|
||
currentSessionDetailKey = null;
|
||
updateContentTitle();
|
||
showStatsAndSearch();
|
||
renderLiteTasks();
|
||
});
|
||
});
|
||
|
||
// View Navigation (Project Overview, MCP Manager, etc.)
|
||
document.querySelectorAll('.nav-item[data-view]').forEach(item => {
|
||
item.addEventListener('click', () => {
|
||
cleanupPreviousView();
|
||
|
||
setActiveNavItem(item);
|
||
currentView = item.dataset.view;
|
||
currentFilter = null;
|
||
currentLiteType = null;
|
||
currentSessionDetailKey = null;
|
||
updateContentTitle();
|
||
|
||
// Route to appropriate view
|
||
if (currentView === 'mcp-manager') {
|
||
renderMcpManager();
|
||
} else if (currentView === 'project-overview') {
|
||
renderProjectOverview();
|
||
} else if (currentView === 'explorer') {
|
||
renderExplorer();
|
||
} else if (currentView === 'cli-manager') {
|
||
renderCliManager();
|
||
} else if (currentView === 'cli-history') {
|
||
renderCliHistoryView();
|
||
// Register destroy function for cli-history view
|
||
if (typeof window.destroyCliHistoryView === 'function') {
|
||
currentViewDestroy = window.destroyCliHistoryView;
|
||
}
|
||
} else if (currentView === 'hook-manager') {
|
||
renderHookManager();
|
||
} else if (currentView === 'memory') {
|
||
renderMemoryView();
|
||
} else if (currentView === 'prompt-history') {
|
||
renderPromptHistoryView();
|
||
} else if (currentView === 'skills-manager') {
|
||
renderSkillsManager();
|
||
} else if (currentView === 'rules-manager') {
|
||
renderRulesManager();
|
||
} else if (currentView === 'claude-manager') {
|
||
renderClaudeManager();
|
||
// Register destroy function for claude-manager view
|
||
if (typeof window.initClaudeManager === 'function') {
|
||
window.initClaudeManager();
|
||
}
|
||
} else if (currentView === 'graph-explorer') {
|
||
renderGraphExplorer();
|
||
} else if (currentView === 'help') {
|
||
renderHelpView();
|
||
} else if (currentView === 'core-memory') {
|
||
if (typeof renderCoreMemoryView === 'function') {
|
||
renderCoreMemoryView();
|
||
} else {
|
||
console.error('renderCoreMemoryView not defined - please refresh the page');
|
||
}
|
||
} else if (currentView === 'codexlens-manager') {
|
||
if (typeof renderCodexLensManager === 'function') {
|
||
renderCodexLensManager();
|
||
} else {
|
||
console.error('renderCodexLensManager not defined - please refresh the page');
|
||
}
|
||
} else if (currentView === 'api-settings') {
|
||
if (typeof renderApiSettings === 'function') {
|
||
renderApiSettings();
|
||
} else {
|
||
console.error('renderApiSettings not defined - please refresh the page');
|
||
}
|
||
} else if (currentView === 'issue-manager') {
|
||
if (typeof renderIssueManager === 'function') {
|
||
renderIssueManager();
|
||
} else {
|
||
console.error('renderIssueManager not defined - please refresh the page');
|
||
}
|
||
} else if (currentView === 'issue-discovery') {
|
||
if (typeof renderIssueDiscovery === 'function') {
|
||
renderIssueDiscovery();
|
||
} else {
|
||
console.error('renderIssueDiscovery not defined - please refresh the page');
|
||
}
|
||
} else if (currentView === 'loop-monitor') {
|
||
if (typeof renderLoopMonitor === 'function') {
|
||
renderLoopMonitor();
|
||
// Register destroy function for cleanup
|
||
currentViewDestroy = window.destroyLoopMonitor;
|
||
} else {
|
||
console.error('renderLoopMonitor not defined - please refresh the page');
|
||
}
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function setActiveNavItem(item) {
|
||
document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
|
||
item.classList.add('active');
|
||
}
|
||
|
||
function updateContentTitle() {
|
||
const titleEl = document.getElementById('contentTitle');
|
||
if (currentView === 'project-overview') {
|
||
titleEl.textContent = t('title.projectOverview');
|
||
} else if (currentView === 'mcp-manager') {
|
||
titleEl.textContent = t('title.mcpManagement');
|
||
} else if (currentView === 'explorer') {
|
||
titleEl.textContent = t('title.fileExplorer');
|
||
} else if (currentView === 'cli-manager') {
|
||
titleEl.textContent = t('title.cliTools');
|
||
} else if (currentView === 'cli-history') {
|
||
titleEl.textContent = t('title.cliHistory');
|
||
} else if (currentView === 'hook-manager') {
|
||
titleEl.textContent = t('title.hookManager');
|
||
} else if (currentView === 'memory') {
|
||
titleEl.textContent = t('title.memoryModule');
|
||
} else if (currentView === 'prompt-history') {
|
||
titleEl.textContent = t('title.promptHistory');
|
||
} else if (currentView === 'skills-manager') {
|
||
titleEl.textContent = t('title.skillsManager');
|
||
} else if (currentView === 'rules-manager') {
|
||
titleEl.textContent = t('title.rulesManager');
|
||
} else if (currentView === 'claude-manager') {
|
||
titleEl.textContent = t('title.claudeManager');
|
||
} else if (currentView === 'graph-explorer') {
|
||
titleEl.textContent = t('title.graphExplorer');
|
||
} else if (currentView === 'help') {
|
||
titleEl.textContent = t('title.helpGuide');
|
||
} else if (currentView === 'core-memory') {
|
||
titleEl.textContent = t('title.coreMemory');
|
||
} else if (currentView === 'codexlens-manager') {
|
||
titleEl.textContent = t('title.codexLensManager');
|
||
} else if (currentView === 'api-settings') {
|
||
titleEl.textContent = t('title.apiSettings');
|
||
} else if (currentView === 'issue-manager') {
|
||
titleEl.textContent = t('title.issueManager');
|
||
} else if (currentView === 'issue-discovery') {
|
||
titleEl.textContent = t('title.issueDiscovery');
|
||
} else if (currentView === 'loop-monitor') {
|
||
titleEl.textContent = t('title.loopMonitor') || 'Loop Monitor';
|
||
} else if (currentView === 'liteTasks') {
|
||
const names = { 'lite-plan': t('title.litePlanSessions'), 'lite-fix': t('title.liteFixSessions'), 'multi-cli-plan': t('title.multiCliPlanSessions') || 'Multi-CLI Plan Sessions' };
|
||
titleEl.textContent = names[currentLiteType] || t('title.liteTasks');
|
||
} else if (currentView === 'multiCliDetail') {
|
||
titleEl.textContent = t('title.multiCliDetail') || 'Multi-CLI Discussion Detail';
|
||
} else if (currentView === 'sessionDetail') {
|
||
titleEl.textContent = t('title.sessionDetail');
|
||
} else if (currentView === 'liteTaskDetail') {
|
||
titleEl.textContent = t('title.liteTaskDetail');
|
||
} else {
|
||
const names = { 'all': t('title.allSessions'), 'active': t('title.activeSessions'), 'archived': t('title.archivedSessions') };
|
||
titleEl.textContent = names[currentFilter] || t('title.sessions');
|
||
}
|
||
}
|
||
|
||
// Search
|
||
function initSearch() {
|
||
const input = document.getElementById('searchInput');
|
||
input.addEventListener('input', (e) => {
|
||
const query = e.target.value.toLowerCase();
|
||
document.querySelectorAll('.session-card').forEach(card => {
|
||
const text = card.textContent.toLowerCase();
|
||
card.style.display = text.includes(query) ? '' : 'none';
|
||
});
|
||
});
|
||
}
|
||
|
||
// Refresh Workspace
|
||
function initRefreshButton() {
|
||
const btn = document.getElementById('refreshWorkspace');
|
||
if (btn) {
|
||
btn.addEventListener('click', refreshWorkspace);
|
||
}
|
||
}
|
||
|
||
async function refreshWorkspace() {
|
||
const btn = document.getElementById('refreshWorkspace');
|
||
|
||
// Add spinning animation
|
||
btn.classList.add('refreshing');
|
||
btn.disabled = true;
|
||
|
||
try {
|
||
if (window.SERVER_MODE) {
|
||
// Reload data from server
|
||
const data = await loadDashboardData(projectPath);
|
||
if (data) {
|
||
// Clear and repopulate stores
|
||
Object.keys(sessionDataStore).forEach(k => delete sessionDataStore[k]);
|
||
Object.keys(liteTaskDataStore).forEach(k => delete liteTaskDataStore[k]);
|
||
|
||
// Populate stores
|
||
[...(data.activeSessions || []), ...(data.archivedSessions || [])].forEach(s => {
|
||
const sessionKey = `session-${s.session_id}`.replace(/[^a-zA-Z0-9-]/g, '-');
|
||
sessionDataStore[sessionKey] = s;
|
||
});
|
||
|
||
[...(data.liteTasks?.litePlan || []), ...(data.liteTasks?.liteFix || [])].forEach(s => {
|
||
const sessionKey = `lite-${s.session_id}`.replace(/[^a-zA-Z0-9-]/g, '-');
|
||
liteTaskDataStore[sessionKey] = s;
|
||
});
|
||
|
||
// Update global data
|
||
window.workflowData = data;
|
||
|
||
// Update sidebar counts
|
||
updateSidebarCounts(data);
|
||
|
||
// Re-render current view
|
||
if (currentView === 'sessions') {
|
||
renderSessions();
|
||
} else if (currentView === 'liteTasks') {
|
||
renderLiteTasks();
|
||
} else if (currentView === 'sessionDetail' && currentSessionDetailKey) {
|
||
showSessionDetailPage(currentSessionDetailKey);
|
||
} else if (currentView === 'liteTaskDetail' && currentSessionDetailKey) {
|
||
showLiteTaskDetailPage(currentSessionDetailKey);
|
||
} else if (currentView === 'project-overview') {
|
||
renderProjectOverview();
|
||
}
|
||
|
||
showRefreshToast(t('toast.workspaceRefreshed'), 'success');
|
||
}
|
||
} else {
|
||
// Non-server mode: just reload page
|
||
window.location.reload();
|
||
}
|
||
} catch (error) {
|
||
console.error('Refresh failed:', error);
|
||
showRefreshToast(t('toast.refreshFailed', { error: error.message }), 'error');
|
||
} finally {
|
||
btn.classList.remove('refreshing');
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
|
||
function updateSidebarCounts(data) {
|
||
// Update session counts
|
||
const activeCount = document.querySelector('.nav-item[data-filter="active"] .nav-count');
|
||
const archivedCount = document.querySelector('.nav-item[data-filter="archived"] .nav-count');
|
||
const allCount = document.querySelector('.nav-item[data-filter="all"] .nav-count');
|
||
|
||
if (activeCount) activeCount.textContent = data.activeSessions?.length || 0;
|
||
if (archivedCount) archivedCount.textContent = data.archivedSessions?.length || 0;
|
||
if (allCount) allCount.textContent = (data.activeSessions?.length || 0) + (data.archivedSessions?.length || 0);
|
||
|
||
// Update lite task counts (using ID selectors to match dashboard.html structure)
|
||
const litePlanCount = document.getElementById('badgeLitePlan');
|
||
const liteFixCount = document.getElementById('badgeLiteFix');
|
||
const multiCliPlanCount = document.getElementById('badgeMultiCliPlan');
|
||
|
||
if (litePlanCount) litePlanCount.textContent = data.liteTasks?.litePlan?.length || 0;
|
||
if (liteFixCount) liteFixCount.textContent = data.liteTasks?.liteFix?.length || 0;
|
||
if (multiCliPlanCount) multiCliPlanCount.textContent = data.liteTasks?.multiCliPlan?.length || 0;
|
||
}
|
||
|
||
// ========== Navigation Badge Aggregation ==========
|
||
|
||
/**
|
||
* Update a single badge element by ID
|
||
* @param {string} badgeId - Element ID
|
||
* @param {number|undefined} count - Badge count value
|
||
*/
|
||
function updateBadgeById(badgeId, count) {
|
||
const badge = document.getElementById(badgeId);
|
||
if (badge && count !== undefined) {
|
||
badge.textContent = count;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Fetch and update all navigation badges at once
|
||
* Called on dashboard initialization and path switch
|
||
*/
|
||
async function updateAllNavigationBadges() {
|
||
if (!window.SERVER_MODE) return;
|
||
|
||
try {
|
||
const response = await fetch('/api/nav-status?path=' + encodeURIComponent(projectPath));
|
||
if (!response.ok) {
|
||
console.warn('[Nav Status] Failed to fetch:', response.status);
|
||
return;
|
||
}
|
||
|
||
const status = await response.json();
|
||
|
||
// Update each badge
|
||
updateBadgeById('badgeIssues', status.issues?.count);
|
||
updateBadgeById('badgeDiscovery', status.discoveries?.count);
|
||
updateBadgeById('badgeSkills', status.skills?.count);
|
||
updateBadgeById('badgeRules', status.rules?.count);
|
||
updateBadgeById('badgeClaude', status.claude?.count);
|
||
updateBadgeById('badgeHooks', status.hooks?.count);
|
||
|
||
console.log('[Nav Status] Badges updated:', status);
|
||
} catch (err) {
|
||
console.error('[Nav Status] Error fetching status:', err);
|
||
// Graceful degradation - badges will update when user visits each page
|
||
}
|
||
}
|
||
|
||
function showRefreshToast(message, type) {
|
||
// Remove existing toast
|
||
const existing = document.querySelector('.status-toast');
|
||
if (existing) existing.remove();
|
||
|
||
const toast = document.createElement('div');
|
||
toast.className = `status-toast ${type}`;
|
||
toast.textContent = message;
|
||
document.body.appendChild(toast);
|
||
|
||
// Increase display time to 3.5 seconds for better visibility
|
||
setTimeout(() => {
|
||
toast.classList.add('fade-out');
|
||
setTimeout(() => toast.remove(), 300);
|
||
}, 3500);
|
||
}
|