Files
Claude-Code-Workflow/ccw/src/templates/dashboard-js/state.js
catlog22 93d3df1e08 feat: add task queue sidebar and resume functionality for CLI sessions
- Implemented task queue sidebar for managing active tasks with filtering options.
- Added functionality to close notification sidebar when opening task queue.
- Enhanced CLI history view to support resuming previous sessions with optional prompts.
- Updated CLI executor to handle resuming sessions for Codex, Gemini, and Qwen tools.
- Introduced utility functions for finding CLI history directories recursively.
- Improved task queue data management and rendering logic.
2025-12-13 11:51:53 +08:00

239 lines
8.8 KiB
JavaScript

// ========================================
// State Management
// ========================================
// Global state variables and template placeholders
// This module must be loaded first as other modules depend on these variables
// ========== Data Placeholders ==========
// These placeholders are replaced by the dashboard generator at build time
let workflowData = {{WORKFLOW_DATA}};
let projectPath = '{{PROJECT_PATH}}';
let recentPaths = {{RECENT_PATHS}};
// ========== Application State ==========
// Current filter for session list view ('all', 'active', 'archived')
let currentFilter = 'all';
// Current lite task type ('lite-plan', 'lite-fix', or null)
let currentLiteType = null;
// Current view mode ('sessions', 'liteTasks', 'project-overview', 'sessionDetail', 'liteTaskDetail')
let currentView = 'sessions';
// Current session detail key (null when not in detail view)
let currentSessionDetailKey = null;
// ========== Data Stores ==========
// Store session data for modal/detail access
// Key: session key, Value: session data object
const sessionDataStore = {};
// Store lite task session data for detail page access
// Key: session key, Value: lite session data object
const liteTaskDataStore = {};
// Store task JSON data in a global map instead of inline script tags
// Key: unique task ID, Value: raw task JSON data
const taskJsonStore = {};
// ========== Global Notification Queue ==========
// Notification queue visible from any view (persisted to localStorage)
const NOTIFICATION_STORAGE_KEY = 'ccw_notifications';
const NOTIFICATION_MAX_STORED = 100;
// Load notifications from localStorage on init
let globalNotificationQueue = loadNotificationsFromStorage();
let isNotificationPanelVisible = false;
/**
* Load notifications from localStorage
* @returns {Array} Notification array
*/
function loadNotificationsFromStorage() {
try {
const stored = localStorage.getItem(NOTIFICATION_STORAGE_KEY);
if (stored) {
const parsed = JSON.parse(stored);
// Filter out notifications older than 7 days
const sevenDaysAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);
return parsed.filter(n => new Date(n.timestamp).getTime() > sevenDaysAgo);
}
} catch (e) {
console.error('[Notifications] Failed to load from storage:', e);
}
return [];
}
/**
* Save notifications to localStorage
*/
function saveNotificationsToStorage() {
try {
// Keep only the last N notifications
const toSave = globalNotificationQueue.slice(0, NOTIFICATION_MAX_STORED);
localStorage.setItem(NOTIFICATION_STORAGE_KEY, JSON.stringify(toSave));
} catch (e) {
console.error('[Notifications] Failed to save to storage:', e);
}
}
// ========== Event Handler ==========
/**
* Handle granular workflow events from CLI
* @param {Object} event - Event object with type, sessionId, payload
*/
function handleWorkflowEvent(event) {
const { type, payload, sessionId, entityId } = event;
switch(type) {
case 'SESSION_CREATED':
// Add to activeSessions array
if (payload) {
const sessionData = {
session_id: sessionId,
...(payload.metadata || { status: 'planning', created_at: new Date().toISOString() }),
location: 'active'
};
// Add to store
const key = `session-${sessionId}`.replace(/[^a-zA-Z0-9-]/g, '-');
sessionDataStore[key] = sessionData;
// Add to workflowData
if (!workflowData.activeSessions) workflowData.activeSessions = [];
workflowData.activeSessions.push(sessionData);
}
break;
case 'SESSION_ARCHIVED':
// Move from active to archived
if (!workflowData.activeSessions) workflowData.activeSessions = [];
if (!workflowData.archivedSessions) workflowData.archivedSessions = [];
const activeIndex = workflowData.activeSessions.findIndex(s => s.session_id === sessionId);
if (activeIndex !== -1) {
const session = workflowData.activeSessions.splice(activeIndex, 1)[0];
session.location = 'archived';
if (payload && payload.metadata) {
Object.assign(session, payload.metadata);
}
workflowData.archivedSessions.push(session);
// Update store
const key = `session-${sessionId}`.replace(/[^a-zA-Z0-9-]/g, '-');
sessionDataStore[key] = session;
}
break;
case 'TASK_UPDATED':
// Find task in session and merge payload
const taskSessionKey = `session-${sessionId}`.replace(/[^a-zA-Z0-9-]/g, '-');
const taskSession = sessionDataStore[taskSessionKey];
if (taskSession && taskSession.tasks) {
const task = taskSession.tasks.find(t => t.task_id === entityId);
if (task && payload) {
Object.assign(task, payload);
}
}
break;
case 'SESSION_UPDATED':
// Update session metadata
const sessionKey = `session-${sessionId}`.replace(/[^a-zA-Z0-9-]/g, '-');
const session = sessionDataStore[sessionKey];
if (session && payload) {
Object.assign(session, payload);
// Update in workflowData arrays
const activeSession = workflowData.activeSessions?.find(s => s.session_id === sessionId);
const archivedSession = workflowData.archivedSessions?.find(s => s.session_id === sessionId);
if (activeSession) Object.assign(activeSession, payload);
if (archivedSession) Object.assign(archivedSession, payload);
}
break;
case 'TASK_CREATED':
// Add new task to session
const tcSessionKey = `session-${sessionId}`.replace(/[^a-zA-Z0-9-]/g, '-');
const tcSession = sessionDataStore[tcSessionKey];
if (tcSession) {
if (!tcSession.tasks) tcSession.tasks = [];
// Check if task already exists (by entityId or task_id in payload)
const taskId = entityId || (payload && payload.task_id);
const existingTask = tcSession.tasks.find(t => t.task_id === taskId);
if (!existingTask && payload) {
tcSession.tasks.push(payload);
}
}
break;
case 'SUMMARY_WRITTEN':
// Update session summary count or mark task as having summary
const swSessionKey = `session-${sessionId}`.replace(/[^a-zA-Z0-9-]/g, '-');
const swSession = sessionDataStore[swSessionKey];
if (swSession) {
if (!swSession.summaries) swSession.summaries = [];
swSession.summaries.push({ task_id: entityId, content: payload });
// Update task status if found
if (swSession.tasks && entityId) {
const task = swSession.tasks.find(t => t.task_id === entityId);
if (task) task.has_summary = true;
}
}
break;
case 'PLAN_UPDATED':
// Update session plan reference
const puSessionKey = `session-${sessionId}`.replace(/[^a-zA-Z0-9-]/g, '-');
const puSession = sessionDataStore[puSessionKey];
if (puSession) {
puSession.has_plan = true;
puSession.plan_updated_at = new Date().toISOString();
}
break;
case 'REVIEW_UPDATED':
// Update session review data
const ruSessionKey = `session-${sessionId}`.replace(/[^a-zA-Z0-9-]/g, '-');
const ruSession = sessionDataStore[ruSessionKey];
if (ruSession) {
if (!ruSession.review) ruSession.review = { dimensions: [], iterations: [], fixes: [] };
// Track review updates by type based on entityId pattern (prevent duplicates)
if (event.contentType === 'review-dim') {
if (!ruSession.review.dimensions.includes(entityId)) ruSession.review.dimensions.push(entityId);
} else if (event.contentType === 'review-iter') {
if (!ruSession.review.iterations.includes(entityId)) ruSession.review.iterations.push(entityId);
} else if (event.contentType === 'review-fix') {
if (!ruSession.review.fixes.includes(entityId)) ruSession.review.fixes.push(entityId);
}
ruSession.has_review = true;
}
break;
case 'CONTENT_WRITTEN':
// Generic content write - just log for debugging
console.log(`[State] Content written: ${event.contentType} for ${sessionId}`);
break;
case 'FILE_DELETED':
// File deleted from session - log and trigger refresh
console.log(`[State] File deleted: ${payload?.file_path || payload?.deleted} from ${sessionId}`);
break;
case 'DIRECTORY_CREATED':
// Directory created in session - log and trigger refresh
console.log(`[State] Directory created: ${payload?.directories?.join(', ') || 'unknown'} in ${sessionId}`);
break;
}
// Trigger UI updates
if (typeof updateStats === 'function') updateStats();
if (typeof updateBadges === 'function') updateBadges();
if (typeof updateCarousel === 'function') updateCarousel();
if (typeof refreshTaskQueue === 'function') refreshTaskQueue();
// Re-render current view if needed
if (currentView === 'sessions' && typeof renderSessions === 'function') {
renderSessions();
}
}