claude-code-workflow 6.3.11 → 6.3.13

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 (33) hide show
  1. package/.claude/CLAUDE.md +33 -33
  2. package/.claude/agents/issue-plan-agent.md +77 -5
  3. package/.claude/agents/issue-queue-agent.md +122 -18
  4. package/.claude/commands/issue/execute.md +53 -40
  5. package/.claude/commands/issue/new.md +113 -11
  6. package/.claude/commands/issue/plan.md +112 -37
  7. package/.claude/commands/issue/queue.md +28 -18
  8. package/.claude/skills/software-manual/scripts/assemble_docsify.py +584 -0
  9. package/.claude/skills/software-manual/templates/css/docsify-base.css +984 -0
  10. package/.claude/skills/software-manual/templates/docsify-shell.html +466 -0
  11. package/.claude/workflows/cli-templates/schemas/issues-jsonl-schema.json +141 -168
  12. package/.claude/workflows/cli-templates/schemas/solution-schema.json +3 -2
  13. package/.codex/prompts/issue-execute.md +3 -3
  14. package/.codex/prompts/issue-queue.md +3 -3
  15. package/ccw/dist/commands/issue.d.ts.map +1 -1
  16. package/ccw/dist/commands/issue.js +2 -1
  17. package/ccw/dist/commands/issue.js.map +1 -1
  18. package/ccw/src/commands/issue.ts +2 -1
  19. package/ccw/src/templates/dashboard-css/33-cli-stream-viewer.css +580 -467
  20. package/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js +532 -461
  21. package/ccw/src/templates/dashboard-js/components/notifications.js +774 -774
  22. package/ccw/src/templates/dashboard-js/i18n.js +4 -0
  23. package/ccw/src/templates/dashboard.html +10 -0
  24. package/ccw/src/tools/claude-cli-tools.ts +388 -388
  25. package/codex-lens/src/codexlens/__pycache__/config.cpython-313.pyc +0 -0
  26. package/codex-lens/src/codexlens/config.py +19 -3
  27. package/codex-lens/src/codexlens/search/__pycache__/ranking.cpython-313.pyc +0 -0
  28. package/codex-lens/src/codexlens/search/ranking.py +15 -4
  29. package/codex-lens/src/codexlens/semantic/__pycache__/vector_store.cpython-313.pyc +0 -0
  30. package/codex-lens/src/codexlens/semantic/vector_store.py +57 -47
  31. package/codex-lens/src/codexlens/storage/__pycache__/registry.cpython-313.pyc +0 -0
  32. package/codex-lens/src/codexlens/storage/registry.py +114 -101
  33. package/package.json +83 -83
