claude-code-workflow 6.3.36 → 6.3.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/workflow/lite-fix.md +108 -9
- package/.claude/skills/ccw-loop/README.md +303 -0
- package/.claude/skills/ccw-loop/SKILL.md +259 -0
- package/.claude/skills/ccw-loop/phases/actions/action-complete.md +320 -0
- package/.claude/skills/ccw-loop/phases/actions/action-debug-with-file.md +485 -0
- package/.claude/skills/ccw-loop/phases/actions/action-develop-with-file.md +365 -0
- package/.claude/skills/ccw-loop/phases/actions/action-init.md +200 -0
- package/.claude/skills/ccw-loop/phases/actions/action-menu.md +192 -0
- package/.claude/skills/ccw-loop/phases/actions/action-validate-with-file.md +307 -0
- package/.claude/skills/ccw-loop/phases/orchestrator.md +486 -0
- package/.claude/skills/ccw-loop/phases/state-schema.md +474 -0
- package/.claude/skills/ccw-loop/specs/action-catalog.md +300 -0
- package/.claude/skills/ccw-loop/specs/loop-requirements.md +192 -0
- package/.claude/skills/ccw-loop/templates/progress-template.md +175 -0
- package/.claude/skills/ccw-loop/templates/understanding-template.md +303 -0
- package/.claude/skills/ccw-loop/templates/validation-template.md +258 -0
- package/ccw/dist/cli.d.ts.map +1 -1
- package/ccw/dist/cli.js +8 -1
- package/ccw/dist/cli.js.map +1 -1
- package/ccw/dist/commands/cli.d.ts.map +1 -1
- package/ccw/dist/commands/cli.js +14 -1
- package/ccw/dist/commands/cli.js.map +1 -1
- package/ccw/dist/commands/loop.d.ts +10 -0
- package/ccw/dist/commands/loop.d.ts.map +1 -0
- package/ccw/dist/commands/loop.js +289 -0
- package/ccw/dist/commands/loop.js.map +1 -0
- package/ccw/dist/core/dashboard-generator.d.ts.map +1 -1
- package/ccw/dist/core/dashboard-generator.js +4 -1
- package/ccw/dist/core/dashboard-generator.js.map +1 -1
- package/ccw/dist/core/routes/claude-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/claude-routes.js +5 -3
- package/ccw/dist/core/routes/claude-routes.js.map +1 -1
- package/ccw/dist/core/routes/cli-routes.d.ts +6 -0
- package/ccw/dist/core/routes/cli-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/cli-routes.js +42 -13
- package/ccw/dist/core/routes/cli-routes.js.map +1 -1
- package/ccw/dist/core/routes/cli-settings-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/cli-settings-routes.js +44 -0
- package/ccw/dist/core/routes/cli-settings-routes.js.map +1 -1
- package/ccw/dist/core/routes/codexlens/semantic-handlers.d.ts.map +1 -1
- package/ccw/dist/core/routes/codexlens/semantic-handlers.js +3 -2
- package/ccw/dist/core/routes/codexlens/semantic-handlers.js.map +1 -1
- package/ccw/dist/core/routes/core-memory-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/core-memory-routes.js +4 -2
- package/ccw/dist/core/routes/core-memory-routes.js.map +1 -1
- package/ccw/dist/core/routes/files-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/files-routes.js +4 -2
- package/ccw/dist/core/routes/files-routes.js.map +1 -1
- package/ccw/dist/core/routes/loop-routes.d.ts +24 -0
- package/ccw/dist/core/routes/loop-routes.d.ts.map +1 -0
- package/ccw/dist/core/routes/loop-routes.js +334 -0
- package/ccw/dist/core/routes/loop-routes.js.map +1 -0
- package/ccw/dist/core/routes/loop-v2-routes.d.ts +35 -0
- package/ccw/dist/core/routes/loop-v2-routes.d.ts.map +1 -0
- package/ccw/dist/core/routes/loop-v2-routes.js +1208 -0
- package/ccw/dist/core/routes/loop-v2-routes.js.map +1 -0
- package/ccw/dist/core/routes/memory-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/memory-routes.js +2 -1
- package/ccw/dist/core/routes/memory-routes.js.map +1 -1
- package/ccw/dist/core/routes/task-routes.d.ts +12 -0
- package/ccw/dist/core/routes/task-routes.d.ts.map +1 -0
- package/ccw/dist/core/routes/task-routes.js +321 -0
- package/ccw/dist/core/routes/task-routes.js.map +1 -0
- package/ccw/dist/core/routes/test-loop-routes.d.ts +11 -0
- package/ccw/dist/core/routes/test-loop-routes.d.ts.map +1 -0
- package/ccw/dist/core/routes/test-loop-routes.js +298 -0
- package/ccw/dist/core/routes/test-loop-routes.js.map +1 -0
- package/ccw/dist/core/server.d.ts.map +1 -1
- package/ccw/dist/core/server.js +43 -3
- package/ccw/dist/core/server.js.map +1 -1
- package/ccw/dist/core/websocket.d.ts +59 -0
- package/ccw/dist/core/websocket.d.ts.map +1 -1
- package/ccw/dist/core/websocket.js +34 -0
- package/ccw/dist/core/websocket.js.map +1 -1
- package/ccw/dist/tools/claude-cli-tools.d.ts +40 -0
- package/ccw/dist/tools/claude-cli-tools.d.ts.map +1 -1
- package/ccw/dist/tools/claude-cli-tools.js +119 -0
- package/ccw/dist/tools/claude-cli-tools.js.map +1 -1
- package/ccw/dist/tools/loop-manager.d.ts +84 -0
- package/ccw/dist/tools/loop-manager.d.ts.map +1 -0
- package/ccw/dist/tools/loop-manager.js +425 -0
- package/ccw/dist/tools/loop-manager.js.map +1 -0
- package/ccw/dist/tools/loop-state-manager.d.ts +47 -0
- package/ccw/dist/tools/loop-state-manager.d.ts.map +1 -0
- package/ccw/dist/tools/loop-state-manager.js +149 -0
- package/ccw/dist/tools/loop-state-manager.js.map +1 -0
- package/ccw/dist/tools/loop-task-manager.d.ts +138 -0
- package/ccw/dist/tools/loop-task-manager.d.ts.map +1 -0
- package/ccw/dist/tools/loop-task-manager.js +270 -0
- package/ccw/dist/tools/loop-task-manager.js.map +1 -0
- package/ccw/dist/types/index.d.ts +1 -0
- package/ccw/dist/types/index.d.ts.map +1 -1
- package/ccw/dist/types/index.js +1 -0
- package/ccw/dist/types/index.js.map +1 -1
- package/ccw/dist/types/loop.d.ts +257 -0
- package/ccw/dist/types/loop.d.ts.map +1 -0
- package/ccw/dist/types/loop.js +17 -0
- package/ccw/dist/types/loop.js.map +1 -0
- package/ccw/src/cli.ts +9 -1
- package/ccw/src/commands/cli.ts +14 -1
- package/ccw/src/commands/loop.ts +344 -0
- package/ccw/src/core/dashboard-generator.ts +4 -1
- package/ccw/src/core/routes/claude-routes.ts +5 -3
- package/ccw/src/core/routes/cli-routes.ts +47 -15
- package/ccw/src/core/routes/cli-settings-routes.ts +47 -0
- package/ccw/src/core/routes/codexlens/semantic-handlers.ts +3 -2
- package/ccw/src/core/routes/core-memory-routes.ts +4 -2
- package/ccw/src/core/routes/files-routes.ts +4 -2
- package/ccw/src/core/routes/loop-routes.ts +386 -0
- package/ccw/src/core/routes/loop-v2-routes.ts +1412 -0
- package/ccw/src/core/routes/memory-routes.ts +2 -1
- package/ccw/src/core/routes/task-routes.ts +361 -0
- package/ccw/src/core/routes/test-loop-routes.ts +312 -0
- package/ccw/src/core/server.ts +44 -3
- package/ccw/src/core/websocket.ts +104 -0
- package/ccw/src/templates/dashboard-css/12-cli-legacy.css +56 -0
- package/ccw/src/templates/dashboard-css/33-cli-stream-viewer.css +55 -0
- package/ccw/src/templates/dashboard-css/36-loop-monitor.css +1896 -0
- package/ccw/src/templates/dashboard-css/36-loop-monitor.css.backup +1877 -0
- package/ccw/src/templates/dashboard-js/components/cli-status.js +64 -3
- package/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js +251 -110
- package/ccw/src/templates/dashboard-js/components/navigation.js +10 -0
- package/ccw/src/templates/dashboard-js/components/notifications.js +16 -0
- package/ccw/src/templates/dashboard-js/i18n.js +475 -1
- package/ccw/src/templates/dashboard-js/views/cli-manager.js +3 -2
- package/ccw/src/templates/dashboard-js/views/loop-monitor.js +3244 -0
- package/ccw/src/templates/dashboard.html +20 -2
- package/ccw/src/tools/claude-cli-tools.ts +143 -0
- package/ccw/src/tools/loop-manager.ts +519 -0
- package/ccw/src/tools/loop-state-manager.ts +173 -0
- package/ccw/src/tools/loop-task-manager.ts +380 -0
- package/ccw/src/types/index.ts +1 -0
- package/ccw/src/types/loop.ts +316 -0
- package/package.json +1 -1
|
@@ -771,9 +771,14 @@ function renderCliStatus() {
|
|
|
771
771
|
container.innerHTML = `
|
|
772
772
|
<div class="cli-status-header">
|
|
773
773
|
<h3><i data-lucide="terminal" class="w-4 h-4"></i> CLI Tools</h3>
|
|
774
|
-
<
|
|
775
|
-
<
|
|
776
|
-
|
|
774
|
+
<div class="cli-status-actions">
|
|
775
|
+
<button class="btn-icon" onclick="syncBuiltinTools()" title="Sync tool availability with installed CLI tools">
|
|
776
|
+
<i data-lucide="sync" class="w-4 h-4"></i>
|
|
777
|
+
</button>
|
|
778
|
+
<button class="btn-icon" onclick="refreshAllCliStatus()" title="Refresh">
|
|
779
|
+
<i data-lucide="refresh-cw" class="w-4 h-4"></i>
|
|
780
|
+
</button>
|
|
781
|
+
</div>
|
|
777
782
|
</div>
|
|
778
783
|
${ccwInstallHtml}
|
|
779
784
|
<div class="cli-tools-grid">
|
|
@@ -825,6 +830,62 @@ function setPromptFormat(format) {
|
|
|
825
830
|
showRefreshToast(`Prompt format set to ${format.toUpperCase()}`, 'success');
|
|
826
831
|
}
|
|
827
832
|
|
|
833
|
+
/**
|
|
834
|
+
* Sync builtin tools availability with installed CLI tools
|
|
835
|
+
* Checks system PATH and updates cli-tools.json accordingly
|
|
836
|
+
*/
|
|
837
|
+
async function syncBuiltinTools() {
|
|
838
|
+
const syncButton = document.querySelector('[onclick="syncBuiltinTools()"]');
|
|
839
|
+
if (syncButton) {
|
|
840
|
+
syncButton.disabled = true;
|
|
841
|
+
const icon = syncButton.querySelector('i');
|
|
842
|
+
if (icon) icon.classList.add('spin');
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
try {
|
|
846
|
+
const response = await csrfFetch('/api/cli/settings/sync-tools', {
|
|
847
|
+
method: 'POST',
|
|
848
|
+
headers: { 'Content-Type': 'application/json' }
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
if (!response.ok) {
|
|
852
|
+
throw new Error('Sync failed');
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
const result = await response.json();
|
|
856
|
+
|
|
857
|
+
// Reload the config after sync
|
|
858
|
+
await loadCliToolsConfig();
|
|
859
|
+
await loadAllStatuses();
|
|
860
|
+
renderCliStatus();
|
|
861
|
+
|
|
862
|
+
// Show summary of changes
|
|
863
|
+
const { enabled, disabled, unchanged } = result.changes;
|
|
864
|
+
let message = 'Tools synced: ';
|
|
865
|
+
const parts = [];
|
|
866
|
+
if (enabled.length > 0) parts.push(`${enabled.join(', ')} enabled`);
|
|
867
|
+
if (disabled.length > 0) parts.push(`${disabled.join(', ')} disabled`);
|
|
868
|
+
if (unchanged.length > 0) parts.push(`${unchanged.length} unchanged`);
|
|
869
|
+
message += parts.join(', ');
|
|
870
|
+
|
|
871
|
+
showRefreshToast(message, 'success');
|
|
872
|
+
|
|
873
|
+
// Also invalidate the CLI tool cache to ensure fresh checks
|
|
874
|
+
if (window.cacheManager) {
|
|
875
|
+
window.cacheManager.delete('cli-tools-status');
|
|
876
|
+
}
|
|
877
|
+
} catch (err) {
|
|
878
|
+
console.error('Failed to sync tools:', err);
|
|
879
|
+
showRefreshToast('Failed to sync tools: ' + (err.message || String(err)), 'error');
|
|
880
|
+
} finally {
|
|
881
|
+
if (syncButton) {
|
|
882
|
+
syncButton.disabled = false;
|
|
883
|
+
const icon = syncButton.querySelector('i');
|
|
884
|
+
if (icon) icon.classList.remove('spin');
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
828
889
|
function setSmartContextEnabled(enabled) {
|
|
829
890
|
smartContextEnabled = enabled;
|
|
830
891
|
localStorage.setItem('ccw-smart-context', enabled.toString());
|
|
@@ -10,7 +10,7 @@ let streamScrollHandler = null; // Track scroll listener
|
|
|
10
10
|
let streamStatusTimers = []; // Track status update timers
|
|
11
11
|
|
|
12
12
|
// ===== State Management =====
|
|
13
|
-
let cliStreamExecutions = {}; // { executionId: { tool, mode, output, status, startTime, endTime } }
|
|
13
|
+
let cliStreamExecutions = {}; // { executionId: { tool, mode, output, status, startTime, endTime, recovered } }
|
|
14
14
|
let activeStreamTab = null;
|
|
15
15
|
let autoScrollEnabled = true;
|
|
16
16
|
let isCliStreamViewerOpen = false;
|
|
@@ -18,116 +18,212 @@ let searchFilter = ''; // Search filter for output content
|
|
|
18
18
|
|
|
19
19
|
const MAX_OUTPUT_LINES = 5000; // Prevent memory issues
|
|
20
20
|
|
|
21
|
+
// ===== Sync State Management =====
|
|
22
|
+
let syncPromise = null; // Track ongoing sync to prevent duplicates
|
|
23
|
+
let syncTimeoutId = null; // Debounce timeout ID
|
|
24
|
+
let lastSyncTime = 0; // Track last successful sync time
|
|
25
|
+
const SYNC_DEBOUNCE_MS = 300; // Debounce delay for sync calls
|
|
26
|
+
const SYNC_TIMEOUT_MS = 10000; // 10 second timeout for sync requests
|
|
27
|
+
|
|
21
28
|
// ===== State Synchronization =====
|
|
22
29
|
/**
|
|
23
30
|
* Sync active executions from server
|
|
24
31
|
* Called on initialization to recover state when view is opened mid-execution
|
|
32
|
+
* Also called on WebSocket reconnection to restore CLI viewer state
|
|
33
|
+
*
|
|
34
|
+
* Features:
|
|
35
|
+
* - Debouncing: Prevents rapid successive sync calls
|
|
36
|
+
* - Deduplication: Only one sync at a time
|
|
37
|
+
* - Timeout handling: 10 second timeout for sync requests
|
|
38
|
+
* - Recovery flag: Marks recovered sessions for visual indicator
|
|
25
39
|
*/
|
|
26
40
|
async function syncActiveExecutions() {
|
|
27
41
|
// Only sync in server mode
|
|
28
42
|
if (!window.SERVER_MODE) return;
|
|
29
43
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (!executions || executions.length === 0) return;
|
|
36
|
-
|
|
37
|
-
let needsUiUpdate = false;
|
|
38
|
-
|
|
39
|
-
executions.forEach(exec => {
|
|
40
|
-
const existing = cliStreamExecutions[exec.id];
|
|
41
|
-
|
|
42
|
-
// Parse historical output from server
|
|
43
|
-
const historicalLines = [];
|
|
44
|
-
if (exec.output) {
|
|
45
|
-
const lines = exec.output.split('\n');
|
|
46
|
-
const startIndex = Math.max(0, lines.length - MAX_OUTPUT_LINES + 1);
|
|
47
|
-
lines.slice(startIndex).forEach(line => {
|
|
48
|
-
if (line.trim()) {
|
|
49
|
-
historicalLines.push({
|
|
50
|
-
type: 'stdout',
|
|
51
|
-
content: line,
|
|
52
|
-
timestamp: exec.startTime || Date.now()
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
}
|
|
44
|
+
// Deduplication: if a sync is already in progress, return that promise
|
|
45
|
+
if (syncPromise) {
|
|
46
|
+
console.log('[CLI Stream] Sync already in progress, skipping');
|
|
47
|
+
return syncPromise;
|
|
48
|
+
}
|
|
57
49
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const missingLines = historicalLines.filter(h => !existingContentSet.has(h.content));
|
|
50
|
+
// Clear any pending debounced sync
|
|
51
|
+
if (syncTimeoutId) {
|
|
52
|
+
clearTimeout(syncTimeoutId);
|
|
53
|
+
syncTimeoutId = null;
|
|
54
|
+
}
|
|
64
55
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
56
|
+
syncPromise = (async function() {
|
|
57
|
+
try {
|
|
58
|
+
// Create timeout promise
|
|
59
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
60
|
+
setTimeout(() => reject(new Error('Sync timeout')), SYNC_TIMEOUT_MS);
|
|
61
|
+
});
|
|
69
62
|
|
|
70
|
-
|
|
71
|
-
|
|
63
|
+
// Race between fetch and timeout
|
|
64
|
+
const response = await Promise.race([
|
|
65
|
+
fetch('/api/cli/active'),
|
|
66
|
+
timeoutPromise
|
|
67
|
+
]);
|
|
72
68
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
console.warn('[CLI Stream] Sync response not OK:', response.status);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
77
73
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
74
|
+
const { executions } = await response.json();
|
|
75
|
+
|
|
76
|
+
// Handle empty response gracefully
|
|
77
|
+
if (!executions || executions.length === 0) {
|
|
78
|
+
console.log('[CLI Stream] No active executions to sync');
|
|
81
79
|
return;
|
|
82
80
|
}
|
|
83
81
|
|
|
84
|
-
needsUiUpdate =
|
|
82
|
+
let needsUiUpdate = false;
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
lastSyncTime = now;
|
|
85
|
+
|
|
86
|
+
executions.forEach(exec => {
|
|
87
|
+
const existing = cliStreamExecutions[exec.id];
|
|
88
|
+
|
|
89
|
+
// Parse historical output from server with type detection
|
|
90
|
+
const historicalLines = [];
|
|
91
|
+
if (exec.output) {
|
|
92
|
+
const lines = exec.output.split('\n');
|
|
93
|
+
const startIndex = Math.max(0, lines.length - MAX_OUTPUT_LINES + 1);
|
|
94
|
+
lines.slice(startIndex).forEach(line => {
|
|
95
|
+
if (line.trim()) {
|
|
96
|
+
// Detect type from content prefix for proper formatting
|
|
97
|
+
const parsed = parseMessageType(line);
|
|
98
|
+
// Map parsed type to chunkType for rendering
|
|
99
|
+
const typeMap = {
|
|
100
|
+
system: 'system',
|
|
101
|
+
thinking: 'thought',
|
|
102
|
+
response: 'stdout',
|
|
103
|
+
result: 'metadata',
|
|
104
|
+
error: 'stderr',
|
|
105
|
+
warning: 'stderr',
|
|
106
|
+
info: 'metadata'
|
|
107
|
+
};
|
|
108
|
+
historicalLines.push({
|
|
109
|
+
type: parsed.hasPrefix ? (typeMap[parsed.type] || 'stdout') : 'stdout',
|
|
110
|
+
content: line, // Keep original content with prefix
|
|
111
|
+
timestamp: exec.startTime || Date.now()
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
85
116
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
117
|
+
if (existing) {
|
|
118
|
+
// Already tracked by WebSocket events - merge historical output
|
|
119
|
+
// Only prepend historical lines that are not already in the output
|
|
120
|
+
// (WebSocket events only add NEW output, so historical output should come before)
|
|
121
|
+
const existingContentSet = new Set(existing.output.map(o => o.content));
|
|
122
|
+
const missingLines = historicalLines.filter(h => !existingContentSet.has(h.content));
|
|
123
|
+
|
|
124
|
+
if (missingLines.length > 0) {
|
|
125
|
+
// Find the system start message index (skip it when prepending)
|
|
126
|
+
const systemMsgIndex = existing.output.findIndex(o => o.type === 'system');
|
|
127
|
+
const insertIndex = systemMsgIndex >= 0 ? systemMsgIndex + 1 : 0;
|
|
128
|
+
|
|
129
|
+
// Prepend missing historical lines after system message
|
|
130
|
+
existing.output.splice(insertIndex, 0, ...missingLines);
|
|
131
|
+
|
|
132
|
+
// Trim if too long
|
|
133
|
+
if (existing.output.length > MAX_OUTPUT_LINES) {
|
|
134
|
+
existing.output = existing.output.slice(-MAX_OUTPUT_LINES);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
needsUiUpdate = true;
|
|
138
|
+
console.log(`[CLI Stream] Merged ${missingLines.length} historical lines for ${exec.id}`);
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
95
142
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
143
|
+
needsUiUpdate = true;
|
|
144
|
+
|
|
145
|
+
// New execution - rebuild full state with recovered flag
|
|
146
|
+
cliStreamExecutions[exec.id] = {
|
|
147
|
+
tool: exec.tool || 'cli',
|
|
148
|
+
mode: exec.mode || 'analysis',
|
|
149
|
+
output: [],
|
|
150
|
+
status: exec.status || 'running',
|
|
151
|
+
startTime: exec.startTime || Date.now(),
|
|
152
|
+
endTime: exec.status !== 'running' ? Date.now() : null,
|
|
153
|
+
recovered: true // Mark as recovered for visual indicator
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// Add system start message
|
|
157
|
+
cliStreamExecutions[exec.id].output.push({
|
|
158
|
+
type: 'system',
|
|
159
|
+
content: `[${new Date(exec.startTime).toLocaleTimeString()}] CLI execution started: ${exec.tool} (${exec.mode} mode)`,
|
|
160
|
+
timestamp: exec.startTime
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Add historical output
|
|
164
|
+
cliStreamExecutions[exec.id].output.push(...historicalLines);
|
|
165
|
+
|
|
166
|
+
// Add recovery notice for completed executions
|
|
167
|
+
if (exec.isComplete) {
|
|
168
|
+
cliStreamExecutions[exec.id].output.push({
|
|
169
|
+
type: 'system',
|
|
170
|
+
content: `[Session recovered from server - ${exec.status}]`,
|
|
171
|
+
timestamp: now
|
|
172
|
+
});
|
|
173
|
+
}
|
|
101
174
|
});
|
|
102
175
|
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
176
|
+
// Update UI if we recovered or merged any executions
|
|
177
|
+
if (needsUiUpdate) {
|
|
178
|
+
// Set active tab to first running execution, or first recovered if none running
|
|
179
|
+
const runningExec = executions.find(e => e.status === 'running');
|
|
180
|
+
if (runningExec && !activeStreamTab) {
|
|
181
|
+
activeStreamTab = runningExec.id;
|
|
182
|
+
} else if (!runningExec && executions.length > 0 && !activeStreamTab) {
|
|
183
|
+
// If no running executions, select the first recovered one
|
|
184
|
+
activeStreamTab = executions[0].id;
|
|
185
|
+
}
|
|
106
186
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
// Set active tab to first running execution
|
|
110
|
-
const runningExec = executions.find(e => e.status === 'running');
|
|
111
|
-
if (runningExec && !activeStreamTab) {
|
|
112
|
-
activeStreamTab = runningExec.id;
|
|
113
|
-
}
|
|
187
|
+
renderStreamTabs();
|
|
188
|
+
updateStreamBadge();
|
|
114
189
|
|
|
115
|
-
|
|
116
|
-
|
|
190
|
+
// If viewer is open, render content. If not, open it if we have any recovered executions.
|
|
191
|
+
if (isCliStreamViewerOpen) {
|
|
192
|
+
renderStreamContent(activeStreamTab);
|
|
193
|
+
} else if (executions.length > 0) {
|
|
194
|
+
// Automatically open the viewer if it's closed and we just synced any executions
|
|
195
|
+
// (running or completed - user might refresh after completion to see the output)
|
|
196
|
+
toggleCliStreamViewer();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
117
199
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
200
|
+
console.log(`[CLI Stream] Synced ${executions.length} active execution(s)`);
|
|
201
|
+
} catch (e) {
|
|
202
|
+
if (e.message === 'Sync timeout') {
|
|
203
|
+
console.warn('[CLI Stream] Sync request timed out after', SYNC_TIMEOUT_MS, 'ms');
|
|
204
|
+
} else {
|
|
205
|
+
console.error('[CLI Stream] Sync failed:', e);
|
|
124
206
|
}
|
|
207
|
+
} finally {
|
|
208
|
+
syncPromise = null; // Clear the promise to allow future syncs
|
|
125
209
|
}
|
|
210
|
+
})();
|
|
211
|
+
|
|
212
|
+
return syncPromise;
|
|
213
|
+
}
|
|
126
214
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
215
|
+
/**
|
|
216
|
+
* Debounced sync function - prevents rapid successive sync calls
|
|
217
|
+
* Use this when multiple sync triggers may happen in quick succession
|
|
218
|
+
*/
|
|
219
|
+
function syncActiveExecutionsDebounced() {
|
|
220
|
+
if (syncTimeoutId) {
|
|
221
|
+
clearTimeout(syncTimeoutId);
|
|
130
222
|
}
|
|
223
|
+
syncTimeoutId = setTimeout(function() {
|
|
224
|
+
syncTimeoutId = null;
|
|
225
|
+
syncActiveExecutions();
|
|
226
|
+
}, SYNC_DEBOUNCE_MS);
|
|
131
227
|
}
|
|
132
228
|
|
|
133
229
|
// ===== Initialization =====
|
|
@@ -502,19 +598,24 @@ function renderStreamTabs() {
|
|
|
502
598
|
tabsContainer.innerHTML = execIds.map(id => {
|
|
503
599
|
const exec = cliStreamExecutions[id];
|
|
504
600
|
const isActive = id === activeStreamTab;
|
|
505
|
-
const
|
|
506
|
-
|
|
601
|
+
const isRecovered = exec.recovered === true;
|
|
602
|
+
|
|
603
|
+
// Recovery badge HTML
|
|
604
|
+
const recoveryBadge = isRecovered
|
|
605
|
+
? `<span class="cli-stream-recovery-badge" title="Session recovered after page refresh">Recovered</span>`
|
|
606
|
+
: '';
|
|
607
|
+
|
|
507
608
|
return `
|
|
508
|
-
<div class="cli-stream-tab ${isActive ? 'active' : ''}"
|
|
509
|
-
onclick="switchStreamTab('${id}')"
|
|
609
|
+
<div class="cli-stream-tab ${isActive ? 'active' : ''} ${isRecovered ? 'recovered' : ''}"
|
|
610
|
+
onclick="switchStreamTab('${id}')"
|
|
510
611
|
data-execution-id="${id}">
|
|
511
612
|
<span class="cli-stream-tab-status ${exec.status}"></span>
|
|
512
613
|
<span class="cli-stream-tab-tool">${escapeHtml(exec.tool)}</span>
|
|
513
614
|
<span class="cli-stream-tab-mode">${exec.mode}</span>
|
|
514
|
-
|
|
615
|
+
${recoveryBadge}
|
|
616
|
+
<button class="cli-stream-tab-close"
|
|
515
617
|
onclick="event.stopPropagation(); closeStream('${id}')"
|
|
516
|
-
title="${
|
|
517
|
-
${canClose ? '' : 'disabled'}>×</button>
|
|
618
|
+
title="${_streamT('cliStream.close')}">×</button>
|
|
518
619
|
</div>
|
|
519
620
|
`;
|
|
520
621
|
}).join('');
|
|
@@ -589,29 +690,35 @@ function renderStreamContent(executionId) {
|
|
|
589
690
|
function renderStreamStatus(executionId) {
|
|
590
691
|
const statusContainer = document.getElementById('cliStreamStatus');
|
|
591
692
|
if (!statusContainer) return;
|
|
592
|
-
|
|
693
|
+
|
|
593
694
|
const exec = executionId ? cliStreamExecutions[executionId] : null;
|
|
594
|
-
|
|
695
|
+
|
|
595
696
|
if (!exec) {
|
|
596
697
|
statusContainer.innerHTML = '';
|
|
597
698
|
return;
|
|
598
699
|
}
|
|
599
|
-
|
|
600
|
-
const duration = exec.endTime
|
|
700
|
+
|
|
701
|
+
const duration = exec.endTime
|
|
601
702
|
? formatDuration(exec.endTime - exec.startTime)
|
|
602
703
|
: formatDuration(Date.now() - exec.startTime);
|
|
603
|
-
|
|
604
|
-
const statusLabel = exec.status === 'running'
|
|
704
|
+
|
|
705
|
+
const statusLabel = exec.status === 'running'
|
|
605
706
|
? _streamT('cliStream.running')
|
|
606
707
|
: exec.status === 'completed'
|
|
607
708
|
? _streamT('cliStream.completed')
|
|
608
709
|
: _streamT('cliStream.error');
|
|
609
|
-
|
|
710
|
+
|
|
711
|
+
// Recovery badge for status bar
|
|
712
|
+
const recoveryBadge = exec.recovered
|
|
713
|
+
? `<span class="cli-status-recovery-badge">Recovered</span>`
|
|
714
|
+
: '';
|
|
715
|
+
|
|
610
716
|
statusContainer.innerHTML = `
|
|
611
717
|
<div class="cli-stream-status-info">
|
|
612
718
|
<div class="cli-stream-status-item">
|
|
613
719
|
<span class="cli-stream-tab-status ${exec.status}"></span>
|
|
614
720
|
<span>${statusLabel}</span>
|
|
721
|
+
${recoveryBadge}
|
|
615
722
|
</div>
|
|
616
723
|
<div class="cli-stream-status-item">
|
|
617
724
|
<i data-lucide="clock"></i>
|
|
@@ -623,15 +730,15 @@ function renderStreamStatus(executionId) {
|
|
|
623
730
|
</div>
|
|
624
731
|
</div>
|
|
625
732
|
<div class="cli-stream-status-actions">
|
|
626
|
-
<button class="cli-stream-toggle-btn ${autoScrollEnabled ? 'active' : ''}"
|
|
627
|
-
onclick="toggleAutoScroll()"
|
|
733
|
+
<button class="cli-stream-toggle-btn ${autoScrollEnabled ? 'active' : ''}"
|
|
734
|
+
onclick="toggleAutoScroll()"
|
|
628
735
|
title="${_streamT('cliStream.autoScroll')}">
|
|
629
736
|
<i data-lucide="arrow-down-to-line"></i>
|
|
630
737
|
<span data-i18n="cliStream.autoScroll">${_streamT('cliStream.autoScroll')}</span>
|
|
631
738
|
</button>
|
|
632
739
|
</div>
|
|
633
740
|
`;
|
|
634
|
-
|
|
741
|
+
|
|
635
742
|
if (typeof lucide !== 'undefined') lucide.createIcons();
|
|
636
743
|
|
|
637
744
|
// Update duration periodically for running executions
|
|
@@ -656,52 +763,85 @@ function switchStreamTab(executionId) {
|
|
|
656
763
|
function updateStreamBadge() {
|
|
657
764
|
const badge = document.getElementById('cliStreamBadge');
|
|
658
765
|
if (!badge) return;
|
|
659
|
-
|
|
766
|
+
|
|
660
767
|
const runningCount = Object.values(cliStreamExecutions).filter(e => e.status === 'running').length;
|
|
661
|
-
|
|
768
|
+
const totalCount = Object.keys(cliStreamExecutions).length;
|
|
769
|
+
|
|
662
770
|
if (runningCount > 0) {
|
|
663
771
|
badge.textContent = runningCount;
|
|
664
772
|
badge.classList.add('has-running');
|
|
773
|
+
} else if (totalCount > 0) {
|
|
774
|
+
// Show badge for completed executions too (with a different style)
|
|
775
|
+
badge.textContent = totalCount;
|
|
776
|
+
badge.classList.remove('has-running');
|
|
777
|
+
badge.classList.add('has-completed');
|
|
665
778
|
} else {
|
|
666
779
|
badge.textContent = '';
|
|
667
|
-
badge.classList.remove('has-running');
|
|
780
|
+
badge.classList.remove('has-running', 'has-completed');
|
|
668
781
|
}
|
|
669
782
|
}
|
|
670
783
|
|
|
671
784
|
// ===== User Actions =====
|
|
672
785
|
function closeStream(executionId) {
|
|
673
786
|
const exec = cliStreamExecutions[executionId];
|
|
674
|
-
if (!exec
|
|
675
|
-
|
|
787
|
+
if (!exec) return;
|
|
788
|
+
|
|
789
|
+
// Note: We now allow closing running tasks - this just removes from view,
|
|
790
|
+
// the actual CLI process continues on the server
|
|
676
791
|
delete cliStreamExecutions[executionId];
|
|
677
|
-
|
|
792
|
+
|
|
678
793
|
// Switch to another tab if this was active
|
|
679
794
|
if (activeStreamTab === executionId) {
|
|
680
795
|
const remaining = Object.keys(cliStreamExecutions);
|
|
681
796
|
activeStreamTab = remaining.length > 0 ? remaining[0] : null;
|
|
682
797
|
}
|
|
683
|
-
|
|
798
|
+
|
|
684
799
|
renderStreamTabs();
|
|
685
800
|
renderStreamContent(activeStreamTab);
|
|
686
801
|
updateStreamBadge();
|
|
802
|
+
|
|
803
|
+
// If no executions left, close the viewer
|
|
804
|
+
if (Object.keys(cliStreamExecutions).length === 0) {
|
|
805
|
+
toggleCliStreamViewer();
|
|
806
|
+
}
|
|
687
807
|
}
|
|
688
808
|
|
|
689
809
|
function clearCompletedStreams() {
|
|
690
810
|
const toRemove = Object.keys(cliStreamExecutions).filter(
|
|
691
811
|
id => cliStreamExecutions[id].status !== 'running'
|
|
692
812
|
);
|
|
693
|
-
|
|
813
|
+
|
|
694
814
|
toRemove.forEach(id => delete cliStreamExecutions[id]);
|
|
695
|
-
|
|
815
|
+
|
|
696
816
|
// Update active tab if needed
|
|
697
817
|
if (activeStreamTab && !cliStreamExecutions[activeStreamTab]) {
|
|
698
818
|
const remaining = Object.keys(cliStreamExecutions);
|
|
699
819
|
activeStreamTab = remaining.length > 0 ? remaining[0] : null;
|
|
700
820
|
}
|
|
701
|
-
|
|
821
|
+
|
|
702
822
|
renderStreamTabs();
|
|
703
823
|
renderStreamContent(activeStreamTab);
|
|
704
824
|
updateStreamBadge();
|
|
825
|
+
|
|
826
|
+
// If no executions left, close the viewer
|
|
827
|
+
if (Object.keys(cliStreamExecutions).length === 0) {
|
|
828
|
+
toggleCliStreamViewer();
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function clearAllStreams() {
|
|
833
|
+
// Clear all executions (both running and completed)
|
|
834
|
+
const allIds = Object.keys(cliStreamExecutions);
|
|
835
|
+
|
|
836
|
+
allIds.forEach(id => delete cliStreamExecutions[id]);
|
|
837
|
+
activeStreamTab = null;
|
|
838
|
+
|
|
839
|
+
renderStreamTabs();
|
|
840
|
+
renderStreamContent(null);
|
|
841
|
+
updateStreamBadge();
|
|
842
|
+
|
|
843
|
+
// Close the viewer since there's nothing to show
|
|
844
|
+
toggleCliStreamViewer();
|
|
705
845
|
}
|
|
706
846
|
|
|
707
847
|
function toggleAutoScroll() {
|
|
@@ -839,6 +979,7 @@ window.handleCliStreamError = handleCliStreamError;
|
|
|
839
979
|
window.switchStreamTab = switchStreamTab;
|
|
840
980
|
window.closeStream = closeStream;
|
|
841
981
|
window.clearCompletedStreams = clearCompletedStreams;
|
|
982
|
+
window.clearAllStreams = clearAllStreams;
|
|
842
983
|
window.toggleAutoScroll = toggleAutoScroll;
|
|
843
984
|
window.handleSearchInput = handleSearchInput;
|
|
844
985
|
window.clearSearch = clearSearch;
|
|
@@ -183,6 +183,14 @@ function initNavigation() {
|
|
|
183
183
|
} else {
|
|
184
184
|
console.error('renderIssueDiscovery not defined - please refresh the page');
|
|
185
185
|
}
|
|
186
|
+
} else if (currentView === 'loop-monitor') {
|
|
187
|
+
if (typeof renderLoopMonitor === 'function') {
|
|
188
|
+
renderLoopMonitor();
|
|
189
|
+
// Register destroy function for cleanup
|
|
190
|
+
currentViewDestroy = window.destroyLoopMonitor;
|
|
191
|
+
} else {
|
|
192
|
+
console.error('renderLoopMonitor not defined - please refresh the page');
|
|
193
|
+
}
|
|
186
194
|
}
|
|
187
195
|
});
|
|
188
196
|
});
|
|
@@ -231,6 +239,8 @@ function updateContentTitle() {
|
|
|
231
239
|
titleEl.textContent = t('title.issueManager');
|
|
232
240
|
} else if (currentView === 'issue-discovery') {
|
|
233
241
|
titleEl.textContent = t('title.issueDiscovery');
|
|
242
|
+
} else if (currentView === 'loop-monitor') {
|
|
243
|
+
titleEl.textContent = t('title.loopMonitor') || 'Loop Monitor';
|
|
234
244
|
} else if (currentView === 'liteTasks') {
|
|
235
245
|
const names = { 'lite-plan': t('title.litePlanSessions'), 'lite-fix': t('title.liteFixSessions'), 'multi-cli-plan': t('title.multiCliPlanSessions') || 'Multi-CLI Plan Sessions' };
|
|
236
246
|
titleEl.textContent = names[currentLiteType] || t('title.liteTasks');
|
|
@@ -140,6 +140,22 @@ function initWebSocket() {
|
|
|
140
140
|
|
|
141
141
|
wsConnection.onopen = () => {
|
|
142
142
|
console.log('[WS] Connected');
|
|
143
|
+
|
|
144
|
+
// Trigger CLI stream sync on WebSocket reconnection
|
|
145
|
+
// This allows the viewer to recover after page refresh
|
|
146
|
+
if (typeof syncActiveExecutions === 'function') {
|
|
147
|
+
syncActiveExecutions().then(function() {
|
|
148
|
+
console.log('[WS] CLI executions synced after connection');
|
|
149
|
+
}).catch(function(err) {
|
|
150
|
+
console.warn('[WS] Failed to sync CLI executions:', err);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Emit custom event for other components to handle reconnection
|
|
155
|
+
const reconnectEvent = new CustomEvent('websocket-reconnected', {
|
|
156
|
+
detail: { timestamp: Date.now() }
|
|
157
|
+
});
|
|
158
|
+
window.dispatchEvent(reconnectEvent);
|
|
143
159
|
};
|
|
144
160
|
|
|
145
161
|
wsConnection.onmessage = (event) => {
|