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.
Files changed (134) hide show
  1. package/.claude/commands/workflow/lite-fix.md +108 -9
  2. package/.claude/skills/ccw-loop/README.md +303 -0
  3. package/.claude/skills/ccw-loop/SKILL.md +259 -0
  4. package/.claude/skills/ccw-loop/phases/actions/action-complete.md +320 -0
  5. package/.claude/skills/ccw-loop/phases/actions/action-debug-with-file.md +485 -0
  6. package/.claude/skills/ccw-loop/phases/actions/action-develop-with-file.md +365 -0
  7. package/.claude/skills/ccw-loop/phases/actions/action-init.md +200 -0
  8. package/.claude/skills/ccw-loop/phases/actions/action-menu.md +192 -0
  9. package/.claude/skills/ccw-loop/phases/actions/action-validate-with-file.md +307 -0
  10. package/.claude/skills/ccw-loop/phases/orchestrator.md +486 -0
  11. package/.claude/skills/ccw-loop/phases/state-schema.md +474 -0
  12. package/.claude/skills/ccw-loop/specs/action-catalog.md +300 -0
  13. package/.claude/skills/ccw-loop/specs/loop-requirements.md +192 -0
  14. package/.claude/skills/ccw-loop/templates/progress-template.md +175 -0
  15. package/.claude/skills/ccw-loop/templates/understanding-template.md +303 -0
  16. package/.claude/skills/ccw-loop/templates/validation-template.md +258 -0
  17. package/ccw/dist/cli.d.ts.map +1 -1
  18. package/ccw/dist/cli.js +8 -1
  19. package/ccw/dist/cli.js.map +1 -1
  20. package/ccw/dist/commands/cli.d.ts.map +1 -1
  21. package/ccw/dist/commands/cli.js +14 -1
  22. package/ccw/dist/commands/cli.js.map +1 -1
  23. package/ccw/dist/commands/loop.d.ts +10 -0
  24. package/ccw/dist/commands/loop.d.ts.map +1 -0
  25. package/ccw/dist/commands/loop.js +289 -0
  26. package/ccw/dist/commands/loop.js.map +1 -0
  27. package/ccw/dist/core/dashboard-generator.d.ts.map +1 -1
  28. package/ccw/dist/core/dashboard-generator.js +4 -1
  29. package/ccw/dist/core/dashboard-generator.js.map +1 -1
  30. package/ccw/dist/core/routes/claude-routes.d.ts.map +1 -1
  31. package/ccw/dist/core/routes/claude-routes.js +5 -3
  32. package/ccw/dist/core/routes/claude-routes.js.map +1 -1
  33. package/ccw/dist/core/routes/cli-routes.d.ts +6 -0
  34. package/ccw/dist/core/routes/cli-routes.d.ts.map +1 -1
  35. package/ccw/dist/core/routes/cli-routes.js +42 -13
  36. package/ccw/dist/core/routes/cli-routes.js.map +1 -1
  37. package/ccw/dist/core/routes/cli-settings-routes.d.ts.map +1 -1
  38. package/ccw/dist/core/routes/cli-settings-routes.js +44 -0
  39. package/ccw/dist/core/routes/cli-settings-routes.js.map +1 -1
  40. package/ccw/dist/core/routes/codexlens/semantic-handlers.d.ts.map +1 -1
  41. package/ccw/dist/core/routes/codexlens/semantic-handlers.js +3 -2
  42. package/ccw/dist/core/routes/codexlens/semantic-handlers.js.map +1 -1
  43. package/ccw/dist/core/routes/core-memory-routes.d.ts.map +1 -1
  44. package/ccw/dist/core/routes/core-memory-routes.js +4 -2
  45. package/ccw/dist/core/routes/core-memory-routes.js.map +1 -1
  46. package/ccw/dist/core/routes/files-routes.d.ts.map +1 -1
  47. package/ccw/dist/core/routes/files-routes.js +4 -2
  48. package/ccw/dist/core/routes/files-routes.js.map +1 -1
  49. package/ccw/dist/core/routes/loop-routes.d.ts +24 -0
  50. package/ccw/dist/core/routes/loop-routes.d.ts.map +1 -0
  51. package/ccw/dist/core/routes/loop-routes.js +334 -0
  52. package/ccw/dist/core/routes/loop-routes.js.map +1 -0
  53. package/ccw/dist/core/routes/loop-v2-routes.d.ts +35 -0
  54. package/ccw/dist/core/routes/loop-v2-routes.d.ts.map +1 -0
  55. package/ccw/dist/core/routes/loop-v2-routes.js +1208 -0
  56. package/ccw/dist/core/routes/loop-v2-routes.js.map +1 -0
  57. package/ccw/dist/core/routes/memory-routes.d.ts.map +1 -1
  58. package/ccw/dist/core/routes/memory-routes.js +2 -1
  59. package/ccw/dist/core/routes/memory-routes.js.map +1 -1
  60. package/ccw/dist/core/routes/task-routes.d.ts +12 -0
  61. package/ccw/dist/core/routes/task-routes.d.ts.map +1 -0
  62. package/ccw/dist/core/routes/task-routes.js +321 -0
  63. package/ccw/dist/core/routes/task-routes.js.map +1 -0
  64. package/ccw/dist/core/routes/test-loop-routes.d.ts +11 -0
  65. package/ccw/dist/core/routes/test-loop-routes.d.ts.map +1 -0
  66. package/ccw/dist/core/routes/test-loop-routes.js +298 -0
  67. package/ccw/dist/core/routes/test-loop-routes.js.map +1 -0
  68. package/ccw/dist/core/server.d.ts.map +1 -1
  69. package/ccw/dist/core/server.js +43 -3
  70. package/ccw/dist/core/server.js.map +1 -1
  71. package/ccw/dist/core/websocket.d.ts +59 -0
  72. package/ccw/dist/core/websocket.d.ts.map +1 -1
  73. package/ccw/dist/core/websocket.js +34 -0
  74. package/ccw/dist/core/websocket.js.map +1 -1
  75. package/ccw/dist/tools/claude-cli-tools.d.ts +40 -0
  76. package/ccw/dist/tools/claude-cli-tools.d.ts.map +1 -1
  77. package/ccw/dist/tools/claude-cli-tools.js +119 -0
  78. package/ccw/dist/tools/claude-cli-tools.js.map +1 -1
  79. package/ccw/dist/tools/loop-manager.d.ts +84 -0
  80. package/ccw/dist/tools/loop-manager.d.ts.map +1 -0
  81. package/ccw/dist/tools/loop-manager.js +425 -0
  82. package/ccw/dist/tools/loop-manager.js.map +1 -0
  83. package/ccw/dist/tools/loop-state-manager.d.ts +47 -0
  84. package/ccw/dist/tools/loop-state-manager.d.ts.map +1 -0
  85. package/ccw/dist/tools/loop-state-manager.js +149 -0
  86. package/ccw/dist/tools/loop-state-manager.js.map +1 -0
  87. package/ccw/dist/tools/loop-task-manager.d.ts +138 -0
  88. package/ccw/dist/tools/loop-task-manager.d.ts.map +1 -0
  89. package/ccw/dist/tools/loop-task-manager.js +270 -0
  90. package/ccw/dist/tools/loop-task-manager.js.map +1 -0
  91. package/ccw/dist/types/index.d.ts +1 -0
  92. package/ccw/dist/types/index.d.ts.map +1 -1
  93. package/ccw/dist/types/index.js +1 -0
  94. package/ccw/dist/types/index.js.map +1 -1
  95. package/ccw/dist/types/loop.d.ts +257 -0
  96. package/ccw/dist/types/loop.d.ts.map +1 -0
  97. package/ccw/dist/types/loop.js +17 -0
  98. package/ccw/dist/types/loop.js.map +1 -0
  99. package/ccw/src/cli.ts +9 -1
  100. package/ccw/src/commands/cli.ts +14 -1
  101. package/ccw/src/commands/loop.ts +344 -0
  102. package/ccw/src/core/dashboard-generator.ts +4 -1
  103. package/ccw/src/core/routes/claude-routes.ts +5 -3
  104. package/ccw/src/core/routes/cli-routes.ts +47 -15
  105. package/ccw/src/core/routes/cli-settings-routes.ts +47 -0
  106. package/ccw/src/core/routes/codexlens/semantic-handlers.ts +3 -2
  107. package/ccw/src/core/routes/core-memory-routes.ts +4 -2
  108. package/ccw/src/core/routes/files-routes.ts +4 -2
  109. package/ccw/src/core/routes/loop-routes.ts +386 -0
  110. package/ccw/src/core/routes/loop-v2-routes.ts +1412 -0
  111. package/ccw/src/core/routes/memory-routes.ts +2 -1
  112. package/ccw/src/core/routes/task-routes.ts +361 -0
  113. package/ccw/src/core/routes/test-loop-routes.ts +312 -0
  114. package/ccw/src/core/server.ts +44 -3
  115. package/ccw/src/core/websocket.ts +104 -0
  116. package/ccw/src/templates/dashboard-css/12-cli-legacy.css +56 -0
  117. package/ccw/src/templates/dashboard-css/33-cli-stream-viewer.css +55 -0
  118. package/ccw/src/templates/dashboard-css/36-loop-monitor.css +1896 -0
  119. package/ccw/src/templates/dashboard-css/36-loop-monitor.css.backup +1877 -0
  120. package/ccw/src/templates/dashboard-js/components/cli-status.js +64 -3
  121. package/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js +251 -110
  122. package/ccw/src/templates/dashboard-js/components/navigation.js +10 -0
  123. package/ccw/src/templates/dashboard-js/components/notifications.js +16 -0
  124. package/ccw/src/templates/dashboard-js/i18n.js +475 -1
  125. package/ccw/src/templates/dashboard-js/views/cli-manager.js +3 -2
  126. package/ccw/src/templates/dashboard-js/views/loop-monitor.js +3244 -0
  127. package/ccw/src/templates/dashboard.html +20 -2
  128. package/ccw/src/tools/claude-cli-tools.ts +143 -0
  129. package/ccw/src/tools/loop-manager.ts +519 -0
  130. package/ccw/src/tools/loop-state-manager.ts +173 -0
  131. package/ccw/src/tools/loop-task-manager.ts +380 -0
  132. package/ccw/src/types/index.ts +1 -0
  133. package/ccw/src/types/loop.ts +316 -0
  134. 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
