feat: Enhance multi-CLI session handling and UI updates

- Added loading of plan.json in scanMultiCliDir to improve task extraction.
- Implemented normalization of tasks from plan.json format to support new UI.
- Updated CSS for multi-CLI plan summary and task item badges for better visibility.
- Refactored hook-manager to use Node.js for cross-platform compatibility in command execution.
- Improved i18n support for new CLI tool configuration in the hook wizard.
- Enhanced lite-tasks view to utilize normalized tasks and provide better fallback mechanisms.
- Updated memory-update-queue to return string messages for better integration with hooks.
This commit is contained in:
catlog22
2026-01-15 15:20:20 +08:00
parent e22b525e9c
commit 0eda520fd7
6 changed files with 615 additions and 85 deletions

View File

@@ -238,10 +238,11 @@ async function scanMultiCliDir(dir: string): Promise<MultiCliSession[]> {
.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<MultiCliSession[]> {
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

View File

@@ -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));
}

View File

@@ -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);

View File

@@ -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': '超时(秒)',

View File

@@ -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) {
<span class="tab-text">${t('tab.tasks') || 'Tasks'}</span>
<span class="tab-count">${tasks.length}</span>
</button>
<button class="detail-tab" data-tab="plan" onclick="switchMultiCliDetailTab('plan')">
<span class="tab-icon"><i data-lucide="ruler" class="w-4 h-4"></i></span>
<span class="tab-text">${t('tab.plan') || 'Plan'}</span>
</button>
<button class="detail-tab" data-tab="discussion" onclick="switchMultiCliDetailTab('discussion')">
<span class="tab-icon"><i data-lucide="messages-square" class="w-4 h-4"></i></span>
<span class="tab-text">${t('multiCli.tab.discussion') || 'Discussion'}</span>
@@ -440,7 +437,8 @@ function showMultiCliDetailPage(sessionKey) {
*/
function renderMultiCliToolbar(session) {
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 taskCount = tasks.length;
let toolbarHtml = `
@@ -473,8 +471,8 @@ function renderMultiCliToolbar(session) {
toolbarHtml += `
<div class="toolbar-task-list">
${tasks.map((task, idx) => {
const taskTitle = task.title || task.summary || `Task ${idx + 1}`;
const taskScope = task.scope || '';
const taskTitle = task.title || task.name || task.summary || `Task ${idx + 1}`;
const taskScope = task.meta?.scope || task.scope || '';
const taskIdValue = task.id || `T${idx + 1}`;
return `
@@ -650,9 +648,6 @@ function switchMultiCliDetailTab(tabName) {
case 'tasks':
contentArea.innerHTML = renderMultiCliTasksTab(session);
break;
case 'plan':
contentArea.innerHTML = renderMultiCliPlanTab(session);
break;
case 'discussion':
contentArea.innerHTML = renderMultiCliDiscussionSection(session);
break;
@@ -680,32 +675,91 @@ function switchMultiCliDetailTab(tabName) {
// ============================================
/**
* Render Tasks tab - displays tasks from plan.json (same style as lite-plan)
* Render Tasks tab - displays plan summary + tasks (same style as lite-plan)
* Uses session.tasks (normalized tasks) with fallback to session.plan.tasks
*/
function renderMultiCliTasksTab(session) {
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 || []);
// Populate drawer tasks for click-to-open functionality
currentDrawerTasks = tasks;
let sections = [];
// Extract plan info from multiple sources (plan.json, synthesis, or session)
// plan.json: task_description, solution.name, execution_flow
// synthesis: solutions[].summary, solutions[].implementation_plan.approach
const taskDescription = plan.task_description || session.topicTitle || '';
const solutionName = plan.solution?.name || (plan.solutions?.[0]?.name) || '';
const solutionSummary = plan.solutions?.[0]?.summary || '';
const approach = plan.solutions?.[0]?.implementation_plan?.approach || plan.execution_flow || '';
const feasibility = plan.solution?.feasibility || plan.solutions?.[0]?.feasibility;
const effort = plan.solution?.effort || plan.solutions?.[0]?.effort || '';
const risk = plan.solution?.risk || plan.solutions?.[0]?.risk || '';
// Plan Summary Section (if any info available)
const hasInfo = taskDescription || solutionName || solutionSummary || approach || plan.summary;
if (hasInfo) {
let planInfo = [];
// Task description (main objective)
if (taskDescription) {
planInfo.push(`<p class="plan-summary-text"><strong>${t('plan.objective') || 'Objective'}:</strong> ${escapeHtml(taskDescription)}</p>`);
}
// Solution name and summary
if (solutionName) {
planInfo.push(`<p class="plan-solution-text"><strong>${t('plan.solution') || 'Solution'}:</strong> ${escapeHtml(solutionName)}</p>`);
}
if (solutionSummary) {
planInfo.push(`<p class="plan-summary-text">${escapeHtml(solutionSummary)}</p>`);
}
// Legacy summary field
if (plan.summary && !taskDescription && !solutionSummary) {
planInfo.push(`<p class="plan-summary-text">${escapeHtml(plan.summary)}</p>`);
}
// Approach/execution flow
if (approach) {
planInfo.push(`<p class="plan-approach-text"><strong>${t('plan.approach') || 'Approach'}:</strong> ${escapeHtml(approach)}</p>`);
}
// Metadata badges - concise format
let metaBadges = [];
if (feasibility) metaBadges.push(`<span class="meta-badge feasibility">${Math.round(feasibility * 100)}%</span>`);
if (effort) metaBadges.push(`<span class="meta-badge effort ${escapeHtml(effort)}">${escapeHtml(effort)}</span>`);
if (risk) metaBadges.push(`<span class="meta-badge risk ${escapeHtml(risk)}">${escapeHtml(risk)} risk</span>`);
// Legacy badges
if (plan.severity) metaBadges.push(`<span class="meta-badge severity ${escapeHtml(plan.severity)}">${escapeHtml(plan.severity)}</span>`);
if (plan.complexity) metaBadges.push(`<span class="meta-badge complexity">${escapeHtml(plan.complexity)}</span>`);
if (plan.estimated_time) metaBadges.push(`<span class="meta-badge time">${escapeHtml(plan.estimated_time)}</span>`);
sections.push(`
<div class="plan-summary-section">
${planInfo.join('')}
${metaBadges.length ? `<div class="plan-meta-badges">${metaBadges.join(' ')}</div>` : ''}
</div>
`);
}
// Tasks Section
if (tasks.length === 0) {
return `
sections.push(`
<div class="tab-empty-state">
<div class="empty-icon"><i data-lucide="clipboard-list" class="w-12 h-12"></i></div>
<div class="empty-title">${t('empty.noTasks') || 'No Tasks'}</div>
<div class="empty-text">${t('empty.noTasksText') || 'No tasks available for this session.'}</div>
</div>
`;
}
return `
<div class="tasks-tab-content">
`);
} else {
sections.push(`
<div class="tasks-list" id="multiCliTasksListContent">
${tasks.map((task, idx) => renderMultiCliTaskItem(session.id, task, idx)).join('')}
</div>
</div>
`;
`);
}
return `<div class="tasks-tab-content">${sections.join('')}</div>`;
}
/**
@@ -1375,12 +1429,15 @@ function renderMultiCliTaskItem(sessionId, task, idx) {
const taskJsonId = `multi-cli-task-${sessionId}-${taskId}`.replace(/[^a-zA-Z0-9-]/g, '-');
taskJsonStore[taskJsonId] = task;
// Get preview info
const action = task.action || '';
const scope = task.scope || task.file || '';
const modCount = task.modification_points?.length || 0;
const implCount = task.implementation?.length || 0;
const acceptCount = task.acceptance?.length || 0;
// Get preview info - handle both normalized and raw formats
// Normalized: meta.type, meta.scope, context.focus_paths, context.acceptance, flow_control.implementation_approach
// Raw: action, scope, file, modification_points, implementation, acceptance
const taskType = task.meta?.type || task.action || '';
const scope = task.meta?.scope || task.scope || task.file || '';
const filesCount = task.context?.focus_paths?.length || task.files?.length || task.modification_points?.length || 0;
const implCount = task.flow_control?.implementation_approach?.length || task.implementation?.length || 0;
const acceptCount = task.context?.acceptance?.length || task.acceptance?.length || task.acceptance_criteria?.length || 0;
const dependsCount = task.context?.depends_on?.length || task.depends_on?.length || 0;
// Escape for data attributes
const safeSessionId = escapeHtml(sessionId);
@@ -1390,15 +1447,16 @@ function renderMultiCliTaskItem(sessionId, task, idx) {
<div class="detail-task-item-full multi-cli-task-item" data-session-id="${safeSessionId}" data-task-id="${safeTaskId}" style="cursor: pointer;" title="Click to view details">
<div class="task-item-header-lite">
<span class="task-id-badge">${escapeHtml(taskId)}</span>
<span class="task-title">${escapeHtml(task.title || task.summary || 'Untitled')}</span>
<span class="task-title">${escapeHtml(task.title || task.name || task.summary || 'Untitled')}</span>
<button class="btn-view-json" data-task-json-id="${taskJsonId}" data-task-display-id="${safeTaskId}">{ } JSON</button>
</div>
<div class="task-item-meta-lite">
${action ? `<span class="meta-badge action">${escapeHtml(action)}</span>` : ''}
${taskType ? `<span class="meta-badge action">${escapeHtml(taskType)}</span>` : ''}
${scope ? `<span class="meta-badge scope">${escapeHtml(scope)}</span>` : ''}
${modCount > 0 ? `<span class="meta-badge mods">${modCount} mods</span>` : ''}
${filesCount > 0 ? `<span class="meta-badge files">${filesCount} files</span>` : ''}
${implCount > 0 ? `<span class="meta-badge impl">${implCount} steps</span>` : ''}
${acceptCount > 0 ? `<span class="meta-badge accept">${acceptCount} acceptance</span>` : ''}
${acceptCount > 0 ? `<span class="meta-badge accept">${acceptCount} criteria</span>` : ''}
${dependsCount > 0 ? `<span class="meta-badge depends">${dependsCount} deps</span>` : ''}
</div>
</div>
`;
@@ -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(`
<div class="lite-card">
<div class="lite-card-header">
@@ -1536,14 +1605,15 @@ function renderMultiCliTaskOverview(task) {
<h4 class="lite-card-title">Description</h4>
</div>
<div class="lite-card-body">
<p class="lite-description">${escapeHtml(task.description)}</p>
${keyPoint ? `<p class="lite-key-point"><strong>Key Point:</strong> ${escapeHtml(keyPoint)}</p>` : ''}
${description ? `<p class="lite-description">${escapeHtml(description)}</p>` : ''}
</div>
</div>
`);
}
// Scope Card
if (task.scope || task.file) {
if (scope) {
sections.push(`
<div class="lite-card">
<div class="lite-card-header">
@@ -1552,15 +1622,49 @@ function renderMultiCliTaskOverview(task) {
</div>
<div class="lite-card-body">
<div class="lite-scope-box">
<code>${escapeHtml(task.scope || task.file)}</code>
<code>${escapeHtml(scope)}</code>
</div>
</div>
</div>
`);
}
// Dependencies Card
if (dependsOn.length > 0) {
sections.push(`
<div class="lite-card">
<div class="lite-card-header">
<span class="lite-card-icon">🔗</span>
<h4 class="lite-card-title">Dependencies</h4>
</div>
<div class="lite-card-body">
<div class="lite-deps-list">
${dependsOn.map(dep => `<span class="dep-badge">${escapeHtml(dep)}</span>`).join('')}
</div>
</div>
</div>
`);
}
// Focus Paths / Files Card
if (focusPaths.length > 0) {
sections.push(`
<div class="lite-card">
<div class="lite-card-header">
<span class="lite-card-icon">📁</span>
<h4 class="lite-card-title">Target Files</h4>
</div>
<div class="lite-card-body">
<ul class="lite-file-list">
${focusPaths.map(f => `<li><code>${escapeHtml(f)}</code></li>`).join('')}
</ul>
</div>
</div>
`);
}
// Acceptance Criteria Card
if (task.acceptance?.length) {
if (acceptance.length > 0) {
sections.push(`
<div class="lite-card">
<div class="lite-card-header">
@@ -1569,7 +1673,7 @@ function renderMultiCliTaskOverview(task) {
</div>
<div class="lite-card-body">
<ul class="lite-acceptance-list">
${task.acceptance.map(ac => `<li>${escapeHtml(ac)}</li>`).join('')}
${acceptance.map(ac => `<li>${escapeHtml(ac)}</li>`).join('')}
</ul>
</div>
</div>
@@ -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(`
<div class="drawer-section">
<h4 class="drawer-section-title">
<i data-lucide="list-ordered" class="w-4 h-4"></i>
Implementation Steps
</h4>
<ol class="impl-steps-detail-list">
${implApproach.map((step, idx) => `
<li class="impl-step-item">
<span class="step-num">${step.step || (idx + 1)}</span>
<span class="step-text">${escapeHtml(step.action || step)}</span>
</li>
`).join('')}
</ol>
</div>
`);
} else if (modPoints.length > 0) {
sections.push(`
<div class="drawer-section">
<h4 class="drawer-section-title">
@@ -1612,7 +1739,7 @@ function renderMultiCliTaskImplementation(task) {
Modification Points
</h4>
<ul class="mod-points-detail-list">
${task.modification_points.map(mp => `
${modPoints.map(mp => `
<li class="mod-point-item">
<code class="mod-file">${escapeHtml(mp.file || '')}</code>
${mp.target ? `<span class="mod-target">→ ${escapeHtml(mp.target)}</span>` : ''}
@@ -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(`
<div class="drawer-section">
<h4 class="drawer-section-title">
@@ -1634,7 +1761,7 @@ function renderMultiCliTaskImplementation(task) {
Implementation Steps
</h4>
<ol class="impl-steps-detail-list">
${task.implementation.map((step, idx) => `
${rawImpl.map((step, idx) => `
<li class="impl-step-item">
<span class="step-num">${idx + 1}</span>
<span class="step-text">${escapeHtml(step)}</span>
@@ -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);

View File

@@ -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