diff --git a/ccw/src/core/lite-scanner.ts b/ccw/src/core/lite-scanner.ts index 7fa4b35f..dbd3c93e 100644 --- a/ccw/src/core/lite-scanner.ts +++ b/ccw/src/core/lite-scanner.ts @@ -238,10 +238,11 @@ async function scanMultiCliDir(dir: string): Promise { .map(async (entry) => { const sessionPath = join(dir, entry.name); - const [createdAt, syntheses, sessionState] = await Promise.all([ + const [createdAt, syntheses, sessionState, planJson] = await Promise.all([ getCreatedTime(sessionPath), loadRoundSyntheses(sessionPath), loadSessionState(sessionPath), + loadPlanJson(sessionPath), ]); // Extract data from syntheses @@ -258,13 +259,20 @@ async function scanMultiCliDir(dir: string): Promise { const status = sessionState?.status || (latestSynthesis?.convergence?.recommendation === 'converged' ? 'converged' : 'analyzing'); + // Use plan.json if available, otherwise extract from synthesis + const plan = planJson || latestSynthesis; + // Use tasks from plan.json if available, otherwise extract from synthesis + const tasks = (planJson as any)?.tasks?.length > 0 + ? normalizePlanJsonTasks((planJson as any).tasks) + : extractTasksFromSyntheses(syntheses); + const session: MultiCliSession = { id: entry.name, type: 'multi-cli-plan', path: sessionPath, createdAt, - plan: latestSynthesis, - tasks: extractTasksFromSyntheses(syntheses), + plan, + tasks, progress, // Extended multi-cli specific fields roundCount, @@ -548,6 +556,53 @@ function normalizeSolutionTask(task: SolutionTask, solution: Solution): Normaliz }; } +/** + * Normalize tasks from plan.json format to NormalizedTask[] + * plan.json tasks have: id, name, description, depends_on, status, files, key_point, acceptance_criteria + * @param tasks - Tasks array from plan.json + * @returns Normalized tasks + */ +function normalizePlanJsonTasks(tasks: unknown[]): NormalizedTask[] { + if (!Array.isArray(tasks)) return []; + + return tasks.map((task: any): NormalizedTask | null => { + if (!task || !task.id) return null; + + return { + id: task.id, + title: task.name || task.title || 'Untitled Task', + status: task.status || 'pending', + meta: { + type: 'implementation', + agent: null, + scope: task.scope || null, + module: null + }, + context: { + requirements: task.description ? [task.description] : (task.key_point ? [task.key_point] : []), + focus_paths: task.files?.map((f: any) => typeof f === 'string' ? f : f.file) || [], + acceptance: task.acceptance_criteria || [], + depends_on: task.depends_on || [] + }, + flow_control: { + implementation_approach: task.files?.map((f: any, i: number) => { + const filePath = typeof f === 'string' ? f : f.file; + const action = typeof f === 'string' ? 'modify' : f.action; + const line = typeof f === 'string' ? null : f.line; + return { + step: `Step ${i + 1}`, + action: `${action} ${filePath}${line ? ` at line ${line}` : ''}` + }; + }) || [] + }, + _raw: { + task, + estimated_complexity: task.estimated_complexity + } + }; + }).filter((task): task is NormalizedTask => task !== null); +} + /** * Load plan.json or fix-plan.json from session directory * @param sessionPath - Session directory path diff --git a/ccw/src/templates/dashboard-css/04-lite-tasks.css b/ccw/src/templates/dashboard-css/04-lite-tasks.css index efbe533f..285d8742 100644 --- a/ccw/src/templates/dashboard-css/04-lite-tasks.css +++ b/ccw/src/templates/dashboard-css/04-lite-tasks.css @@ -1281,7 +1281,7 @@ .multi-cli-status.pending, .multi-cli-status.exploring, .multi-cli-status.initialized { -) background: hsl(var(--muted)); + background: hsl(var(--muted)); color: hsl(var(--muted-foreground)); } @@ -3621,3 +3621,328 @@ } } +/* =================================== + Multi-CLI Plan Summary Section + =================================== */ + +/* Plan Summary Section - card-like styling */ +.plan-summary-section { + background: hsl(var(--card)); + border: 1px solid hsl(var(--border)); + border-radius: 0.5rem; + padding: 1rem 1.25rem; + margin-bottom: 1.25rem; +} + +.plan-summary-section:hover { + border-color: hsl(var(--purple, 280 60% 50%) / 0.3); +} + +/* Plan text styles */ +.plan-summary-text, +.plan-solution-text, +.plan-approach-text { + font-size: 0.875rem; + line-height: 1.6; + color: hsl(var(--foreground)); + margin: 0 0 0.75rem 0; +} + +.plan-summary-text:last-child, +.plan-solution-text:last-child, +.plan-approach-text:last-child { + margin-bottom: 0; +} + +.plan-summary-text strong, +.plan-solution-text strong, +.plan-approach-text strong { + color: hsl(var(--muted-foreground)); + font-weight: 600; + margin-right: 0.5rem; +} + +/* Plan meta badges container */ +.plan-meta-badges { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-top: 0.75rem; + padding-top: 0.75rem; + border-top: 1px solid hsl(var(--border) / 0.5); +} + +/* Feasibility badge */ +.feasibility-badge { + display: inline-flex; + align-items: center; + padding: 0.25rem 0.625rem; + background: hsl(var(--primary) / 0.1); + color: hsl(var(--primary)); + border-radius: 0.25rem; + font-size: 0.75rem; + font-weight: 500; +} + +/* Effort badge variants */ +.effort-badge { + display: inline-flex; + align-items: center; + padding: 0.25rem 0.625rem; + border-radius: 0.25rem; + font-size: 0.75rem; + font-weight: 500; +} + +.effort-badge.low { + background: hsl(var(--success-light, 142 70% 95%)); + color: hsl(var(--success, 142 70% 45%)); +} + +.effort-badge.medium { + background: hsl(var(--warning-light, 45 90% 95%)); + color: hsl(var(--warning, 45 90% 40%)); +} + +.effort-badge.high { + background: hsl(var(--destructive) / 0.1); + color: hsl(var(--destructive)); +} + +/* Complexity badge */ +.complexity-badge { + display: inline-flex; + align-items: center; + padding: 0.25rem 0.625rem; + background: hsl(var(--muted)); + color: hsl(var(--foreground)); + border-radius: 0.25rem; + font-size: 0.75rem; + font-weight: 500; +} + +/* Time badge */ +.time-badge { + display: inline-flex; + align-items: center; + padding: 0.25rem 0.625rem; + background: hsl(var(--info-light, 220 80% 95%)); + color: hsl(var(--info, 220 80% 55%)); + border-radius: 0.25rem; + font-size: 0.75rem; + font-weight: 500; +} + +/* =================================== + Multi-CLI Task Item Additional Badges + =================================== */ + +/* Files meta badge */ +.meta-badge.files { + background: hsl(var(--purple, 280 60% 50%) / 0.1); + color: hsl(var(--purple, 280 60% 50%)); +} + +/* Depends meta badge */ +.meta-badge.depends { + background: hsl(var(--info-light, 220 80% 95%)); + color: hsl(var(--info, 220 80% 55%)); +} + +/* Multi-CLI Task Item Full - enhanced padding */ +.detail-task-item-full.multi-cli-task-item { + background: hsl(var(--card)); + border: 1px solid hsl(var(--border)); + border-radius: 0.5rem; + padding: 0.875rem 1rem; + transition: all 0.2s ease; + border-left: 3px solid hsl(var(--primary) / 0.5); +} + +.detail-task-item-full.multi-cli-task-item:hover { + border-color: hsl(var(--primary) / 0.4); + border-left-color: hsl(var(--primary)); + box-shadow: 0 2px 8px hsl(var(--primary) / 0.1); + background: hsl(var(--hover)); +} + +/* Task ID badge enhancement */ +.task-id-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 2.5rem; + padding: 0.25rem 0.5rem; + background: hsl(var(--purple, 280 60% 50%)); + color: white; + border-radius: 0.25rem; + font-size: 0.75rem; + font-weight: 600; + flex-shrink: 0; +} + +/* Tasks list container */ +.tasks-list { + display: flex; + flex-direction: column; + gap: 0.625rem; +} + +/* Plan section styling (for Plan tab) */ +.plan-section { + background: hsl(var(--muted) / 0.3); + border: 1px solid hsl(var(--border)); + border-radius: 0.5rem; + padding: 1rem; + margin-bottom: 1rem; +} + +.plan-section:last-child { + margin-bottom: 0; +} + +.plan-section-title { + font-size: 0.9rem; + font-weight: 600; + color: hsl(var(--foreground)); + margin-bottom: 0.75rem; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.plan-tab-content { + display: flex; + flex-direction: column; + gap: 0; +} + +.tasks-tab-content { + display: flex; + flex-direction: column; + gap: 1rem; +} + +/* =================================== + Plan Summary Meta Badges + =================================== */ + +/* Base meta badge style (plan summary) */ +.plan-meta-badges .meta-badge { + display: inline-block; + padding: 0.25rem 0.625rem; + border-radius: 0.375rem; + font-size: 0.75rem; + font-weight: 500; + white-space: nowrap; +} + +/* Feasibility badge */ +.meta-badge.feasibility { + background: hsl(var(--success) / 0.15); + color: hsl(var(--success)); + border: 1px solid hsl(var(--success) / 0.3); +} + +/* Effort badges */ +.meta-badge.effort { + background: hsl(var(--muted)); + color: hsl(var(--foreground)); +} + +.meta-badge.effort.low { + background: hsl(142 70% 50% / 0.15); + color: hsl(142 70% 35%); +} + +.meta-badge.effort.medium { + background: hsl(30 90% 50% / 0.15); + color: hsl(30 90% 40%); +} + +.meta-badge.effort.high { + background: hsl(0 70% 50% / 0.15); + color: hsl(0 70% 45%); +} + +/* Risk badges */ +.meta-badge.risk { + background: hsl(var(--muted)); + color: hsl(var(--foreground)); +} + +.meta-badge.risk.low { + background: hsl(142 70% 50% / 0.15); + color: hsl(142 70% 35%); +} + +.meta-badge.risk.medium { + background: hsl(30 90% 50% / 0.15); + color: hsl(30 90% 40%); +} + +.meta-badge.risk.high { + background: hsl(0 70% 50% / 0.15); + color: hsl(0 70% 45%); +} + +/* Severity badges */ +.meta-badge.severity { + background: hsl(var(--muted)); + color: hsl(var(--foreground)); +} + +.meta-badge.severity.low { + background: hsl(142 70% 50% / 0.15); + color: hsl(142 70% 35%); +} + +.meta-badge.severity.medium { + background: hsl(30 90% 50% / 0.15); + color: hsl(30 90% 40%); +} + +.meta-badge.severity.high, +.meta-badge.severity.critical { + background: hsl(0 70% 50% / 0.15); + color: hsl(0 70% 45%); +} + +/* Complexity badge */ +.meta-badge.complexity { + background: hsl(var(--muted)); + color: hsl(var(--muted-foreground)); +} + +/* Time badge */ +.meta-badge.time { + background: hsl(220 80% 50% / 0.15); + color: hsl(220 80% 45%); +} + +/* Task item action badge */ +.meta-badge.action { + background: hsl(var(--primary) / 0.15); + color: hsl(var(--primary)); +} + +/* Task item scope badge */ +.meta-badge.scope { + background: hsl(var(--muted)); + color: hsl(var(--muted-foreground)); + font-family: var(--font-mono); + font-size: 0.7rem; +} + +/* Task item impl steps badge */ +.meta-badge.impl { + background: hsl(280 60% 50% / 0.1); + color: hsl(280 60% 50%); +} + +/* Task item acceptance criteria badge */ +.meta-badge.accept { + background: hsl(var(--success) / 0.1); + color: hsl(var(--success)); +} + diff --git a/ccw/src/templates/dashboard-js/components/hook-manager.js b/ccw/src/templates/dashboard-js/components/hook-manager.js index 91aacf72..37e8b4bd 100644 --- a/ccw/src/templates/dashboard-js/components/hook-manager.js +++ b/ccw/src/templates/dashboard-js/components/hook-manager.js @@ -52,12 +52,13 @@ const HOOK_TEMPLATES = { 'memory-update-queue': { event: 'Stop', matcher: '', - command: 'bash', - args: ['-c', 'ccw tool exec memory_queue "{\\"action\\":\\"add\\",\\"path\\":\\"$CLAUDE_PROJECT_DIR\\"}"'], + command: 'node', + args: ['-e', "require('child_process').spawnSync(process.platform==='win32'?'cmd':'ccw',process.platform==='win32'?['/c','ccw','tool','exec','memory_queue',JSON.stringify({action:'add',path:process.env.CLAUDE_PROJECT_DIR,tool:'gemini'})]:['tool','exec','memory_queue',JSON.stringify({action:'add',path:process.env.CLAUDE_PROJECT_DIR,tool:'gemini'})],{stdio:'inherit'})"], description: 'Queue CLAUDE.md update when session ends (batched by threshold/timeout)', category: 'memory', configurable: true, config: { + tool: { type: 'select', default: 'gemini', options: ['gemini', 'qwen', 'codex', 'opencode'], label: 'CLI Tool' }, threshold: { type: 'number', default: 5, min: 1, max: 20, label: 'Threshold (paths)', step: 1 }, timeout: { type: 'number', default: 300, min: 60, max: 1800, label: 'Timeout (seconds)', step: 60 } } @@ -66,8 +67,8 @@ const HOOK_TEMPLATES = { 'skill-context-keyword': { event: 'UserPromptSubmit', matcher: '', - command: 'bash', - args: ['-c', 'ccw tool exec skill_context_loader --stdin'], + command: 'node', + args: ['-e', "const p=JSON.parse(process.env.HOOK_INPUT||'{}');require('child_process').spawnSync('ccw',['tool','exec','skill_context_loader',JSON.stringify({prompt:p.user_prompt||''})],{stdio:'inherit'})"], description: 'Load SKILL context based on keyword matching in user prompt', category: 'skill', configurable: true, @@ -79,8 +80,8 @@ const HOOK_TEMPLATES = { 'skill-context-auto': { event: 'UserPromptSubmit', matcher: '', - command: 'bash', - args: ['-c', 'ccw tool exec skill_context_loader --stdin --mode auto'], + command: 'node', + args: ['-e', "const p=JSON.parse(process.env.HOOK_INPUT||'{}');require('child_process').spawnSync('ccw',['tool','exec','skill_context_loader',JSON.stringify({mode:'auto',prompt:p.user_prompt||''})],{stdio:'inherit'})"], description: 'Auto-detect and load SKILL based on skill name in prompt', category: 'skill', configurable: false @@ -195,6 +196,7 @@ const WIZARD_TEMPLATES = { } ], configFields: [ + { key: 'tool', type: 'select', label: 'CLI Tool', default: 'gemini', options: ['gemini', 'qwen', 'codex', 'opencode'], description: 'CLI tool for CLAUDE.md generation' }, { key: 'threshold', type: 'number', label: 'Threshold (paths)', default: 5, min: 1, max: 20, step: 1, description: 'Number of paths to trigger batch update' }, { key: 'timeout', type: 'number', label: 'Timeout (seconds)', default: 300, min: 60, max: 1800, step: 60, description: 'Auto-flush queue after this time' } ] @@ -748,6 +750,7 @@ function renderWizardModalContent() { // Helper to get translated field labels const getFieldLabel = (fieldKey) => { const labels = { + 'tool': t('hook.wizard.cliTool') || 'CLI Tool', 'threshold': t('hook.wizard.thresholdPaths') || 'Threshold (paths)', 'timeout': t('hook.wizard.timeoutSeconds') || 'Timeout (seconds)' }; @@ -756,6 +759,7 @@ function renderWizardModalContent() { const getFieldDesc = (fieldKey) => { const descs = { + 'tool': t('hook.wizard.cliToolDesc') || 'CLI tool for CLAUDE.md generation', 'threshold': t('hook.wizard.thresholdPathsDesc') || 'Number of paths to trigger batch update', 'timeout': t('hook.wizard.timeoutSecondsDesc') || 'Auto-flush queue after this time' }; @@ -1121,20 +1125,19 @@ function generateWizardCommand() { keywords: c.keywords.split(',').map(k => k.trim()).filter(k => k) })); - const params = JSON.stringify({ configs: configJson, prompt: '$CLAUDE_PROMPT' }); - return `ccw tool exec skill_context_loader '${params}'`; + // Use node + spawnSync for cross-platform JSON handling + const paramsObj = { configs: configJson, prompt: '${p.user_prompt}' }; + return `node -e "const p=JSON.parse(process.env.HOOK_INPUT||'{}');require('child_process').spawnSync('ccw',['tool','exec','skill_context_loader',JSON.stringify(${JSON.stringify(paramsObj).replace('${p.user_prompt}', "'+p.user_prompt+'")})],{stdio:'inherit'})"`; } else { - // auto mode - const params = JSON.stringify({ mode: 'auto', prompt: '$CLAUDE_PROMPT' }); - return `ccw tool exec skill_context_loader '${params}'`; + // auto mode - use node + spawnSync + return `node -e "const p=JSON.parse(process.env.HOOK_INPUT||'{}');require('child_process').spawnSync('ccw',['tool','exec','skill_context_loader',JSON.stringify({mode:'auto',prompt:p.user_prompt||''})],{stdio:'inherit'})"`; } } // Handle memory-update wizard (default) - // Now uses memory_queue for batched updates with configurable threshold/timeout - // The command adds to queue, configuration is applied separately via submitHookWizard - const params = `"{\\"action\\":\\"add\\",\\"path\\":\\"$CLAUDE_PROJECT_DIR\\"}"`; - return `ccw tool exec memory_queue ${params}`; + // Use node + spawnSync for cross-platform JSON handling + const selectedTool = wizardConfig.tool || 'gemini'; + return `node -e "require('child_process').spawnSync(process.platform==='win32'?'cmd':'ccw',process.platform==='win32'?['/c','ccw','tool','exec','memory_queue',JSON.stringify({action:'add',path:process.env.CLAUDE_PROJECT_DIR,tool:'${selectedTool}'})]:['tool','exec','memory_queue',JSON.stringify({action:'add',path:process.env.CLAUDE_PROJECT_DIR,tool:'${selectedTool}'})],{stdio:'inherit'})"`; } async function submitHookWizard() { @@ -1217,13 +1220,18 @@ async function submitHookWizard() { const baseTemplate = HOOK_TEMPLATES[selectedOption.templateId]; if (!baseTemplate) return; - const command = generateWizardCommand(); - - const hookData = { - command: 'bash', - args: ['-c', command] + // Build hook data with configured values + let hookData = { + command: baseTemplate.command, + args: [...baseTemplate.args] }; + // For memory-update wizard, use configured tool in args (cross-platform) + if (wizard.id === 'memory-update') { + const selectedTool = wizardConfig.tool || 'gemini'; + hookData.args = ['-e', `require('child_process').spawnSync(process.platform==='win32'?'cmd':'ccw',process.platform==='win32'?['/c','ccw','tool','exec','memory_queue',JSON.stringify({action:'add',path:process.env.CLAUDE_PROJECT_DIR,tool:'${selectedTool}'})]:['tool','exec','memory_queue',JSON.stringify({action:'add',path:process.env.CLAUDE_PROJECT_DIR,tool:'${selectedTool}'})],{stdio:'inherit'})`]; + } + if (baseTemplate.matcher) { hookData.matcher = baseTemplate.matcher; } @@ -1232,6 +1240,7 @@ async function submitHookWizard() { // For memory-update wizard, also configure queue settings if (wizard.id === 'memory-update') { + const selectedTool = wizardConfig.tool || 'gemini'; const threshold = wizardConfig.threshold || 5; const timeout = wizardConfig.timeout || 300; try { @@ -1242,7 +1251,7 @@ async function submitHookWizard() { body: JSON.stringify({ tool: 'memory_queue', params: configParams }) }); if (response.ok) { - showRefreshToast(`Queue configured: threshold=${threshold}, timeout=${timeout}s`, 'success'); + showRefreshToast(`Queue configured: tool=${selectedTool}, threshold=${threshold}, timeout=${timeout}s`, 'success'); } } catch (e) { console.warn('Failed to configure memory queue:', e); diff --git a/ccw/src/templates/dashboard-js/i18n.js b/ccw/src/templates/dashboard-js/i18n.js index 0502a211..06a97c72 100644 --- a/ccw/src/templates/dashboard-js/i18n.js +++ b/ccw/src/templates/dashboard-js/i18n.js @@ -1107,6 +1107,8 @@ const i18n = { 'hook.wizard.memoryUpdateDesc': 'Queue-based CLAUDE.md updates with configurable threshold and timeout', 'hook.wizard.queueBasedUpdate': 'Queue-Based Update', 'hook.wizard.queueBasedUpdateDesc': 'Batch updates when threshold reached or timeout expires', + 'hook.wizard.cliTool': 'CLI Tool', + 'hook.wizard.cliToolDesc': 'CLI tool for CLAUDE.md generation', 'hook.wizard.thresholdPaths': 'Threshold (paths)', 'hook.wizard.thresholdPathsDesc': 'Number of paths to trigger batch update', 'hook.wizard.timeoutSeconds': 'Timeout (seconds)', @@ -3347,6 +3349,8 @@ const i18n = { 'hook.wizard.memoryUpdateDesc': '基于队列的 CLAUDE.md 更新,支持阈值和超时配置', 'hook.wizard.queueBasedUpdate': '队列批量更新', 'hook.wizard.queueBasedUpdateDesc': '达到路径数量阈值或超时时批量更新', + 'hook.wizard.cliTool': 'CLI 工具', + 'hook.wizard.cliToolDesc': '用于生成 CLAUDE.md 的 CLI 工具', 'hook.wizard.thresholdPaths': '阈值(路径数)', 'hook.wizard.thresholdPathsDesc': '触发批量更新的路径数量', 'hook.wizard.timeoutSeconds': '超时(秒)', diff --git a/ccw/src/templates/dashboard-js/views/lite-tasks.js b/ccw/src/templates/dashboard-js/views/lite-tasks.js index 67dcee59..b5696d30 100644 --- a/ccw/src/templates/dashboard-js/views/lite-tasks.js +++ b/ccw/src/templates/dashboard-js/views/lite-tasks.js @@ -362,7 +362,8 @@ function showMultiCliDetailPage(sessionKey) { const container = document.getElementById('mainContent'); const metadata = session.metadata || {}; const plan = session.plan || {}; - const tasks = plan.tasks || []; + // Use session.tasks (normalized from backend) with fallback to plan.tasks + const tasks = session.tasks?.length > 0 ? session.tasks : (plan.tasks || []); const roundCount = metadata.roundId || session.roundCount || 1; const status = session.status || 'analyzing'; @@ -401,10 +402,6 @@ function showMultiCliDetailPage(sessionKey) { ${t('tab.tasks') || 'Tasks'} ${tasks.length} -
- ${action ? `${escapeHtml(action)}` : ''} + ${taskType ? `${escapeHtml(taskType)}` : ''} ${scope ? `${escapeHtml(scope)}` : ''} - ${modCount > 0 ? `${modCount} mods` : ''} + ${filesCount > 0 ? `${filesCount} files` : ''} ${implCount > 0 ? `${implCount} steps` : ''} - ${acceptCount > 0 ? `${acceptCount} acceptance` : ''} + ${acceptCount > 0 ? `${acceptCount} criteria` : ''} + ${dependsCount > 0 ? `${dependsCount} deps` : ''}
`; @@ -1442,16 +1500,18 @@ function initMultiCliTaskClickHandlers() { */ function openTaskDrawerForMultiCli(sessionId, taskId) { const session = liteTaskDataStore[currentSessionDetailKey]; - if (!session || !session.plan) return; + if (!session) return; - const task = session.plan.tasks?.find(t => (t.id || `T${session.plan.tasks.indexOf(t) + 1}`) === taskId); + // Use session.tasks (normalized from backend) with fallback to plan.tasks + const tasks = session.tasks?.length > 0 ? session.tasks : (session.plan?.tasks || []); + const task = tasks.find(t => (t.id || `T${tasks.indexOf(t) + 1}`) === taskId); if (!task) return; // Set current drawer tasks - currentDrawerTasks = session.plan.tasks || []; + currentDrawerTasks = tasks; window._currentDrawerSession = session; - document.getElementById('drawerTaskTitle').textContent = task.title || task.summary || taskId; + document.getElementById('drawerTaskTitle').textContent = task.title || task.name || task.summary || taskId; document.getElementById('drawerContent').innerHTML = renderMultiCliTaskDrawerContent(task, session); document.getElementById('taskDetailDrawer').classList.add('open'); document.getElementById('drawerOverlay').classList.add('active'); @@ -1523,12 +1583,21 @@ function switchMultiCliDrawerTab(tabName) { /** * Render multi-cli task overview section + * Handles both normalized format (meta, context, flow_control) and raw format */ function renderMultiCliTaskOverview(task) { let sections = []; - // Description Card - if (task.description) { + // Extract from both normalized and raw formats + const description = task.description || (task.context?.requirements?.length > 0 ? task.context.requirements.join('\n') : ''); + const scope = task.meta?.scope || task.scope || task.file || ''; + const acceptance = task.context?.acceptance || task.acceptance || task.acceptance_criteria || []; + const dependsOn = task.context?.depends_on || task.depends_on || []; + const focusPaths = task.context?.focus_paths || task.files?.map(f => typeof f === 'string' ? f : f.file) || []; + const keyPoint = task._raw?.task?.key_point || task.key_point || ''; + + // Description/Key Point Card + if (description || keyPoint) { sections.push(`
@@ -1536,14 +1605,15 @@ function renderMultiCliTaskOverview(task) {

Description

-

${escapeHtml(task.description)}

+ ${keyPoint ? `

Key Point: ${escapeHtml(keyPoint)}

` : ''} + ${description ? `

${escapeHtml(description)}

` : ''}
`); } // Scope Card - if (task.scope || task.file) { + if (scope) { sections.push(`
@@ -1552,15 +1622,49 @@ function renderMultiCliTaskOverview(task) {
- ${escapeHtml(task.scope || task.file)} + ${escapeHtml(scope)}
`); } + // Dependencies Card + if (dependsOn.length > 0) { + sections.push(` +
+
+ 🔗 +

Dependencies

+
+
+
+ ${dependsOn.map(dep => `${escapeHtml(dep)}`).join('')} +
+
+
+ `); + } + + // Focus Paths / Files Card + if (focusPaths.length > 0) { + sections.push(` +
+
+ 📁 +

Target Files

+
+
+
    + ${focusPaths.map(f => `
  • ${escapeHtml(f)}
  • `).join('')} +
+
+
+ `); + } + // Acceptance Criteria Card - if (task.acceptance?.length) { + if (acceptance.length > 0) { sections.push(`
@@ -1569,7 +1673,7 @@ function renderMultiCliTaskOverview(task) {
    - ${task.acceptance.map(ac => `
  • ${escapeHtml(ac)}
  • `).join('')} + ${acceptance.map(ac => `
  • ${escapeHtml(ac)}
  • `).join('')}
@@ -1599,12 +1703,35 @@ function renderMultiCliTaskOverview(task) { /** * Render multi-cli task implementation section + * Handles both normalized format (flow_control.implementation_approach) and raw format */ function renderMultiCliTaskImplementation(task) { let sections = []; - // Modification Points - if (task.modification_points?.length) { + // Get implementation steps from normalized or raw format + const implApproach = task.flow_control?.implementation_approach || []; + const rawImpl = task.implementation || []; + const modPoints = task.modification_points || []; + + // Modification Points / Flow Control Implementation Approach + if (implApproach.length > 0) { + sections.push(` +
+

+ + Implementation Steps +

+
    + ${implApproach.map((step, idx) => ` +
  1. + ${step.step || (idx + 1)} + ${escapeHtml(step.action || step)} +
  2. + `).join('')} +
+
+ `); + } else if (modPoints.length > 0) { sections.push(`

@@ -1612,7 +1739,7 @@ function renderMultiCliTaskImplementation(task) { Modification Points

    - ${task.modification_points.map(mp => ` + ${modPoints.map(mp => `
  • ${escapeHtml(mp.file || '')} ${mp.target ? `→ ${escapeHtml(mp.target)}` : ''} @@ -1625,8 +1752,8 @@ function renderMultiCliTaskImplementation(task) { `); } - // Implementation Steps - if (task.implementation?.length) { + // Raw Implementation Steps (if not already rendered via implApproach) + if (rawImpl.length > 0 && implApproach.length === 0) { sections.push(`

    @@ -1634,7 +1761,7 @@ function renderMultiCliTaskImplementation(task) { Implementation Steps

      - ${task.implementation.map((step, idx) => ` + ${rawImpl.map((step, idx) => `
    1. ${idx + 1} ${escapeHtml(step)} @@ -1665,10 +1792,26 @@ function renderMultiCliTaskImplementation(task) { /** * Render multi-cli task files section + * Handles both normalized format (context.focus_paths) and raw format */ function renderMultiCliTaskFiles(task) { const files = []; + // Collect from normalized format (context.focus_paths) + if (task.context?.focus_paths) { + task.context.focus_paths.forEach(f => { + if (f && !files.includes(f)) files.push(f); + }); + } + + // Collect from raw files array (plan.json format) + if (task.files) { + task.files.forEach(f => { + const filePath = typeof f === 'string' ? f : f.file; + if (filePath && !files.includes(filePath)) files.push(filePath); + }); + } + // Collect from modification_points if (task.modification_points) { task.modification_points.forEach(mp => { @@ -1676,7 +1819,7 @@ function renderMultiCliTaskFiles(task) { }); } - // Collect from scope/file + // Collect from scope/file (legacy) if (task.scope && !files.includes(task.scope)) files.push(task.scope); if (task.file && !files.includes(task.file)) files.push(task.file); diff --git a/ccw/src/tools/memory-update-queue.js b/ccw/src/tools/memory-update-queue.js index bb860061..d3c0641a 100644 --- a/ccw/src/tools/memory-update-queue.js +++ b/ccw/src/tools/memory-update-queue.js @@ -391,11 +391,7 @@ async function execute(params) { if (timeoutCheck.flushed) { // Queue was flushed due to timeout, add to fresh queue const result = addToQueue(path, { tool, strategy }); - return { - ...result, - timeoutFlushed: true, - flushResult: timeoutCheck.result - }; + return `[MemoryQueue] Timeout flush (${timeoutCheck.result.processed} items) → ${result.message}`; } const addResult = addToQueue(path, { tool, strategy }); @@ -403,14 +399,12 @@ async function execute(params) { // Auto-flush if threshold reached if (addResult.willFlush) { const flushResult = await flushQueue(); - return { - ...addResult, - flushed: true, - flushResult - }; + // Return string for hook-friendly output + return `[MemoryQueue] ${addResult.message} → Flushed ${flushResult.processed} items`; } - return addResult; + // Return string for hook-friendly output + return `[MemoryQueue] ${addResult.message}`; case 'status': // Check timeout first