- <button class="btn-icon" onclick="refreshAllCliStatus()" title="Refresh">
775
- <i data-lucide="refresh-cw" class="w-4 h-4"></i>
776
- </button>
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
- try {
31
- const response = await fetch('/api/cli/active');
32
- if (!response.ok) return;
33
-
34
- const { executions } = await response.json();
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
- if (existing) {
59
- // Already tracked by WebSocket events - merge historical output
60
- // Only prepend historical lines that are not already in the output
61
- // (WebSocket events only add NEW output, so historical output should come before)
62
- const existingContentSet = new Set(existing.output.map(o => o.content));
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
- if (missingLines.length > 0) {
66
- // Find the system start message index (skip it when prepending)
67
- const systemMsgIndex = existing.output.findIndex(o => o.type === 'system');
68
- const insertIndex = systemMsgIndex >= 0 ? systemMsgIndex + 1 : 0;
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
- // Prepend missing historical lines after system message
71
- existing.output.splice(insertIndex, 0, ...missingLines);
63
+ // Race between fetch and timeout
64
+ const response = await Promise.race([
65
+ fetch('/api/cli/active'),
66
+ timeoutPromise
67
+ ]);
72
68
 
73
- // Trim if too long
74
- if (existing.output.length > MAX_OUTPUT_LINES) {
75
- existing.output = existing.output.slice(-MAX_OUTPUT_LINES);
76
- }
69
+ if (!response.ok) {
70
+ console.warn('[CLI Stream] Sync response not OK:', response.status);
71
+ return;
72
+ }
77
73
 
78
- needsUiUpdate = true;
79
- console.log(`[CLI Stream] Merged ${missingLines.length} historical lines for ${exec.id}`);
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 = true;
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
- // New execution - rebuild full state
87
- cliStreamExecutions[exec.id] = {
88
- tool: exec.tool || 'cli',
89
- mode: exec.mode || 'analysis',
90
- output: [],
91
- status: exec.status || 'running',
92
- startTime: exec.startTime || Date.now(),
93
- endTime: null
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
- // Add system start message
97
- cliStreamExecutions[exec.id].output.push({
98
- type: 'system',
99
- content: `[${new Date(exec.startTime).toLocaleTimeString()}] CLI execution started: ${exec.tool} (${exec.mode} mode)`,
100
- timestamp: exec.startTime
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
- // Add historical output
104
- cliStreamExecutions[exec.id].output.push(...historicalLines);
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
- // Update UI if we recovered or merged any executions
108
- if (needsUiUpdate) {
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
- renderStreamTabs();
116
- updateStreamBadge();
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
- // If viewer is open, render content. If not, and there's a running execution, open it.
119
- if (isCliStreamViewerOpen) {
120
- renderStreamContent(activeStreamTab);
121
- } else if (executions.some(e => e.status === 'running')) {
122
- // Automatically open the viewer if it's closed and we just synced a running task
123
- toggleCliStreamViewer();
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
- console.log(`[CLI Stream] Synced ${executions.length} active execution(s)`);
128
- } catch (e) {
129
- console.error('[CLI Stream] Sync failed:', e);
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 canClose = exec.status !== 'running';
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
- <button class="cli-stream-tab-close ${canClose ? '' : 'disabled'}"
615
+ ${recoveryBadge}
616
+ <button class="cli-stream-tab-close"
515
617
  onclick="event.stopPropagation(); closeStream('${id}')"
516
- title="${canClose ? _streamT('cliStream.close') : _streamT('cliStream.cannotCloseRunning')}"
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 || exec.status === 'running') return;
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) => {