@@ -1,774 +1,774 @@
1
- // ==========================================
2
- // NOTIFICATIONS COMPONENT
3
- // ==========================================
4
- // Real-time silent refresh (no notification bubbles)
5
-
6
- /**
7
- * Format JSON object for display in notifications
8
- * Parses JSON strings and formats objects into readable key-value pairs
9
- * @param {Object|string} obj - Object or JSON string to format
10
- * @param {number} maxLen - Max string length (unused, kept for compatibility)
11
- * @returns {string} Formatted string with key: value pairs
12
- */
13
- function formatJsonDetails(obj, maxLen = 150) {
14
- // Handle null/undefined
15
- if (obj === null || obj === undefined) return '';
16
-
17
- // If it is a string, try to parse as JSON
18
- if (typeof obj === 'string') {
19
- // Check if it looks like JSON
20
- const trimmed = obj.trim();
21
- if ((trimmed.startsWith('{') && trimmed.endsWith('}')) ||
22
- (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
23
- try {
24
- obj = JSON.parse(trimmed);
25
- } catch (e) {
26
- // Not valid JSON, return as-is
27
- return obj;
28
- }
29
- } else {
30
- // Plain string, return as-is
31
- return obj;
32
- }
33
- }
34
-
35
- // Handle non-objects (numbers, booleans, etc.)
36
- if (typeof obj !== 'object') return String(obj);
37
-
38
- // Handle arrays
39
- if (Array.isArray(obj)) {
40
- if (obj.length === 0) return '(empty array)';
41
- return obj.slice(0, 5).map((item, i) => {
42
- const itemStr = typeof item === 'object' ? JSON.stringify(item) : String(item);
43
- return `[${i}] ${itemStr.length > 50 ? itemStr.substring(0, 47) + '...' : itemStr}`;
44
- }).join('\n') + (obj.length > 5 ? `\n... +${obj.length - 5} more` : '');
45
- }
46
-
47
- // Handle objects - format as readable key: value pairs
48
- try {
49
- const entries = Object.entries(obj);
50
- if (entries.length === 0) return '(empty object)';
51
-
52
- // Format each entry with proper value display
53
- const lines = entries.slice(0, 8).map(([key, val]) => {
54
- let valStr;
55
- if (val === null) {
56
- valStr = 'null';
57
- } else if (val === undefined) {
58
- valStr = 'undefined';
59
- } else if (typeof val === 'boolean') {
60
- valStr = val ? 'true' : 'false';
61
- } else if (typeof val === 'number') {
62
- valStr = String(val);
63
- } else if (typeof val === 'object') {
64
- valStr = JSON.stringify(val);
65
- if (valStr.length > 40) valStr = valStr.substring(0, 37) + '...';
66
- } else {
67
- valStr = String(val);
68
- if (valStr.length > 50) valStr = valStr.substring(0, 47) + '...';
69
- }
70
- return `${key}: ${valStr}`;
71
- });
72
-
73
- if (entries.length > 8) {
74
- lines.push(`... +${entries.length - 8} more fields`);
75
- }
76
-
77
- return lines.join('\n');
78
- } catch (e) {
79
- // Fallback to stringified version
80
- const str = JSON.stringify(obj);
81
- return str.length > 200 ? str.substring(0, 197) + '...' : str;
82
- }
83
- }
84
-
85
- let wsConnection = null;
86
- let autoRefreshInterval = null;
87
- let lastDataHash = null;
88
- const AUTO_REFRESH_INTERVAL_MS = 30000; // 30 seconds
89
-
90
- // Custom event handlers registry for components to subscribe to specific events
91
- const wsEventHandlers = {};
92
-
93
- /**
94
- * Register a custom handler for a specific WebSocket event type
95
- * @param {string} eventType - The event type to listen for
96
- * @param {Function} handler - The handler function
97
- */
98
- function registerWsEventHandler(eventType, handler) {
99
- if (!wsEventHandlers[eventType]) {
100
- wsEventHandlers[eventType] = [];
101
- }
102
- wsEventHandlers[eventType].push(handler);
103
- }
104
-
105
- /**
106
- * Unregister a custom handler for a specific WebSocket event type
107
- * @param {string} eventType - The event type
108
- * @param {Function} handler - The handler function to remove
109
- */
110
- function unregisterWsEventHandler(eventType, handler) {
111
- if (wsEventHandlers[eventType]) {
112
- wsEventHandlers[eventType] = wsEventHandlers[eventType].filter(h => h !== handler);
113
- }
114
- }
115
-
116
- /**
117
- * Dispatch event to registered handlers
118
- * @param {string} eventType - The event type
119
- * @param {Object} data - The full event data
120
- */
121
- function dispatchToEventHandlers(eventType, data) {
122
- if (wsEventHandlers[eventType]) {
123
- wsEventHandlers[eventType].forEach(handler => {
124
- try {
125
- handler(data);
126
- } catch (e) {
127
- console.error('[WS] Error in custom handler for', eventType, e);
128
- }
129
- });
130
- }
131
- }
132
-
133
- // ========== WebSocket Connection ==========
134
- function initWebSocket() {
135
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
136
- const wsUrl = `${protocol}//${window.location.host}/ws`;
137
-
138
- try {
139
- wsConnection = new WebSocket(wsUrl);
140
-
141
- wsConnection.onopen = () => {
142
- console.log('[WS] Connected');
143
- };
144
-
145
- wsConnection.onmessage = (event) => {
146
- try {
147
- const data = JSON.parse(event.data);
148
- handleNotification(data);
149
- } catch (e) {
150
- console.error('[WS] Failed to parse message:', e);
151
- }
152
- };
153
-
154
- wsConnection.onclose = () => {
155
- console.log('[WS] Disconnected, reconnecting in 5s...');
156
- setTimeout(initWebSocket, 5000);
157
- };
158
-
159
- wsConnection.onerror = (error) => {
160
- console.error('[WS] Error:', error);
161
- };
162
- } catch (e) {
163
- console.log('[WS] WebSocket not available, using polling');
164
- }
165
- }
166
-
167
- // ========== Notification Handler ==========
168
- function handleNotification(data) {
169
- const { type, payload } = data;
170
-
171
- // Silent refresh - no notification bubbles
172
- switch (type) {
173
- case 'session_updated':
174
- case 'summary_written':
175
- case 'task_completed':
176
- case 'new_session':
177
- // Just refresh data silently
178
- refreshIfNeeded();
179
- // Optionally highlight in carousel if it's the current session
180
- if (payload.sessionId && typeof carouselGoTo === 'function') {
181
- carouselGoTo(payload.sessionId);
182
- }
183
- break;
184
-
185
- case 'SESSION_CREATED':
186
- case 'SESSION_ARCHIVED':
187
- case 'TASK_UPDATED':
188
- case 'SESSION_UPDATED':
189
- case 'TASK_CREATED':
190
- case 'SUMMARY_WRITTEN':
191
- case 'PLAN_UPDATED':
192
- case 'REVIEW_UPDATED':
193
- case 'CONTENT_WRITTEN':
194
- case 'FILE_DELETED':
195
- case 'DIRECTORY_CREATED':
196
- // Route to state reducer for granular updates
197
- if (typeof handleWorkflowEvent === 'function') {
198
- handleWorkflowEvent({ type, ...payload });
199
- } else {
200
- // Fallback to full refresh if reducer not available
201
- refreshIfNeeded();
202
- }
203
- break;
204
-
205
- case 'tool_execution':
206
- // Handle tool execution notifications from MCP tools
207
- handleToolExecutionNotification(payload);
208
- break;
209
-
210
- case 'cli_execution':
211
- // Handle CLI command notifications (ccw cli -p)
212
- handleCliCommandNotification(payload);
213
- break;
214
-
215
- // CLI Tool Execution Events
216
- case 'CLI_EXECUTION_STARTED':
217
- if (typeof handleCliExecutionStarted === 'function') {
218
- handleCliExecutionStarted(payload);
219
- }
220
- // Route to CLI Stream Viewer
221
- if (typeof handleCliStreamStarted === 'function') {
222
- handleCliStreamStarted(payload);
223
- }
224
- break;
225
-
226
- case 'CLI_OUTPUT':
227
- if (typeof handleCliOutput === 'function') {
228
- handleCliOutput(payload);
229
- }
230
- // Route to CLI Stream Viewer
231
- if (typeof handleCliStreamOutput === 'function') {
232
- handleCliStreamOutput(payload);
233
- }
234
- break;
235
-
236
- case 'CLI_EXECUTION_COMPLETED':
237
- if (typeof handleCliExecutionCompleted === 'function') {
238
- handleCliExecutionCompleted(payload);
239
- }
240
- // Route to CLI Stream Viewer
241
- if (typeof handleCliStreamCompleted === 'function') {
242
- handleCliStreamCompleted(payload);
243
- }
244
- break;
245
-
246
- case 'CLI_EXECUTION_ERROR':
247
- if (typeof handleCliExecutionError === 'function') {
248
- handleCliExecutionError(payload);
249
- }
250
- // Route to CLI Stream Viewer
251
- if (typeof handleCliStreamError === 'function') {
252
- handleCliStreamError(payload);
253
- }
254
- break;
255
-
256
- // CLI Review Events
257
- case 'CLI_REVIEW_UPDATED':
258
- if (typeof handleCliReviewUpdated === 'function') {
259
- handleCliReviewUpdated(payload);
260
- }
261
- // Also refresh CLI history to show review status
262
- if (typeof refreshCliHistory === 'function') {
263
- refreshCliHistory();
264
- }
265
- break;
266
-
267
- // System Notify Events (from CLI commands)
268
- case 'REFRESH_REQUIRED':
269
- handleRefreshRequired(payload);
270
- break;
271
-
272
- case 'MEMORY_UPDATED':
273
- if (typeof handleMemoryUpdated === 'function') {
274
- handleMemoryUpdated(payload);
275
- }
276
- // Force refresh of memory view
277
- if (typeof loadMemoryStats === 'function') {
278
- loadMemoryStats().then(function() {
279
- if (typeof renderHotspotsColumn === 'function') renderHotspotsColumn();
280
- }).catch(function(err) {
281
- console.error('[Memory] Failed to refresh stats:', err);
282
- });
283
- }
284
- break;
285
-
286
- case 'HISTORY_UPDATED':
287
- // Refresh CLI history when updated externally
288
- if (typeof refreshCliHistory === 'function') {
289
- refreshCliHistory();
290
- }
291
- break;
292
-
293
- case 'INSIGHT_GENERATED':
294
- // Refresh insights when new insight is generated
295
- if (typeof loadInsightsHistory === 'function') {
296
- loadInsightsHistory();
297
- }
298
- break;
299
-
300
- case 'ACTIVE_MEMORY_SYNCED':
301
- // Handle Active Memory sync completion
302
- if (typeof addGlobalNotification === 'function') {
303
- const { filesAnalyzed, tool, usedCli } = payload;
304
- const method = usedCli ? `CLI (${tool})` : 'Basic';
305
- addGlobalNotification(
306
- 'success',
307
- 'Active Memory synced',
308
- {
309
- 'Files Analyzed': filesAnalyzed,
310
- 'Method': method,
311
- 'Timestamp': new Date(payload.timestamp).toLocaleTimeString()
312
- },
313
- 'Memory'
314
- );
315
- }
316
- // Refresh Active Memory status
317
- if (typeof loadActiveMemoryStatus === 'function') {
318
- loadActiveMemoryStatus().catch(function(err) {
319
- console.error('[Active Memory] Failed to refresh status:', err);
320
- });
321
- }
322
- console.log('[Active Memory] Sync completed:', payload);
323
- break;
324
-
325
- case 'CLAUDE_FILE_SYNCED':
326
- // Handle CLAUDE.md file sync completion
327
- if (typeof addGlobalNotification === 'function') {
328
- const { path, level, tool, mode } = payload;
329
- const fileName = path.split(/[/\\]/).pop();
330
- addGlobalNotification(
331
- 'success',
332
- `${fileName} synced`,
333
- {
334
- 'Level': level,
335
- 'Tool': tool,
336
- 'Mode': mode,
337
- 'Time': new Date(payload.timestamp).toLocaleTimeString()
338
- },
339
- 'CLAUDE.md'
340
- );
341
- }
342
- // Refresh file list
343
- if (typeof loadClaudeFiles === 'function') {
344
- loadClaudeFiles().then(() => {
345
- // Re-render the view to show updated content
346
- if (typeof renderClaudeManager === 'function') {
347
- renderClaudeManager();
348
- }
349
- }).catch(err => console.error('[CLAUDE.md] Failed to refresh files:', err));
350
- }
351
- console.log('[CLAUDE.md] Sync completed:', payload);
352
- break;
353
-
354
- case 'CLI_TOOL_INSTALLED':
355
- // Handle CLI tool installation completion
356
- if (typeof addGlobalNotification === 'function') {
357
- const { tool } = payload;
358
- addGlobalNotification(
359
- 'success',
360
- `${tool} installed successfully`,
361
- {
362
- 'Tool': tool,
363
- 'Time': new Date(payload.timestamp).toLocaleTimeString()
364
- },
365
- 'CLI Tools'
366
- );
367
- }
368
- // Refresh CLI manager
369
- if (typeof loadCliToolStatus === 'function') {
370
- loadCliToolStatus().then(() => {
371
- if (typeof renderToolsSection === 'function') {
372
- renderToolsSection();
373
- }
374
- }).catch(err => console.error('[CLI Tools] Failed to refresh status:', err));
375
- }
376
- console.log('[CLI Tools] Installation completed:', payload);
377
- break;
378
-
379
- case 'CLI_TOOL_UNINSTALLED':
380
- // Handle CLI tool uninstallation completion
381
- if (typeof addGlobalNotification === 'function') {
382
- const { tool } = payload;
383
- addGlobalNotification(
384
- 'success',
385
- `${tool} uninstalled successfully`,
386
- {
387
- 'Tool': tool,
388
- 'Time': new Date(payload.timestamp).toLocaleTimeString()
389
- },
390
- 'CLI Tools'
391
- );
392
- }
393
- // Refresh CLI manager
394
- if (typeof loadCliToolStatus === 'function') {
395
- loadCliToolStatus().then(() => {
396
- if (typeof renderToolsSection === 'function') {
397
- renderToolsSection();
398
- }
399
- }).catch(err => console.error('[CLI Tools] Failed to refresh status:', err));
400
- }
401
- console.log('[CLI Tools] Uninstallation completed:', payload);
402
- break;
403
-
404
- case 'CODEXLENS_INSTALLED':
405
- // Handle CodexLens installation completion
406
- if (typeof addGlobalNotification === 'function') {
407
- const { version } = payload;
408
- addGlobalNotification(
409
- 'success',
410
- `CodexLens installed successfully`,
411
- {
412
- 'Version': version || 'latest',
413
- 'Time': new Date(payload.timestamp).toLocaleTimeString()
414
- },
415
- 'CodexLens'
416
- );
417
- }
418
- // Refresh CLI status if active
419
- if (typeof loadCodexLensStatus === 'function') {
420
- loadCodexLensStatus().then(() => {
421
- if (typeof renderCliStatus === 'function') {
422
- renderCliStatus();
423
- }
424
- });
425
- }
426
- console.log('[CodexLens] Installation completed:', payload);
427
- break;
428
-
429
- case 'CODEXLENS_UNINSTALLED':
430
- // Handle CodexLens uninstallation completion
431
- if (typeof addGlobalNotification === 'function') {
432
- addGlobalNotification(
433
- 'success',
434
- `CodexLens uninstalled successfully`,
435
- {
436
- 'Time': new Date(payload.timestamp).toLocaleTimeString()
437
- },
438
- 'CodexLens'
439
- );
440
- }
441
- // Refresh CLI status if active
442
- if (typeof loadCodexLensStatus === 'function') {
443
- loadCodexLensStatus().then(() => {
444
- if (typeof renderCliStatus === 'function') {
445
- renderCliStatus();
446
- }
447
- });
448
- }
449
- console.log('[CodexLens] Uninstallation completed:', payload);
450
- break;
451
-
452
- case 'CODEXLENS_INDEX_PROGRESS':
453
- // Handle CodexLens index progress updates
454
- dispatchToEventHandlers('CODEXLENS_INDEX_PROGRESS', data);
455
- console.log('[CodexLens] Index progress:', payload.stage, payload.percent + '%');
456
- break;
457
-
458
- default:
459
- console.log('[WS] Unknown notification type:', type);
460
- }
461
- }
462
-
463
- /**
464
- * Handle tool execution notifications from MCP tools
465
- * @param {Object} payload - Tool execution payload
466
- */
467
- function handleToolExecutionNotification(payload) {
468
- const { toolName, status, params, result, error, timestamp } = payload;
469
-
470
- // Determine notification type and message
471
- let notifType = 'info';
472
- let message = `Tool: ${toolName}`;
473
- let details = null;
474
-
475
- switch (status) {
476
- case 'started':
477
- notifType = 'info';
478
- message = `Executing ${toolName}...`;
479
- // Pass raw object for HTML formatting
480
- if (params) {
481
- details = params;
482
- }
483
- break;
484
-
485
- case 'completed':
486
- notifType = 'success';
487
- message = `${toolName} completed`;
488
- // Pass raw object for HTML formatting
489
- if (result) {
490
- if (result._truncated) {
491
- details = result.preview;
492
- } else {
493
- details = result;
494
- }
495
- }
496
- break;
497
-
498
- case 'failed':
499
- notifType = 'error';
500
- message = `${toolName} failed`;
501
- details = error || 'Unknown error';
502
- break;
503
-
504
- default:
505
- notifType = 'info';
506
- message = `${toolName}: ${status}`;
507
- }
508
-
509
- // Add to global notifications - pass objects directly for HTML formatting
510
- if (typeof addGlobalNotification === 'function') {
511
- addGlobalNotification(notifType, message, details, 'MCP');
512
- }
513
-
514
- // Log to console
515
- console.log(`[MCP] ${status}: ${toolName}`, payload);
516
- }
517
-
518
- /**
519
- * Handle CLI command notifications (ccw cli -p)
520
- * @param {Object} payload - CLI execution payload
521
- */
522
- function handleCliCommandNotification(payload) {
523
- const { event, tool, mode, prompt_preview, execution_id, success, duration_ms, status, error, turn_count, custom_id } = payload;
524
-
525
- let notifType = 'info';
526
- let message = '';
527
- let details = null;
528
-
529
- switch (event) {
530
- case 'started':
531
- notifType = 'info';
532
- message = `CLI ${tool} started`;
533
- // Pass structured object for rich display
534
- details = {
535
- mode: mode,
536
- prompt: prompt_preview
537
- };
538
- if (custom_id) {
539
- details.id = custom_id;
540
- }
541
- break;
542
-
543
- case 'completed':
544
- if (success) {
545
- notifType = 'success';
546
- const turnStr = turn_count > 1 ? ` (turn ${turn_count})` : '';
547
- message = `CLI ${tool} completed${turnStr}`;
548
- // Pass structured object for rich display
549
- details = {
550
- duration: duration_ms ? `${(duration_ms / 1000).toFixed(1)}s` : '-',
551
- execution_id: execution_id
552
- };
553
- if (turn_count > 1) {
554
- details.turns = turn_count;
555
- }
556
- } else {
557
- notifType = 'error';
558
- message = `CLI ${tool} failed`;
559
- details = {
560
- status: status || 'Unknown error',
561
- execution_id: execution_id
562
- };
563
- }
564
- break;
565
-
566
- case 'error':
567
- notifType = 'error';
568
- message = `CLI ${tool} error`;
569
- details = error || 'Unknown error';
570
- break;
571
-
572
- default:
573
- notifType = 'info';
574
- message = `CLI ${tool}: ${event}`;
575
- }
576
-
577
- // Add to global notifications - pass objects for HTML formatting
578
- if (typeof addGlobalNotification === 'function') {
579
- addGlobalNotification(notifType, message, details, 'CLI');
580
- }
581
-
582
- // Refresh CLI history if on history view
583
- if (event === 'completed' && typeof currentView !== 'undefined' &&
584
- (currentView === 'history' || currentView === 'cli-history')) {
585
- if (typeof loadCliHistory === 'function' && typeof renderCliHistoryView === 'function') {
586
- loadCliHistory().then(() => renderCliHistoryView());
587
- }
588
- }
589
-
590
- // Log to console
591
- console.log(`[CLI Command] ${event}: ${tool}`, payload);
592
- }
593
-
594
- // ========== Auto Refresh ==========
595
- function initAutoRefresh() {
596
- // Calculate initial hash
597
- lastDataHash = calculateDataHash();
598
-
599
- // Start polling interval
600
- autoRefreshInterval = setInterval(checkForChanges, AUTO_REFRESH_INTERVAL_MS);
601
- }
602
-
603
- function calculateDataHash() {
604
- if (!workflowData) return null;
605
-
606
- // Simple hash based on key data points
607
- const hashData = {
608
- activeSessions: (workflowData.activeSessions || []).length,
609
- archivedSessions: (workflowData.archivedSessions || []).length,
610
- totalTasks: workflowData.statistics?.totalTasks || 0,
611
- completedTasks: workflowData.statistics?.completedTasks || 0,
612
- generatedAt: workflowData.generatedAt
613
- };
614
-
615
- return JSON.stringify(hashData);
616
- }
617
-
618
- async function checkForChanges() {
619
- if (!window.SERVER_MODE) return;
620
-
621
- try {
622
- const response = await fetch(`/api/data?path=${encodeURIComponent(projectPath)}`);
623
- if (!response.ok) return;
624
-
625
- const newData = await response.json();
626
- const newHash = JSON.stringify({
627
- activeSessions: (newData.activeSessions || []).length,
628
- archivedSessions: (newData.archivedSessions || []).length,
629
- totalTasks: newData.statistics?.totalTasks || 0,
630
- completedTasks: newData.statistics?.completedTasks || 0,
631
- generatedAt: newData.generatedAt
632
- });
633
-
634
- if (newHash !== lastDataHash) {
635
- lastDataHash = newHash;
636
- // Silent refresh - no notification
637
- await refreshWorkspaceData(newData);
638
- }
639
- } catch (e) {
640
- console.error('[AutoRefresh] Check failed:', e);
641
- }
642
- }
643
-
644
- async function refreshIfNeeded() {
645
- if (!window.SERVER_MODE) return;
646
-
647
- try {
648
- const response = await fetch(`/api/data?path=${encodeURIComponent(projectPath)}`);
649
- if (!response.ok) return;
650
-
651
- const newData = await response.json();
652
- await refreshWorkspaceData(newData);
653
- } catch (e) {
654
- console.error('[Refresh] Failed:', e);
655
- }
656
- }
657
-
658
- async function refreshWorkspaceData(newData) {
659
- // Update global data
660
- window.workflowData = newData;
661
-
662
- // Clear and repopulate stores
663
- Object.keys(sessionDataStore).forEach(k => delete sessionDataStore[k]);
664
- Object.keys(liteTaskDataStore).forEach(k => delete liteTaskDataStore[k]);
665
-
666
- [...(newData.activeSessions || []), ...(newData.archivedSessions || [])].forEach(s => {
667
- const key = `session-${s.session_id}`.replace(/[^a-zA-Z0-9-]/g, '-');
668
- sessionDataStore[key] = s;
669
- });
670
-
671
- [...(newData.liteTasks?.litePlan || []), ...(newData.liteTasks?.liteFix || [])].forEach(s => {
672
- const key = `lite-${s.session_id}`.replace(/[^a-zA-Z0-9-]/g, '-');
673
- liteTaskDataStore[key] = s;
674
- });
675
-
676
- // Update UI silently
677
- updateStats();
678
- updateBadges();
679
- updateCarousel();
680
-
681
- // Re-render current view if needed
682
- if (currentView === 'sessions') {
683
- renderSessions();
684
- } else if (currentView === 'liteTasks') {
685
- renderLiteTasks();
686
- }
687
-
688
- lastDataHash = calculateDataHash();
689
- }
690
-
691
- /**
692
- * Handle REFRESH_REQUIRED events from CLI commands
693
- * @param {Object} payload - Contains scope (memory|history|insights|all)
694
- */
695
- function handleRefreshRequired(payload) {
696
- const scope = payload?.scope || 'all';
697
- console.log('[WS] Refresh required for scope:', scope);
698
-
699
- switch (scope) {
700
- case 'memory':
701
- // Refresh memory stats and graph
702
- if (typeof loadMemoryStats === 'function') {
703
- loadMemoryStats().then(function() {
704
- if (typeof renderHotspotsColumn === 'function') renderHotspotsColumn();
705
- });
706
- }
707
- if (typeof loadMemoryGraph === 'function') {
708
- loadMemoryGraph();
709
- }
710
- break;
711
-
712
- case 'history':
713
- // Refresh CLI history
714
- if (typeof refreshCliHistory === 'function') {
715
- refreshCliHistory();
716
- }
717
- break;
718
-
719
- case 'insights':
720
- // Refresh insights history
721
- if (typeof loadInsightsHistory === 'function') {
722
- loadInsightsHistory();
723
- }
724
- break;
725
-
726
- case 'all':
727
- default:
728
- // Refresh everything
729
- refreshIfNeeded();
730
- if (typeof loadMemoryStats === 'function') {
731
- loadMemoryStats().then(function() {
732
- if (typeof renderHotspotsColumn === 'function') renderHotspotsColumn();
733
- });
734
- }
735
- if (typeof refreshCliHistory === 'function') {
736
- refreshCliHistory();
737
- }
738
- if (typeof loadInsightsHistory === 'function') {
739
- loadInsightsHistory();
740
- }
741
- break;
742
- }
743
- }
744
-
745
- // ========== Cleanup ==========
746
- function stopAutoRefresh() {
747
- if (autoRefreshInterval) {
748
- clearInterval(autoRefreshInterval);
749
- autoRefreshInterval = null;
750
- }
751
- }
752
-
753
- function closeWebSocket() {
754
- if (wsConnection) {
755
- wsConnection.close();
756
- wsConnection = null;
757
- }
758
- }
759
-
760
- // ========== Navigation Helper ==========
761
- function goToSession(sessionId) {
762
- // Find session in carousel and navigate
763
- const sessionKey = `session-${sessionId}`.replace(/[^a-zA-Z0-9-]/g, '-');
764
-
765
- // Jump to session in carousel if visible
766
- if (typeof carouselGoTo === 'function') {
767
- carouselGoTo(sessionId);
768
- }
769
-
770
- // Navigate to session detail
771
- if (sessionDataStore[sessionKey]) {
772
- showSessionDetailPage(sessionKey);
773
- }
774
- }
1
+ // ==========================================
2
+ // NOTIFICATIONS COMPONENT
3
+ // ==========================================
4
+ // Real-time silent refresh (no notification bubbles)
5
+
6
+ /**
7
+ * Format JSON object for display in notifications
8
+ * Parses JSON strings and formats objects into readable key-value pairs
9
+ * @param {Object|string} obj - Object or JSON string to format
10
+ * @param {number} maxLen - Max string length (unused, kept for compatibility)
11
+ * @returns {string} Formatted string with key: value pairs
12
+ */
13
+ function formatJsonDetails(obj, maxLen = 150) {
14
+ // Handle null/undefined
15
+ if (obj === null || obj === undefined) return '';
16
+
17
+ // If it is a string, try to parse as JSON
18
+ if (typeof obj === 'string') {
19
+ // Check if it looks like JSON
20
+ const trimmed = obj.trim();
21
+ if ((trimmed.startsWith('{') && trimmed.endsWith('}')) ||
22
+ (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
23
+ try {
24
+ obj = JSON.parse(trimmed);
25
+ } catch (e) {
26
+ // Not valid JSON, return as-is
27
+ return obj;
28
+ }
29
+ } else {
30
+ // Plain string, return as-is
31
+ return obj;
32
+ }
33
+ }
34
+
35
+ // Handle non-objects (numbers, booleans, etc.)
36
+ if (typeof obj !== 'object') return String(obj);
37
+
38
+ // Handle arrays
39
+ if (Array.isArray(obj)) {
40
+ if (obj.length === 0) return '(empty array)';
41
+ return obj.slice(0, 5).map((item, i) => {
42
+ const itemStr = typeof item === 'object' ? JSON.stringify(item) : String(item);
43
+ return `[${i}] ${itemStr.length > 50 ? itemStr.substring(0, 47) + '...' : itemStr}`;
44
+ }).join('\n') + (obj.length > 5 ? `\n... +${obj.length - 5} more` : '');
45
+ }
46
+
47
+ // Handle objects - format as readable key: value pairs
48
+ try {
49
+ const entries = Object.entries(obj);
50
+ if (entries.length === 0) return '(empty object)';
51
+
52
+ // Format each entry with proper value display
53
+ const lines = entries.slice(0, 8).map(([key, val]) => {
54
+ let valStr;
55
+ if (val === null) {
56
+ valStr = 'null';
57
+ } else if (val === undefined) {
58
+ valStr = 'undefined';
59
+ } else if (typeof val === 'boolean') {
60
+ valStr = val ? 'true' : 'false';
61
+ } else if (typeof val === 'number') {
62
+ valStr = String(val);
63
+ } else if (typeof val === 'object') {
64
+ valStr = JSON.stringify(val);
65
+ if (valStr.length > 40) valStr = valStr.substring(0, 37) + '...';
66
+ } else {
67
+ valStr = String(val);
68
+ if (valStr.length > 50) valStr = valStr.substring(0, 47) + '...';
69
+ }
70
+ return `${key}: ${valStr}`;
71
+ });
72
+
73
+ if (entries.length > 8) {
74
+ lines.push(`... +${entries.length - 8} more fields`);
75
+ }
76
+
77
+ return lines.join('\n');
78
+ } catch (e) {
79
+ // Fallback to stringified version
80
+ const str = JSON.stringify(obj);
81
+ return str.length > 200 ? str.substring(0, 197) + '...' : str;
82
+ }
83
+ }
84
+
85
+ let wsConnection = null;
86
+ let autoRefreshInterval = null;
87
+ let lastDataHash = null;
88
+ const AUTO_REFRESH_INTERVAL_MS = 30000; // 30 seconds
89
+
90
+ // Custom event handlers registry for components to subscribe to specific events
91
+ const wsEventHandlers = {};
92
+
93
+ /**
94
+ * Register a custom handler for a specific WebSocket event type
95
+ * @param {string} eventType - The event type to listen for
96
+ * @param {Function} handler - The handler function
97
+ */
98
+ function registerWsEventHandler(eventType, handler) {
99
+ if (!wsEventHandlers[eventType]) {
100
+ wsEventHandlers[eventType] = [];
101
+ }
102
+ wsEventHandlers[eventType].push(handler);
103
+ }
104
+
105
+ /**
106
+ * Unregister a custom handler for a specific WebSocket event type
107
+ * @param {string} eventType - The event type
108
+ * @param {Function} handler - The handler function to remove
109
+ */
110
+ function unregisterWsEventHandler(eventType, handler) {
111
+ if (wsEventHandlers[eventType]) {
112
+ wsEventHandlers[eventType] = wsEventHandlers[eventType].filter(h => h !== handler);
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Dispatch event to registered handlers
118
+ * @param {string} eventType - The event type
119
+ * @param {Object} data - The full event data
120
+ */
121
+ function dispatchToEventHandlers(eventType, data) {
122
+ if (wsEventHandlers[eventType]) {
123
+ wsEventHandlers[eventType].forEach(handler => {
124
+ try {
125
+ handler(data);
126
+ } catch (e) {
127
+ console.error('[WS] Error in custom handler for', eventType, e);
128
+ }
129
+ });
130
+ }
131
+ }
132
+
133
+ // ========== WebSocket Connection ==========
134
+ function initWebSocket() {
135
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
136
+ const wsUrl = `${protocol}//${window.location.host}/ws`;
137
+
138
+ try {
139
+ wsConnection = new WebSocket(wsUrl);
140
+
141
+ wsConnection.onopen = () => {
142
+ console.log('[WS] Connected');
143
+ };
144
+
145
+ wsConnection.onmessage = (event) => {
146
+ try {
147
+ const data = JSON.parse(event.data);
148
+ handleNotification(data);
149
+ } catch (e) {
150
+ console.error('[WS] Failed to parse message:', e);
151
+ }
152
+ };
153
+
154
+ wsConnection.onclose = () => {
155
+ console.log('[WS] Disconnected, reconnecting in 5s...');
156
+ setTimeout(initWebSocket, 5000);
157
+ };
158
+
159
+ wsConnection.onerror = (error) => {
160
+ console.error('[WS] Error:', error);
161
+ };
162
+ } catch (e) {
163
+ console.log('[WS] WebSocket not available, using polling');
164
+ }
165
+ }
166
+
167
+ // ========== Notification Handler ==========
168
+ function handleNotification(data) {
169
+ const { type, payload } = data;
170
+
171
+ // Silent refresh - no notification bubbles
172
+ switch (type) {
173
+ case 'session_updated':
174
+ case 'summary_written':
175
+ case 'task_completed':
176
+ case 'new_session':
177
+ // Just refresh data silently
178
+ refreshIfNeeded();
179
+ // Optionally highlight in carousel if it's the current session
180
+ if (payload.sessionId && typeof carouselGoTo === 'function') {
181
+ carouselGoTo(payload.sessionId);
182
+ }
183
+ break;
184
+
185
+ case 'SESSION_CREATED':
186
+ case 'SESSION_ARCHIVED':
187
+ case 'TASK_UPDATED':
188
+ case 'SESSION_UPDATED':
189
+ case 'TASK_CREATED':
190
+ case 'SUMMARY_WRITTEN':
191
+ case 'PLAN_UPDATED':
192
+ case 'REVIEW_UPDATED':
193
+ case 'CONTENT_WRITTEN':
194
+ case 'FILE_DELETED':
195
+ case 'DIRECTORY_CREATED':
196
+ // Route to state reducer for granular updates
197
+ if (typeof handleWorkflowEvent === 'function') {
198
+ handleWorkflowEvent({ type, ...payload });
199
+ } else {
200
+ // Fallback to full refresh if reducer not available
201
+ refreshIfNeeded();
202
+ }
203
+ break;
204
+
205
+ case 'tool_execution':
206
+ // Handle tool execution notifications from MCP tools
207
+ handleToolExecutionNotification(payload);
208
+ break;
209
+
210
+ case 'cli_execution':
211
+ // Handle CLI command notifications (ccw cli -p)
212
+ handleCliCommandNotification(payload);
213
+ break;
214
+
215
+ // CLI Tool Execution Events
216
+ case 'CLI_EXECUTION_STARTED':
217
+ if (typeof handleCliExecutionStarted === 'function') {
218
+ handleCliExecutionStarted(payload);
219
+ }
220
+ // Route to CLI Stream Viewer
221
+ if (typeof handleCliStreamStarted === 'function') {
222
+ handleCliStreamStarted(payload);
223
+ }
224
+ break;
225
+
226
+ case 'CLI_OUTPUT':
227
+ if (typeof handleCliOutput === 'function') {
228
+ handleCliOutput(payload);
229
+ }
230
+ // Route to CLI Stream Viewer
231
+ if (typeof handleCliStreamOutput === 'function') {
232
+ handleCliStreamOutput(payload);
233
+ }
234
+ break;
235
+
236
+ case 'CLI_EXECUTION_COMPLETED':
237
+ if (typeof handleCliExecutionCompleted === 'function') {
238
+ handleCliExecutionCompleted(payload);
239
+ }
240
+ // Route to CLI Stream Viewer
241
+ if (typeof handleCliStreamCompleted === 'function') {
242
+ handleCliStreamCompleted(payload);
243
+ }
244
+ break;
245
+
246
+ case 'CLI_EXECUTION_ERROR':
247
+ if (typeof handleCliExecutionError === 'function') {
248
+ handleCliExecutionError(payload);
249
+ }
250
+ // Route to CLI Stream Viewer
251
+ if (typeof handleCliStreamError === 'function') {
252
+ handleCliStreamError(payload);
253
+ }
254
+ break;
255
+
256
+ // CLI Review Events
257
+ case 'CLI_REVIEW_UPDATED':
258
+ if (typeof handleCliReviewUpdated === 'function') {
259
+ handleCliReviewUpdated(payload);
260
+ }
261
+ // Also refresh CLI history to show review status
262
+ if (typeof refreshCliHistory === 'function') {
263
+ refreshCliHistory();
264
+ }
265
+ break;
266
+
267
+ // System Notify Events (from CLI commands)
268
+ case 'REFRESH_REQUIRED':
269
+ handleRefreshRequired(payload);
270
+ break;
271
+
272
+ case 'MEMORY_UPDATED':
273
+ if (typeof handleMemoryUpdated === 'function') {
274
+ handleMemoryUpdated(payload);
275
+ }
276
+ // Force refresh of memory view
277
+ if (typeof loadMemoryStats === 'function') {
278
+ loadMemoryStats().then(function() {
279
+ if (typeof renderHotspotsColumn === 'function') renderHotspotsColumn();
280
+ }).catch(function(err) {
281
+ console.error('[Memory] Failed to refresh stats:', err);
282
+ });
283
+ }
284
+ break;
285
+
286
+ case 'HISTORY_UPDATED':
287
+ // Refresh CLI history when updated externally
288
+ if (typeof refreshCliHistory === 'function') {
289
+ refreshCliHistory();
290
+ }
291
+ break;
292
+
293
+ case 'INSIGHT_GENERATED':
294
+ // Refresh insights when new insight is generated
295
+ if (typeof loadInsightsHistory === 'function') {
296
+ loadInsightsHistory();
297
+ }
298
+ break;
299
+
300
+ case 'ACTIVE_MEMORY_SYNCED':
301
+ // Handle Active Memory sync completion
302
+ if (typeof addGlobalNotification === 'function') {
303
+ const { filesAnalyzed, tool, usedCli } = payload;
304
+ const method = usedCli ? `CLI (${tool})` : 'Basic';
305
+ addGlobalNotification(
306
+ 'success',
307
+ 'Active Memory synced',
308
+ {
309
+ 'Files Analyzed': filesAnalyzed,
310
+ 'Method': method,
311
+ 'Timestamp': new Date(payload.timestamp).toLocaleTimeString()
312
+ },
313
+ 'Memory'
314
+ );
315
+ }
316
+ // Refresh Active Memory status
317
+ if (typeof loadActiveMemoryStatus === 'function') {
318
+ loadActiveMemoryStatus().catch(function(err) {
319
+ console.error('[Active Memory] Failed to refresh status:', err);
320
+ });
321
+ }
322
+ console.log('[Active Memory] Sync completed:', payload);
323
+ break;
324
+
325
+ case 'CLAUDE_FILE_SYNCED':
326
+ // Handle CLAUDE.md file sync completion
327
+ if (typeof addGlobalNotification === 'function') {
328
+ const { path, level, tool, mode } = payload;
329
+ const fileName = path.split(/[/\\]/).pop();
330
+ addGlobalNotification(
331
+ 'success',
332
+ `${fileName} synced`,
333
+ {
334
+ 'Level': level,
335
+ 'Tool': tool,
336
+ 'Mode': mode,
337
+ 'Time': new Date(payload.timestamp).toLocaleTimeString()
338
+ },
339
+ 'CLAUDE.md'
340
+ );
341
+ }
342
+ // Refresh file list
343
+ if (typeof loadClaudeFiles === 'function') {
344
+ loadClaudeFiles().then(() => {
345
+ // Re-render the view to show updated content
346
+ if (typeof renderClaudeManager === 'function') {
347
+ renderClaudeManager();
348
+ }
349
+ }).catch(err => console.error('[CLAUDE.md] Failed to refresh files:', err));
350
+ }
351
+ console.log('[CLAUDE.md] Sync completed:', payload);
352
+ break;
353
+
354
+ case 'CLI_TOOL_INSTALLED':
355
+ // Handle CLI tool installation completion
356
+ if (typeof addGlobalNotification === 'function') {
357
+ const { tool } = payload;
358
+ addGlobalNotification(
359
+ 'success',
360
+ `${tool} installed successfully`,
361
+ {
362
+ 'Tool': tool,
363
+ 'Time': new Date(payload.timestamp).toLocaleTimeString()
364
+ },
365
+ 'CLI Tools'
366
+ );
367
+ }
368
+ // Refresh CLI manager
369
+ if (typeof loadCliToolStatus === 'function') {
370
+ loadCliToolStatus().then(() => {
371
+ if (typeof renderToolsSection === 'function') {
372
+ renderToolsSection();
373
+ }
374
+ }).catch(err => console.error('[CLI Tools] Failed to refresh status:', err));
375
+ }
376
+ console.log('[CLI Tools] Installation completed:', payload);
377
+ break;
378
+
379
+ case 'CLI_TOOL_UNINSTALLED':
380
+ // Handle CLI tool uninstallation completion
381
+ if (typeof addGlobalNotification === 'function') {
382
+ const { tool } = payload;
383
+ addGlobalNotification(
384
+ 'success',
385
+ `${tool} uninstalled successfully`,
386
+ {
387
+ 'Tool': tool,
388
+ 'Time': new Date(payload.timestamp).toLocaleTimeString()
389
+ },
390
+ 'CLI Tools'
391
+ );
392
+ }
393
+ // Refresh CLI manager
394
+ if (typeof loadCliToolStatus === 'function') {
395
+ loadCliToolStatus().then(() => {
396
+ if (typeof renderToolsSection === 'function') {
397
+ renderToolsSection();
398
+ }
399
+ }).catch(err => console.error('[CLI Tools] Failed to refresh status:', err));
400
+ }
401
+ console.log('[CLI Tools] Uninstallation completed:', payload);
402
+ break;
403
+
404
+ case 'CODEXLENS_INSTALLED':
405
+ // Handle CodexLens installation completion
406
+ if (typeof addGlobalNotification === 'function') {
407
+ const { version } = payload;
408
+ addGlobalNotification(
409
+ 'success',
410
+ `CodexLens installed successfully`,
411
+ {
412
+ 'Version': version || 'latest',
413
+ 'Time': new Date(payload.timestamp).toLocaleTimeString()
414
+ },
415
+ 'CodexLens'
416
+ );
417
+ }
418
+ // Refresh CLI status if active
419
+ if (typeof loadCodexLensStatus === 'function') {
420
+ loadCodexLensStatus().then(() => {
421
+ if (typeof renderCliStatus === 'function') {
422
+ renderCliStatus();
423
+ }
424
+ });
425
+ }
426
+ console.log('[CodexLens] Installation completed:', payload);
427
+ break;
428
+
429
+ case 'CODEXLENS_UNINSTALLED':
430
+ // Handle CodexLens uninstallation completion
431
+ if (typeof addGlobalNotification === 'function') {
432
+ addGlobalNotification(
433
+ 'success',
434
+ `CodexLens uninstalled successfully`,
435
+ {
436
+ 'Time': new Date(payload.timestamp).toLocaleTimeString()
437
+ },
438
+ 'CodexLens'
439
+ );
440
+ }
441
+ // Refresh CLI status if active
442
+ if (typeof loadCodexLensStatus === 'function') {
443
+ loadCodexLensStatus().then(() => {
444
+ if (typeof renderCliStatus === 'function') {
445
+ renderCliStatus();
446
+ }
447
+ });
448
+ }
449
+ console.log('[CodexLens] Uninstallation completed:', payload);
450
+ break;
451
+
452
+ case 'CODEXLENS_INDEX_PROGRESS':
453
+ // Handle CodexLens index progress updates
454
+ dispatchToEventHandlers('CODEXLENS_INDEX_PROGRESS', data);
455
+ console.log('[CodexLens] Index progress:', payload.stage, payload.percent + '%');
456
+ break;
457
+
458
+ default:
459
+ console.log('[WS] Unknown notification type:', type);
460
+ }
461
+ }
462
+
463
+ /**
464
+ * Handle tool execution notifications from MCP tools
465
+ * @param {Object} payload - Tool execution payload
466
+ */
467
+ function handleToolExecutionNotification(payload) {
468
+ const { toolName, status, params, result, error, timestamp } = payload;
469
+
470
+ // Determine notification type and message
471
+ let notifType = 'info';
472
+ let message = `Tool: ${toolName}`;
473
+ let details = null;
474
+
475
+ switch (status) {
476
+ case 'started':
477
+ notifType = 'info';
478
+ message = `Executing ${toolName}...`;
479
+ // Pass raw object for HTML formatting
480
+ if (params) {
481
+ details = params;
482
+ }
483
+ break;
484
+
485
+ case 'completed':
486
+ notifType = 'success';
487
+ message = `${toolName} completed`;
488
+ // Pass raw object for HTML formatting
489
+ if (result) {
490
+ if (result._truncated) {
491
+ details = result.preview;
492
+ } else {
493
+ details = result;
494
+ }
495
+ }
496
+ break;
497
+
498
+ case 'failed':
499
+ notifType = 'error';
500
+ message = `${toolName} failed`;
501
+ details = error || 'Unknown error';
502
+ break;
503
+
504
+ default:
505
+ notifType = 'info';
506
+ message = `${toolName}: ${status}`;
507
+ }
508
+
509
+ // Add to global notifications - pass objects directly for HTML formatting
510
+ if (typeof addGlobalNotification === 'function') {
511
+ addGlobalNotification(notifType, message, details, 'MCP');
512
+ }
513
+
514
+ // Log to console
515
+ console.log(`[MCP] ${status}: ${toolName}`, payload);
516
+ }
517
+
518
+ /**
519
+ * Handle CLI command notifications (ccw cli -p)
520
+ * @param {Object} payload - CLI execution payload
521
+ */
522
+ function handleCliCommandNotification(payload) {
523
+ const { event, tool, mode, prompt_preview, execution_id, success, duration_ms, status, error, turn_count, custom_id } = payload;
524
+
525
+ let notifType = 'info';
526
+ let message = '';
527
+ let details = null;
528
+
529
+ switch (event) {
530
+ case 'started':
531
+ notifType = 'info';
532
+ message = `CLI ${tool} started`;
533
+ // Pass structured object for rich display
534
+ details = {
535
+ mode: mode,
536
+ prompt: prompt_preview
537
+ };
538
+ if (custom_id) {
539
+ details.id = custom_id;
540
+ }
541
+ break;
542
+
543
+ case 'completed':
544
+ if (success) {
545
+ notifType = 'success';
546
+ const turnStr = turn_count > 1 ? ` (turn ${turn_count})` : '';
547
+ message = `CLI ${tool} completed${turnStr}`;
548
+ // Pass structured object for rich display
549
+ details = {
550
+ duration: duration_ms ? `${(duration_ms / 1000).toFixed(1)}s` : '-',
551
+ execution_id: execution_id
552
+ };
553
+ if (turn_count > 1) {
554
+ details.turns = turn_count;
555
+ }
556
+ } else {
557
+ notifType = 'error';
558
+ message = `CLI ${tool} failed`;
559
+ details = {
560
+ status: status || 'Unknown error',
561
+ execution_id: execution_id
562
+ };
563
+ }
564
+ break;
565
+
566
+ case 'error':
567
+ notifType = 'error';
568
+ message = `CLI ${tool} error`;
569
+ details = error || 'Unknown error';
570
+ break;
571
+
572
+ default:
573
+ notifType = 'info';
574
+ message = `CLI ${tool}: ${event}`;
575
+ }
576
+
577
+ // Add to global notifications - pass objects for HTML formatting
578
+ if (typeof addGlobalNotification === 'function') {
579
+ addGlobalNotification(notifType, message, details, 'CLI');
580
+ }
581
+
582
+ // Refresh CLI history if on history view
583
+ if (event === 'completed' && typeof currentView !== 'undefined' &&
584
+ (currentView === 'history' || currentView === 'cli-history')) {
585
+ if (typeof loadCliHistory === 'function' && typeof renderCliHistoryView === 'function') {
586
+ loadCliHistory().then(() => renderCliHistoryView());
587
+ }
588
+ }
589
+
590
+ // Log to console
591
+ console.log(`[CLI Command] ${event}: ${tool}`, payload);
592
+ }
593
+
594
+ // ========== Auto Refresh ==========
595
+ function initAutoRefresh() {
596
+ // Calculate initial hash
597
+ lastDataHash = calculateDataHash();
598
+
599
+ // Start polling interval
600
+ autoRefreshInterval = setInterval(checkForChanges, AUTO_REFRESH_INTERVAL_MS);
601
+ }
602
+
603
+ function calculateDataHash() {
604
+ if (!workflowData) return null;
605
+
606
+ // Simple hash based on key data points
607
+ const hashData = {
608
+ activeSessions: (workflowData.activeSessions || []).length,
609
+ archivedSessions: (workflowData.archivedSessions || []).length,
610
+ totalTasks: workflowData.statistics?.totalTasks || 0,
611
+ completedTasks: workflowData.statistics?.completedTasks || 0,
612
+ generatedAt: workflowData.generatedAt
613
+ };
614
+
615
+ return JSON.stringify(hashData);
616
+ }
617
+
618
+ async function checkForChanges() {
619
+ if (!window.SERVER_MODE) return;
620
+
621
+ try {
622
+ const response = await fetch(`/api/data?path=${encodeURIComponent(projectPath)}`);
623
+ if (!response.ok) return;
624
+
625
+ const newData = await response.json();
626
+ const newHash = JSON.stringify({
627
+ activeSessions: (newData.activeSessions || []).length,
628
+ archivedSessions: (newData.archivedSessions || []).length,
629
+ totalTasks: newData.statistics?.totalTasks || 0,
630
+ completedTasks: newData.statistics?.completedTasks || 0,
631
+ generatedAt: newData.generatedAt
632
+ });
633
+
634
+ if (newHash !== lastDataHash) {
635
+ lastDataHash = newHash;
636
+ // Silent refresh - no notification
637
+ await refreshWorkspaceData(newData);
638
+ }
639
+ } catch (e) {
640
+ console.error('[AutoRefresh] Check failed:', e);
641
+ }
642
+ }
643
+
644
+ async function refreshIfNeeded() {
645
+ if (!window.SERVER_MODE) return;
646
+
647
+ try {
648
+ const response = await fetch(`/api/data?path=${encodeURIComponent(projectPath)}`);
649
+ if (!response.ok) return;
650
+
651
+ const newData = await response.json();
652
+ await refreshWorkspaceData(newData);
653
+ } catch (e) {
654
+ console.error('[Refresh] Failed:', e);
655
+ }
656
+ }
657
+
658
+ async function refreshWorkspaceData(newData) {
659
+ // Update global data
660
+ window.workflowData = newData;
661
+
662
+ // Clear and repopulate stores
663
+ Object.keys(sessionDataStore).forEach(k => delete sessionDataStore[k]);
664
+ Object.keys(liteTaskDataStore).forEach(k => delete liteTaskDataStore[k]);
665
+
666
+ [...(newData.activeSessions || []), ...(newData.archivedSessions || [])].forEach(s => {
667
+ const key = `session-${s.session_id}`.replace(/[^a-zA-Z0-9-]/g, '-');
668
+ sessionDataStore[key] = s;
669
+ });
670
+
671
+ [...(newData.liteTasks?.litePlan || []), ...(newData.liteTasks?.liteFix || [])].forEach(s => {
672
+ const key = `lite-${s.session_id}`.replace(/[^a-zA-Z0-9-]/g, '-');
673
+ liteTaskDataStore[key] = s;
674
+ });
675
+
676
+ // Update UI silently
677
+ updateStats();
678
+ updateBadges();
679
+ updateCarousel();
680
+
681
+ // Re-render current view if needed
682
+ if (currentView === 'sessions') {
683
+ renderSessions();
684
+ } else if (currentView === 'liteTasks') {
685
+ renderLiteTasks();
686
+ }
687
+
688
+ lastDataHash = calculateDataHash();
689
+ }
690
+
691
+ /**
692
+ * Handle REFRESH_REQUIRED events from CLI commands
693
+ * @param {Object} payload - Contains scope (memory|history|insights|all)
694
+ */
695
+ function handleRefreshRequired(payload) {
696
+ const scope = payload?.scope || 'all';
697
+ console.log('[WS] Refresh required for scope:', scope);
698
+
699
+ switch (scope) {
700
+ case 'memory':
701
+ // Refresh memory stats and graph
702
+ if (typeof loadMemoryStats === 'function') {
703
+ loadMemoryStats().then(function() {
704
+ if (typeof renderHotspotsColumn === 'function') renderHotspotsColumn();
705
+ });
706
+ }
707
+ if (typeof loadMemoryGraph === 'function') {
708
+ loadMemoryGraph();
709
+ }
710
+ break;
711
+
712
+ case 'history':
713
+ // Refresh CLI history
714
+ if (typeof refreshCliHistory === 'function') {
715
+ refreshCliHistory();
716
+ }
717
+ break;
718
+
719
+ case 'insights':
720
+ // Refresh insights history
721
+ if (typeof loadInsightsHistory === 'function') {
722
+ loadInsightsHistory();
723
+ }
724
+ break;
725
+
726
+ case 'all':
727
+ default:
728
+ // Refresh everything
729
+ refreshIfNeeded();
730
+ if (typeof loadMemoryStats === 'function') {
731
+ loadMemoryStats().then(function() {
732
+ if (typeof renderHotspotsColumn === 'function') renderHotspotsColumn();
733
+ });
734
+ }
735
+ if (typeof refreshCliHistory === 'function') {
736
+ refreshCliHistory();
737
+ }
738
+ if (typeof loadInsightsHistory === 'function') {
739
+ loadInsightsHistory();
740
+ }
741
+ break;
742
+ }
743
+ }
744
+
745
+ // ========== Cleanup ==========
746
+ function stopAutoRefresh() {
747
+ if (autoRefreshInterval) {
748
+ clearInterval(autoRefreshInterval);
749
+ autoRefreshInterval = null;
750
+ }
751
+ }
752
+
753
+ function closeWebSocket() {
754
+ if (wsConnection) {
755
+ wsConnection.close();
756
+ wsConnection = null;
757
+ }
758
+ }
759
+
760
+ // ========== Navigation Helper ==========
761
+ function goToSession(sessionId) {
762
+ // Find session in carousel and navigate
763
+ const sessionKey = `session-${sessionId}`.replace(/[^a-zA-Z0-9-]/g, '-');
764
+
765
+ // Jump to session in carousel if visible
766
+ if (typeof carouselGoTo === 'function') {
767
+ carouselGoTo(sessionId);
768
+ }
769
+
770
+ // Navigate to session detail
771
+ if (sessionDataStore[sessionKey]) {
772
+ showSessionDetailPage(sessionKey);
773
+ }
774
+ }