mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-09 02:24:11 +08:00
feat(ccw): add session manager tool with auto workspace detection
- Add session_manager tool for workflow session lifecycle management - Add ccw session CLI command with subcommands: - list, init, status, task, stats, delete, read, write, update, archive, mkdir - Implement auto workspace detection (traverse up to find .workflow) - Implement auto session location detection (active, archived, lite-plan, lite-fix) - Add dashboard notifications for tool executions via WebSocket - Add granular event types (SESSION_CREATED, TASK_UPDATED, etc.) - Add status_history auto-tracking for task status changes - Update workflow session commands to document ccw session usage 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -60,11 +60,91 @@ function handleNotification(data) {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'SESSION_CREATED':
|
||||
case 'SESSION_ARCHIVED':
|
||||
case 'TASK_UPDATED':
|
||||
case 'SESSION_UPDATED':
|
||||
case 'TASK_CREATED':
|
||||
case 'SUMMARY_WRITTEN':
|
||||
case 'PLAN_UPDATED':
|
||||
case 'REVIEW_UPDATED':
|
||||
case 'CONTENT_WRITTEN':
|
||||
// Route to state reducer for granular updates
|
||||
if (typeof handleWorkflowEvent === 'function') {
|
||||
handleWorkflowEvent({ type, ...payload });
|
||||
} else {
|
||||
// Fallback to full refresh if reducer not available
|
||||
refreshIfNeeded();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'tool_execution':
|
||||
// Handle tool execution notifications from CLI
|
||||
handleToolExecutionNotification(payload);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('[WS] Unknown notification type:', type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle tool execution notifications from CLI
|
||||
* @param {Object} payload - Tool execution payload
|
||||
*/
|
||||
function handleToolExecutionNotification(payload) {
|
||||
const { toolName, status, params, result, error, timestamp } = payload;
|
||||
|
||||
// Determine notification type and message
|
||||
let notifType = 'info';
|
||||
let message = `Tool: ${toolName}`;
|
||||
let details = null;
|
||||
|
||||
switch (status) {
|
||||
case 'started':
|
||||
notifType = 'info';
|
||||
message = `Executing ${toolName}...`;
|
||||
if (params) {
|
||||
// Show truncated params
|
||||
const paramStr = JSON.stringify(params);
|
||||
details = paramStr.length > 100 ? paramStr.substring(0, 100) + '...' : paramStr;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'completed':
|
||||
notifType = 'success';
|
||||
message = `${toolName} completed`;
|
||||
if (result) {
|
||||
// Show truncated result
|
||||
if (result._truncated) {
|
||||
details = result.preview;
|
||||
} else {
|
||||
const resultStr = JSON.stringify(result);
|
||||
details = resultStr.length > 150 ? resultStr.substring(0, 150) + '...' : resultStr;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'failed':
|
||||
notifType = 'error';
|
||||
message = `${toolName} failed`;
|
||||
details = error || 'Unknown error';
|
||||
break;
|
||||
|
||||
default:
|
||||
notifType = 'info';
|
||||
message = `${toolName}: ${status}`;
|
||||
}
|
||||
|
||||
// Add to global notifications
|
||||
if (typeof addGlobalNotification === 'function') {
|
||||
addGlobalNotification(notifType, message, details, 'CLI');
|
||||
}
|
||||
|
||||
// Log to console
|
||||
console.log(`[CLI] ${status}: ${toolName}`, payload);
|
||||
}
|
||||
|
||||
// ========== Auto Refresh ==========
|
||||
function initAutoRefresh() {
|
||||
// Calculate initial hash
|
||||
|
||||
@@ -39,4 +39,153 @@ const taskJsonStore = {};
|
||||
// ========== Global Notification Queue ==========
|
||||
// Notification queue visible from any view
|
||||
let globalNotificationQueue = [];
|
||||
let isNotificationPanelVisible = false;
|
||||
let isNotificationPanelVisible = false;
|
||||
// ========== 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;
|
||||
}
|
||||
|
||||
// Trigger UI updates
|
||||
if (typeof updateStats === 'function') updateStats();
|
||||
if (typeof updateBadges === 'function') updateBadges();
|
||||
if (typeof updateCarousel === 'function') updateCarousel();
|
||||
|
||||
// Re-render current view if needed
|
||||
if (currentView === 'sessions' && typeof renderSessions === 'function') {
|
||||
renderSessions();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user