From 26a325efff3f1944faeeb46dff1db74bc91b6128 Mon Sep 17 00:00:00 2001 From: catlog22 Date: Sun, 7 Dec 2025 17:35:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=9C=80=E8=BF=91?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD=EF=BC=8C?= =?UTF-8?q?=E5=8C=85=E6=8B=AC=E5=88=A0=E9=99=A4=E8=B7=AF=E5=BE=84=E7=9A=84?= =?UTF-8?q?API=E5=92=8C=E5=89=8D=E7=AB=AF=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ccw/src/core/server.js | 133 ++++++---- ccw/src/templates/dashboard-js/api.js | 48 +++- .../dashboard-js/components/navigation.js | 23 +- .../dashboard-js/components/tabs-context.js | 134 ++++++++++ .../dashboard-js/views/session-detail.js | 7 +- ccw/src/templates/dashboard.css | 239 ++++++++++++++++++ ccw/src/utils/path-resolver.js | 25 ++ 7 files changed, 560 insertions(+), 49 deletions(-) diff --git a/ccw/src/core/server.js b/ccw/src/core/server.js index 1db5fab5..268f0898 100644 --- a/ccw/src/core/server.js +++ b/ccw/src/core/server.js @@ -6,7 +6,7 @@ import { homedir } from 'os'; import { createHash } from 'crypto'; import { scanSessions } from './session-scanner.js'; import { aggregateData } from './data-aggregator.js'; -import { resolvePath, getRecentPaths, trackRecentPath, normalizePathForDisplay, getWorkflowDir } from '../utils/path-resolver.js'; +import { resolvePath, getRecentPaths, trackRecentPath, removeRecentPath, normalizePathForDisplay, getWorkflowDir } from '../utils/path-resolver.js'; // Claude config file path const CLAUDE_CONFIG_PATH = join(homedir(), '.claude.json'); @@ -123,6 +123,19 @@ export async function startServer(options = {}) { return; } + // API: Remove a recent path + if (pathname === '/api/remove-recent-path' && req.method === 'POST') { + handlePostRequest(req, res, async (body) => { + const { path } = body; + if (!path) { + return { error: 'path is required', status: 400 }; + } + const removed = removeRecentPath(path); + return { success: removed, paths: getRecentPaths() }; + }); + return; + } + // API: Get session detail data (context, summaries, impl-plan, review) if (pathname === '/api/session-detail') { const sessionPath = url.searchParams.get('path'); @@ -610,58 +623,92 @@ async function getSessionDetailData(sessionPath, dataType) { } } - // Load explorations for lite tasks (exploration-*.json files) + // Load explorations (exploration-*.json files) - check .process/ first, then session root if (dataType === 'context' || dataType === 'explorations' || dataType === 'all') { result.explorations = { manifest: null, data: {} }; - // Look for explorations-manifest.json - const manifestFile = join(normalizedPath, 'explorations-manifest.json'); - if (existsSync(manifestFile)) { - try { - result.explorations.manifest = JSON.parse(readFileSync(manifestFile, 'utf8')); + // Try .process/ first (standard workflow sessions), then session root (lite tasks) + const searchDirs = [ + join(normalizedPath, '.process'), + normalizedPath + ]; - // Load each exploration file based on manifest - const explorations = result.explorations.manifest.explorations || []; - for (const exp of explorations) { - const expFile = join(normalizedPath, exp.file); - if (existsSync(expFile)) { - try { - result.explorations.data[exp.angle] = JSON.parse(readFileSync(expFile, 'utf8')); - } catch (e) { - // Skip unreadable exploration files + for (const searchDir of searchDirs) { + if (!existsSync(searchDir)) continue; + + // Look for explorations-manifest.json + const manifestFile = join(searchDir, 'explorations-manifest.json'); + if (existsSync(manifestFile)) { + try { + result.explorations.manifest = JSON.parse(readFileSync(manifestFile, 'utf8')); + + // Load each exploration file based on manifest + const explorations = result.explorations.manifest.explorations || []; + for (const exp of explorations) { + const expFile = join(searchDir, exp.file); + if (existsSync(expFile)) { + try { + result.explorations.data[exp.angle] = JSON.parse(readFileSync(expFile, 'utf8')); + } catch (e) { + // Skip unreadable exploration files + } } } + break; // Found manifest, stop searching + } catch (e) { + result.explorations.manifest = null; + } + } else { + // Fallback: scan for exploration-*.json files directly + try { + const files = readdirSync(searchDir).filter(f => f.startsWith('exploration-') && f.endsWith('.json')); + if (files.length > 0) { + // Create synthetic manifest + result.explorations.manifest = { + exploration_count: files.length, + explorations: files.map((f, i) => ({ + angle: f.replace('exploration-', '').replace('.json', ''), + file: f, + index: i + 1 + })) + }; + + // Load each file + for (const file of files) { + const angle = file.replace('exploration-', '').replace('.json', ''); + try { + result.explorations.data[angle] = JSON.parse(readFileSync(join(searchDir, file), 'utf8')); + } catch (e) { + // Skip unreadable files + } + } + break; // Found explorations, stop searching + } + } catch (e) { + // Directory read failed } - } catch (e) { - result.explorations.manifest = null; } - } else { - // Fallback: scan for exploration-*.json files directly - try { - const files = readdirSync(normalizedPath).filter(f => f.startsWith('exploration-') && f.endsWith('.json')); - if (files.length > 0) { - // Create synthetic manifest - result.explorations.manifest = { - exploration_count: files.length, - explorations: files.map((f, i) => ({ - angle: f.replace('exploration-', '').replace('.json', ''), - file: f, - index: i + 1 - })) - }; + } + } - // Load each file - for (const file of files) { - const angle = file.replace('exploration-', '').replace('.json', ''); - try { - result.explorations.data[angle] = JSON.parse(readFileSync(join(normalizedPath, file), 'utf8')); - } catch (e) { - // Skip unreadable files - } - } + // Load conflict resolution decisions (conflict-resolution-decisions.json) + if (dataType === 'context' || dataType === 'conflict' || dataType === 'all') { + result.conflictResolution = null; + + // Try .process/ first (standard workflow sessions) + const conflictFiles = [ + join(normalizedPath, '.process', 'conflict-resolution-decisions.json'), + join(normalizedPath, 'conflict-resolution-decisions.json') + ]; + + for (const conflictFile of conflictFiles) { + if (existsSync(conflictFile)) { + try { + result.conflictResolution = JSON.parse(readFileSync(conflictFile, 'utf8')); + break; // Found file, stop searching + } catch (e) { + // Skip unreadable file } - } catch (e) { - // Directory read failed } } } diff --git a/ccw/src/templates/dashboard-js/api.js b/ccw/src/templates/dashboard-js/api.js index c1e81f03..225572a0 100644 --- a/ccw/src/templates/dashboard-js/api.js +++ b/ccw/src/templates/dashboard-js/api.js @@ -118,13 +118,57 @@ function refreshRecentPaths() { recentPaths.forEach(path => { const item = document.createElement('div'); item.className = 'path-item' + (path === projectPath ? ' active' : ''); - item.textContent = path; item.dataset.path = path; - item.addEventListener('click', () => selectPath(path)); + + // Path text + const pathText = document.createElement('span'); + pathText.className = 'path-text'; + pathText.textContent = path; + pathText.addEventListener('click', () => selectPath(path)); + item.appendChild(pathText); + + // Delete button (only for non-current paths) + if (path !== projectPath) { + const deleteBtn = document.createElement('button'); + deleteBtn.className = 'path-delete-btn'; + deleteBtn.innerHTML = '×'; + deleteBtn.title = 'Remove from recent'; + deleteBtn.addEventListener('click', async (e) => { + e.stopPropagation(); + await removeRecentPathFromList(path); + }); + item.appendChild(deleteBtn); + } + recentContainer.appendChild(item); }); } +/** + * Remove a path from recent paths list + */ +async function removeRecentPathFromList(path) { + try { + const response = await fetch('/api/remove-recent-path', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path }) + }); + + if (response.ok) { + const data = await response.json(); + if (data.success) { + recentPaths = data.paths; + refreshRecentPaths(); + showRefreshToast('Path removed', 'success'); + } + } + } catch (err) { + console.error('Failed to remove path:', err); + showRefreshToast('Failed to remove path', 'error'); + } +} + // ========== File System Access ========== /** diff --git a/ccw/src/templates/dashboard-js/components/navigation.js b/ccw/src/templates/dashboard-js/components/navigation.js index 8bb3d265..1f24b284 100644 --- a/ccw/src/templates/dashboard-js/components/navigation.js +++ b/ccw/src/templates/dashboard-js/components/navigation.js @@ -12,9 +12,28 @@ function initPathSelector() { recentPaths.forEach(path => { const item = document.createElement('div'); item.className = 'path-item' + (path === projectPath ? ' active' : ''); - item.textContent = path; item.dataset.path = path; - item.addEventListener('click', () => selectPath(path)); + + // Path text + const pathText = document.createElement('span'); + pathText.className = 'path-text'; + pathText.textContent = path; + pathText.addEventListener('click', () => selectPath(path)); + item.appendChild(pathText); + + // Delete button (only for non-current paths) + if (path !== projectPath) { + const deleteBtn = document.createElement('button'); + deleteBtn.className = 'path-delete-btn'; + deleteBtn.innerHTML = '×'; + deleteBtn.title = 'Remove from recent'; + deleteBtn.addEventListener('click', async (e) => { + e.stopPropagation(); + await removeRecentPathFromList(path); + }); + item.appendChild(deleteBtn); + } + recentContainer.appendChild(item); }); } diff --git a/ccw/src/templates/dashboard-js/components/tabs-context.js b/ccw/src/templates/dashboard-js/components/tabs-context.js index b74756c7..b5b100d0 100644 --- a/ccw/src/templates/dashboard-js/components/tabs-context.js +++ b/ccw/src/templates/dashboard-js/components/tabs-context.js @@ -961,3 +961,137 @@ function renderConflictDetectionSection(conflictDetection) { return sections.join(''); } + +// ========================================== +// Session Context Tab Rendering (Standard Sessions) +// ========================================== +// Combines context-package, explorations, and conflict resolution + +function renderSessionContextContent(context, explorations, conflictResolution) { + let sections = []; + + // Render conflict resolution decisions if available + if (conflictResolution) { + sections.push(renderConflictResolutionContext(conflictResolution)); + } + + // Render explorations if available (from exploration-*.json files) + if (explorations && explorations.manifest) { + sections.push(renderExplorationContext(explorations)); + } + + // Render context-package.json content + if (context) { + const contextJson = JSON.stringify(context, null, 2); + window._currentContextJson = contextJson; + + // Use existing renderContextContent for detailed rendering + sections.push(\` +
+ \${renderContextContent(context)} +
+ \`); + } + + // If we have any sections, wrap them + if (sections.length > 0) { + return \`
\${sections.join('')}
\`; + } + + return \` +
+
📦
+
No Context Data
+
No context-package.json, exploration files, or conflict resolution data found for this session.
+
+ \`; +} + +// ========================================== +// Conflict Resolution Context Rendering +// ========================================== + +function renderConflictResolutionContext(conflictResolution) { + if (!conflictResolution) { + return ''; + } + + let sections = []; + + // Header + sections.push(\` +
+

⚖️ Conflict Resolution Decisions

+
+ Session: \${escapeHtml(conflictResolution.session_id || 'N/A')} + \${conflictResolution.resolved_at ? \`Resolved: \${formatDate(conflictResolution.resolved_at)}\` : ''} +
+
+ \`); + + // User decisions + if (conflictResolution.user_decisions && Object.keys(conflictResolution.user_decisions).length > 0) { + const decisions = Object.entries(conflictResolution.user_decisions); + + sections.push(\` +
+
+ + +
+ +
+ \`); + } + + // Resolved conflicts + if (conflictResolution.resolved_conflicts && conflictResolution.resolved_conflicts.length > 0) { + sections.push(\` +
+
+ + +
+ +
+ \`); + } + + return \`
\${sections.join('')}
\`; +} diff --git a/ccw/src/templates/dashboard-js/views/session-detail.js b/ccw/src/templates/dashboard-js/views/session-detail.js index c0ad84e0..e447d076 100644 --- a/ccw/src/templates/dashboard-js/views/session-detail.js +++ b/ccw/src/templates/dashboard-js/views/session-detail.js @@ -408,12 +408,15 @@ async function loadAndRenderContextTab(session, contentArea) { contentArea.innerHTML = '
Loading context data...
'; try { - // Try to load context-package.json from server + // Try to load context data from server (includes context, explorations, conflictResolution) if (window.SERVER_MODE && session.path) { const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=context`); if (response.ok) { const data = await response.json(); - contentArea.innerHTML = renderContextContent(data.context); + contentArea.innerHTML = renderSessionContextContent(data.context, data.explorations, data.conflictResolution); + + // Initialize collapsible sections for explorations + initCollapsibleSections(contentArea); return; } } diff --git a/ccw/src/templates/dashboard.css b/ccw/src/templates/dashboard.css index e52eeac1..a2be9e24 100644 --- a/ccw/src/templates/dashboard.css +++ b/ccw/src/templates/dashboard.css @@ -437,6 +437,44 @@ body { word-break: break-all; } +.path-menu .path-item { + display: flex; + align-items: center; + justify-content: space-between; +} + +.path-menu .path-text { + cursor: pointer; +} + +.path-delete-btn { + width: 20px; + height: 20px; + border: none; + background: transparent; + color: hsl(var(--muted-foreground)); + font-size: 16px; + font-weight: bold; + cursor: pointer; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: opacity 0.15s, background 0.15s, color 0.15s; + flex-shrink: 0; + margin-left: 8px; +} + +.path-menu .path-item:hover .path-delete-btn { + opacity: 1; +} + +.path-delete-btn:hover { + background: hsl(var(--destructive) / 0.1); + color: hsl(var(--destructive)); +} + /* =================================== Session Detail Page =================================== */ @@ -3807,6 +3845,207 @@ ol.step-commands code { display: none; } +/* ========================================== + Conflict Resolution Context Styles + ========================================== */ +.conflict-resolution-context { + padding: 16px; +} + +.conflict-resolution-header { + margin-bottom: 20px; + padding: 16px; + background: linear-gradient(135deg, var(--bg-warning, #fffbeb) 0%, var(--bg-secondary, #f9fafb) 100%); + border-radius: 12px; + border: 1px solid var(--border-warning, #fcd34d); +} + +.conflict-resolution-header h4 { + font-size: 15px; + font-weight: 600; + color: var(--text-primary, #111827); + margin: 0 0 12px 0; +} + +.conflict-meta { + display: flex; + flex-wrap: wrap; + gap: 16px; + font-size: 12px; + color: var(--text-secondary, #6b7280); +} + +.conflict-meta .meta-item { + display: flex; + align-items: center; + gap: 4px; +} + +.conflict-meta strong { + color: var(--text-primary, #111827); + font-weight: 500; +} + +/* Decisions Section */ +.conflict-decisions-section, +.resolved-conflicts-section { + margin-bottom: 16px; + border: 1px solid var(--border-color, #e5e7eb); + border-radius: 8px; + overflow: hidden; +} + +.decisions-list, +.conflicts-list { + display: flex; + flex-direction: column; + gap: 12px; + padding: 12px; +} + +.decision-item { + padding: 14px; + background: var(--bg-primary, #fff); + border: 1px solid var(--border-color, #e5e7eb); + border-radius: 8px; + transition: border-color 0.15s ease; +} + +.decision-item:hover { + border-color: var(--primary, #3b82f6); +} + +.decision-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 8px; +} + +.decision-key { + font-size: 14px; + font-weight: 600; + color: var(--text-primary, #111827); + text-transform: capitalize; +} + +.decision-choice { + padding: 4px 10px; + background: var(--primary-bg, #eff6ff); + color: var(--primary, #3b82f6); + border-radius: 4px; + font-size: 12px; + font-weight: 500; +} + +.decision-description { + font-size: 13px; + color: var(--text-secondary, #6b7280); + margin: 0 0 8px 0; + line-height: 1.5; +} + +.decision-implications { + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid var(--border-color, #e5e7eb); +} + +.implications-label { + font-size: 11px; + font-weight: 600; + color: var(--text-muted, #9ca3af); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.implications-list { + margin: 6px 0 0 0; + padding-left: 16px; + font-size: 12px; + color: var(--text-secondary, #6b7280); + line-height: 1.6; +} + +.implications-list li { + margin-bottom: 4px; +} + +/* Resolved Conflicts */ +.resolved-conflict-item { + padding: 12px; + background: var(--bg-secondary, #f9fafb); + border-radius: 6px; + border: 1px solid var(--border-color, #e5e7eb); +} + +.conflict-row { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; +} + +.conflict-id { + font-family: var(--font-mono, monospace); + font-size: 12px; + font-weight: 600; + color: var(--text-primary, #111827); + padding: 2px 6px; + background: var(--bg-primary, #fff); + border-radius: 4px; +} + +.conflict-category-badge { + padding: 2px 8px; + background: var(--primary-bg, #eff6ff); + color: var(--primary, #3b82f6); + border-radius: 4px; + font-size: 10px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.conflict-brief { + font-size: 13px; + color: var(--text-primary, #374151); + margin-bottom: 8px; + line-height: 1.5; +} + +.conflict-strategy { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; +} + +.strategy-label { + color: var(--text-muted, #9ca3af); +} + +.strategy-value { + color: var(--text-success, #065f46); + font-weight: 500; + padding: 2px 8px; + background: var(--bg-success, #d1fae5); + border-radius: 4px; +} + +/* Session Context Combined */ +.session-context-combined { + display: flex; + flex-direction: column; + gap: 20px; +} + +.session-context-section { + margin-top: 0; +} + + /* Plan Context Section in Lite Tasks */ .plan-context-section { margin-top: 16px; diff --git a/ccw/src/utils/path-resolver.js b/ccw/src/utils/path-resolver.js index 8763d8b3..9f296e45 100644 --- a/ccw/src/utils/path-resolver.js +++ b/ccw/src/utils/path-resolver.js @@ -252,3 +252,28 @@ export function clearRecentPaths() { // Ignore errors } } + +/** + * Remove a specific path from recent paths + * @param {string} pathToRemove - Path to remove + * @returns {boolean} - True if removed, false if not found + */ +export function removeRecentPath(pathToRemove) { + try { + const normalized = normalizePathForDisplay(resolvePath(pathToRemove)); + let paths = getRecentPaths(); + const originalLength = paths.length; + + // Filter out the path to remove + paths = paths.filter(p => normalizePathForDisplay(p) !== normalized); + + if (paths.length < originalLength) { + // Save updated list + writeFileSync(RECENT_PATHS_FILE, JSON.stringify({ paths }, null, 2), 'utf8'); + return true; + } + return false; + } catch { + return false; + } +}