fix(multi-cli): complete solution details display in summary tab (#98)

Fixed issue where multi-CLI planning solution cards only showed count,
feasibility, effort, and risk badges but had empty content area.

Changes:
- Enhanced renderMultiCliSummaryContent() to extract and display all solution fields
  - Solution name (name/title)
  - Feasibility score (feasibility)
  - Effort level (effort)
  - Risk level (risk)
  - Summary/description (summary)
  - Pros list (pros)
  - Cons list (cons)

- Added CSS styles for solution cards
  - .solution-details, .details-label, .details-list
  - .solution-header, .solution-title-row, .solution-badges
  - .badge with variants for feasibility/effort/risk

- Fixed related issues:
  - Added multiCliPlan support to backend data structures
  - Exposed liteTaskDataStore to window for global access
  - Fixed header left-alignment in detail pages
  - Added 'active' class to tab content for visibility

Files modified:
- ccw/src/templates/dashboard-js/views/lite-tasks.js
- ccw/src/templates/dashboard-css/04-lite-tasks.css
- ccw/src/core/server.ts
- ccw/src/core/routes/system-routes.ts
- ccw/src/templates/dashboard-js/state.js
- ccw/src/templates/dashboard-css/02-session.css
- ccw/src/config/litellm-api-config-manager.ts (fix homedir import)

Closes #98
This commit is contained in:
catlog22
2026-01-22 15:30:35 +08:00
parent 02531c4d15
commit 2fffe78dc9
7 changed files with 174 additions and 19 deletions

View File

@@ -4,9 +4,10 @@
*/
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
import { homedir } from 'os';
import { StoragePaths, GlobalPaths, ensureStorageDir } from './storage-paths.js';
import { getCodexLensDataDir } from '../utils/codexlens-path.js';
import type {
LiteLLMApiConfig,
ProviderCredential,
@@ -798,7 +799,7 @@ export function syncCodexLensConfig(baseDir: string): { success: boolean; messag
const rotationConfig = config.codexlensEmbeddingRotation;
// Get CodexLens settings path
const codexlensDir = join(homedir(), '.codexlens');
const codexlensDir = getCodexLensDataDir();
const settingsPath = join(codexlensDir, 'settings.json');
// Ensure directory exists

View File

@@ -145,7 +145,7 @@ async function getWorkflowData(projectPath: string): Promise<any> {
generatedAt: new Date().toISOString(),
activeSessions: [],
archivedSessions: [],
liteTasks: { litePlan: [], liteFix: [] },
liteTasks: { litePlan: [], liteFix: [], multiCliPlan: [] },
reviewData: { dimensions: {} },
projectOverview: null,
statistics: {
@@ -155,7 +155,8 @@ async function getWorkflowData(projectPath: string): Promise<any> {
completedTasks: 0,
reviewFindings: 0,
litePlanCount: 0,
liteFixCount: 0
liteFixCount: 0,
multiCliPlanCount: 0
},
projectPath: normalizePathForDisplay(resolvedPath),
recentPaths: getRecentPaths()

View File

@@ -29,7 +29,7 @@ import { handleLiteLLMApiRoutes } from './routes/litellm-api-routes.js';
import { handleNavStatusRoutes } from './routes/nav-status-routes.js';
import { handleAuthRoutes } from './routes/auth-routes.js';
import { handleLoopRoutes } from './routes/loop-routes.js';
import { handleLoopV2Routes } from './routes/loop-v2-routes.js';
import { handleLoopV2Routes, initializeCliToolsCache } from './routes/loop-v2-routes.js';
import { handleTestLoopRoutes } from './routes/test-loop-routes.js';
import { handleTaskRoutes } from './routes/task-routes.js';
@@ -383,10 +383,10 @@ function generateServerDashboard(initialPath: string): string {
generatedAt: new Date().toISOString(),
activeSessions: [],
archivedSessions: [],
liteTasks: { litePlan: [], liteFix: [] },
liteTasks: { litePlan: [], liteFix: [], multiCliPlan: [] },
reviewData: { dimensions: {} },
projectOverview: null,
statistics: { totalSessions: 0, activeSessions: 0, totalTasks: 0, completedTasks: 0, reviewFindings: 0, litePlanCount: 0, liteFixCount: 0 }
statistics: { totalSessions: 0, activeSessions: 0, totalTasks: 0, completedTasks: 0, reviewFindings: 0, litePlanCount: 0, liteFixCount: 0, multiCliPlanCount: 0 }
};
// Replace JS placeholders
@@ -723,6 +723,9 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
console.log(`WebSocket endpoint available at ws://${host}:${serverPort}/ws`);
console.log(`Hook endpoint available at POST http://${host}:${serverPort}/api/hook`);
// Initialize CLI tools cache for Loop V2 routes
initializeCliToolsCache();
// Start periodic cleanup of stale CLI executions (every 2 minutes)
const CLEANUP_INTERVAL_MS = 2 * 60 * 1000;
const cleanupInterval = setInterval(cleanupStaleExecutions, CLEANUP_INTERVAL_MS);

View File

@@ -464,6 +464,7 @@
display: flex;
flex-direction: column;
gap: 1rem;
align-items: flex-start;
}
.btn-back {
@@ -492,6 +493,7 @@
.detail-title-row {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 1rem;
flex-wrap: wrap;
}

View File

@@ -1732,12 +1732,25 @@
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 0.75rem;
padding: 0.75rem 1rem;
background: hsl(var(--muted) / 0.2);
border-bottom: 1px solid hsl(var(--border));
}
.solution-title-row {
display: flex;
align-items: center;
gap: 0.5rem;
flex: 1;
}
.solution-badges {
display: flex;
align-items: center;
gap: 0.5rem;
flex-shrink: 0;
}
.solution-title {
font-weight: 600;
font-size: 0.95rem;
@@ -2782,6 +2795,39 @@
border-bottom: 1px solid hsl(var(--border));
}
.solution-details {
padding: 0.75rem 1rem;
border-bottom: 1px solid hsl(var(--border) / 0.5);
}
.solution-details:last-child {
border-bottom: none;
}
.details-label {
font-size: 0.8125rem;
font-weight: 600;
margin: 0 0 0.5rem 0;
color: hsl(var(--foreground));
}
.details-list {
margin: 0;
padding-left: 1.25rem;
list-style: disc;
}
.details-list li {
font-size: 0.8125rem;
line-height: 1.6;
color: hsl(var(--muted-foreground));
margin-bottom: 0.375rem;
}
.details-list li:last-child {
margin-bottom: 0;
}
.solution-approach,
.solution-dependencies,
.solution-concerns {
@@ -3976,6 +4022,62 @@
border-top: 1px solid hsl(var(--border) / 0.5);
}
/* Badge - generic badge styling */
.badge {
display: inline-flex;
align-items: center;
padding: 0.25rem 0.625rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 500;
white-space: nowrap;
}
.badge.feasibility {
background: hsl(var(--primary) / 0.1);
color: hsl(var(--primary));
}
.badge.effort {
background: hsl(var(--muted));
color: hsl(var(--foreground));
}
.badge.effort.low {
background: hsl(142 70% 50% / 0.15);
color: hsl(142 70% 35%);
}
.badge.effort.medium {
background: hsl(30 90% 50% / 0.15);
color: hsl(30 90% 40%);
}
.badge.effort.high {
background: hsl(0 70% 50% / 0.15);
color: hsl(0 70% 45%);
}
.badge.risk {
background: hsl(var(--muted));
color: hsl(var(--foreground));
}
.badge.risk.low {
background: hsl(142 70% 50% / 0.15);
color: hsl(142 70% 35%);
}
.badge.risk.medium {
background: hsl(30 90% 50% / 0.15);
color: hsl(30 90% 40%);
}
.badge.risk.high {
background: hsl(0 70% 50% / 0.15);
color: hsl(0 70% 45%);
}
/* Feasibility badge */
.feasibility-badge {
display: inline-flex;

View File

@@ -36,10 +36,12 @@ const sessionDataStore = {};
// Store lite task session data for detail page access
// Key: session key, Value: lite session data object
const liteTaskDataStore = {};
window.liteTaskDataStore = 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 = {};
window.taskJsonStore = taskJsonStore;
// ========== Global Notification Queue ==========
// Notification queue visible from any view (persisted to localStorage)

View File

@@ -418,7 +418,7 @@ function showMultiCliDetailPage(sessionKey) {
</div>
<!-- Tab Content -->
<div class="detail-tab-content" id="multiCliDetailTabContent">
<div class="detail-tab-content active" id="multiCliDetailTabContent">
${renderMultiCliTasksTab(session)}
</div>
</div>
@@ -653,12 +653,17 @@ function switchMultiCliDetailTab(tabName) {
break;
case 'context':
loadAndRenderMultiCliContextTab(session, contentArea);
contentArea.classList.add('active');
return; // Early return as this is async
case 'summary':
loadAndRenderMultiCliSummaryTab(session, contentArea);
contentArea.classList.add('active');
return; // Early return as this is async
}
// Add active class to show content
contentArea.classList.add('active');
// Re-initialize after tab switch
setTimeout(() => {
if (typeof lucide !== 'undefined') lucide.createIcons();
@@ -1097,13 +1102,52 @@ function renderMultiCliSummaryContent(summary, session) {
<span class="section-label"><i data-lucide="lightbulb" class="w-4 h-4 inline mr-1"></i> ${t('multiCli.summary.solutions')} (${synthesis.solutions.length})</span>
</div>
<div class="collapsible-content collapsed">
${synthesis.solutions.map((sol, idx) => `
<div class="solution-summary-item">
<span class="solution-num">#${idx + 1}</span>
<span class="solution-name">${escapeHtml(getI18nText(sol.title) || sol.id || `${t('multiCli.summary.solution')} ${idx + 1}`)}</span>
${sol.feasibility?.score ? `<span class="feasibility-badge">${Math.round(sol.feasibility.score * 100)}%</span>` : ''}
</div>
`).join('')}
${synthesis.solutions.map((sol, idx) => {
const name = getI18nText(sol.name) || sol.title || sol.id || `${t('multiCli.summary.solution')} ${idx + 1}`;
const summary = getI18nText(sol.summary) || '';
const feasibility = sol.feasibility?.score || sol.feasibility || 0;
const effort = sol.effort || '';
const risk = sol.risk || '';
const pros = sol.pros || [];
const cons = sol.cons || [];
return `
<div class="solution-card">
<div class="solution-header">
<div class="solution-title-row">
<span class="solution-num">#${idx + 1}</span>
<span class="solution-name">${escapeHtml(name)}</span>
</div>
<div class="solution-badges">
${feasibility ? `<span class="badge feasibility">${Math.round(feasibility * 100)}%</span>` : ''}
${effort ? `<span class="badge effort ${escapeHtml(effort)}">${escapeHtml(effort)}</span>` : ''}
${risk ? `<span class="badge risk ${escapeHtml(risk)}">${escapeHtml(risk)}</span>` : ''}
</div>
</div>
${summary ? `
<div class="solution-summary">
<p>${escapeHtml(summary)}</p>
</div>
` : ''}
${pros.length > 0 ? `
<div class="solution-details">
<h5 class="details-label">✓ ${t('multiCli.summary.pros') || 'Pros'}:</h5>
<ul class="details-list">
${pros.map(p => `<li>${escapeHtml(getI18nText(p) || p)}</li>`).join('')}
</ul>
</div>
` : ''}
${cons.length > 0 ? `
<div class="solution-details">
<h5 class="details-label">✗ ${t('multiCli.summary.cons') || 'Cons'}:</h5>
<ul class="details-list">
${cons.map(c => `<li>${escapeHtml(getI18nText(c) || c)}</li>`).join('')}
</ul>
</div>
` : ''}
</div>
`;
}).join('')}
</div>
</div>
`);
@@ -2722,12 +2766,12 @@ function showLiteTaskDetailPage(sessionKey) {
<div class="detail-header">
<button class="btn-back" onclick="goBackToLiteTasks()">
<span class="back-icon">←</span>
<span>Back to ${session.type === 'lite-plan' ? 'Lite Plan' : 'Lite Fix'}</span>
<span>${session.type === 'lite-plan' ? t('lite.backToList', { type: 'Plan' }) : session.type === 'lite-fix' ? t('lite.backToList', { type: 'Fix' }) : t('multiCli.backToList')}</span>
</button>
<div class="detail-title-row">
<h2 class="detail-session-id">${session.type === 'lite-plan' ? '<i data-lucide="file-edit" class="w-5 h-5 inline mr-2"></i>' : '<i data-lucide="wrench" class="w-5 h-5 inline mr-2"></i>'} ${escapeHtml(session.id)}</h2>
<h2 class="detail-session-id">${session.type === 'lite-plan' ? '<i data-lucide="file-edit" class="w-5 h-5 inline mr-2"></i>' : session.type === 'lite-fix' ? '<i data-lucide="wrench" class="w-5 h-5 inline mr-2"></i>' : '<i data-lucide="speech-icon" class="w-5 h-5 inline mr-2"></i>'} ${escapeHtml(session.id)}</h2>
<div class="detail-badges">
<span class="session-type-badge ${session.type}">${session.type}</span>
<span class="session-type-badge ${session.type}">${session.type === 'multi-cli-plan' ? 'MULTI-CLI' : session.type}</span>
</div>
</div>
</div>