mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-11 02:33:51 +08:00
feat: add CCW Loop System for automated iterative workflow execution
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
This commit is contained in:
@@ -66,6 +66,27 @@
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
/* CLI status actions container */
|
||||
.cli-status-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
/* Spin animation for sync icon */
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.cli-tools-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
|
||||
1024
ccw/src/templates/dashboard-css/36-loop-monitor.css
Normal file
1024
ccw/src/templates/dashboard-css/36-loop-monitor.css
Normal file
File diff suppressed because it is too large
Load Diff
@@ -771,9 +771,14 @@ function renderCliStatus() {
|
||||
container.innerHTML = `
|
||||
<div class="cli-status-header">
|
||||
<h3><i data-lucide="terminal" class="w-4 h-4"></i> CLI Tools</h3>
|
||||
<button class="btn-icon" onclick="refreshAllCliStatus()" title="Refresh">
|
||||
<i data-lucide="refresh-cw" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<div class="cli-status-actions">
|
||||
<button class="btn-icon" onclick="syncBuiltinTools()" title="Sync tool availability with installed CLI tools">
|
||||
<i data-lucide="sync" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<button class="btn-icon" onclick="refreshAllCliStatus()" title="Refresh">
|
||||
<i data-lucide="refresh-cw" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
${ccwInstallHtml}
|
||||
<div class="cli-tools-grid">
|
||||
@@ -825,6 +830,62 @@ function setPromptFormat(format) {
|
||||
showRefreshToast(`Prompt format set to ${format.toUpperCase()}`, 'success');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync builtin tools availability with installed CLI tools
|
||||
* Checks system PATH and updates cli-tools.json accordingly
|
||||
*/
|
||||
async function syncBuiltinTools() {
|
||||
const syncButton = document.querySelector('[onclick="syncBuiltinTools()"]');
|
||||
if (syncButton) {
|
||||
syncButton.disabled = true;
|
||||
const icon = syncButton.querySelector('i');
|
||||
if (icon) icon.classList.add('spin');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await csrfFetch('/api/cli/settings/sync-tools', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Sync failed');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// Reload the config after sync
|
||||
await loadCliToolsConfig();
|
||||
await loadAllStatuses();
|
||||
renderCliStatus();
|
||||
|
||||
// Show summary of changes
|
||||
const { enabled, disabled, unchanged } = result.changes;
|
||||
let message = 'Tools synced: ';
|
||||
const parts = [];
|
||||
if (enabled.length > 0) parts.push(`${enabled.join(', ')} enabled`);
|
||||
if (disabled.length > 0) parts.push(`${disabled.join(', ')} disabled`);
|
||||
if (unchanged.length > 0) parts.push(`${unchanged.length} unchanged`);
|
||||
message += parts.join(', ');
|
||||
|
||||
showRefreshToast(message, 'success');
|
||||
|
||||
// Also invalidate the CLI tool cache to ensure fresh checks
|
||||
if (window.cacheManager) {
|
||||
window.cacheManager.delete('cli-tools-status');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to sync tools:', err);
|
||||
showRefreshToast('Failed to sync tools: ' + (err.message || String(err)), 'error');
|
||||
} finally {
|
||||
if (syncButton) {
|
||||
syncButton.disabled = false;
|
||||
const icon = syncButton.querySelector('i');
|
||||
if (icon) icon.classList.remove('spin');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setSmartContextEnabled(enabled) {
|
||||
smartContextEnabled = enabled;
|
||||
localStorage.setItem('ccw-smart-context', enabled.toString());
|
||||
|
||||
@@ -183,6 +183,14 @@ function initNavigation() {
|
||||
} 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');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -231,6 +239,8 @@ function updateContentTitle() {
|
||||
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');
|
||||
|
||||
@@ -87,6 +87,10 @@ const i18n = {
|
||||
'nav.liteFix': 'Lite Fix',
|
||||
'nav.multiCliPlan': 'Multi-CLI Plan',
|
||||
|
||||
// Sidebar - Loops section
|
||||
'nav.loops': 'Loops',
|
||||
'nav.loopMonitor': 'Monitor',
|
||||
|
||||
// Sidebar - MCP section
|
||||
'nav.mcpServers': 'MCP Servers',
|
||||
'nav.manage': 'Manage',
|
||||
@@ -2144,6 +2148,51 @@ const i18n = {
|
||||
'title.issueManager': 'Issue Manager',
|
||||
'title.issueDiscovery': 'Issue Discovery',
|
||||
|
||||
// Loop Monitor
|
||||
'title.loopMonitor': 'Loop Monitor',
|
||||
'loop.title': 'Loop Monitor',
|
||||
'loop.status.created': 'Created',
|
||||
'loop.status.running': 'Running',
|
||||
'loop.status.paused': 'Paused',
|
||||
'loop.status.completed': 'Completed',
|
||||
'loop.status.failed': 'Failed',
|
||||
'loop.tabs.timeline': 'Timeline',
|
||||
'loop.tabs.logs': 'Logs',
|
||||
'loop.tabs.variables': 'Variables',
|
||||
'loop.buttons.pause': 'Pause',
|
||||
'loop.buttons.resume': 'Resume',
|
||||
'loop.buttons.stop': 'Stop',
|
||||
'loop.buttons.retry': 'Retry',
|
||||
'loop.buttons.newLoop': 'New Loop',
|
||||
'loop.empty': 'No active loops',
|
||||
'loop.metric.iteration': 'Iteration',
|
||||
'loop.metric.step': 'Step',
|
||||
'loop.metric.duration': 'Duration',
|
||||
'loop.task.id': 'Task',
|
||||
'loop.created': 'Created',
|
||||
'loop.updated': 'Updated',
|
||||
'loop.progress': 'Progress',
|
||||
'loop.cliSequence': 'CLI Sequence',
|
||||
'loop.stateVariables': 'State Variables',
|
||||
'loop.executionHistory': 'Execution History',
|
||||
'loop.failureReason': 'Failure Reason',
|
||||
'loop.noLoopsFound': 'No loops found',
|
||||
'loop.selectLoop': 'Select a loop to view details',
|
||||
'loop.tasks': 'Tasks',
|
||||
'loop.createTaskTitle': 'Create Loop Task',
|
||||
'loop.loopsCount': 'loops',
|
||||
'loop.paused': 'Loop paused',
|
||||
'loop.resumed': 'Loop resumed',
|
||||
'loop.stopped': 'Loop stopped',
|
||||
'loop.startedSuccess': 'Loop started',
|
||||
'loop.taskDescription': 'Description',
|
||||
'loop.maxIterations': 'Max Iterations',
|
||||
'loop.errorPolicy': 'Error Policy',
|
||||
'loop.pauseOnError': 'Pause on error',
|
||||
'loop.retryAutomatically': 'Retry automatically',
|
||||
'loop.failImmediate': 'Fail immediately',
|
||||
'loop.successCondition': 'Success Condition',
|
||||
|
||||
// Issue Discovery
|
||||
'discovery.title': 'Issue Discovery',
|
||||
'discovery.description': 'Discover potential issues from multiple perspectives',
|
||||
@@ -2438,6 +2487,10 @@ const i18n = {
|
||||
'nav.liteFix': '轻量修复',
|
||||
'nav.multiCliPlan': '多CLI规划',
|
||||
|
||||
// Sidebar - Loops section
|
||||
'nav.loops': '循环',
|
||||
'nav.loopMonitor': '监控器',
|
||||
|
||||
// Sidebar - MCP section
|
||||
'nav.mcpServers': 'MCP 服务器',
|
||||
'nav.manage': '管理',
|
||||
@@ -4507,6 +4560,51 @@ const i18n = {
|
||||
'title.issueManager': '议题管理器',
|
||||
'title.issueDiscovery': '议题发现',
|
||||
|
||||
// Loop Monitor
|
||||
'title.loopMonitor': '循环监控',
|
||||
'loop.title': '循环监控',
|
||||
'loop.status.created': '已创建',
|
||||
'loop.status.running': '运行中',
|
||||
'loop.status.paused': '已暂停',
|
||||
'loop.status.completed': '已完成',
|
||||
'loop.status.failed': '失败',
|
||||
'loop.tabs.timeline': '时间线',
|
||||
'loop.tabs.logs': '日志',
|
||||
'loop.tabs.variables': '变量',
|
||||
'loop.buttons.pause': '暂停',
|
||||
'loop.buttons.resume': '恢复',
|
||||
'loop.buttons.stop': '停止',
|
||||
'loop.buttons.retry': '重试',
|
||||
'loop.buttons.newLoop': '新建循环',
|
||||
'loop.empty': '没有活跃的循环',
|
||||
'loop.metric.iteration': '迭代',
|
||||
'loop.metric.step': '步骤',
|
||||
'loop.metric.duration': '耗时',
|
||||
'loop.task.id': '任务',
|
||||
'loop.created': '创建时间',
|
||||
'loop.updated': '更新时间',
|
||||
'loop.progress': '进度',
|
||||
'loop.cliSequence': 'CLI 序列',
|
||||
'loop.stateVariables': '状态变量',
|
||||
'loop.executionHistory': '执行历史',
|
||||
'loop.failureReason': '失败原因',
|
||||
'loop.noLoopsFound': '未找到循环',
|
||||
'loop.selectLoop': '选择一个循环查看详情',
|
||||
'loop.tasks': '任务',
|
||||
'loop.createTaskTitle': '创建循环任务',
|
||||
'loop.loopsCount': '个循环',
|
||||
'loop.paused': '循环已暂停',
|
||||
'loop.resumed': '循环已恢复',
|
||||
'loop.stopped': '循环已停止',
|
||||
'loop.startedSuccess': '循环已启动',
|
||||
'loop.taskDescription': '描述',
|
||||
'loop.maxIterations': '最大迭代数',
|
||||
'loop.errorPolicy': '错误策略',
|
||||
'loop.pauseOnError': '错误时暂停',
|
||||
'loop.retryAutomatically': '自动重试',
|
||||
'loop.failImmediate': '立即失败',
|
||||
'loop.successCondition': '成功条件',
|
||||
|
||||
// Issue Discovery
|
||||
'discovery.title': '议题发现',
|
||||
'discovery.description': '从多个视角发现潜在问题',
|
||||
|
||||
1002
ccw/src/templates/dashboard-js/views/loop-monitor.js
Normal file
1002
ccw/src/templates/dashboard-js/views/loop-monitor.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -525,6 +525,21 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Loops Section -->
|
||||
<div class="mb-2" id="loopsNav">
|
||||
<div class="flex items-center px-4 py-2 text-sm font-semibold text-muted-foreground uppercase tracking-wide">
|
||||
<i data-lucide="repeat" class="nav-section-icon mr-2"></i>
|
||||
<span class="nav-section-title" data-i18n="nav.loops">Loops</span>
|
||||
</div>
|
||||
<ul class="space-y-0.5">
|
||||
<li class="nav-item flex items-center gap-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-view="loop-monitor" data-tooltip="Loop Monitor">
|
||||
<i data-lucide="activity" class="nav-icon text-cyan"></i>
|
||||
<span class="nav-text flex-1" data-i18n="nav.loopMonitor">Monitor</span>
|
||||
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-cyan-light text-cyan" id="badgeLoops">0</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Issues Section -->
|
||||
<div class="mb-2" id="issuesNav">
|
||||
<div class="flex items-center px-4 py-2 text-sm font-semibold text-muted-foreground uppercase tracking-wide">
|
||||
|
||||
Reference in New Issue
Block a user