feat: enhance workflow session management with status updates and CSS styling

This commit is contained in:
catlog22
2025-12-10 10:01:07 +08:00
parent 5114a942dc
commit 417f3c0f8c
8 changed files with 143 additions and 27 deletions

View File

@@ -108,7 +108,7 @@ export function run(argv) {
// Tool command
program
.command('tool [subcommand] [args]')
.command('tool [subcommand] [args...]')
.description('Execute CCW tools')
.option('--path <path>', 'File path (for edit_file)')
.option('--old <text>', 'Old text to replace (for edit_file)')

View File

@@ -68,11 +68,15 @@ async function schemaAction(options) {
/**
* Execute a tool with given parameters
* @param {string} toolName - Tool name
* @param {string|undefined} jsonParams - JSON string of parameters
* @param {Object} options - CLI options (--path, --old, --new for edit_file)
*/
async function execAction(toolName, options) {
async function execAction(toolName, jsonParams, options) {
if (!toolName) {
console.error(chalk.red('Tool name is required'));
console.error(chalk.gray('Usage: ccw tool exec edit_file --path file.txt --old "old" --new "new"'));
console.error(chalk.gray('Usage: ccw tool exec <tool_name> \'{"param": "value"}\''));
console.error(chalk.gray(' ccw tool exec edit_file --path file.txt --old "old" --new "new"'));
process.exit(1);
}
@@ -83,10 +87,20 @@ async function execAction(toolName, options) {
process.exit(1);
}
// Build params from CLI options
const params = {};
// Build params from CLI options or JSON
let params = {};
if (toolName === 'edit_file') {
// Check if JSON params provided
if (jsonParams && jsonParams.trim().startsWith('{')) {
try {
params = JSON.parse(jsonParams);
} catch (e) {
console.error(chalk.red('Invalid JSON parameters'));
console.error(chalk.gray(`Parse error: ${e.message}`));
process.exit(1);
}
} else if (toolName === 'edit_file') {
// Legacy support for edit_file with --path, --old, --new options
if (!options.path || !options.old || !options.new) {
console.error(chalk.red('edit_file requires --path, --old, and --new parameters'));
console.error(chalk.gray('Usage: ccw tool exec edit_file --path file.txt --old "old text" --new "new text"'));
@@ -95,11 +109,13 @@ async function execAction(toolName, options) {
params.path = options.path;
params.oldText = options.old;
params.newText = options.new;
} else {
console.error(chalk.red(`Tool "${toolName}" is not supported via CLI parameters`));
console.error(chalk.gray('Currently only edit_file is supported'));
} else if (jsonParams) {
// Non-JSON string provided but not for edit_file
console.error(chalk.red('Parameters must be valid JSON'));
console.error(chalk.gray(`Usage: ccw tool exec ${toolName} '{"param": "value"}'`));
process.exit(1);
}
// If no params provided, use empty object (tool may have defaults)
// Execute tool
const result = await executeTool(toolName, params);
@@ -110,18 +126,24 @@ async function execAction(toolName, options) {
/**
* Tool command entry point
* @param {string} subcommand - Subcommand (list, schema, exec)
* @param {string[]} args - Arguments array [toolName, jsonParams, ...]
* @param {Object} options - CLI options
*/
export async function toolCommand(subcommand, args, options) {
// args is now an array due to [args...] in cli.js
const argsArray = Array.isArray(args) ? args : (args ? [args] : []);
// Handle subcommands
switch (subcommand) {
case 'list':
await listAction();
break;
case 'schema':
await schemaAction({ name: args });
await schemaAction({ name: argsArray[0] });
break;
case 'exec':
await execAction(args, options);
await execAction(argsArray[0], argsArray[1], options);
break;
default:
console.log(chalk.bold.cyan('\nCCW Tool System\n'));
@@ -133,6 +155,7 @@ export async function toolCommand(subcommand, args, options) {
console.log('Usage:');
console.log(chalk.gray(' ccw tool list'));
console.log(chalk.gray(' ccw tool schema edit_file'));
console.log(chalk.gray(' ccw tool exec <tool_name> \'{"param": "value"}\''));
console.log(chalk.gray(' ccw tool exec edit_file --path file.txt --old "old text" --new "new text"'));
}
}

View File

@@ -60,6 +60,28 @@
color: hsl(var(--muted-foreground));
}
.session-status.planning {
background: hsl(260 70% 90%);
color: hsl(260 70% 45%);
animation: planning-pulse 2s ease-in-out infinite;
}
@keyframes planning-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
/* Planning card style - dashed border with subtle animation */
.session-card.planning {
border: 2px dashed hsl(260 70% 60%);
background: linear-gradient(135deg, hsl(var(--card)) 0%, hsl(260 70% 97%) 100%);
}
.session-card.planning:hover {
border-color: hsl(260 70% 50%);
box-shadow: 0 4px 12px hsl(260 70% 50% / 0.2);
}
.session-type-badge {
font-size: 0.65rem;
font-weight: 500;

View File

@@ -98,27 +98,48 @@ function renderSessionCard(session) {
const isActive = session._isActive !== false;
const date = session.created_at;
// Detect planning status from session.status field
const isPlanning = session.status === 'planning';
// Get session type badge
const sessionType = session.type || 'workflow';
const typeBadge = sessionType !== 'workflow' ? `<span class="session-type-badge ${sessionType}">${sessionType}</span>` : '';
// Determine status badge class and text
// Priority: archived > planning > active
let statusClass, statusText;
if (!isActive) {
// Archived sessions always show as ARCHIVED regardless of status field
statusClass = 'archived';
statusText = 'ARCHIVED';
} else if (isPlanning) {
statusClass = 'planning';
statusText = 'PLANNING';
} else {
statusClass = 'active';
statusText = 'ACTIVE';
}
// Store session data for modal
const sessionKey = `session-${session.session_id}`.replace(/[^a-zA-Z0-9-]/g, '-');
sessionDataStore[sessionKey] = session;
// Special rendering for review sessions
if (sessionType === 'review') {
return renderReviewSessionCard(session, sessionKey, typeBadge, isActive, date);
return renderReviewSessionCard(session, sessionKey, typeBadge, isActive, isPlanning, date);
}
// Card class includes planning modifier for special styling (only for active sessions)
const cardClass = isActive && isPlanning ? 'session-card planning' : 'session-card';
return `
<div class="session-card" onclick="showSessionDetailPage('${sessionKey}')">
<div class="${cardClass}" onclick="showSessionDetailPage('${sessionKey}')">
<div class="session-header">
<div class="session-title">${escapeHtml(session.session_id || 'Unknown')}</div>
<div class="session-badges">
${typeBadge}
<span class="session-status ${isActive ? 'active' : 'archived'}">
${isActive ? 'ACTIVE' : 'ARCHIVED'}
<span class="session-status ${statusClass}">
${statusText}
</span>
</div>
</div>
@@ -127,7 +148,7 @@ function renderSessionCard(session) {
<span class="session-meta-item"><i data-lucide="calendar" class="w-3.5 h-3.5 inline mr-1"></i>${formatDate(date)}</span>
<span class="session-meta-item"><i data-lucide="list-checks" class="w-3.5 h-3.5 inline mr-1"></i>${taskCount} tasks</span>
</div>
${taskCount > 0 ? `
${taskCount > 0 && !isPlanning ? `
<div class="progress-container">
<span class="progress-label">Progress</span>
<div class="progress-bar-wrapper">
@@ -144,7 +165,7 @@ function renderSessionCard(session) {
}
// Special card rendering for review sessions
function renderReviewSessionCard(session, sessionKey, typeBadge, isActive, date) {
function renderReviewSessionCard(session, sessionKey, typeBadge, isActive, isPlanning, date) {
// Calculate findings stats from reviewDimensions
const dimensions = session.reviewDimensions || [];
let totalFindings = 0;
@@ -162,14 +183,31 @@ function renderReviewSessionCard(session, sessionKey, typeBadge, isActive, date)
lowCount += findings.filter(f => f.severity === 'low').length;
});
// Determine status badge class and text
// Priority: archived > planning > active
let statusClass, statusText;
if (!isActive) {
statusClass = 'archived';
statusText = 'ARCHIVED';
} else if (isPlanning) {
statusClass = 'planning';
statusText = 'PLANNING';
} else {
statusClass = 'active';
statusText = 'ACTIVE';
}
// Card class includes planning modifier for special styling (only for active sessions)
const cardClass = isActive && isPlanning ? 'session-card planning' : 'session-card';
return `
<div class="session-card" onclick="showSessionDetailPage('${sessionKey}')">
<div class="${cardClass}" onclick="showSessionDetailPage('${sessionKey}')">
<div class="session-header">
<div class="session-title">${escapeHtml(session.session_id || 'Unknown')}</div>
<div class="session-badges">
${typeBadge}
<span class="session-status ${isActive ? 'active' : 'archived'}">
${isActive ? 'ACTIVE' : 'ARCHIVED'}
<span class="session-status ${statusClass}">
${statusText}
</span>
</div>
</div>