Files
Claude-Code-Workflow/ccw/test-dashboard.html

9087 lines
254 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CCW Dashboard</title>
<!-- Google Fonts: Inter -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<!-- Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: ['class', '[data-theme="dark"]'],
safelist: [
// Background colors
'bg-card', 'bg-background', 'bg-hover', 'bg-accent', 'bg-muted', 'bg-primary', 'bg-success', 'bg-warning',
'bg-success-light', 'bg-warning-light', 'bg-primary-light', 'bg-sidebar-background', 'bg-destructive',
'bg-destructive/5', 'bg-destructive/10', 'bg-warning/5',
// Text colors
'text-foreground', 'text-muted-foreground', 'text-primary', 'text-card-foreground', 'text-success', 'text-warning',
'text-primary-foreground', 'text-accent-foreground', 'text-sidebar-foreground', 'text-destructive',
// Border colors
'border', 'border-border', 'border-primary', 'border-success', 'border-warning', 'border-muted',
'border-l-success', 'border-l-warning', 'border-l-muted-foreground', 'border-l-primary',
// Layout
'rounded', 'rounded-lg', 'rounded-md', 'rounded-full', 'shadow', 'shadow-sm', 'shadow-md', 'shadow-lg',
'p-2', 'p-3', 'p-4', 'p-5', 'px-3', 'px-4', 'px-5', 'py-2', 'py-3', 'py-4',
'm-2', 'mb-2', 'mb-3', 'mb-4', 'mt-2', 'mt-3', 'mt-4', 'mx-2', 'my-2',
'gap-2', 'gap-3', 'gap-4', 'space-y-2', 'space-y-3',
// Flex & Grid
'flex', 'flex-1', 'flex-col', 'flex-wrap', 'items-center', 'items-start', 'justify-between', 'justify-center',
'grid', 'grid-cols-1', 'grid-cols-2', 'grid-cols-3',
// Sizing
'w-full', 'w-5', 'w-8', 'h-2', 'h-5', 'h-8', 'min-w-0', 'max-w-full',
// Text
'text-xs', 'text-sm', 'text-base', 'text-lg', 'text-xl', 'text-2xl', 'text-3xl',
'font-medium', 'font-semibold', 'font-bold', 'font-mono', 'truncate', 'uppercase',
// States & Transitions
'hover:shadow-md', 'hover:bg-hover', 'hover:-translate-y-1', 'hover:text-foreground',
'transition-all', 'duration-200', 'duration-300', 'cursor-pointer',
// Opacity & visibility
'opacity-50', 'opacity-80', 'hidden', 'block', 'inline', 'inline-flex',
// Position
'relative', 'absolute', 'fixed', 'sticky', 'top-0', 'right-0', 'left-0', 'bottom-0',
'z-10', 'z-40', 'z-50', 'overflow-hidden', 'overflow-y-auto',
],
theme: {
extend: {
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: 'hsl(var(--card))',
'card-foreground': 'hsl(var(--card-foreground))',
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
primary: 'hsl(var(--primary))',
'primary-foreground': 'hsl(var(--primary-foreground))',
'primary-light': 'hsl(var(--primary-light))',
secondary: 'hsl(var(--secondary))',
'secondary-foreground': 'hsl(var(--secondary-foreground))',
accent: 'hsl(var(--accent))',
'accent-foreground': 'hsl(var(--accent-foreground))',
destructive: 'hsl(var(--destructive))',
'destructive-foreground': 'hsl(var(--destructive-foreground))',
muted: 'hsl(var(--muted))',
'muted-foreground': 'hsl(var(--muted-foreground))',
'sidebar-background': 'hsl(var(--sidebar-background))',
'sidebar-foreground': 'hsl(var(--sidebar-foreground))',
hover: 'hsl(var(--hover))',
success: 'hsl(var(--success))',
'success-light': 'hsl(var(--success-light))',
warning: 'hsl(var(--warning))',
'warning-light': 'hsl(var(--warning-light))',
},
fontFamily: {
sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'],
mono: ['Consolas', 'Monaco', 'Courier New', 'monospace'],
},
boxShadow: {
'sm': '0 1px 2px 0 rgb(0 0 0 / 0.05)',
'DEFAULT': '0 2px 8px rgb(0 0 0 / 0.08)',
'md': '0 4px 12px rgb(0 0 0 / 0.1)',
'lg': '0 8px 24px rgb(0 0 0 / 0.12)',
},
},
},
}
</script>
<style>
/* CSS Custom Properties - Light Mode */
:root {
--background: 0 0% 98%;
--foreground: 0 0% 13%;
--card: 0 0% 100%;
--card-foreground: 0 0% 13%;
--border: 0 0% 90%;
--input: 0 0% 90%;
--ring: 220 65% 50%;
--primary: 220 65% 50%;
--primary-foreground: 0 0% 100%;
--primary-light: 220 65% 95%;
--secondary: 220 60% 65%;
--secondary-foreground: 0 0% 100%;
--accent: 220 40% 95%;
--accent-foreground: 0 0% 13%;
--destructive: 8 75% 55%;
--destructive-foreground: 0 0% 100%;
--muted: 0 0% 96%;
--muted-foreground: 0 0% 45%;
--sidebar-background: 0 0% 97%;
--sidebar-foreground: 0 0% 13%;
--hover: 0 0% 93%;
--success: 142 71% 45%;
--success-light: 142 76% 90%;
--warning: 38 92% 50%;
--warning-light: 48 96% 89%;
}
/* Dark Mode */
[data-theme="dark"] {
--background: 220 13% 10%;
--foreground: 0 0% 90%;
--card: 220 13% 14%;
--card-foreground: 0 0% 90%;
--border: 220 13% 20%;
--input: 220 13% 20%;
--ring: 220 65% 55%;
--primary: 220 65% 55%;
--primary-foreground: 0 0% 100%;
--primary-light: 220 50% 25%;
--secondary: 220 60% 60%;
--secondary-foreground: 0 0% 100%;
--accent: 220 30% 20%;
--accent-foreground: 0 0% 90%;
--destructive: 8 70% 50%;
--destructive-foreground: 0 0% 100%;
--muted: 220 13% 18%;
--muted-foreground: 0 0% 55%;
--sidebar-background: 220 13% 12%;
--sidebar-foreground: 0 0% 90%;
--hover: 220 13% 22%;
--success: 142 71% 40%;
--success-light: 142 50% 20%;
--warning: 38 85% 45%;
--warning-light: 40 50% 20%;
}
/* Scrollbar styling */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: hsl(var(--border)); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: hsl(var(--muted-foreground)); }
/* Sidebar collapse state */
.sidebar.collapsed { width: 60px; }
.sidebar.collapsed .nav-text,
.sidebar.collapsed .nav-section-title,
.sidebar.collapsed .badge,
.sidebar.collapsed .toggle-text { display: none; }
.sidebar.collapsed .nav-section-header { justify-content: center; padding: 12px 0; }
.sidebar.collapsed .nav-item { justify-content: center; padding: 10px 0; }
.sidebar.collapsed .toggle-icon { transform: rotate(180deg); }
/* Path menu open state */
.path-menu.open { display: block; }
/* Mobile sidebar */
@media (max-width: 768px) {
.sidebar {
position: fixed;
left: -260px;
top: 56px;
height: calc(100vh - 56px);
z-index: 200;
box-shadow: 0 8px 24px rgb(0 0 0 / 0.15);
}
.sidebar.open { left: 0; }
.sidebar-overlay.open { display: block; }
.menu-toggle-btn { display: block !important; }
}
/* Task drawer */
.task-detail-drawer {
transform: translateX(100%);
transition: transform 0.3s ease;
}
.task-detail-drawer.open { transform: translateX(0); }
.drawer-overlay.active { display: block; }
/* Injected from dashboard.css */
/* ===================================
Dashboard - Complementary Styles
================================== */
/* This file contains only essential CSS that cannot be achieved
with Tailwind utilities. All layout, colors, and basic styling
are handled by Tailwind classes in dashboard.html.
CSS variables are defined inline in dashboard.html <style> block. */
/* Scrollbar styling (cannot be done in Tailwind) */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: hsl(var(--border));
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: hsl(var(--muted-foreground));
}
/* Sidebar collapse state (JavaScript-toggled class) */
.sidebar.collapsed {
width: 60px;
}
.sidebar.collapsed .nav-text,
.sidebar.collapsed .nav-section-title,
.sidebar.collapsed .badge,
.sidebar.collapsed .toggle-text {
display: none;
}
.sidebar.collapsed .nav-section-header {
justify-content: center;
padding: 12px 0;
}
.sidebar.collapsed .nav-item {
justify-content: center;
padding: 10px 0;
}
.sidebar.collapsed .toggle-icon {
transform: rotate(180deg);
}
/* Path menu open state (JavaScript-toggled class) */
.path-menu.open {
display: block;
}
/* Mobile sidebar (responsive behavior beyond Tailwind) */
@media (max-width: 768px) {
.sidebar {
position: fixed;
left: -260px;
top: 56px;
height: calc(100vh - 56px);
z-index: 200;
box-shadow: 0 8px 24px rgb(0 0 0 / 0.15);
}
.sidebar.open {
left: 0;
}
.sidebar-overlay {
display: none;
}
.sidebar-overlay.open {
display: block;
}
.menu-toggle-btn {
display: block !important;
}
}
/* Task detail drawer (complex transform animation) */
.task-detail-drawer {
transform: translateX(100%);
transition: transform 0.3s ease;
}
.task-detail-drawer.open {
transform: translateX(0);
}
.drawer-overlay.active {
display: block;
}
/* ===================================
Session Cards (used by dashboard.js)
=================================== */
.sessions-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1rem;
}
.session-card {
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 0.5rem;
padding: 1rem;
cursor: pointer;
transition: all 0.2s ease;
}
.session-card:hover {
box-shadow: 0 4px 12px rgb(0 0 0 / 0.1);
transform: translateY(-2px);
}
.session-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.75rem;
}
.session-title {
font-weight: 600;
font-size: 0.9rem;
color: hsl(var(--foreground));
word-break: break-word;
}
.session-badges {
display: flex;
gap: 0.5rem;
flex-shrink: 0;
}
.session-status {
font-size: 0.65rem;
font-weight: 600;
padding: 0.2rem 0.5rem;
border-radius: 9999px;
text-transform: uppercase;
}
.session-status.active {
background: hsl(var(--success-light));
color: hsl(var(--success));
}
.session-status.archived {
background: hsl(var(--muted));
color: hsl(var(--muted-foreground));
}
.session-type-badge {
font-size: 0.65rem;
font-weight: 500;
padding: 0.2rem 0.5rem;
border-radius: 9999px;
background: hsl(var(--accent));
color: hsl(var(--accent-foreground));
}
.session-type-badge.review {
background: hsl(var(--warning-light));
color: hsl(var(--warning));
}
.session-type-badge.test {
background: hsl(220 80% 90%);
color: hsl(220 80% 40%);
}
.session-body {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.session-meta {
display: flex;
gap: 1rem;
font-size: 0.8rem;
color: hsl(var(--muted-foreground));
}
.session-meta-item {
display: flex;
align-items: center;
gap: 0.25rem;
}
.progress-container {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.progress-label {
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
}
.progress-bar-wrapper {
display: flex;
align-items: center;
gap: 0.75rem;
}
.progress-bar {
flex: 1;
height: 6px;
background: hsl(var(--muted));
border-radius: 3px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, hsl(var(--primary)), hsl(var(--success)));
border-radius: 3px;
transition: width 0.3s ease;
}
.progress-text {
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
white-space: nowrap;
}
/* Empty state */
.empty-state {
text-align: center;
padding: 3rem;
color: hsl(var(--muted-foreground));
}
.empty-state-icon {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.5;
}
/* Session detail page */
.session-detail-header {
margin-bottom: 1.5rem;
}
.session-detail-title {
font-size: 1.5rem;
font-weight: 600;
color: hsl(var(--foreground));
margin-bottom: 0.5rem;
}
.session-detail-meta {
display: flex;
gap: 1rem;
flex-wrap: wrap;
font-size: 0.875rem;
color: hsl(var(--muted-foreground));
}
.task-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.task-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.15s ease;
}
.task-item:hover {
background: hsl(var(--hover));
}
.task-item.completed {
border-left: 3px solid hsl(var(--success));
opacity: 0.8;
}
.task-item.in_progress {
border-left: 3px solid hsl(var(--warning));
}
.task-item.pending {
border-left: 3px solid hsl(var(--muted-foreground));
}
.task-checkbox {
width: 1.25rem;
height: 1.25rem;
border-radius: 50%;
border: 2px solid hsl(var(--border));
display: flex;
align-items: center;
justify-content: center;
font-size: 0.7rem;
font-weight: bold;
flex-shrink: 0;
}
.task-item.completed .task-checkbox {
background: hsl(var(--success));
border-color: hsl(var(--success));
color: white;
}
.task-item.completed .task-checkbox::after {
content: '✓';
}
.task-item.in_progress .task-checkbox {
border-color: hsl(var(--warning));
color: hsl(var(--warning));
}
.task-item.in_progress .task-checkbox::after {
content: '⟳';
}
.task-title {
flex: 1;
font-size: 0.875rem;
color: hsl(var(--foreground));
}
.task-id {
font-size: 0.75rem;
font-family: monospace;
color: hsl(var(--muted-foreground));
}
/* Back button */
.back-button {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: hsl(var(--muted));
border: none;
border-radius: 0.375rem;
font-size: 0.875rem;
color: hsl(var(--foreground));
cursor: pointer;
transition: background 0.15s;
margin-bottom: 1rem;
}
.back-button:hover {
background: hsl(var(--hover));
}
/* ===================================
Path Dropdown Menu
=================================== */
.path-menu {
min-width: 320px;
max-height: 400px;
overflow-y: auto;
}
.path-menu .path-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
cursor: pointer;
transition: background 0.15s;
border-bottom: 1px solid hsl(var(--border));
}
.path-menu .path-item:hover {
background: hsl(var(--hover));
}
.path-menu .path-item:last-child {
border-bottom: none;
}
.path-menu .path-icon {
font-size: 1.25rem;
flex-shrink: 0;
}
.path-menu .path-text {
flex: 1;
font-size: 0.875rem;
color: hsl(var(--foreground));
word-break: break-all;
}
/* ===================================
Session Detail Page
=================================== */
.session-detail-page {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.detail-header {
display: flex;
flex-direction: column;
gap: 1rem;
}
.btn-back {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: hsl(var(--muted));
border: none;
border-radius: 0.375rem;
font-size: 0.875rem;
color: hsl(var(--foreground));
cursor: pointer;
transition: background 0.15s;
width: fit-content;
}
.btn-back:hover {
background: hsl(var(--hover));
}
.back-icon {
font-size: 1rem;
}
.detail-title-row {
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}
.detail-session-id {
font-size: 1.5rem;
font-weight: 600;
color: hsl(var(--foreground));
margin: 0;
word-break: break-word;
}
.detail-badges {
display: flex;
gap: 0.5rem;
}
.detail-info-bar {
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
padding: 1rem;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 0.5rem;
}
.info-item {
display: flex;
gap: 0.5rem;
font-size: 0.875rem;
}
.info-label {
color: hsl(var(--muted-foreground));
}
.info-value {
color: hsl(var(--foreground));
font-weight: 500;
}
/* Detail Tabs */
.detail-tabs {
display: flex;
gap: 0.25rem;
border-bottom: 1px solid hsl(var(--border));
overflow-x: auto;
}
.detail-tab {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
background: none;
border: none;
border-bottom: 2px solid transparent;
font-size: 0.875rem;
color: hsl(var(--muted-foreground));
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
}
.detail-tab:hover {
color: hsl(var(--foreground));
background: hsl(var(--hover));
}
.detail-tab.active {
color: hsl(var(--primary));
border-bottom-color: hsl(var(--primary));
}
.tab-icon {
font-size: 1rem;
}
.tab-count {
padding: 0.125rem 0.5rem;
background: hsl(var(--muted));
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
}
.detail-tab.active .tab-count {
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
}
.detail-tab-content {
min-height: 300px;
}
/* Task Stats */
.task-stats {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
}
.task-stat {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 0.375rem;
font-size: 0.875rem;
}
.task-stat.completed { border-left: 3px solid hsl(var(--success)); }
.task-stat.in-progress { border-left: 3px solid hsl(var(--warning)); }
.task-stat.pending { border-left: 3px solid hsl(var(--muted-foreground)); }
.stat-count {
font-weight: 600;
color: hsl(var(--foreground));
}
.stat-label {
color: hsl(var(--muted-foreground));
}
/* Tab Loading */
.tab-loading {
display: flex;
align-items: center;
justify-content: center;
padding: 3rem;
color: hsl(var(--muted-foreground));
font-size: 0.875rem;
}
/* Context/Summary Content */
.context-content,
.summary-content,
.impl-plan-content,
.review-content {
padding: 1rem;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 0.5rem;
}
.context-section,
.summary-section,
.plan-section {
margin-bottom: 1.5rem;
}
.context-section:last-child,
.summary-section:last-child,
.plan-section:last-child {
margin-bottom: 0;
}
/* Plan Tab Styles */
.plan-tab-content {
padding: 1rem 0;
}
.plan-section-title {
font-size: 1rem;
font-weight: 600;
color: hsl(var(--foreground));
margin-bottom: 0.75rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.plan-summary-text,
.plan-approach-text {
color: hsl(var(--foreground));
line-height: 1.6;
padding: 0.75rem 1rem;
background: hsl(var(--muted));
border-radius: 0.5rem;
border-left: 3px solid hsl(var(--primary));
}
.plan-meta-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 0.75rem;
}
.plan-meta-grid .meta-item {
padding: 0.5rem 0.75rem;
background: hsl(var(--muted));
border-radius: 0.25rem;
font-size: 0.875rem;
}
.plan-meta-grid .meta-label {
font-weight: 600;
color: hsl(var(--muted-foreground));
}
/* Lite Task Detail Page */
.lite-task-detail-page .detail-header {
margin-bottom: 1.5rem;
}
.lite-task-detail-page .task-stats-bar {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
padding: 0.75rem 1rem;
background: hsl(var(--muted));
border-radius: 0.5rem;
}
.lite-task-detail-page .task-stat {
font-size: 0.875rem;
color: hsl(var(--muted-foreground));
}
.lite-task-detail-page .task-stat.completed {
color: hsl(var(--success));
}
.lite-task-detail-page .task-stat.in-progress {
color: hsl(var(--warning));
}
/* Context Tab Content */
.context-tab-content {
padding: 1rem 0;
}
.context-tab-content .context-section h4 {
font-size: 0.9rem;
font-weight: 600;
color: hsl(var(--foreground));
margin-bottom: 0.5rem;
}
.context-tab-content .context-section p {
color: hsl(var(--foreground));
line-height: 1.6;
}
.section-title {
font-size: 1rem;
font-weight: 600;
color: hsl(var(--foreground));
margin-bottom: 0.75rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.file-list {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.file-item {
padding: 0.5rem 0.75rem;
background: hsl(var(--muted));
border-radius: 0.25rem;
font-family: monospace;
font-size: 0.8rem;
color: hsl(var(--foreground));
}
/* Code blocks */
pre, code {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
background: hsl(var(--muted));
border-radius: 0.25rem;
}
pre {
padding: 1rem;
overflow-x: auto;
font-size: 0.8rem;
line-height: 1.5;
}
code {
padding: 0.125rem 0.25rem;
font-size: 0.85em;
}
/* Review Findings */
.findings-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.finding-item {
padding: 1rem;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 0.5rem;
border-left: 3px solid hsl(var(--border));
}
.finding-item.critical { border-left-color: hsl(0 70% 50%); }
.finding-item.high { border-left-color: hsl(25 90% 55%); }
.finding-item.medium { border-left-color: hsl(45 90% 50%); }
.finding-item.low { border-left-color: hsl(var(--success)); }
.finding-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.5rem;
}
.finding-title {
font-weight: 600;
color: hsl(var(--foreground));
}
.severity-badge {
padding: 0.125rem 0.5rem;
border-radius: 9999px;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
}
.severity-badge.critical { background: hsl(0 70% 90%); color: hsl(0 70% 40%); }
.severity-badge.high { background: hsl(25 90% 90%); color: hsl(25 90% 35%); }
.severity-badge.medium { background: hsl(45 90% 90%); color: hsl(45 90% 30%); }
.severity-badge.low { background: hsl(var(--success-light)); color: hsl(var(--success)); }
.finding-description {
font-size: 0.875rem;
color: hsl(var(--muted-foreground));
line-height: 1.5;
}
.finding-location {
margin-top: 0.5rem;
font-size: 0.75rem;
font-family: monospace;
color: hsl(var(--muted-foreground));
}
/* ===================================
Tasks Tab Content
=================================== */
.tasks-tab-content {
display: flex;
flex-direction: column;
gap: 1rem;
}
.task-stats-bar {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.task-stat {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 0.5rem;
font-size: 0.875rem;
border-left: 3px solid hsl(var(--border));
}
.task-stat.completed {
border-left-color: hsl(var(--success));
color: hsl(var(--success));
}
.task-stat.in-progress {
border-left-color: hsl(var(--warning));
color: hsl(var(--warning));
}
.task-stat.pending {
border-left-color: hsl(var(--muted-foreground));
color: hsl(var(--muted-foreground));
}
.tasks-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
/* Task Item */
.detail-task-item {
display: flex;
flex-direction: column;
padding: 0.875rem 1rem;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 0.5rem;
border-left: 3px solid hsl(var(--muted-foreground));
transition: all 0.15s ease;
}
.detail-task-item:hover {
background: hsl(var(--hover));
box-shadow: 0 2px 8px rgb(0 0 0 / 0.08);
}
.detail-task-item.completed {
border-left-color: hsl(var(--success));
}
.detail-task-item.in_progress {
border-left-color: hsl(var(--warning));
}
.detail-task-item.pending {
border-left-color: hsl(var(--muted-foreground));
}
/* Status-based background colors for task cards */
.detail-task-item.status-completed {
background: hsl(var(--success) / 0.08);
}
.detail-task-item.status-completed:hover {
background: hsl(var(--success) / 0.12);
}
.detail-task-item.status-in_progress {
background: hsl(var(--warning) / 0.08);
}
.detail-task-item.status-in_progress:hover {
background: hsl(var(--warning) / 0.12);
}
.detail-task-item.status-pending {
background: hsl(var(--muted-foreground) / 0.05);
}
.detail-task-item.status-pending:hover {
background: hsl(var(--muted-foreground) / 0.08);
}
.task-item-header {
display: flex;
align-items: center;
gap: 0.75rem;
}
.task-status-icon {
font-size: 1rem;
width: 1.5rem;
text-align: center;
flex-shrink: 0;
}
.detail-task-item.completed .task-status-icon {
color: hsl(var(--success));
}
.detail-task-item.in_progress .task-status-icon {
color: hsl(var(--warning));
}
.task-id-badge {
padding: 0.125rem 0.5rem;
background: hsl(var(--muted));
border-radius: 0.25rem;
font-size: 0.75rem;
font-family: monospace;
font-weight: 500;
color: hsl(var(--foreground));
flex-shrink: 0;
}
.task-item-header .task-title {
flex: 1;
font-size: 0.875rem;
color: hsl(var(--foreground));
}
.task-status-badge {
padding: 0.125rem 0.5rem;
border-radius: 9999px;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
flex-shrink: 0;
}
.task-status-badge.completed {
background: hsl(var(--success-light));
color: hsl(var(--success));
}
.task-status-badge.in_progress {
background: hsl(var(--warning-light));
color: hsl(var(--warning));
}
.task-status-badge.pending {
background: hsl(var(--muted));
color: hsl(var(--muted-foreground));
}
/* Full Task Item (expanded view) */
.detail-task-item-full {
display: flex;
flex-direction: column;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 0.5rem;
border-left: 3px solid hsl(var(--muted-foreground));
overflow: hidden;
}
.detail-task-item-full.completed {
border-left-color: hsl(var(--success));
}
.detail-task-item-full.in_progress {
border-left-color: hsl(var(--warning));
}
.task-item-header-full {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem;
background: hsl(var(--card));
transition: background 0.15s;
}
.task-item-header-full:hover {
background: hsl(var(--hover));
}
/* Empty State */
.tab-empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 4rem 2rem;
text-align: center;
}
.empty-icon {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.empty-title {
font-size: 1.25rem;
font-weight: 600;
color: hsl(var(--foreground));
margin-bottom: 0.5rem;
}
.empty-text {
font-size: 0.875rem;
color: hsl(var(--muted-foreground));
}
/* Tab Error */
.tab-error {
padding: 2rem;
text-align: center;
color: hsl(var(--destructive));
background: hsl(var(--destructive) / 0.1);
border-radius: 0.5rem;
}
/* Collapsible Sections */
.collapsible-header {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
background: hsl(var(--muted));
cursor: pointer;
transition: background 0.15s;
}
.collapsible-header:hover {
background: hsl(var(--hover));
}
.collapsible-icon {
transition: transform 0.2s;
}
.collapsible-header.open .collapsible-icon {
transform: rotate(90deg);
}
.collapsible-content {
padding: 1rem;
display: none;
}
.collapsible-content.open {
display: block;
}
/* ===================================
Task Drawer (Sidebar Panel)
=================================== */
.drawer-overlay {
display: none;
}
.drawer-overlay.active {
display: block;
}
.drawer-task-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid hsl(var(--border));
}
.drawer-tabs {
display: flex;
gap: 0.25rem;
margin-bottom: 1rem;
border-bottom: 1px solid hsl(var(--border));
overflow-x: auto;
}
.drawer-tab {
padding: 0.625rem 1rem;
background: none;
border: none;
border-bottom: 2px solid transparent;
font-size: 0.875rem;
color: hsl(var(--muted-foreground));
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
}
.drawer-tab:hover {
color: hsl(var(--foreground));
background: hsl(var(--hover));
}
.drawer-tab.active {
color: hsl(var(--primary));
border-bottom-color: hsl(var(--primary));
}
.drawer-tab-content {
flex: 1;
overflow-y: auto;
}
.drawer-panel {
display: none;
}
.drawer-panel.active {
display: block;
}
.drawer-section {
margin-bottom: 1.5rem;
}
.drawer-section-title {
font-size: 0.875rem;
font-weight: 600;
color: hsl(var(--foreground));
margin: 0 0 0.75rem 0;
display: flex;
align-items: center;
gap: 0.5rem;
}
.empty-section {
padding: 1rem;
text-align: center;
color: hsl(var(--muted-foreground));
font-size: 0.875rem;
background: hsl(var(--muted));
border-radius: 0.375rem;
margin-bottom: 1rem;
}
/* Steps List */
.steps-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.step-item {
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 0.75rem;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 0.375rem;
}
.step-number {
width: 1.5rem;
height: 1.5rem;
display: flex;
align-items: center;
justify-content: center;
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
border-radius: 50%;
font-size: 0.75rem;
font-weight: 600;
flex-shrink: 0;
}
.step-content {
flex: 1;
font-size: 0.875rem;
color: hsl(var(--foreground));
line-height: 1.5;
}
.step-description {
font-size: 0.8rem;
color: hsl(var(--muted-foreground));
margin-top: 0.25rem;
}
/* Files List */
.files-list {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.file-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: hsl(var(--muted));
border-radius: 0.25rem;
font-family: monospace;
font-size: 0.8rem;
color: hsl(var(--foreground));
}
.file-icon {
font-size: 0.875rem;
flex-shrink: 0;
}
/* Test Commands */
.test-commands {
margin-top: 1rem;
}
.command-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: hsl(220 13% 18%);
color: hsl(142 71% 60%);
border-radius: 0.25rem;
font-family: monospace;
font-size: 0.8rem;
margin-bottom: 0.375rem;
}
/* Flowchart Container */
.flowchart-container {
min-height: 300px;
background: hsl(var(--muted));
border-radius: 0.5rem;
padding: 1rem;
overflow: auto;
}
.flowchart-container svg {
max-width: 100%;
}
/* JSON View */
.json-view {
background: hsl(var(--muted));
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
font-size: 0.75rem;
line-height: 1.6;
color: hsl(var(--foreground));
max-height: 500px;
overflow-y: auto;
}
/* ===================================
Lite Task Detail Page Additions
=================================== */
/* Path Tags */
.path-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.path-tag {
padding: 0.25rem 0.5rem;
background: hsl(var(--muted));
border-radius: 0.25rem;
font-family: monospace;
font-size: 0.8rem;
color: hsl(var(--foreground));
}
/* JSON Content */
.json-content {
background: hsl(var(--muted));
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
font-size: 0.75rem;
line-height: 1.6;
color: hsl(var(--foreground));
max-height: 500px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-word;
}
/* Button View JSON */
.btn-view-json {
padding: 0.25rem 0.5rem;
background: hsl(var(--muted));
border: 1px solid hsl(var(--border));
border-radius: 0.25rem;
font-size: 0.7rem;
font-family: monospace;
color: hsl(var(--muted-foreground));
cursor: pointer;
transition: all 0.15s;
}
.btn-view-json:hover {
background: hsl(var(--hover));
color: hsl(var(--foreground));
}
/* Context Fields */
.context-fields {
display: flex;
flex-direction: column;
gap: 1rem;
}
.context-section {
padding: 1rem;
background: hsl(var(--muted) / 0.3);
border: 1px solid hsl(var(--border));
border-radius: 0.5rem;
}
.context-section-title {
font-size: 1rem;
font-weight: 600;
color: hsl(var(--foreground));
margin-bottom: 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid hsl(var(--border));
}
.context-field {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.context-label {
font-size: 0.75rem;
font-weight: 600;
color: hsl(var(--muted-foreground));
text-transform: uppercase;
letter-spacing: 0.025em;
}
.context-value {
font-size: 0.875rem;
color: hsl(var(--foreground));
line-height: 1.6;
}
.context-field label {
display: block;
font-size: 0.75rem;
font-weight: 600;
color: hsl(var(--muted-foreground));
margin-bottom: 0.5rem;
text-transform: uppercase;
}
.context-field p {
margin: 0;
font-size: 0.875rem;
color: hsl(var(--foreground));
line-height: 1.5;
}
.context-field ul {
margin: 0;
padding-left: 1.25rem;
font-size: 0.875rem;
color: hsl(var(--foreground));
}
.context-field ul li {
margin-bottom: 0.25rem;
}
/* Modification Points */
.mod-points {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.mod-point {
padding: 0.5rem;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 0.25rem;
}
.mod-target {
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
margin-left: 0.5rem;
}
.mod-change {
margin: 0.5rem 0 0 0;
font-size: 0.8rem;
color: hsl(var(--foreground));
}
/* Implementation Steps */
.impl-steps {
margin: 0;
padding-left: 1.25rem;
font-size: 0.875rem;
color: hsl(var(--foreground));
}
.impl-steps li {
margin-bottom: 0.5rem;
line-height: 1.5;
}
/* Implementation Steps List (Drawer) */
.impl-steps-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.impl-step-item {
background: hsl(var(--muted) / 0.3);
border: 1px solid hsl(var(--border));
border-radius: 0.5rem;
padding: 1rem;
transition: border-color 0.2s;
}
.impl-step-item:hover {
border-color: hsl(var(--primary) / 0.5);
}
.impl-step-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.5rem;
}
.impl-step-number {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.75rem;
height: 1.75rem;
padding: 0 0.5rem;
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
border-radius: 0.375rem;
font-size: 0.75rem;
font-weight: 600;
white-space: nowrap;
}
.impl-step-title {
font-weight: 600;
font-size: 0.9rem;
color: hsl(var(--foreground));
flex: 1;
}
.impl-step-desc {
font-size: 0.85rem;
color: hsl(var(--muted-foreground));
line-height: 1.5;
margin-bottom: 0.75rem;
padding-left: 2.5rem;
}
.impl-step-columns {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 1px solid hsl(var(--border));
}
.impl-step-mods,
.impl-step-flow {
font-size: 0.8rem;
}
.impl-step-mods strong,
.impl-step-flow strong {
display: block;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: hsl(var(--muted-foreground));
margin-bottom: 0.5rem;
}
.impl-step-mods ul,
.impl-step-flow ol {
margin: 0;
padding-left: 1.25rem;
color: hsl(var(--foreground));
}
.impl-step-mods li,
.impl-step-flow li {
margin-bottom: 0.375rem;
line-height: 1.4;
}
.impl-step-mods code {
font-size: 0.75rem;
padding: 0.125rem 0.375rem;
background: hsl(var(--muted));
border-radius: 0.25rem;
color: hsl(var(--primary));
}
.impl-step-deps {
margin-top: 0.75rem;
padding-top: 0.5rem;
border-top: 1px dashed hsl(var(--border));
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
}
.dep-badge {
display: inline-block;
padding: 0.125rem 0.5rem;
background: hsl(var(--muted));
border-radius: 0.25rem;
font-size: 0.7rem;
margin-left: 0.25rem;
}
/* Field Groups */
.field-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.field-row {
display: flex;
gap: 0.5rem;
font-size: 0.875rem;
}
.field-label {
font-weight: 500;
color: hsl(var(--muted-foreground));
min-width: 100px;
}
.field-value {
color: hsl(var(--foreground));
flex: 1;
}
.json-value-null {
color: hsl(var(--muted-foreground));
font-style: italic;
}
.json-value-boolean {
color: hsl(220 80% 60%);
}
.json-value-number {
color: hsl(142 71% 45%);
}
.json-value-string {
color: hsl(var(--foreground));
}
/* Array Items */
.array-value {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
}
.array-item {
padding: 0.125rem 0.375rem;
background: hsl(var(--muted));
border-radius: 0.25rem;
font-size: 0.8rem;
}
.array-item.path-item {
font-family: monospace;
background: hsl(var(--accent));
}
/* Nested Array */
.nested-array {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-top: 0.25rem;
}
.array-object {
padding: 0.5rem;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 0.25rem;
}
.array-object-header {
font-size: 0.7rem;
font-weight: 600;
color: hsl(var(--muted-foreground));
margin-bottom: 0.25rem;
}
/* Collapsible Sections */
.collapsible-section {
border-top: 1px solid hsl(var(--border));
}
.collapsible-header {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
cursor: pointer;
transition: background 0.15s;
}
.collapsible-header:hover {
background: hsl(var(--hover));
}
.collapse-icon {
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
transition: transform 0.2s;
}
.collapsible-header.expanded .collapse-icon {
transform: rotate(90deg);
}
.section-label {
font-size: 0.75rem;
font-weight: 600;
color: hsl(var(--foreground));
text-transform: uppercase;
}
.section-preview {
flex: 1;
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
text-align: right;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.collapsible-content {
padding: 1rem;
background: hsl(var(--muted));
}
.collapsible-content.collapsed {
display: none;
}
/* Summary Tab */
.summary-tab-content {
display: flex;
flex-direction: column;
gap: 1rem;
}
.summary-item-collapsible {
border: 1px solid hsl(var(--border));
border-radius: 0.5rem;
overflow: hidden;
}
.summary-collapsible-header {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
background: hsl(var(--card));
cursor: pointer;
transition: background 0.15s;
}
.summary-collapsible-header:hover {
background: hsl(var(--hover));
}
.summary-name {
font-weight: 600;
color: hsl(var(--foreground));
}
.summary-preview {
flex: 1;
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.summary-collapsible-content {
padding: 1rem;
background: hsl(var(--muted));
}
.summary-collapsible-content.collapsed {
display: none;
}
.summary-content-pre {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
font-size: 0.8rem;
line-height: 1.6;
}
/* Summary Item Direct (No collapse) */
.summary-item-direct {
border: 1px solid hsl(var(--border));
border-radius: 0.5rem;
padding: 1rem;
background: hsl(var(--card));
}
.summary-item-direct .summary-content-pre {
margin-top: 0.5rem;
padding: 0.75rem;
background: hsl(var(--muted));
border-radius: 0.375rem;
}
.markdown-content {
background: hsl(var(--muted));
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
font-size: 0.8rem;
line-height: 1.6;
color: hsl(var(--foreground));
max-height: 600px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-word;
margin: 0;
}
/* JSON Modal */
.json-modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
opacity: 0;
transition: opacity 0.2s;
}
.json-modal-overlay.active {
opacity: 1;
}
.json-modal {
background: hsl(var(--card));
border-radius: 0.5rem;
width: 90%;
max-width: 700px;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 8px 24px rgb(0 0 0 / 0.2);
}
.json-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
border-bottom: 1px solid hsl(var(--border));
}
.json-modal-title {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 600;
color: hsl(var(--foreground));
}
.json-modal-close {
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
font-size: 1.5rem;
color: hsl(var(--muted-foreground));
cursor: pointer;
border-radius: 0.25rem;
}
.json-modal-close:hover {
background: hsl(var(--hover));
color: hsl(var(--foreground));
}
.json-modal-body {
flex: 1;
overflow: auto;
padding: 1rem;
}
.json-modal-content {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
font-size: 0.75rem;
line-height: 1.6;
color: hsl(var(--foreground));
}
.json-modal-footer {
padding: 1rem;
border-top: 1px solid hsl(var(--border));
display: flex;
justify-content: flex-end;
}
.btn-copy-json {
padding: 0.5rem 1rem;
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
border: none;
border-radius: 0.375rem;
font-size: 0.875rem;
cursor: pointer;
transition: opacity 0.15s;
}
.btn-copy-json:hover {
opacity: 0.9;
}
/* Flowchart Fallback */
.flowchart-fallback {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
color: hsl(var(--muted-foreground));
font-size: 0.875rem;
}
/* ===================================
Markdown Modal
=================================== */
.markdown-modal.hidden {
display: none;
}
.md-tab-btn {
color: hsl(var(--muted-foreground));
}
.md-tab-btn.active {
background: hsl(var(--background));
color: hsl(var(--foreground));
font-weight: 500;
}
.md-tab-btn:hover:not(.active) {
color: hsl(var(--foreground));
}
/* Markdown Preview Prose Styles */
.markdown-preview {
color: hsl(var(--foreground));
line-height: 1.7;
}
.markdown-preview h1,
.markdown-preview h2,
.markdown-preview h3,
.markdown-preview h4,
.markdown-preview h5,
.markdown-preview h6 {
color: hsl(var(--foreground));
font-weight: 600;
margin-top: 1.5em;
margin-bottom: 0.5em;
line-height: 1.3;
}
.markdown-preview h1 { font-size: 1.75rem; border-bottom: 1px solid hsl(var(--border)); padding-bottom: 0.3em; }
.markdown-preview h2 { font-size: 1.5rem; border-bottom: 1px solid hsl(var(--border)); padding-bottom: 0.3em; }
.markdown-preview h3 { font-size: 1.25rem; }
.markdown-preview h4 { font-size: 1.1rem; }
.markdown-preview p {
margin-bottom: 1em;
}
.markdown-preview ul,
.markdown-preview ol {
margin-bottom: 1em;
padding-left: 1.5em;
}
.markdown-preview li {
margin-bottom: 0.25em;
}
.markdown-preview code {
background: hsl(var(--muted));
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
font-size: 0.875em;
color: hsl(var(--primary));
}
.markdown-preview pre {
background: hsl(var(--muted));
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
margin-bottom: 1em;
}
.markdown-preview pre code {
background: none;
padding: 0;
color: hsl(var(--foreground));
}
.markdown-preview blockquote {
border-left: 3px solid hsl(var(--primary));
padding-left: 1rem;
margin-left: 0;
margin-bottom: 1em;
color: hsl(var(--muted-foreground));
font-style: italic;
}
.markdown-preview table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1em;
}
.markdown-preview th,
.markdown-preview td {
border: 1px solid hsl(var(--border));
padding: 0.5rem 0.75rem;
text-align: left;
}
.markdown-preview th {
background: hsl(var(--muted));
font-weight: 600;
}
.markdown-preview a {
color: hsl(var(--primary));
text-decoration: underline;
}
.markdown-preview hr {
border: none;
border-top: 1px solid hsl(var(--border));
margin: 1.5em 0;
}
/* View Details Button */
.btn-view-details {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.75rem;
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
border: none;
border-radius: 0.375rem;
font-size: 0.8rem;
cursor: pointer;
transition: opacity 0.15s;
}
.btn-view-details:hover {
opacity: 0.9;
}
.summary-item-card {
background: hsl(var(--muted) / 0.3);
border: 1px solid hsl(var(--border));
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 0.75rem;
}
.summary-item-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.summary-item-name {
font-weight: 500;
color: hsl(var(--foreground));
display: flex;
align-items: center;
gap: 0.5rem;
}
.summary-item-preview {
font-size: 0.8rem;
color: hsl(var(--muted-foreground));
margin-top: 0.5rem;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.impl-plan-card {
background: hsl(var(--muted) / 0.3);
border: 1px solid hsl(var(--border));
border-radius: 0.5rem;
padding: 1rem;
}
.impl-plan-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.75rem;
}
.impl-plan-title {
font-weight: 600;
color: hsl(var(--foreground));
display: flex;
align-items: center;
gap: 0.5rem;
}
.impl-plan-preview {
font-size: 0.8rem;
color: hsl(var(--muted-foreground));
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* ===================================
Context Package UI
=================================== */
.context-package-container {
display: flex;
flex-direction: column;
gap: 1rem;
}
.ctx-header {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 0.75rem;
border-bottom: 1px solid hsl(var(--border));
}
.ctx-main-title {
font-size: 1.125rem;
font-weight: 600;
color: hsl(var(--foreground));
margin: 0;
}
.ctx-section {
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 0.5rem;
overflow: hidden;
}
.ctx-section-header {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.875rem 1rem;
background: hsl(var(--muted) / 0.3);
cursor: pointer;
user-select: none;
transition: background 0.15s;
}
.ctx-section-header:hover {
background: hsl(var(--muted) / 0.5);
}
.ctx-collapse-icon {
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
width: 1rem;
}
.ctx-section-title {
font-weight: 600;
font-size: 0.9rem;
color: hsl(var(--foreground));
flex: 1;
}
.ctx-section-content {
padding: 1rem;
}
.ctx-section-content.collapsed {
display: none;
}
.ctx-badge {
padding: 0.25rem 0.625rem;
border-radius: 9999px;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
}
.ctx-badge-count {
padding: 0.125rem 0.5rem;
background: hsl(var(--muted));
border-radius: 9999px;
font-size: 0.7rem;
color: hsl(var(--muted-foreground));
}
.ctx-description {
font-size: 0.875rem;
color: hsl(var(--foreground));
line-height: 1.6;
margin-bottom: 0.75rem;
}
.ctx-keywords {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
margin-bottom: 0.75rem;
}
.ctx-keyword-tag {
padding: 0.25rem 0.5rem;
background: hsl(var(--primary) / 0.1);
color: hsl(var(--primary));
border-radius: 0.25rem;
font-size: 0.75rem;
}
.ctx-meta-row {
display: flex;
gap: 0.5rem;
font-size: 0.8rem;
margin-bottom: 0.375rem;
}
.ctx-meta-label {
color: hsl(var(--muted-foreground));
min-width: 70px;
}
.ctx-meta-value {
color: hsl(var(--foreground));
}
.ctx-stack-grid {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.ctx-stack-group {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
.ctx-stack-label {
font-size: 0.75rem;
font-weight: 600;
color: hsl(var(--muted-foreground));
min-width: 90px;
text-transform: uppercase;
}
.ctx-stack-items {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
}
.ctx-stack-badge {
padding: 0.25rem 0.5rem;
background: hsl(var(--muted));
border-radius: 0.25rem;
font-size: 0.75rem;
color: hsl(var(--foreground));
}
.ctx-subsection {
margin-bottom: 1rem;
}
.ctx-subsection:last-child {
margin-bottom: 0;
}
.ctx-subsection-title {
font-size: 0.8rem;
font-weight: 600;
color: hsl(var(--muted-foreground));
margin: 0 0 0.5rem 0;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.ctx-pattern-list {
margin: 0;
padding-left: 1.25rem;
font-size: 0.85rem;
color: hsl(var(--foreground));
}
.ctx-pattern-list li {
margin-bottom: 0.375rem;
line-height: 1.4;
}
.ctx-conventions-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 0.75rem;
}
.ctx-convention-item {
background: hsl(var(--muted) / 0.3);
border-radius: 0.375rem;
padding: 0.75rem;
}
.ctx-convention-key {
font-size: 0.75rem;
font-weight: 600;
color: hsl(var(--primary));
text-transform: capitalize;
display: block;
margin-bottom: 0.375rem;
}
.ctx-convention-value {
font-size: 0.7rem;
margin: 0;
overflow-x: auto;
color: hsl(var(--foreground));
}
.ctx-integration-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.ctx-integration-item {
display: flex;
gap: 0.75rem;
font-size: 0.8rem;
}
.ctx-integration-key {
font-weight: 500;
color: hsl(var(--muted-foreground));
min-width: 140px;
text-transform: capitalize;
}
.ctx-integration-value {
color: hsl(var(--foreground));
flex: 1;
}
.ctx-assets-tabs {
display: flex;
gap: 0.25rem;
border-bottom: 1px solid hsl(var(--border));
margin-bottom: 0.75rem;
padding-bottom: 0.5rem;
}
.ctx-asset-tab {
padding: 0.375rem 0.75rem;
background: none;
border: none;
border-radius: 0.25rem;
font-size: 0.8rem;
color: hsl(var(--muted-foreground));
cursor: pointer;
transition: all 0.15s;
}
.ctx-asset-tab:hover {
background: hsl(var(--muted) / 0.5);
}
.ctx-asset-tab.active {
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
}
.ctx-asset-panel {
display: none;
}
.ctx-asset-panel.active {
display: block;
}
.ctx-asset-item {
padding: 0.75rem;
background: hsl(var(--muted) / 0.2);
border-radius: 0.375rem;
margin-bottom: 0.5rem;
}
.ctx-asset-main {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.ctx-asset-icon {
font-size: 1rem;
}
.ctx-asset-path {
font-size: 0.8rem;
color: hsl(var(--primary));
word-break: break-all;
}
.ctx-relevance-badge {
padding: 0.125rem 0.375rem;
background: hsl(var(--primary) / 0.15);
color: hsl(var(--primary));
border-radius: 0.25rem;
font-size: 0.65rem;
font-weight: 600;
}
.ctx-asset-role {
display: inline-block;
padding: 0.125rem 0.375rem;
background: hsl(var(--muted));
border-radius: 0.25rem;
font-size: 0.7rem;
color: hsl(var(--muted-foreground));
margin-left: auto;
}
.ctx-asset-note {
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
margin-top: 0.375rem;
font-style: italic;
}
.ctx-asset-tags {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
margin-top: 0.375rem;
}
.ctx-mini-tag {
padding: 0.125rem 0.375rem;
background: hsl(var(--muted));
border-radius: 0.25rem;
font-size: 0.65rem;
color: hsl(var(--foreground));
}
.ctx-asset-exports {
font-size: 0.7rem;
color: hsl(var(--muted-foreground));
margin-top: 0.25rem;
}
.ctx-asset-exports code {
color: hsl(var(--foreground));
}
/* Dependencies Styles */
.ctx-deps-section {
margin-bottom: 1rem;
}
.ctx-deps-title {
font-size: 0.75rem;
font-weight: 600;
color: hsl(var(--muted-foreground));
margin-bottom: 0.5rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.ctx-deps-list {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.ctx-dep-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem 0.75rem;
background: hsl(var(--muted) / 0.2);
border-radius: 0.375rem;
}
.ctx-dep-name {
font-size: 0.8rem;
color: hsl(var(--foreground));
font-weight: 500;
}
.ctx-dep-type {
padding: 0.125rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.65rem;
font-weight: 600;
}
.ctx-dep-type.critical {
background: hsl(var(--destructive) / 0.15);
color: hsl(var(--destructive));
}
.ctx-dep-type.important {
background: hsl(38 92% 50% / 0.15);
color: hsl(38 92% 45%);
}
.ctx-dep-type.optional {
background: hsl(var(--muted));
color: hsl(var(--muted-foreground));
}
.ctx-dep-reason {
font-size: 0.7rem;
color: hsl(var(--muted-foreground));
margin-top: 0.25rem;
}
/* Conflicts & Risk Styles */
.ctx-risk-section {
margin-bottom: 1rem;
}
.ctx-risk-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.ctx-risk-label {
font-size: 0.75rem;
font-weight: 600;
color: hsl(var(--muted-foreground));
text-transform: uppercase;
letter-spacing: 0.05em;
}
.ctx-risk-badge {
padding: 0.25rem 0.75rem;
border-radius: 0.375rem;
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
}
.ctx-risk-badge.high {
background: hsl(var(--destructive) / 0.15);
color: hsl(var(--destructive));
}
.ctx-risk-badge.medium {
background: hsl(38 92% 50% / 0.15);
color: hsl(38 92% 45%);
}
.ctx-risk-badge.low {
background: hsl(142 76% 36% / 0.15);
color: hsl(142 76% 36%);
}
.ctx-potential-conflicts {
margin-bottom: 1rem;
}
.ctx-conflict-item {
padding: 0.75rem;
background: hsl(var(--destructive) / 0.05);
border-left: 3px solid hsl(var(--destructive));
border-radius: 0.375rem;
margin-bottom: 0.5rem;
}
.ctx-conflict-type {
font-size: 0.7rem;
font-weight: 600;
color: hsl(var(--destructive));
text-transform: uppercase;
margin-bottom: 0.25rem;
}
.ctx-conflict-desc {
font-size: 0.8rem;
color: hsl(var(--foreground));
}
/* Resolved & Historical Conflicts */
.ctx-resolved-section {
margin-bottom: 1rem;
}
.ctx-resolved-title {
font-size: 0.75rem;
font-weight: 600;
color: hsl(142 76% 36%);
margin-bottom: 0.5rem;
}
.ctx-resolved-item {
padding: 0.75rem;
background: hsl(142 76% 36% / 0.05);
border-left: 3px solid hsl(142 76% 36%);
border-radius: 0.375rem;
margin-bottom: 0.5rem;
}
.ctx-resolved-type {
font-size: 0.7rem;
font-weight: 600;
color: hsl(142 76% 36%);
text-transform: uppercase;
margin-bottom: 0.25rem;
}
.ctx-resolved-desc {
font-size: 0.8rem;
color: hsl(var(--foreground));
margin-bottom: 0.375rem;
}
.ctx-resolution {
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
padding: 0.5rem;
background: hsl(var(--muted) / 0.3);
border-radius: 0.25rem;
}
.ctx-resolution-label {
font-weight: 600;
color: hsl(var(--foreground));
}
.ctx-historical-section {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid hsl(var(--border));
}
.ctx-historical-title {
font-size: 0.75rem;
font-weight: 600;
color: hsl(var(--muted-foreground));
margin-bottom: 0.5rem;
}
.ctx-historical-item {
padding: 0.5rem 0.75rem;
background: hsl(var(--muted) / 0.2);
border-radius: 0.375rem;
margin-bottom: 0.375rem;
display: flex;
align-items: center;
justify-content: space-between;
}
.ctx-historical-pattern {
font-size: 0.8rem;
color: hsl(var(--foreground));
}
.ctx-historical-count {
font-size: 0.7rem;
padding: 0.125rem 0.5rem;
background: hsl(var(--muted));
border-radius: 0.25rem;
color: hsl(var(--muted-foreground));
}
/* Empty State */
.ctx-empty {
padding: 1.5rem;
text-align: center;
color: hsl(var(--muted-foreground));
font-size: 0.85rem;
font-style: italic;
}
/* Grid Lines for Context Package */
.ctx-section {
border: 1px solid hsl(var(--border) / 0.5);
}
.ctx-section-header {
border-bottom: 1px solid hsl(var(--border) / 0.3);
}
.ctx-meta-grid {
border: 1px solid hsl(var(--border) / 0.3);
border-radius: 0.375rem;
overflow: hidden;
}
.ctx-meta-item {
border-bottom: 1px solid hsl(var(--border) / 0.2);
}
.ctx-meta-item:last-child {
border-bottom: none;
}
.ctx-tech-grid {
border: 1px solid hsl(var(--border) / 0.3);
border-radius: 0.375rem;
padding: 0.75rem;
}
.ctx-arch-subsection {
border: 1px solid hsl(var(--border) / 0.3);
border-radius: 0.375rem;
overflow: hidden;
}
.ctx-arch-title {
border-bottom: 1px solid hsl(var(--border) / 0.2);
padding-bottom: 0.375rem;
margin-bottom: 0.5rem;
}
.ctx-pattern-item {
border-bottom: 1px dashed hsl(var(--border) / 0.3);
padding-bottom: 0.5rem;
}
.ctx-pattern-item:last-child {
border-bottom: none;
padding-bottom: 0;
}
.ctx-asset-tabs {
border-bottom: 1px solid hsl(var(--border) / 0.5);
padding-bottom: 0.5rem;
margin-bottom: 0.75rem;
}
.ctx-asset-item {
border: 1px solid hsl(var(--border) / 0.3);
}
.ctx-deps-section {
border: 1px solid hsl(var(--border) / 0.3);
border-radius: 0.375rem;
padding: 0.75rem;
}
.ctx-dep-item {
border: 1px solid hsl(var(--border) / 0.2);
}
.ctx-conflict-item,
.ctx-resolved-item {
border: 1px solid hsl(var(--border) / 0.3);
}
.ctx-historical-section {
border-top: 1px dashed hsl(var(--border) / 0.5);
}
.ctx-historical-item {
border: 1px solid hsl(var(--border) / 0.2);
}
/* List Markers for Implementation Steps */
.impl-step-mods ul,
.impl-step-flow ol {
list-style-position: inside;
padding-left: 0.5rem;
}
.impl-step-mods ul {
list-style-type: disc;
}
.impl-step-mods ul li {
padding: 0.25rem 0;
padding-left: 0.25rem;
}
.impl-step-flow ol {
list-style-type: decimal;
counter-reset: none;
}
.impl-step-flow ol li {
padding: 0.25rem 0;
padding-left: 0.25rem;
}
.impl-step-flow ol li::marker {
color: hsl(var(--primary));
font-weight: 600;
}
.impl-step-mods ul li::marker {
color: hsl(var(--muted-foreground));
}
/* Context Package List Markers */
.ctx-pattern-list {
list-style-type: decimal;
list-style-position: inside;
padding-left: 0.5rem;
}
.ctx-pattern-item {
display: list-item;
}
.ctx-convention-list,
.ctx-integration-list {
list-style-type: disc;
list-style-position: inside;
padding-left: 0.5rem;
}
.ctx-deps-list {
counter-reset: dep-counter;
}
.ctx-dep-item {
counter-increment: dep-counter;
position: relative;
padding-left: 1.75rem;
}
.ctx-dep-item::before {
content: counter(dep-counter) ".";
position: absolute;
left: 0.5rem;
color: hsl(var(--muted-foreground));
font-weight: 600;
font-size: 0.75rem;
}
/* Enhanced List Markers */
.ctx-pattern-list li {
padding: 0.375rem 0;
line-height: 1.5;
}
.ctx-pattern-list li::marker {
color: hsl(var(--primary));
font-weight: 600;
}
/* Task Detail Lists */
.impl-steps {
list-style-type: decimal;
list-style-position: inside;
padding-left: 0.5rem;
}
.impl-steps li {
padding: 0.375rem 0;
line-height: 1.5;
}
.impl-steps li::marker {
color: hsl(var(--primary));
font-weight: 600;
}
.mod-points {
counter-reset: mod-counter;
}
.mod-point {
counter-increment: mod-counter;
position: relative;
padding-left: 1.5rem;
margin-bottom: 0.375rem;
}
.mod-point::before {
content: counter(mod-counter) ".";
position: absolute;
left: 0;
color: hsl(var(--muted-foreground));
font-weight: 600;
font-size: 0.8rem;
}
/* Dependencies Section - Improved Layout */
.ctx-deps-grid {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.ctx-dep-card {
display: flex;
align-items: baseline;
gap: 0.75rem;
padding: 0.625rem 0.75rem;
background: hsl(var(--muted) / 0.15);
border: 1px solid hsl(var(--border) / 0.3);
border-radius: 0.375rem;
font-size: 0.8rem;
}
.ctx-dep-name {
font-weight: 600;
color: hsl(var(--foreground));
min-width: 100px;
}
.ctx-dep-version {
padding: 0.125rem 0.5rem;
background: hsl(var(--primary) / 0.15);
color: hsl(var(--primary));
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 500;
font-family: monospace;
}
.ctx-dep-usage {
color: hsl(var(--muted-foreground));
font-size: 0.8rem;
flex: 1;
}
/* Internal Dependencies */
.ctx-internal-deps {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.ctx-internal-dep {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: hsl(var(--muted) / 0.1);
border: 1px solid hsl(var(--border) / 0.2);
border-radius: 0.375rem;
font-size: 0.8rem;
}
.ctx-dep-from,
.ctx-dep-to {
padding: 0.25rem 0.5rem;
background: hsl(var(--muted));
border-radius: 0.25rem;
font-size: 0.8rem;
font-family: monospace;
color: hsl(var(--foreground));
}
.ctx-dep-arrow {
color: hsl(var(--muted-foreground));
font-size: 0.8rem;
}
.ctx-dep-type {
margin-left: auto;
padding: 0.2rem 0.5rem;
background: hsl(var(--primary) / 0.1);
color: hsl(var(--primary));
border-radius: 0.25rem;
font-size: 0.7rem;
font-weight: 500;
}
/* Coding Conventions - Parsed Values */
.ctx-conventions-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 0.75rem;
}
.ctx-convention-item {
background: hsl(var(--card));
border: 1px solid hsl(var(--border) / 0.5);
border-radius: 0.5rem;
overflow: hidden;
}
.ctx-convention-key {
padding: 0.5rem 0.75rem;
background: hsl(var(--muted) / 0.3);
font-weight: 600;
font-size: 0.8rem;
color: hsl(var(--foreground));
text-transform: capitalize;
border-bottom: 1px solid hsl(var(--border) / 0.3);
}
.ctx-convention-body {
padding: 0.625rem 0.75rem;
}
.ctx-conv-entries {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.ctx-conv-entry {
display: flex;
gap: 0.5rem;
font-size: 0.8rem;
line-height: 1.4;
}
.ctx-conv-label {
color: hsl(var(--muted-foreground));
min-width: 80px;
flex-shrink: 0;
text-transform: capitalize;
}
.ctx-conv-value {
color: hsl(var(--foreground));
word-break: break-word;
}
.ctx-conv-text {
font-size: 0.8rem;
color: hsl(var(--foreground));
}
.ctx-conv-list {
list-style-type: disc;
list-style-position: inside;
padding-left: 0.25rem;
font-size: 0.8rem;
}
.ctx-conv-list li {
padding: 0.125rem 0;
}
.ctx-conv-empty {
color: hsl(var(--muted-foreground));
font-style: italic;
}
/* Pre-Analysis Sub-Step Numbering */
ol.step-commands {
list-style-type: decimal;
list-style-position: inside;
padding-left: 0.25rem;
margin: 0.375rem 0;
}
ol.step-commands li {
padding: 0.25rem 0;
font-size: 0.8rem;
}
ol.step-commands li::marker {
color: hsl(var(--muted-foreground));
font-weight: 500;
}
ol.step-commands code {
background: hsl(var(--muted) / 0.5);
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
font-size: 0.75rem;
}
.step-output-label {
color: hsl(var(--muted-foreground));
font-weight: 500;
}
/* ========================================
Review Session Page Styles
======================================== */
/* Review Progress Section */
.review-progress-section {
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 0.5rem;
padding: 1.25rem;
margin-bottom: 1.5rem;
}
.review-progress-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
}
.review-progress-header h3 {
font-size: 1rem;
font-weight: 600;
color: hsl(var(--foreground));
margin: 0;
}
.phase-badge {
padding: 0.25rem 0.75rem;
border-radius: 1rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.phase-badge.in-progress {
background: hsl(var(--primary) / 0.15);
color: hsl(var(--primary));
}
.phase-badge.completed {
background: hsl(142 76% 36% / 0.15);
color: hsl(142 76% 36%);
}
/* Summary Cards Grid */
.review-summary-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
margin-bottom: 1.25rem;
}
@media (max-width: 768px) {
.review-summary-grid {
grid-template-columns: repeat(2, 1fr);
}
}
.summary-card {
background: hsl(var(--muted) / 0.3);
border: 1px solid hsl(var(--border) / 0.5);
border-radius: 0.5rem;
padding: 1rem;
text-align: center;
transition: transform 0.15s, box-shadow 0.15s;
}
.summary-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px hsl(var(--foreground) / 0.1);
}
.summary-card.critical {
border-color: hsl(0 70% 50% / 0.3);
background: hsl(0 70% 50% / 0.05);
}
.summary-card.high {
border-color: hsl(25 90% 50% / 0.3);
background: hsl(25 90% 50% / 0.05);
}
.summary-icon {
font-size: 1.5rem;
margin-bottom: 0.375rem;
}
.summary-value {
font-size: 1.75rem;
font-weight: 700;
color: hsl(var(--foreground));
line-height: 1.2;
}
.summary-card.critical .summary-value {
color: hsl(0 70% 45%);
}
.summary-card.high .summary-value {
color: hsl(25 90% 45%);
}
.summary-label {
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
text-transform: uppercase;
font-weight: 500;
letter-spacing: 0.025em;
}
/* Dimension Timeline */
.dimension-timeline {
display: flex;
gap: 0.5rem;
overflow-x: auto;
padding: 0.5rem 0;
}
.dimension-item {
flex: 0 0 auto;
min-width: 120px;
padding: 0.75rem;
background: hsl(var(--muted) / 0.2);
border: 1px solid hsl(var(--border) / 0.5);
border-radius: 0.375rem;
text-align: center;
transition: all 0.15s;
cursor: pointer;
}
.dimension-item:hover {
background: hsl(var(--muted) / 0.4);
}
.dimension-item.completed {
border-color: hsl(142 76% 36% / 0.5);
background: hsl(142 76% 36% / 0.05);
}
.dimension-item.in-progress {
border-color: hsl(var(--primary) / 0.5);
background: hsl(var(--primary) / 0.05);
}
.dimension-number {
font-size: 0.7rem;
font-weight: 600;
color: hsl(var(--muted-foreground));
margin-bottom: 0.25rem;
}
.dimension-name {
font-size: 0.8rem;
font-weight: 500;
color: hsl(var(--foreground));
margin-bottom: 0.25rem;
}
.dimension-stats {
font-size: 0.7rem;
color: hsl(var(--muted-foreground));
}
/* Review Findings Section */
.review-findings-section {
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 0.5rem;
padding: 1.25rem;
margin-bottom: 1.5rem;
}
.findings-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
flex-wrap: wrap;
gap: 0.75rem;
}
.findings-header h3 {
font-size: 1rem;
font-weight: 600;
color: hsl(var(--foreground));
margin: 0;
}
.findings-filters {
display: flex;
gap: 0.375rem;
}
.filter-btn {
padding: 0.375rem 0.75rem;
border: 1px solid hsl(var(--border));
border-radius: 0.25rem;
background: transparent;
color: hsl(var(--muted-foreground));
font-size: 0.75rem;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
}
.filter-btn:hover {
background: hsl(var(--muted) / 0.5);
}
.filter-btn.active {
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
border-color: hsl(var(--primary));
}
/* Findings Grid */
.findings-grid {
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.dimension-findings-group {
border: 1px solid hsl(var(--border) / 0.5);
border-radius: 0.5rem;
overflow: hidden;
}
.dimension-group-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
background: hsl(var(--muted) / 0.3);
border-bottom: 1px solid hsl(var(--border) / 0.3);
}
.dimension-badge {
padding: 0.25rem 0.625rem;
background: hsl(var(--primary) / 0.15);
color: hsl(var(--primary));
border-radius: 0.25rem;
font-size: 0.8rem;
font-weight: 600;
}
.dimension-count {
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
}
.findings-cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 0.75rem;
padding: 1rem;
}
.finding-card {
background: hsl(var(--card));
border: 1px solid hsl(var(--border) / 0.5);
border-radius: 0.375rem;
padding: 0.875rem;
transition: all 0.15s;
}
.finding-card:hover {
box-shadow: 0 2px 8px hsl(var(--foreground) / 0.08);
}
.finding-card.severity-critical {
border-left: 3px solid hsl(0 70% 50%);
}
.finding-card.severity-high {
border-left: 3px solid hsl(25 90% 50%);
}
.finding-card.severity-medium {
border-left: 3px solid hsl(45 90% 45%);
}
.finding-card.severity-low {
border-left: 3px solid hsl(142 76% 36%);
}
.finding-card-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.finding-card-title {
font-size: 0.85rem;
font-weight: 600;
color: hsl(var(--foreground));
margin-bottom: 0.375rem;
line-height: 1.4;
}
.finding-card-desc {
font-size: 0.8rem;
color: hsl(var(--muted-foreground));
line-height: 1.5;
margin-bottom: 0.5rem;
}
.finding-card-file {
font-size: 0.75rem;
color: hsl(var(--primary));
font-family: monospace;
}
.fix-status-badge {
padding: 0.125rem 0.5rem;
border-radius: 0.75rem;
font-size: 0.65rem;
font-weight: 600;
text-transform: uppercase;
}
.fix-status-badge.status-pending {
background: hsl(var(--muted));
color: hsl(var(--muted-foreground));
}
.fix-status-badge.status-in-progress {
background: hsl(var(--primary) / 0.15);
color: hsl(var(--primary));
}
.fix-status-badge.status-fixed {
background: hsl(142 76% 36% / 0.15);
color: hsl(142 76% 36%);
}
.fix-status-badge.status-failed {
background: hsl(0 70% 50% / 0.15);
color: hsl(0 70% 50%);
}
/* Review Header Controls */
.review-header-controls {
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
margin-bottom: 1rem;
padding: 0.75rem 1rem;
background: hsl(var(--muted) / 0.2);
border: 1px solid hsl(var(--border) / 0.5);
border-radius: 0.375rem;
}
.selection-controls {
display: flex;
align-items: center;
gap: 0.5rem;
}
.selection-counter {
font-size: 0.8rem;
color: hsl(var(--muted-foreground));
font-weight: 500;
}
.selection-btn {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
}
.finding-checkbox {
width: 16px;
height: 16px;
cursor: pointer;
accent-color: hsl(var(--primary));
}
.export-btn-fix {
padding: 0.5rem 1rem;
background: hsl(142 76% 36%);
color: white;
border: none;
border-radius: 0.25rem;
font-size: 0.8rem;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
margin-left: auto;
}
.export-btn-fix:hover {
background: hsl(142 76% 30%);
}
.export-btn-fix:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Empty State */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem;
color: hsl(var(--muted-foreground));
}
.empty-icon {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.empty-text {
font-size: 0.9rem;
}
/* Exploration Context Styles */
.exploration-context {
padding: 16px;
}
.exploration-header {
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border-color, #e5e7eb);
}
.exploration-header h4 {
font-size: 14px;
font-weight: 500;
color: var(--text-primary, #111827);
margin: 0 0 8px 0;
line-height: 1.4;
}
.exploration-meta {
display: flex;
gap: 16px;
font-size: 12px;
color: var(--text-secondary, #6b7280);
}
.exploration-meta .meta-item strong {
color: var(--text-primary, #111827);
}
.exploration-section {
margin-bottom: 8px;
border: 1px solid var(--border-color, #e5e7eb);
border-radius: 6px;
overflow: hidden;
}
.exploration-section .collapsible-header {
padding: 10px 12px;
background: var(--bg-secondary, #f9fafb);
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
}
.exploration-section .collapsible-header:hover {
background: var(--bg-hover, #f3f4f6);
}
.exploration-section .collapsible-content {
padding: 12px;
background: var(--bg-primary, #fff);
}
.exploration-section .collapsible-content.collapsed {
display: none;
}
.exp-field {
margin-bottom: 16px;
}
.exp-field:last-child {
margin-bottom: 0;
}
.exp-field label {
display: block;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
color: var(--text-secondary, #6b7280);
margin-bottom: 6px;
letter-spacing: 0.5px;
}
.exp-field p {
font-size: 13px;
line-height: 1.6;
color: var(--text-primary, #374151);
margin: 0;
}
.relevant-files-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.file-item-exp {
padding: 8px 10px;
background: var(--bg-secondary, #f9fafb);
border-radius: 4px;
border-left: 3px solid var(--primary, #3b82f6);
}
.file-item-exp .file-path code {
font-size: 12px;
color: var(--text-primary, #111827);
}
.file-item-exp .file-relevance {
font-size: 11px;
color: var(--text-secondary, #6b7280);
margin-top: 4px;
}
.file-item-exp .file-rationale {
font-size: 12px;
color: var(--text-tertiary, #9ca3af);
margin-top: 4px;
line-height: 1.4;
}
.more-files {
font-size: 12px;
color: var(--text-secondary, #6b7280);
font-style: italic;
padding: 8px 0;
}
.clarification-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.clarification-item {
padding: 10px;
background: var(--bg-warning, #fffbeb);
border: 1px solid var(--border-warning, #fcd34d);
border-radius: 6px;
}
.clarification-question {
font-size: 13px;
font-weight: 500;
color: var(--text-primary, #111827);
margin-bottom: 8px;
}
.clarification-options {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.option-badge {
padding: 4px 8px;
background: var(--bg-primary, #fff);
border: 1px solid var(--border-color, #e5e7eb);
border-radius: 4px;
font-size: 11px;
color: var(--text-secondary, #6b7280);
}
.option-badge.recommended {
background: var(--bg-success, #d1fae5);
border-color: var(--border-success, #34d399);
color: var(--text-success, #065f46);
}
/* Plan drawer styles for lite tasks */
.mod-point-item {
padding: 8px 10px;
background: var(--bg-secondary, #f9fafb);
border-radius: 4px;
margin-bottom: 8px;
border-left: 3px solid var(--primary, #3b82f6);
}
.mod-point-file code {
font-size: 12px;
}
.mod-point-target {
font-size: 11px;
color: var(--text-secondary, #6b7280);
margin-top: 4px;
}
.mod-point-change {
font-size: 12px;
color: var(--text-primary, #374151);
margin-top: 4px;
}
.implementation-steps-list {
padding-left: 20px;
margin: 0;
}
.implementation-steps-list li {
font-size: 13px;
line-height: 1.6;
margin-bottom: 6px;
color: var(--text-primary, #374151);
}
.ref-pattern,
.ref-files,
.ref-examples {
margin-bottom: 8px;
}
.acceptance-list {
padding-left: 20px;
margin: 0;
}
.acceptance-list li {
font-size: 13px;
line-height: 1.5;
margin-bottom: 4px;
}
.dependencies-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.task-description {
font-size: 13px;
line-height: 1.6;
color: var(--text-primary, #374151);
}
/* Lite Task List Item Styles */
.lite-task-item {
border: 1px solid var(--border-color, #e5e7eb);
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 8px;
transition: all 0.2s ease;
background: var(--bg-primary, #fff);
}
.lite-task-item:hover {
border-color: var(--primary, #3b82f6);
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.1);
}
.task-item-header-lite {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.task-item-header-lite .task-title {
flex: 1;
font-weight: 500;
color: var(--text-primary, #111827);
}
.task-item-meta-lite {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.meta-badge {
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
}
.meta-badge.action {
background: var(--bg-primary-light, #eff6ff);
color: var(--primary, #3b82f6);
border: 1px solid var(--primary, #3b82f6);
}
.meta-badge.scope {
background: var(--bg-secondary, #f9fafb);
color: var(--text-secondary, #6b7280);
}
.meta-badge.mods {
background: var(--bg-warning, #fffbeb);
color: var(--warning, #d97706);
}
.meta-badge.impl {
background: var(--bg-success, #ecfdf5);
color: var(--success, #059669);
}
.meta-badge.accept {
background: var(--bg-info, #eff6ff);
color: var(--info, #2563eb);
}
/* Lite Task Drawer Styles */
.action-badge {
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
background: var(--primary, #3b82f6);
color: white;
text-transform: uppercase;
}
.scope-path {
display: block;
padding: 8px 12px;
background: var(--bg-secondary, #f9fafb);
border-radius: 4px;
font-size: 13px;
}
.impl-steps-list {
list-style: none;
padding: 0;
margin: 0;
counter-reset: step-counter;
}
.impl-step-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 10px 0;
border-bottom: 1px solid var(--border-color, #e5e7eb);
}
.impl-step-item:last-child {
border-bottom: none;
}
.step-number {
flex-shrink: 0;
min-width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: var(--primary, #3b82f6);
color: white;
border-radius: 6px;
font-size: 13px;
font-weight: 700;
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.3);
}
.step-text {
flex: 1;
font-size: 13px;
line-height: 1.5;
color: var(--text-primary, #374151);
}
.mod-points-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.mod-point-card {
padding: 14px 16px;
background: var(--bg-primary, #fff);
border-radius: 8px;
border: 1px solid var(--border-color, #e5e7eb);
border-left: 4px solid var(--primary, #3b82f6);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
transition: all 0.2s ease;
}
.mod-point-card:hover {
border-color: var(--primary, #3b82f6);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15);
}
.mod-point-card .mod-file code {
font-size: 12px;
color: var(--primary, #3b82f6);
font-weight: 500;
}
.mod-point-card .mod-target {
font-size: 12px;
color: var(--text-secondary, #6b7280);
margin-top: 4px;
}
.mod-point-card .mod-change {
font-size: 13px;
color: var(--text-primary, #374151);
margin-top: 6px;
line-height: 1.4;
}
.ref-item {
margin-bottom: 10px;
}
.ref-item:last-child {
margin-bottom: 0;
}
.ref-files-list {
margin: 4px 0 0 16px;
padding: 0;
}
.ref-files-list li {
margin-bottom: 4px;
}
/* ===================================
Markdown Modal & View Button Styles
================================== */
.btn-view-modal {
padding: 4px 12px;
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
border: none;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
margin-left: auto;
}
.btn-view-modal:hover {
background: hsl(var(--primary) / 0.9);
transform: translateY(-1px);
}
.markdown-modal .markdown-preview {
color: hsl(var(--foreground));
line-height: 1.6;
}
.markdown-modal .markdown-preview h1,
.markdown-modal .markdown-preview h2,
.markdown-modal .markdown-preview h3,
.markdown-modal .markdown-preview h4 {
color: hsl(var(--foreground));
font-weight: 600;
margin-top: 1.5em;
margin-bottom: 0.5em;
}
.markdown-modal .markdown-preview h1 { font-size: 1.8em; }
.markdown-modal .markdown-preview h2 { font-size: 1.5em; }
.markdown-modal .markdown-preview h3 { font-size: 1.3em; }
.markdown-modal .markdown-preview h4 { font-size: 1.1em; }
.markdown-modal .markdown-preview code {
background: hsl(var(--muted));
padding: 2px 6px;
border-radius: 3px;
font-size: 0.9em;
font-family: 'Consolas', 'Monaco', monospace;
}
.markdown-modal .markdown-preview pre {
background: hsl(var(--muted));
padding: 12px;
border-radius: 6px;
overflow-x: auto;
margin: 1em 0;
}
.markdown-modal .markdown-preview pre code {
background: transparent;
padding: 0;
}
.markdown-modal .markdown-preview ul,
.markdown-modal .markdown-preview ol {
margin: 1em 0;
padding-left: 2em;
}
.markdown-modal .markdown-preview li {
margin: 0.5em 0;
}
.markdown-modal .markdown-preview blockquote {
border-left: 4px solid hsl(var(--primary));
padding-left: 1em;
margin: 1em 0;
color: hsl(var(--muted-foreground));
}
.markdown-modal .markdown-preview table {
border-collapse: collapse;
width: 100%;
margin: 1em 0;
}
.markdown-modal .markdown-preview th,
.markdown-modal .markdown-preview td {
border: 1px solid hsl(var(--border));
padding: 8px 12px;
text-align: left;
}
.markdown-modal .markdown-preview th {
background: hsl(var(--muted));
font-weight: 600;
}
.md-tab-btn {
cursor: pointer;
transition: all 0.2s;
}
.md-tab-btn:not(.active) {
color: hsl(var(--muted-foreground));
}
.md-tab-btn.active {
background: hsl(var(--background));
color: hsl(var(--foreground));
font-weight: 500;
}
</style>
</head>
<body class="font-sans bg-background text-foreground leading-normal">
<div class="flex flex-col min-h-screen">
<!-- Top Bar -->
<header class="flex items-center justify-between px-5 h-14 bg-card border-b border-border sticky top-0 z-50 shadow-sm">
<div class="flex items-center gap-4">
<button class="hidden md:hidden p-2 text-foreground hover:bg-hover rounded menu-toggle-btn" id="menuToggle"></button>
<div class="flex items-center gap-2 text-lg font-semibold text-primary">
<span class="text-2xl"></span>
<span class="hidden sm:inline">Claude Code Workflow</span>
</div>
</div>
<!-- Right Side Actions -->
<div class="flex items-center gap-3">
<!-- Path Selector -->
<div class="flex items-center gap-2 relative">
<label class="hidden sm:inline text-sm text-muted-foreground">Project:</label>
<div class="relative">
<button class="flex items-center gap-2 px-3 py-1.5 bg-background border border-border rounded text-sm text-foreground hover:bg-hover max-w-[300px]" id="pathButton">
<span class="truncate max-w-[240px]" id="currentPath">D:/Claude_dms3</span>
<span class="text-xs text-muted-foreground"></span>
</button>
<div class="path-menu hidden absolute top-full right-0 mt-1 bg-card border border-border rounded-lg shadow-lg min-w-[280px] z-50" id="pathMenu">
<div class="px-3 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wide">Recent Projects</div>
<div id="recentPaths" class="border-t border-border">
<!-- Dynamic recent paths -->
</div>
<div class="p-2 border-t border-border">
<button class="w-full flex items-center justify-center gap-2 px-3 py-2 bg-background border border-border rounded text-sm text-muted-foreground hover:bg-hover" id="browsePath">
📂 Browse...
</button>
</div>
</div>
</div>
</div>
<!-- Theme Toggle -->
<button class="p-2 text-xl hover:bg-hover rounded" id="themeToggle" title="Toggle theme">🌙</button>
</div>
</header>
<!-- Sidebar Overlay (mobile) -->
<div class="sidebar-overlay hidden fixed inset-0 bg-black/50 z-40" id="sidebarOverlay"></div>
<!-- Main Layout -->
<div class="flex flex-1">
<!-- Sidebar -->
<aside class="sidebar w-64 bg-sidebar-background border-r border-border flex flex-col sticky top-14 h-[calc(100vh-56px)] overflow-y-auto transition-all duration-300" id="sidebar">
<nav class="flex-1 py-3">
<!-- Project Overview Section -->
<div class="mb-2" id="projectOverviewNav">
<div class="flex items-center px-4 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wide">
<span class="mr-2">🏗️</span>
<span class="nav-section-title">Project</span>
</div>
<ul class="space-y-0.5">
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-view="project-overview" data-tooltip="Project Overview">
<span>📊</span>
<span class="nav-text flex-1">Overview</span>
</li>
</ul>
</div>
<!-- Sessions Section -->
<div class="mb-2">
<div class="flex items-center px-4 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wide">
<span class="mr-2">📁</span>
<span class="nav-section-title">Sessions</span>
</div>
<ul class="space-y-0.5">
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors active bg-accent text-primary font-medium" data-filter="all" data-tooltip="All Sessions">
<span>📋</span>
<span class="nav-text flex-1">All</span>
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-hover text-muted-foreground" id="badgeAll">0</span>
</li>
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-filter="active" data-tooltip="Active Sessions">
<span>🟢</span>
<span class="nav-text flex-1">Active</span>
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-success-light text-success" id="badgeActive">0</span>
</li>
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-filter="archived" data-tooltip="Archived Sessions">
<span>📦</span>
<span class="nav-text flex-1">Archived</span>
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-hover text-muted-foreground" id="badgeArchived">0</span>
</li>
</ul>
</div>
<!-- Lite Tasks Section -->
<div class="mb-2" id="liteTasksNav">
<div class="flex items-center px-4 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wide">
<span class="mr-2"></span>
<span class="nav-section-title">Lite Tasks</span>
</div>
<ul class="space-y-0.5">
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-lite="lite-plan" data-tooltip="Lite Plan Sessions">
<span>📝</span>
<span class="nav-text flex-1">lite-plan</span>
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-hover text-muted-foreground" id="badgeLitePlan">0</span>
</li>
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-lite="lite-fix" data-tooltip="Lite Fix Sessions">
<span>🔧</span>
<span class="nav-text flex-1">lite-fix</span>
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-hover text-muted-foreground" id="badgeLiteFix">0</span>
</li>
</ul>
</div>
</nav>
<!-- Sidebar Footer -->
<div class="p-3 border-t border-border">
<button class="flex items-center justify-center gap-2 w-full px-3 py-2 border border-border rounded text-sm text-muted-foreground hover:bg-hover transition-colors" id="sidebarToggle">
<span class="toggle-icon transition-transform duration-300"></span>
<span class="toggle-text">Collapse</span>
</button>
</div>
</aside>
<!-- Content Area -->
<main class="flex-1 p-6 overflow-y-auto min-w-0">
<!-- Stats Grid -->
<section class="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-4 mb-6">
<div class="bg-card border border-border rounded-lg p-5 text-center hover:shadow-md transition-all duration-200">
<div class="text-2xl mb-2">📊</div>
<div class="text-3xl font-bold text-foreground" id="statTotalSessions">0</div>
<div class="text-sm text-muted-foreground mt-1">Total Sessions</div>
</div>
<div class="bg-card border border-border rounded-lg p-5 text-center hover:shadow-md transition-all duration-200">
<div class="text-2xl mb-2">🟢</div>
<div class="text-3xl font-bold text-foreground" id="statActiveSessions">0</div>
<div class="text-sm text-muted-foreground mt-1">Active Sessions</div>
</div>
<div class="bg-card border border-border rounded-lg p-5 text-center hover:shadow-md transition-all duration-200">
<div class="text-2xl mb-2">📋</div>
<div class="text-3xl font-bold text-foreground" id="statTotalTasks">0</div>
<div class="text-sm text-muted-foreground mt-1">Total Tasks</div>
</div>
<div class="bg-card border border-border rounded-lg p-5 text-center hover:shadow-md transition-all duration-200">
<div class="text-2xl mb-2"></div>
<div class="text-3xl font-bold text-foreground" id="statCompletedTasks">0</div>
<div class="text-sm text-muted-foreground mt-1">Completed Tasks</div>
</div>
</section>
<!-- Content Header -->
<div class="flex items-center justify-between flex-wrap gap-3 mb-5">
<h2 class="text-xl font-semibold text-foreground" id="contentTitle">All Sessions</h2>
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-sm">🔍</span>
<input type="text" placeholder="Search..." id="searchInput"
class="pl-9 pr-4 py-2 w-60 border border-border rounded-lg bg-background text-foreground text-sm focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all">
</div>
</div>
<!-- Main Content Container -->
<section class="main-content" id="mainContent">
<!-- Dynamic content: sessions grid or session detail page -->
</section>
</main>
</div>
<!-- Footer -->
<footer class="flex items-center justify-between px-5 h-10 bg-card border-t border-border text-xs text-muted-foreground">
<div>Generated: <span id="generatedAt">-</span></div>
<div>CCW Dashboard v1.0</div>
</footer>
<!-- Task Detail Drawer -->
<div class="task-detail-drawer fixed top-0 right-0 w-1/2 max-w-full h-full bg-card border-l border-border shadow-lg z-50 flex flex-col" id="taskDetailDrawer">
<div class="flex items-center justify-between px-5 py-4 border-b border-border">
<h3 class="text-lg font-semibold text-foreground" id="drawerTaskTitle">Task Details</h3>
<button class="w-8 h-8 flex items-center justify-center text-xl text-muted-foreground hover:text-foreground hover:bg-hover rounded" onclick="closeTaskDrawer()">&times;</button>
</div>
<div class="flex-1 overflow-y-auto p-5" id="drawerContent">
<!-- Dynamic content -->
</div>
</div>
<div class="drawer-overlay hidden fixed inset-0 bg-black/50 z-40" id="drawerOverlay" onclick="closeTaskDrawer()"></div>
</div>
<!-- Markdown Preview Modal -->
<div id="markdownModal" class="markdown-modal hidden fixed inset-0 z-[100] flex items-center justify-center">
<div class="markdown-modal-backdrop absolute inset-0 bg-black/60" onclick="closeMarkdownModal()"></div>
<div class="markdown-modal-content relative bg-card border border-border rounded-lg shadow-2xl w-[90vw] max-w-4xl h-[85vh] flex flex-col">
<div class="markdown-modal-header flex items-center justify-between px-4 py-3 border-b border-border">
<h3 class="text-lg font-semibold text-foreground" id="markdownModalTitle">Content Preview</h3>
<div class="flex items-center gap-2">
<div class="flex bg-muted rounded-lg p-0.5">
<button id="mdTabRaw" class="md-tab-btn px-3 py-1 text-sm rounded-md transition-colors" onclick="switchMarkdownTab('raw')">Raw</button>
<button id="mdTabPreview" class="md-tab-btn px-3 py-1 text-sm rounded-md transition-colors active" onclick="switchMarkdownTab('preview')">Preview</button>
</div>
<button class="w-8 h-8 flex items-center justify-center text-xl text-muted-foreground hover:text-foreground hover:bg-hover rounded" onclick="closeMarkdownModal()">&times;</button>
</div>
</div>
<div class="markdown-modal-body flex-1 overflow-auto p-4">
<pre id="markdownRaw" class="hidden whitespace-pre-wrap text-sm font-mono text-foreground bg-muted p-4 rounded-lg overflow-auto h-full"></pre>
<div id="markdownPreview" class="markdown-preview prose prose-sm max-w-none"></div>
</div>
</div>
</div>
<!-- D3.js for Flowchart -->
<script src="https://d3js.org/d3.v7.min.js"></script>
<!-- Marked.js for Markdown rendering -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>
// ========================================
// Utility Functions
// ========================================
// General-purpose helper functions used across the application
// ========== HTML/Text Processing ==========
/**
* Escape HTML special characters to prevent XSS attacks
* @param {string} str - String to escape
* @returns {string} Escaped string safe for HTML insertion
*/
function escapeHtml(str) {
if (typeof str !== 'string') return str;
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
/**
* Truncate text to specified maximum length
* @param {string} text - Text to truncate
* @param {number} maxLen - Maximum length (including ellipsis)
* @returns {string} Truncated text with '...' if needed
*/
function truncateText(text, maxLen) {
if (!text) return '';
return text.length > maxLen ? text.substring(0, maxLen - 3) + '...' : text;
}
/**
* Normalize line endings in content
* Handles both literal \r\n escape sequences and actual newlines
* @param {string} content - Content to normalize
* @returns {string} Content with normalized line endings (LF only)
*/
function normalizeLineEndings(content) {
if (!content) return '';
let normalized = content;
// If content has literal \r\n or \n as text (escaped), convert to actual newlines
if (normalized.includes('\\r\\n')) {
normalized = normalized.replace(/\\r\\n/g, '\n');
} else if (normalized.includes('\\n')) {
normalized = normalized.replace(/\\n/g, '\n');
}
// Normalize CRLF to LF for consistent rendering
normalized = normalized.replace(/\r\n/g, '\n');
return normalized;
}
// ========== Date/Time Formatting ==========
/**
* Format ISO date string to human-readable format
* @param {string} dateStr - ISO date string
* @returns {string} Formatted date string (YYYY/MM/DD HH:mm) or '-' if invalid
*/
function formatDate(dateStr) {
if (!dateStr) return '-';
try {
const date = new Date(dateStr);
// Check if date is valid
if (isNaN(date.getTime())) return '-';
// Format: YYYY/MM/DD HH:mm
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}/${month}/${day} ${hours}:${minutes}`;
} catch (e) {
return '-';
}
}
// ========== UI Helpers ==========
/**
* Get color for relevance score visualization
* @param {number} score - Relevance score (0-1)
* @returns {string} CSS color value
*/
function getRelevanceColor(score) {
if (score >= 0.95) return '#10b981';
if (score >= 0.90) return '#3b82f6';
if (score >= 0.80) return '#f59e0b';
return '#6b7280';
}
/**
* Get CSS class for role badge styling
* @param {string} role - Role identifier
* @returns {string} CSS class name
*/
function getRoleBadgeClass(role) {
const roleMap = {
'core-hook': 'primary',
'api-client': 'success',
'api-router': 'info',
'service-layer': 'warning',
'pydantic-schemas': 'secondary',
'orm-model': 'secondary',
'typescript-types': 'info'
};
return roleMap[role] || 'secondary';
}
/**
* Toggle collapsible section visibility
* @param {HTMLElement} header - Section header element
*/
function toggleSection(header) {
const content = header.nextElementSibling;
const icon = header.querySelector('.collapse-icon');
const isCollapsed = content.classList.contains('collapsed');
content.classList.toggle('collapsed');
header.classList.toggle('expanded');
icon.textContent = isCollapsed ? '▼' : '▶';
// Render flowchart if expanding flow_control section
if (isCollapsed && header.querySelector('.section-label')?.textContent === 'flow_control') {
const taskId = content.closest('[data-task-id]')?.dataset.taskId;
if (taskId) {
const task = taskJsonStore[taskId];
if (task?.flow_control) {
setTimeout(() => renderFullFlowchart(task.flow_control), 100);
}
}
}
}
// ========================================
// State Management
// ========================================
// Global state variables and template placeholders
// This module must be loaded first as other modules depend on these variables
// ========== Data Placeholders ==========
// These placeholders are replaced by the dashboard generator at build time
let workflowData = {
"generatedAt": "2025-12-06T15:00:38.671Z",
"activeSessions": [],
"archivedSessions": [],
"liteTasks": {
"litePlan": [],
"liteFix": []
},
"reviewData": {
"dimensions": {}
},
"statistics": {
"totalSessions": 0,
"activeSessions": 0,
"totalTasks": 0,
"completedTasks": 0
}
};
let projectPath = 'D:/Claude_dms3';
let recentPaths = ["D:/Claude_dms3","D:/test"];
// ========== Application State ==========
// Current filter for session list view ('all', 'active', 'archived')
let currentFilter = 'all';
// Current lite task type ('lite-plan', 'lite-fix', or null)
let currentLiteType = null;
// Current view mode ('sessions', 'liteTasks', 'project-overview', 'sessionDetail', 'liteTaskDetail')
let currentView = 'sessions';
// Current session detail key (null when not in detail view)
let currentSessionDetailKey = null;
// ========== Data Stores ==========
// Store session data for modal/detail access
// Key: session key, Value: session data object
const sessionDataStore = {};
// Store lite task session data for detail page access
// Key: session key, Value: lite session data object
const 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 = {};
// ========================================
// API and Data Loading
// ========================================
// Server communication and data loading functions
// Note: Some functions are only available in server mode
// ========== Data Loading ==========
/**
* Load dashboard data from API (server mode only)
* @param {string} path - Project path to load data for
* @returns {Promise<Object|null>} Dashboard data object or null if failed
*/
async function loadDashboardData(path) {
if (!window.SERVER_MODE) {
console.warn('loadDashboardData called in static mode');
return null;
}
try {
const response = await fetch(`/api/dashboard?path=${encodeURIComponent(path)}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (err) {
console.error('Failed to load dashboard data:', err);
return null;
}
}
// ========== Path Management ==========
/**
* Switch to a new project path (server mode only)
* Loads dashboard data and updates UI
* @param {string} path - Project path to switch to
*/
async function switchToPath(path) {
// Show loading state
const container = document.getElementById('mainContent');
container.innerHTML = '<div class="loading">Loading...</div>';
try {
const data = await loadDashboardData(path);
if (data) {
// Update global data
workflowData = data;
projectPath = data.projectPath;
recentPaths = data.recentPaths || [];
// Update UI
document.getElementById('currentPath').textContent = projectPath;
renderDashboard();
refreshRecentPaths();
}
} catch (err) {
console.error('Failed to switch path:', err);
container.innerHTML = '<div class="error">Failed to load project data</div>';
}
}
/**
* Select a path from recent paths list
* @param {string} path - Path to select
*/
async function selectPath(path) {
localStorage.setItem('selectedPath', path);
// Server mode: load data dynamically
if (window.SERVER_MODE) {
await switchToPath(path);
return;
}
// Static mode: show command to run
const modal = document.createElement('div');
modal.className = 'path-modal-overlay';
modal.innerHTML = `
<div class="path-modal">
<div class="path-modal-header">
<span class="path-modal-icon">${icons.terminal}</span>
<h3>Run Command</h3>
</div>
<div class="path-modal-body">
<p>To view the dashboard for this project, run:</p>
<div class="path-modal-command">
<code>ccw view -p "${path}"</code>
<button class="copy-btn" id="copyCommandBtn">${icons.copy} <span>Copy</span></button>
</div>
<p class="path-modal-note" style="margin-top: 12px;">
Or use <code>ccw serve</code> for live path switching.
</p>
</div>
<div class="path-modal-footer">
<button class="path-modal-close" onclick="this.closest('.path-modal-overlay').remove()">OK</button>
</div>
</div>
`;
document.body.appendChild(modal);
// Add copy handler
document.getElementById('copyCommandBtn').addEventListener('click', function() {
navigator.clipboard.writeText('ccw view -p "' + path + '"').then(() => {
this.innerHTML = icons.check + ' <span>Copied!</span>';
setTimeout(() => { this.innerHTML = icons.copy + ' <span>Copy</span>'; }, 2000);
});
});
}
/**
* Refresh recent paths dropdown UI
*/
function refreshRecentPaths() {
const recentContainer = document.getElementById('recentPaths');
recentContainer.innerHTML = '';
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));
recentContainer.appendChild(item);
});
}
// ========== File System Access ==========
/**
* Browse for folder using File System Access API or fallback to input dialog
*/
async function browseForFolder() {
// Try modern File System Access API first
if ('showDirectoryPicker' in window) {
try {
const dirHandle = await window.showDirectoryPicker({
mode: 'read',
startIn: 'documents'
});
// Get the directory name (we can't get full path for security reasons)
const dirName = dirHandle.name;
showPathSelectedModal(dirName, dirHandle);
return;
} catch (err) {
if (err.name === 'AbortError') {
// User cancelled
return;
}
console.warn('Directory picker failed:', err);
}
}
// Fallback: show input dialog
showPathInputModal();
}
// ==========================================
// THEME MANAGEMENT
// ==========================================
function initTheme() {
const saved = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', saved);
updateThemeIcon(saved);
document.getElementById('themeToggle').addEventListener('click', () => {
const current = document.documentElement.getAttribute('data-theme');
const next = current === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
updateThemeIcon(next);
});
}
function updateThemeIcon(theme) {
document.getElementById('themeToggle').textContent = theme === 'light' ? '🌙' : '☀️';
}
// ==========================================
// MODAL DIALOGS
// ==========================================
// SVG Icons
const icons = {
folder: '<svg viewBox="0 0 24 24"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>',
check: '<svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"></polyline></svg>',
copy: '<svg viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>',
terminal: '<svg viewBox="0 0 24 24"><polyline points="4 17 10 11 4 5"></polyline><line x1="12" y1="19" x2="20" y2="19"></line></svg>'
};
function showPathSelectedModal(dirName, dirHandle) {
// Try to guess full path based on current project path
const currentPath = projectPath || '';
const basePath = currentPath.substring(0, currentPath.lastIndexOf('/')) || 'D:/projects';
const suggestedPath = basePath + '/' + dirName;
const modal = document.createElement('div');
modal.className = 'path-modal-overlay';
modal.innerHTML = `
<div class="path-modal">
<div class="path-modal-header">
<span class="path-modal-icon">${icons.folder}</span>
<h3>Folder Selected</h3>
</div>
<div class="path-modal-body">
<div class="selected-folder">
<strong>${dirName}</strong>
</div>
<p class="path-modal-note">
Confirm or edit the full path:
</p>
<div class="path-input-group" style="margin-top: 12px;">
<label>Full path:</label>
<input type="text" id="fullPathInput" value="${suggestedPath}" />
<button class="path-go-btn" id="pathGoBtn">Open</button>
</div>
</div>
<div class="path-modal-footer">
<button class="path-modal-close" id="pathCancelBtn">Cancel</button>
</div>
</div>
`;
document.body.appendChild(modal);
// Add event listeners (use arrow functions to ensure proper scope)
document.getElementById('pathGoBtn').addEventListener('click', () => {
console.log('Open button clicked');
goToPath();
});
document.getElementById('pathCancelBtn').addEventListener('click', () => closePathModal());
// Focus input, select all text, and add enter key listener
setTimeout(() => {
const input = document.getElementById('fullPathInput');
input?.focus();
input?.select();
input?.addEventListener('keypress', (e) => {
if (e.key === 'Enter') goToPath();
});
}, 100);
}
function showPathInputModal() {
const modal = document.createElement('div');
modal.className = 'path-modal-overlay';
modal.innerHTML = `
<div class="path-modal">
<div class="path-modal-header">
<span class="path-modal-icon">${icons.folder}</span>
<h3>Open Project</h3>
</div>
<div class="path-modal-body">
<div class="path-input-group" style="margin-top: 0;">
<label>Project path:</label>
<input type="text" id="fullPathInput" placeholder="D:/projects/my-project" />
<button class="path-go-btn" id="pathGoBtn">Open</button>
</div>
</div>
<div class="path-modal-footer">
<button class="path-modal-close" id="pathCancelBtn">Cancel</button>
</div>
</div>
`;
document.body.appendChild(modal);
// Add event listeners (use arrow functions to ensure proper scope)
document.getElementById('pathGoBtn').addEventListener('click', () => {
console.log('Open button clicked');
goToPath();
});
document.getElementById('pathCancelBtn').addEventListener('click', () => closePathModal());
// Focus input and add enter key listener
setTimeout(() => {
const input = document.getElementById('fullPathInput');
input?.focus();
input?.addEventListener('keypress', (e) => {
if (e.key === 'Enter') goToPath();
});
}, 100);
}
function goToPath() {
const input = document.getElementById('fullPathInput');
const path = input?.value?.trim();
if (path) {
closePathModal();
selectPath(path);
} else {
// Show error - input is empty
input.style.borderColor = 'var(--danger-color)';
input.placeholder = 'Please enter a path';
input.focus();
}
}
function closePathModal() {
const modal = document.querySelector('.path-modal-overlay');
if (modal) {
modal.remove();
}
}
function copyCommand(btn, dirName) {
const input = document.getElementById('fullPathInput');
const path = input?.value?.trim() || `[full-path-to-${dirName}]`;
const command = `ccw view -p "${path}"`;
navigator.clipboard.writeText(command).then(() => {
btn.innerHTML = icons.check + ' <span>Copied!</span>';
setTimeout(() => { btn.innerHTML = icons.copy + ' <span>Copy</span>'; }, 2000);
});
}
function showJsonModal(jsonId, taskId) {
// Get JSON from memory store instead of DOM
const rawTask = taskJsonStore[jsonId];
if (!rawTask) return;
const jsonContent = JSON.stringify(rawTask, null, 2);
// Create modal
const overlay = document.createElement('div');
overlay.className = 'json-modal-overlay';
overlay.innerHTML = `
<div class="json-modal">
<div class="json-modal-header">
<div class="json-modal-title">
<span class="task-id-badge">${escapeHtml(taskId)}</span>
<span>Task JSON</span>
</div>
<button class="json-modal-close" onclick="closeJsonModal(this)">&times;</button>
</div>
<div class="json-modal-body">
<pre class="json-modal-content">${escapeHtml(jsonContent)}</pre>
</div>
<div class="json-modal-footer">
<button class="btn-copy-json" onclick="copyJsonToClipboard(this)">Copy JSON</button>
</div>
</div>
`;
document.body.appendChild(overlay);
// Trigger animation
requestAnimationFrame(() => overlay.classList.add('active'));
// Close on overlay click
overlay.addEventListener('click', (e) => {
if (e.target === overlay) closeJsonModal(overlay.querySelector('.json-modal-close'));
});
// Close on Escape key
const escHandler = (e) => {
if (e.key === 'Escape') {
closeJsonModal(overlay.querySelector('.json-modal-close'));
document.removeEventListener('keydown', escHandler);
}
};
document.addEventListener('keydown', escHandler);
}
function closeJsonModal(btn) {
const overlay = btn.closest('.json-modal-overlay');
overlay.classList.remove('active');
setTimeout(() => overlay.remove(), 200);
}
function copyJsonToClipboard(btn) {
const content = btn.closest('.json-modal').querySelector('.json-modal-content').textContent;
navigator.clipboard.writeText(content).then(() => {
const original = btn.textContent;
btn.textContent = 'Copied!';
setTimeout(() => btn.textContent = original, 2000);
});
}
function openMarkdownModal(title, content, type = 'markdown') {
const modal = document.getElementById('markdownModal');
const titleEl = document.getElementById('markdownModalTitle');
const rawEl = document.getElementById('markdownRaw');
const previewEl = document.getElementById('markdownPreview');
// Normalize line endings
const normalizedContent = normalizeLineEndings(content);
titleEl.textContent = title;
rawEl.textContent = normalizedContent;
// Render preview based on type
if (typeof marked !== 'undefined' && type === 'markdown') {
previewEl.innerHTML = marked.parse(normalizedContent);
} else if (type === 'json') {
// For JSON, try to parse and re-stringify with formatting
try {
const parsed = typeof normalizedContent === 'string' ? JSON.parse(normalizedContent) : normalizedContent;
const formatted = JSON.stringify(parsed, null, 2);
previewEl.innerHTML = '<pre class="whitespace-pre-wrap language-json">' + escapeHtml(formatted) + '</pre>';
} catch (e) {
// If not valid JSON, show as-is
previewEl.innerHTML = '<pre class="whitespace-pre-wrap">' + escapeHtml(normalizedContent) + '</pre>';
}
} else {
// Fallback: simple text with line breaks
previewEl.innerHTML = '<pre class="whitespace-pre-wrap">' + escapeHtml(normalizedContent) + '</pre>';
}
// Show modal and default to preview tab
modal.classList.remove('hidden');
switchMarkdownTab('preview');
}
function closeMarkdownModal() {
const modal = document.getElementById('markdownModal');
modal.classList.add('hidden');
}
function switchMarkdownTab(tab) {
const rawEl = document.getElementById('markdownRaw');
const previewEl = document.getElementById('markdownPreview');
const rawTabBtn = document.getElementById('mdTabRaw');
const previewTabBtn = document.getElementById('mdTabPreview');
if (tab === 'raw') {
rawEl.classList.remove('hidden');
previewEl.classList.add('hidden');
rawTabBtn.classList.add('active', 'bg-background', 'text-foreground');
rawTabBtn.classList.remove('text-muted-foreground');
previewTabBtn.classList.remove('active', 'bg-background', 'text-foreground');
previewTabBtn.classList.add('text-muted-foreground');
} else {
rawEl.classList.add('hidden');
previewEl.classList.remove('hidden');
previewTabBtn.classList.add('active', 'bg-background', 'text-foreground');
previewTabBtn.classList.remove('text-muted-foreground');
rawTabBtn.classList.remove('active', 'bg-background', 'text-foreground');
rawTabBtn.classList.add('text-muted-foreground');
}
}
// Navigation and Routing
// Manages navigation events, active state, content title updates, search, and path selector
// Path Selector
function initPathSelector() {
const btn = document.getElementById('pathButton');
const menu = document.getElementById('pathMenu');
const recentContainer = document.getElementById('recentPaths');
// Render recent paths
if (recentPaths && recentPaths.length > 0) {
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));
recentContainer.appendChild(item);
});
}
btn.addEventListener('click', (e) => {
e.stopPropagation();
menu.classList.toggle('hidden');
});
document.addEventListener('click', () => {
menu.classList.add('hidden');
});
document.getElementById('browsePath').addEventListener('click', async () => {
await browseForFolder();
});
}
// Navigation
function initNavigation() {
document.querySelectorAll('.nav-item[data-filter]').forEach(item => {
item.addEventListener('click', () => {
setActiveNavItem(item);
currentFilter = item.dataset.filter;
currentLiteType = null;
currentView = 'sessions';
currentSessionDetailKey = null;
updateContentTitle();
renderSessions();
});
});
// Lite Tasks Navigation
document.querySelectorAll('.nav-item[data-lite]').forEach(item => {
item.addEventListener('click', () => {
setActiveNavItem(item);
currentLiteType = item.dataset.lite;
currentFilter = null;
currentView = 'liteTasks';
currentSessionDetailKey = null;
updateContentTitle();
renderLiteTasks();
});
});
// Project Overview Navigation
document.querySelectorAll('.nav-item[data-view]').forEach(item => {
item.addEventListener('click', () => {
setActiveNavItem(item);
currentView = item.dataset.view;
currentFilter = null;
currentLiteType = null;
currentSessionDetailKey = null;
updateContentTitle();
renderProjectOverview();
});
});
}
function setActiveNavItem(item) {
document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
item.classList.add('active');
}
function updateContentTitle() {
const titleEl = document.getElementById('contentTitle');
if (currentView === 'project-overview') {
titleEl.textContent = 'Project Overview';
} else if (currentView === 'liteTasks') {
const names = { 'lite-plan': 'Lite Plan Sessions', 'lite-fix': 'Lite Fix Sessions' };
titleEl.textContent = names[currentLiteType] || 'Lite Tasks';
} else if (currentView === 'sessionDetail') {
titleEl.textContent = 'Session Detail';
} else if (currentView === 'liteTaskDetail') {
titleEl.textContent = 'Lite Task Detail';
} else {
const names = { 'all': 'All Sessions', 'active': 'Active Sessions', 'archived': 'Archived Sessions' };
titleEl.textContent = names[currentFilter] || 'Sessions';
}
}
// Search
function initSearch() {
const input = document.getElementById('searchInput');
input.addEventListener('input', (e) => {
const query = e.target.value.toLowerCase();
document.querySelectorAll('.session-card').forEach(card => {
const text = card.textContent.toLowerCase();
card.style.display = text.includes(query) ? '' : 'none';
});
});
}
// ==========================================
// SIDEBAR MANAGEMENT
// ==========================================
function initSidebar() {
const sidebar = document.getElementById('sidebar');
const toggle = document.getElementById('sidebarToggle');
const menuToggle = document.getElementById('menuToggle');
const overlay = document.getElementById('sidebarOverlay');
// Restore collapsed state
if (localStorage.getItem('sidebarCollapsed') === 'true') {
sidebar.classList.add('collapsed');
}
toggle.addEventListener('click', () => {
sidebar.classList.toggle('collapsed');
localStorage.setItem('sidebarCollapsed', sidebar.classList.contains('collapsed'));
});
// Mobile menu
menuToggle.addEventListener('click', () => {
sidebar.classList.toggle('open');
overlay.classList.toggle('open');
});
overlay.addEventListener('click', () => {
sidebar.classList.remove('open');
overlay.classList.remove('open');
});
}
// ==========================================
// Tab Content Renderers - Context Tab
// ==========================================
// Functions for rendering Context tab content in the dashboard
// Helper functions
function getRelevanceColor(score) {
if (score >= 0.95) return '#10b981';
if (score >= 0.90) return '#3b82f6';
if (score >= 0.80) return '#f59e0b';
return '#6b7280';
}
function getRoleBadgeClass(role) {
const roleMap = {
'core-hook': 'primary',
'api-client': 'success',
'api-router': 'info',
'service-layer': 'warning',
'pydantic-schemas': 'secondary',
'orm-model': 'secondary',
'typescript-types': 'info'
};
return roleMap[role] || 'secondary';
}
// ==========================================
// Context Tab Rendering
// ==========================================
function renderContextContent(context) {
if (!context) {
return `
<div class="tab-empty-state">
<div class="empty-icon">📦</div>
<div class="empty-title">No Context Data</div>
<div class="empty-text">No context-package.json found for this session.</div>
</div>
`;
}
const contextJson = JSON.stringify(context, null, 2);
// Store in global variable for modal access
window._currentContextJson = contextJson;
// Parse context structure
const metadata = context.metadata || {};
const projectContext = context.project_context || {};
const techStack = projectContext.tech_stack || metadata.tech_stack || {};
const codingConventions = projectContext.coding_conventions || {};
const architecturePatterns = projectContext.architecture_patterns || [];
const assets = context.assets || {};
const dependencies = context.dependencies || {};
const testContext = context.test_context || {};
const conflictDetection = context.conflict_detection || {};
return `
<div class="context-tab-content space-y-6">
<!-- Header with View JSON button -->
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold text-foreground">Context Package</h3>
<button class="px-4 py-2 bg-primary text-primary-foreground rounded hover:bg-primary/90 transition-colors" onclick="openMarkdownModal('context-package.json', window._currentContextJson, 'json')">
👁️ View JSON
</button>
</div>
<!-- Metadata Section -->
${metadata.task_description || metadata.session_id ? `
<div class="context-section">
<h4 class="context-section-title">📋 Task Metadata</h4>
<div class="space-y-2">
${metadata.task_description ? `
<div class="context-field">
<span class="context-label">Description:</span>
<span class="context-value">${escapeHtml(metadata.task_description)}</span>
</div>
` : ''}
${metadata.session_id ? `
<div class="context-field">
<span class="context-label">Session ID:</span>
<span class="context-value font-mono text-sm">${escapeHtml(metadata.session_id)}</span>
</div>
` : ''}
${metadata.complexity ? `
<div class="context-field">
<span class="context-label">Complexity:</span>
<span class="badge badge-${metadata.complexity}">${escapeHtml(metadata.complexity)}</span>
</div>
` : ''}
${metadata.timestamp ? `
<div class="context-field">
<span class="context-label">Timestamp:</span>
<span class="context-value text-sm text-muted-foreground">${escapeHtml(metadata.timestamp)}</span>
</div>
` : ''}
${metadata.keywords && metadata.keywords.length > 0 ? `
<div class="context-field">
<span class="context-label">Keywords:</span>
<div class="flex flex-wrap gap-1 mt-1">
${metadata.keywords.map(kw => `<span class="badge badge-secondary text-xs">${escapeHtml(kw)}</span>`).join('')}
</div>
</div>
` : ''}
</div>
</div>
` : ''}
<!-- Architecture Patterns -->
${architecturePatterns.length > 0 ? `
<div class="context-section">
<h4 class="context-section-title">🏛️ Architecture Patterns</h4>
<ul class="list-disc list-inside space-y-1 text-sm">
${architecturePatterns.map(p => `<li class="text-foreground">${escapeHtml(p)}</li>`).join('')}
</ul>
</div>
` : ''}
<!-- Tech Stack -->
${Object.keys(techStack).length > 0 ? `
<div class="context-section">
<h4 class="context-section-title">⚙️ Technology Stack</h4>
<div class="space-y-3">
${renderTechStackSection(techStack)}
</div>
</div>
` : ''}
<!-- Coding Conventions -->
${Object.keys(codingConventions).length > 0 ? `
<div class="context-section">
<h4 class="context-section-title">📝 Coding Conventions</h4>
<div class="space-y-3">
${renderCodingConventions(codingConventions)}
</div>
</div>
` : ''}
<!-- Assets Section -->
${Object.keys(assets).length > 0 ? `
<div class="context-section">
<h4 class="context-section-title">📚 Assets & Resources</h4>
<div class="space-y-4">
${renderAssetsSection(assets)}
</div>
</div>
` : ''}
<!-- Dependencies Section -->
${(dependencies.internal && dependencies.internal.length > 0) || (dependencies.external && dependencies.external.length > 0) ? `
<div class="context-section">
<h4 class="context-section-title">🔗 Dependencies</h4>
<div class="space-y-4">
${renderDependenciesSection(dependencies)}
</div>
</div>
` : ''}
<!-- Test Context Section -->
${Object.keys(testContext).length > 0 ? `
<div class="context-section">
<h4 class="context-section-title">🧪 Test Context</h4>
<div class="space-y-4">
${renderTestContextSection(testContext)}
</div>
</div>
` : ''}
<!-- Conflict Detection Section -->
${Object.keys(conflictDetection).length > 0 ? `
<div class="context-section">
<h4 class="context-section-title">⚠️ Conflict Detection & Risk Analysis</h4>
<div class="space-y-4">
${renderConflictDetectionSection(conflictDetection)}
</div>
</div>
` : ''}
</div>
`;
}
function renderTechStackSection(techStack) {
const sections = [];
if (techStack.languages) {
const langs = Array.isArray(techStack.languages) ? techStack.languages : [techStack.languages];
sections.push(`
<div class="context-field">
<span class="context-label">Languages:</span>
<div class="flex flex-wrap gap-1 mt-1">
${langs.map(l => `<span class="badge badge-primary text-xs">${escapeHtml(String(l))}</span>`).join('')}
</div>
</div>
`);
}
if (techStack.frameworks) {
const frameworks = Array.isArray(techStack.frameworks) ? techStack.frameworks : [techStack.frameworks];
sections.push(`
<div class="context-field">
<span class="context-label">Frameworks:</span>
<div class="flex flex-wrap gap-1 mt-1">
${frameworks.map(f => `<span class="badge badge-secondary text-xs">${escapeHtml(String(f))}</span>`).join('')}
</div>
</div>
`);
}
if (techStack.frontend_frameworks) {
const ff = Array.isArray(techStack.frontend_frameworks) ? techStack.frontend_frameworks : [techStack.frontend_frameworks];
sections.push(`
<div class="context-field">
<span class="context-label">Frontend:</span>
<div class="flex flex-wrap gap-1 mt-1">
${ff.map(f => `<span class="badge badge-secondary text-xs">${escapeHtml(String(f))}</span>`).join('')}
</div>
</div>
`);
}
if (techStack.backend_frameworks) {
const bf = Array.isArray(techStack.backend_frameworks) ? techStack.backend_frameworks : [techStack.backend_frameworks];
sections.push(`
<div class="context-field">
<span class="context-label">Backend:</span>
<div class="flex flex-wrap gap-1 mt-1">
${bf.map(f => `<span class="badge badge-secondary text-xs">${escapeHtml(String(f))}</span>`).join('')}
</div>
</div>
`);
}
if (techStack.libraries) {
const libs = techStack.libraries;
if (typeof libs === 'object' && !Array.isArray(libs)) {
Object.entries(libs).forEach(([category, libList]) => {
if (Array.isArray(libList) && libList.length > 0) {
sections.push(`
<div class="context-field">
<span class="context-label">${escapeHtml(category)}:</span>
<ul class="list-disc list-inside ml-4 mt-1 text-sm text-muted-foreground">
${libList.map(lib => `<li>${escapeHtml(String(lib))}</li>`).join('')}
</ul>
</div>
`);
}
});
}
}
return sections.join('');
}
function renderCodingConventions(conventions) {
const sections = [];
if (conventions.naming) {
sections.push(`
<div class="context-field">
<span class="context-label">Naming:</span>
<ul class="list-disc list-inside ml-4 mt-1 text-sm text-muted-foreground">
${Object.entries(conventions.naming).map(([key, val]) =>
`<li><strong>${escapeHtml(key)}:</strong> ${escapeHtml(String(val))}</li>`
).join('')}
</ul>
</div>
`);
}
if (conventions.error_handling) {
sections.push(`
<div class="context-field">
<span class="context-label">Error Handling:</span>
<ul class="list-disc list-inside ml-4 mt-1 text-sm text-muted-foreground">
${Object.entries(conventions.error_handling).map(([key, val]) =>
`<li><strong>${escapeHtml(key)}:</strong> ${escapeHtml(String(val))}</li>`
).join('')}
</ul>
</div>
`);
}
if (conventions.testing) {
sections.push(`
<div class="context-field">
<span class="context-label">Testing:</span>
<ul class="list-disc list-inside ml-4 mt-1 text-sm text-muted-foreground">
${Object.entries(conventions.testing).map(([key, val]) => {
if (Array.isArray(val)) {
return `<li><strong>${escapeHtml(key)}:</strong> ${val.map(v => escapeHtml(String(v))).join(', ')}</li>`;
}
return `<li><strong>${escapeHtml(key)}:</strong> ${escapeHtml(String(val))}</li>`;
}).join('')}
</ul>
</div>
`);
}
return sections.join('');
}
function renderAssetsSection(assets) {
const sections = [];
// Documentation
if (assets.documentation && assets.documentation.length > 0) {
sections.push(`
<div class="asset-category">
<h5 class="asset-category-title">📄 Documentation</h5>
<div class="asset-grid">
${assets.documentation.map(doc => `
<div class="asset-card">
<div class="asset-card-header">
<span class="asset-path">${escapeHtml(doc.path)}</span>
<span class="relevance-score" style="background: ${getRelevanceColor(doc.relevance_score)}">${(doc.relevance_score * 100).toFixed(0)}%</span>
</div>
<div class="asset-card-body">
<div class="asset-scope">${escapeHtml(doc.scope || '')}</div>
${doc.contains && doc.contains.length > 0 ? `
<div class="asset-tags">
${doc.contains.map(tag => `<span class="asset-tag">${escapeHtml(tag)}</span>`).join('')}
</div>
` : ''}
</div>
</div>
`).join('')}
</div>
</div>
`);
}
// Source Code
if (assets.source_code && assets.source_code.length > 0) {
sections.push(`
<div class="asset-category">
<h5 class="asset-category-title">💻 Source Code</h5>
<div class="asset-grid">
${assets.source_code.map(src => `
<div class="asset-card">
<div class="asset-card-header">
<span class="asset-path">${escapeHtml(src.path)}</span>
<span class="relevance-score" style="background: ${getRelevanceColor(src.relevance_score)}">${(src.relevance_score * 100).toFixed(0)}%</span>
</div>
<div class="asset-card-body">
<div class="asset-role-badge badge-${getRoleBadgeClass(src.role)}">${escapeHtml(src.role || '')}</div>
${src.exports && src.exports.length > 0 ? `
<div class="asset-meta"><strong>Exports:</strong> ${src.exports.map(e => `<code class="inline-code">${escapeHtml(e)}</code>`).join(', ')}</div>
` : ''}
${src.features && src.features.length > 0 ? `
<div class="asset-features">${src.features.map(f => `<span class="feature-tag">${escapeHtml(f)}</span>`).join('')}</div>
` : ''}
</div>
</div>
`).join('')}
</div>
</div>
`);
}
// Tests
if (assets.tests && assets.tests.length > 0) {
sections.push(`
<div class="asset-category">
<h5 class="asset-category-title">🧪 Tests</h5>
<div class="asset-grid">
${assets.tests.map(test => `
<div class="asset-card">
<div class="asset-card-header">
<span class="asset-path">${escapeHtml(test.path)}</span>
${test.test_count ? `<span class="test-count-badge">${test.test_count} tests</span>` : ''}
</div>
<div class="asset-card-body">
<div class="asset-type">${escapeHtml(test.type || '')}</div>
${test.test_classes ? `<div class="asset-meta"><strong>Classes:</strong> ${escapeHtml(test.test_classes.join(', '))}</div>` : ''}
${test.coverage ? `<div class="asset-meta"><strong>Coverage:</strong> ${escapeHtml(test.coverage)}</div>` : ''}
</div>
</div>
`).join('')}
</div>
</div>
`);
}
return sections.join('');
}
function renderDependenciesSection(dependencies) {
const sections = [];
// Internal Dependencies
if (dependencies.internal && dependencies.internal.length > 0) {
sections.push(`
<div class="dep-category">
<h5 class="dep-category-title">🔄 Internal Dependencies</h5>
<div class="dep-graph">
${dependencies.internal.slice(0, 10).map(dep => `
<div class="dep-item">
<div class="dep-from">${escapeHtml(dep.from)}</div>
<div class="dep-arrow">
<span class="dep-type-badge badge-${dep.type}">${escapeHtml(dep.type)}</span>
</div>
<div class="dep-to">${escapeHtml(dep.to)}</div>
</div>
`).join('')}
${dependencies.internal.length > 10 ? `<div class="dep-more">... and ${dependencies.internal.length - 10} more</div>` : ''}
</div>
</div>
`);
}
// External Dependencies
if (dependencies.external && dependencies.external.length > 0) {
sections.push(`
<div class="dep-category">
<h5 class="dep-category-title">📦 External Dependencies</h5>
<div class="dep-grid">
${dependencies.external.map(dep => `
<div class="dep-external-card">
<div class="dep-package-name">${escapeHtml(dep.package)}</div>
<div class="dep-version">${escapeHtml(dep.version || '')}</div>
<div class="dep-usage">${escapeHtml(dep.usage || '')}</div>
</div>
`).join('')}
</div>
</div>
`);
}
return sections.join('');
}
function renderTestContextSection(testContext) {
const sections = [];
// Test Frameworks
if (testContext.frameworks) {
const frameworks = testContext.frameworks;
sections.push(`
<div class="test-category">
<h5 class="test-category-title">🛠 Test Frameworks</h5>
<div class="test-frameworks-grid">
${frameworks.backend ? `
<div class="framework-card framework-installed">
<div class="framework-header">
<span class="framework-label">Backend</span>
<span class="framework-name">${escapeHtml(frameworks.backend.name || 'N/A')}</span>
</div>
${frameworks.backend.plugins ? `
<div class="framework-plugins">${frameworks.backend.plugins.map(p => `<span class="plugin-tag">${escapeHtml(p)}</span>`).join('')}</div>
` : ''}
</div>
` : ''}
${frameworks.frontend ? `
<div class="framework-card ${frameworks.frontend.name && frameworks.frontend.name.includes('NONE') ? 'framework-missing' : 'framework-installed'}">
<div class="framework-header">
<span class="framework-label">Frontend</span>
<span class="framework-name">${escapeHtml(frameworks.frontend.name || 'N/A')}</span>
</div>
${frameworks.frontend.recommended ? `<div class="framework-recommended">Recommended: ${escapeHtml(frameworks.frontend.recommended)}</div>` : ''}
${frameworks.frontend.gap ? `<div class="framework-gap">⚠️ ${escapeHtml(frameworks.frontend.gap)}</div>` : ''}
</div>
` : ''}
</div>
</div>
`);
}
// Existing Tests Statistics
if (testContext.existing_tests) {
const tests = testContext.existing_tests;
let totalTests = 0;
let totalClasses = 0;
if (tests.backend) {
if (tests.backend.integration) {
totalTests += tests.backend.integration.tests || 0;
totalClasses += tests.backend.integration.classes || 0;
}
if (tests.backend.api_endpoints) {
totalTests += tests.backend.api_endpoints.tests || 0;
totalClasses += tests.backend.api_endpoints.classes || 0;
}
}
sections.push(`
<div class="test-category">
<h5 class="test-category-title">📊 Test Statistics</h5>
<div class="test-stats-grid">
<div class="stat-card">
<div class="stat-value">${totalTests}</div>
<div class="stat-label">Total Tests</div>
</div>
<div class="stat-card">
<div class="stat-value">${totalClasses}</div>
<div class="stat-label">Test Classes</div>
</div>
${testContext.coverage_config && testContext.coverage_config.target ? `
<div class="stat-card">
<div class="stat-value">${escapeHtml(testContext.coverage_config.target)}</div>
<div class="stat-label">Coverage Target</div>
</div>
` : ''}
</div>
</div>
`);
}
// Test Markers
if (testContext.test_markers) {
sections.push(`
<div class="test-category">
<h5 class="test-category-title">🏷 Test Markers</h5>
<div class="test-markers-grid">
${Object.entries(testContext.test_markers).map(([marker, desc]) => `
<div class="marker-card">
<span class="marker-name">@${escapeHtml(marker)}</span>
<span class="marker-desc">${escapeHtml(desc)}</span>
</div>
`).join('')}
</div>
</div>
`);
}
return sections.join('');
}
function renderConflictDetectionSection(conflictDetection) {
const sections = [];
// Risk Level Indicator
if (conflictDetection.risk_level) {
const riskLevel = conflictDetection.risk_level;
const riskColor = riskLevel === 'high' ? '#ef4444' : riskLevel === 'medium' ? '#f59e0b' : '#10b981';
sections.push(`
<div class="risk-indicator" style="border-color: ${riskColor}">
<div class="risk-level" style="background: ${riskColor}">
${escapeHtml(riskLevel.toUpperCase())} RISK
</div>
${conflictDetection.mitigation_strategy ? `
<div class="risk-mitigation">
<strong>Mitigation Strategy:</strong> ${escapeHtml(conflictDetection.mitigation_strategy)}
</div>
` : ''}
</div>
`);
}
// Risk Factors
if (conflictDetection.risk_factors) {
const factors = conflictDetection.risk_factors;
sections.push(`
<div class="conflict-category">
<h5 class="conflict-category-title"> Risk Factors</h5>
<div class="risk-factors-list">
${factors.test_gaps && factors.test_gaps.length > 0 ? `
<div class="risk-factor">
<strong class="risk-factor-title">Test Gaps:</strong>
<ul class="risk-factor-items">
${factors.test_gaps.map(gap => `<li>${escapeHtml(gap)}</li>`).join('')}
</ul>
</div>
` : ''}
${factors.existing_implementations && factors.existing_implementations.length > 0 ? `
<div class="risk-factor">
<strong class="risk-factor-title">Existing Implementations:</strong>
<ul class="risk-factor-items">
${factors.existing_implementations.map(impl => `<li>${escapeHtml(impl)}</li>`).join('')}
</ul>
</div>
` : ''}
</div>
</div>
`);
}
// Affected Modules
if (conflictDetection.affected_modules && conflictDetection.affected_modules.length > 0) {
sections.push(`
<div class="conflict-category">
<h5 class="conflict-category-title">📦 Affected Modules</h5>
<div class="affected-modules-grid">
${conflictDetection.affected_modules.map(mod => `
<span class="affected-module-tag">${escapeHtml(mod)}</span>
`).join('')}
</div>
</div>
`);
}
// Historical Conflicts
if (conflictDetection.historical_conflicts && conflictDetection.historical_conflicts.length > 0) {
sections.push(`
<div class="conflict-category">
<h5 class="conflict-category-title">📜 Historical Lessons</h5>
<div class="historical-conflicts-list">
${conflictDetection.historical_conflicts.map(conflict => `
<div class="historical-conflict-card">
<div class="conflict-source">Source: ${escapeHtml(conflict.source || 'Unknown')}</div>
${conflict.lesson ? `<div class="conflict-lesson"><strong>Lesson:</strong> ${escapeHtml(conflict.lesson)}</div>` : ''}
${conflict.recommendation ? `<div class="conflict-recommendation"><strong>Recommendation:</strong> ${escapeHtml(conflict.recommendation)}</div>` : ''}
${conflict.challenge ? `<div class="conflict-challenge"><strong>Challenge:</strong> ${escapeHtml(conflict.challenge)}</div>` : ''}
</div>
`).join('')}
</div>
</div>
`);
}
return sections.join('');
}
// ==========================================
// Tab Content Renderers - Other Tabs
// ==========================================
// Functions for rendering Summary, IMPL Plan, Review, and Lite Context tabs
// ==========================================
// Summary Tab Rendering
// ==========================================
function renderSummaryContent(summaries) {
if (!summaries || summaries.length === 0) {
return `
<div class="tab-empty-state">
<div class="empty-icon">📝</div>
<div class="empty-title">No Summaries</div>
<div class="empty-text">No summaries found in .summaries/</div>
</div>
`;
}
// Store summaries in global variable for modal access
window._currentSummaries = summaries;
return `
<div class="summary-tab-content space-y-4">
${summaries.map((s, idx) => {
const normalizedContent = normalizeLineEndings(s.content || '');
return `
<div class="summary-item-direct">
<div class="flex justify-between items-center mb-2">
<h4 class="text-base font-semibold text-foreground">📄 ${escapeHtml(s.name || 'Summary')}</h4>
<button class="btn-view-modal" onclick="openMarkdownModal('${escapeHtml(s.name || 'Summary')}', window._currentSummaries[${idx}].content, 'markdown');">
👁️ View
</button>
</div>
<pre class="summary-content-pre">${escapeHtml(normalizedContent)}</pre>
</div>
`;
}).join('')}
</div>
`;
}
// ==========================================
// IMPL Plan Tab Rendering
// ==========================================
function renderImplPlanContent(implPlan) {
if (!implPlan) {
return `
<div class="tab-empty-state">
<div class="empty-icon">📐</div>
<div class="empty-title">No IMPL Plan</div>
<div class="empty-text">No IMPL_PLAN.md found for this session.</div>
</div>
`;
}
// Normalize and store in global variable for modal access
const normalizedContent = normalizeLineEndings(implPlan);
window._currentImplPlan = normalizedContent;
return `
<div class="impl-plan-tab-content">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold text-foreground">Implementation Plan</h3>
<button class="px-4 py-2 bg-primary text-primary-foreground rounded hover:bg-primary/90 transition-colors" onclick="openMarkdownModal('IMPL_PLAN.md', window._currentImplPlan, 'markdown')">
👁 View in Modal
</button>
</div>
<pre class="markdown-content">${escapeHtml(normalizedContent)}</pre>
</div>
`;
}
// ==========================================
// Review Tab Rendering
// ==========================================
function renderReviewContent(review) {
if (!review || !review.dimensions) {
return `
<div class="tab-empty-state">
<div class="empty-icon">🔍</div>
<div class="empty-title">No Review Data</div>
<div class="empty-text">No review findings in .review/</div>
</div>
`;
}
const dimensions = Object.entries(review.dimensions);
if (dimensions.length === 0) {
return `
<div class="tab-empty-state">
<div class="empty-icon">🔍</div>
<div class="empty-title">No Findings</div>
<div class="empty-text">No review findings found.</div>
</div>
`;
}
return `
<div class="review-tab-content">
${dimensions.map(([dim, rawFindings]) => {
// Normalize findings to always be an array
let findings = [];
if (Array.isArray(rawFindings)) {
findings = rawFindings;
} else if (rawFindings && typeof rawFindings === 'object') {
// If it's an object with a findings array, use that
if (Array.isArray(rawFindings.findings)) {
findings = rawFindings.findings;
} else {
// Wrap single object in array or show raw JSON
findings = [{ title: dim, description: JSON.stringify(rawFindings, null, 2), severity: 'info' }];
}
}
return `
<div class="review-dimension-section">
<div class="dimension-header">
<span class="dimension-name">${escapeHtml(dim)}</span>
<span class="dimension-count">${findings.length} finding${findings.length !== 1 ? 's' : ''}</span>
</div>
<div class="dimension-findings">
${findings.map(f => `
<div class="finding-item ${f.severity || 'medium'}">
<div class="finding-header">
<span class="finding-severity ${f.severity || 'medium'}">${f.severity || 'medium'}</span>
<span class="finding-title">${escapeHtml(f.title || 'Finding')}</span>
</div>
<p class="finding-description">${escapeHtml(f.description || '')}</p>
${f.file ? `<div class="finding-file">📄 ${escapeHtml(f.file)}${f.line ? ':' + f.line : ''}</div>` : ''}
</div>
`).join('')}
</div>
</div>
`}).join('')}
</div>
`;
}
// ==========================================
// Lite Context Tab Rendering
// ==========================================
function renderLiteContextContent(context, session) {
const plan = session.plan || {};
// If we have context from context-package.json
if (context) {
return `
<div class="context-tab-content">
<pre class="json-content">${escapeHtml(JSON.stringify(context, null, 2))}</pre>
</div>
`;
}
// Fallback: show context from plan
if (plan.focus_paths?.length || plan.summary) {
return `
<div class="context-tab-content">
${plan.summary ? `
<div class="context-section">
<h4>Summary</h4>
<p>${escapeHtml(plan.summary)}</p>
</div>
` : ''}
${plan.focus_paths?.length ? `
<div class="context-section">
<h4>Focus Paths</h4>
<div class="path-tags">
${plan.focus_paths.map(p => `<span class="path-tag">${escapeHtml(p)}</span>`).join('')}
</div>
</div>
` : ''}
</div>
`;
}
return `
<div class="tab-empty-state">
<div class="empty-icon">📦</div>
<div class="empty-title">No Context Data</div>
<div class="empty-text">No context-package.json found for this session.</div>
</div>
`;
}
// ==========================================
// Exploration Context Rendering
// ==========================================
function renderExplorationContext(explorations) {
if (!explorations || !explorations.manifest) {
return '';
}
const manifest = explorations.manifest;
const data = explorations.data || {};
let sections = [];
// Header with manifest info
sections.push(`
<div class="exploration-header">
<h4>${escapeHtml(manifest.task_description || 'Exploration Context')}</h4>
<div class="exploration-meta">
<span class="meta-item">Complexity: <strong>${escapeHtml(manifest.complexity || 'N/A')}</strong></span>
<span class="meta-item">Explorations: <strong>${manifest.exploration_count || 0}</strong></span>
</div>
</div>
`);
// Render each exploration angle as collapsible section
const explorationOrder = ['architecture', 'dependencies', 'patterns', 'integration-points'];
const explorationTitles = {
'architecture': 'Architecture',
'dependencies': 'Dependencies',
'patterns': 'Patterns',
'integration-points': 'Integration Points'
};
for (const angle of explorationOrder) {
const expData = data[angle];
if (!expData) continue;
sections.push(`
<div class="exploration-section collapsible-section">
<div class="collapsible-header" onclick="toggleSection(this)">
<span class="collapse-icon"></span>
<span class="section-label">${explorationTitles[angle] || angle}</span>
</div>
<div class="collapsible-content collapsed">
${renderExplorationAngle(angle, expData)}
</div>
</div>
`);
}
return `<div class="exploration-context">${sections.join('')}</div>`;
}
function renderExplorationAngle(angle, data) {
let content = [];
// Project structure (architecture)
if (data.project_structure) {
content.push(`
<div class="exp-field">
<label>Project Structure</label>
<p>${escapeHtml(data.project_structure)}</p>
</div>
`);
}
// Relevant files
if (data.relevant_files && data.relevant_files.length) {
content.push(`
<div class="exp-field">
<label>Relevant Files (${data.relevant_files.length})</label>
<div class="relevant-files-list">
${data.relevant_files.slice(0, 10).map(f => `
<div class="file-item-exp">
<div class="file-path"><code>${escapeHtml(f.path || '')}</code></div>
<div class="file-relevance">Relevance: ${(f.relevance * 100).toFixed(0)}%</div>
${f.rationale ? `<div class="file-rationale">${escapeHtml(f.rationale.substring(0, 200))}...</div>` : ''}
</div>
`).join('')}
${data.relevant_files.length > 10 ? `<div class="more-files">... and ${data.relevant_files.length - 10} more files</div>` : ''}
</div>
</div>
`);
}
// Patterns
if (data.patterns) {
content.push(`
<div class="exp-field">
<label>Patterns</label>
<p class="patterns-text">${escapeHtml(data.patterns)}</p>
</div>
`);
}
// Dependencies
if (data.dependencies) {
content.push(`
<div class="exp-field">
<label>Dependencies</label>
<p>${escapeHtml(data.dependencies)}</p>
</div>
`);
}
// Integration points
if (data.integration_points) {
content.push(`
<div class="exp-field">
<label>Integration Points</label>
<p>${escapeHtml(data.integration_points)}</p>
</div>
`);
}
// Constraints
if (data.constraints) {
content.push(`
<div class="exp-field">
<label>Constraints</label>
<p>${escapeHtml(data.constraints)}</p>
</div>
`);
}
// Clarification needs
if (data.clarification_needs && data.clarification_needs.length) {
content.push(`
<div class="exp-field">
<label>Clarification Needs</label>
<div class="clarification-list">
${data.clarification_needs.map(c => `
<div class="clarification-item">
<div class="clarification-question">${escapeHtml(c.question)}</div>
${c.options && c.options.length ? `
<div class="clarification-options">
${c.options.map((opt, i) => `
<span class="option-badge ${i === c.recommended ? 'recommended' : ''}">${escapeHtml(opt)}</span>
`).join('')}
</div>
` : ''}
</div>
`).join('')}
</div>
</div>
`);
}
return content.join('') || '<p>No data available</p>';
}
// ==========================================
// TASK DRAWER CORE
// ==========================================
// Core drawer functionality and main rendering functions
let currentDrawerTasks = [];
function openTaskDrawer(taskId) {
const task = currentDrawerTasks.find(t => (t.task_id || t.id) === taskId);
if (!task) {
console.error('Task not found:', taskId);
return;
}
document.getElementById('drawerTaskTitle').textContent = task.title || taskId;
document.getElementById('drawerContent').innerHTML = renderTaskDrawerContent(task);
document.getElementById('taskDetailDrawer').classList.add('open');
document.getElementById('drawerOverlay').classList.add('active');
// Initialize flowchart after DOM is updated
setTimeout(() => {
renderFullFlowchart(task.flow_control);
}, 100);
}
function openTaskDrawerForLite(sessionId, taskId) {
const session = liteTaskDataStore[currentSessionDetailKey];
if (!session) return;
const task = session.tasks?.find(t => t.id === taskId);
if (!task) return;
// Set current drawer tasks and session context
currentDrawerTasks = session.tasks || [];
window._currentDrawerSession = session;
document.getElementById('drawerTaskTitle').textContent = task.title || taskId;
// Use dedicated lite task drawer renderer
document.getElementById('drawerContent').innerHTML = renderLiteTaskDrawerContent(task, session);
document.getElementById('taskDetailDrawer').classList.add('open');
document.getElementById('drawerOverlay').classList.add('active');
}
function closeTaskDrawer() {
document.getElementById('taskDetailDrawer').classList.remove('open');
document.getElementById('drawerOverlay').classList.remove('active');
}
function switchDrawerTab(tabName) {
// Update tab buttons
document.querySelectorAll('.drawer-tab').forEach(tab => {
tab.classList.toggle('active', tab.dataset.tab === tabName);
});
// Update tab panels
document.querySelectorAll('.drawer-panel').forEach(panel => {
panel.classList.toggle('active', panel.dataset.tab === tabName);
});
// Render flowchart if switching to flowchart tab
if (tabName === 'flowchart') {
const taskId = document.getElementById('drawerTaskTitle').textContent;
const task = currentDrawerTasks.find(t => t.title === taskId || t.task_id === taskId);
if (task?.flow_control) {
setTimeout(() => renderFullFlowchart(task.flow_control), 50);
}
}
}
function renderTaskDrawerContent(task) {
const fc = task.flow_control || {};
return `
<!-- Task Header -->
<div class="drawer-task-header">
<span class="task-id-badge">${escapeHtml(task.task_id || task.id || 'N/A')}</span>
<span class="task-status-badge ${task.status || 'pending'}">${task.status || 'pending'}</span>
</div>
<!-- Tab Navigation -->
<div class="drawer-tabs">
<button class="drawer-tab active" data-tab="overview" onclick="switchDrawerTab('overview')">Overview</button>
<button class="drawer-tab" data-tab="flowchart" onclick="switchDrawerTab('flowchart')">Flowchart</button>
<button class="drawer-tab" data-tab="files" onclick="switchDrawerTab('files')">Files</button>
<button class="drawer-tab" data-tab="raw" onclick="switchDrawerTab('raw')">Raw JSON</button>
</div>
<!-- Tab Content -->
<div class="drawer-tab-content">
<!-- Overview Tab (default) -->
<div class="drawer-panel active" data-tab="overview">
${renderPreAnalysisSteps(fc.pre_analysis)}
${renderImplementationStepsList(fc.implementation_approach)}
</div>
<!-- Flowchart Tab -->
<div class="drawer-panel" data-tab="flowchart">
<div id="flowchartContainer" class="flowchart-container"></div>
</div>
<!-- Files Tab -->
<div class="drawer-panel" data-tab="files">
${renderTargetFiles(fc.target_files)}
${fc.test_commands ? renderTestCommands(fc.test_commands) : ''}
</div>
<!-- Raw JSON Tab -->
<div class="drawer-panel" data-tab="raw">
<pre class="json-view">${escapeHtml(JSON.stringify(task, null, 2))}</pre>
</div>
</div>
`;
}
function renderLiteTaskDrawerContent(task, session) {
const rawTask = task._raw || task;
return `
<!-- Task Header -->
<div class="drawer-task-header">
<span class="task-id-badge">${escapeHtml(task.task_id || task.id || 'N/A')}</span>
${rawTask.action ? `<span class="action-badge">${escapeHtml(rawTask.action)}</span>` : ''}
</div>
<!-- Tab Navigation -->
<div class="drawer-tabs">
<button class="drawer-tab active" data-tab="overview" onclick="switchDrawerTab('overview')">Overview</button>
<button class="drawer-tab" data-tab="implementation" onclick="switchDrawerTab('implementation')">Implementation</button>
<button class="drawer-tab" data-tab="files" onclick="switchDrawerTab('files')">Files</button>
<button class="drawer-tab" data-tab="raw" onclick="switchDrawerTab('raw')">Raw JSON</button>
</div>
<!-- Tab Content -->
<div class="drawer-tab-content">
<!-- Overview Tab (default) -->
<div class="drawer-panel active" data-tab="overview">
${renderLiteTaskOverview(rawTask)}
</div>
<!-- Implementation Tab -->
<div class="drawer-panel" data-tab="implementation">
${renderLiteTaskImplementation(rawTask)}
</div>
<!-- Files Tab -->
<div class="drawer-panel" data-tab="files">
${renderLiteTaskFiles(rawTask)}
</div>
<!-- Raw JSON Tab -->
<div class="drawer-panel" data-tab="raw">
<pre class="json-view">${escapeHtml(JSON.stringify(rawTask, null, 2))}</pre>
</div>
</div>
`;
}
// Render plan.json task details in drawer (for lite tasks)
function renderPlanTaskDetails(task, session) {
if (!task) return '';
// Get corresponding plan task if available
const planTask = session?.plan?.tasks?.find(pt => pt.id === task.id);
if (!planTask) {
// Fallback: task itself might have plan-like structure
return renderTaskImplementationDetails(task);
}
return renderTaskImplementationDetails(planTask);
}
function renderTaskImplementationDetails(task) {
const sections = [];
// Description
if (task.description) {
sections.push(`
<div class="drawer-section">
<h4 class="drawer-section-title">Description</h4>
<p class="task-description">${escapeHtml(task.description)}</p>
</div>
`);
}
// Modification Points
if (task.modification_points?.length) {
sections.push(`
<div class="drawer-section">
<h4 class="drawer-section-title">Modification Points</h4>
<div class="modification-points-list">
${task.modification_points.map(mp => `
<div class="mod-point-item">
<div class="mod-point-file">
<span class="file-icon">📄</span>
<code>${escapeHtml(mp.file || mp.path || '')}</code>
</div>
${mp.target ? `<div class="mod-point-target">Target: <code>${escapeHtml(mp.target)}</code></div>` : ''}
${mp.change ? `<div class="mod-point-change">${escapeHtml(mp.change)}</div>` : ''}
</div>
`).join('')}
</div>
</div>
`);
}
// Implementation Steps
if (task.implementation?.length) {
sections.push(`
<div class="drawer-section">
<h4 class="drawer-section-title">Implementation Steps</h4>
<ol class="implementation-steps-list">
${task.implementation.map(step => `
<li class="impl-step-item">${escapeHtml(typeof step === 'string' ? step : step.step || JSON.stringify(step))}</li>
`).join('')}
</ol>
</div>
`);
}
// Reference
if (task.reference) {
sections.push(`
<div class="drawer-section">
<h4 class="drawer-section-title">Reference</h4>
${task.reference.pattern ? `<div class="ref-pattern"><strong>Pattern:</strong> ${escapeHtml(task.reference.pattern)}</div>` : ''}
${task.reference.files?.length ? `
<div class="ref-files">
<strong>Files:</strong>
<ul>
${task.reference.files.map(f => `<li><code>${escapeHtml(f)}</code></li>`).join('')}
</ul>
</div>
` : ''}
${task.reference.examples ? `<div class="ref-examples"><strong>Examples:</strong> ${escapeHtml(task.reference.examples)}</div>` : ''}
</div>
`);
}
// Acceptance Criteria
if (task.acceptance?.length) {
sections.push(`
<div class="drawer-section">
<h4 class="drawer-section-title">Acceptance Criteria</h4>
<ul class="acceptance-list">
${task.acceptance.map(a => `<li>${escapeHtml(a)}</li>`).join('')}
</ul>
</div>
`);
}
// Dependencies
if (task.depends_on?.length) {
sections.push(`
<div class="drawer-section">
<h4 class="drawer-section-title">Dependencies</h4>
<div class="dependencies-list">
${task.depends_on.map(dep => `<span class="dep-badge">${escapeHtml(dep)}</span>`).join(' ')}
</div>
</div>
`);
}
return sections.join('');
}
// Render lite task overview
function renderLiteTaskOverview(task) {
let sections = [];
// Description
if (task.description) {
sections.push(`
<div class="drawer-section">
<h4 class="drawer-section-title">Description</h4>
<p class="task-description">${escapeHtml(task.description)}</p>
</div>
`);
}
// Scope
if (task.scope) {
sections.push(`
<div class="drawer-section">
<h4 class="drawer-section-title">Scope</h4>
<code class="scope-path">${escapeHtml(task.scope)}</code>
</div>
`);
}
// Acceptance Criteria
if (task.acceptance && task.acceptance.length > 0) {
sections.push(`
<div class="drawer-section">
<h4 class="drawer-section-title">Acceptance Criteria</h4>
<ul class="acceptance-list">
${task.acceptance.map(a => `<li>${escapeHtml(a)}</li>`).join('')}
</ul>
</div>
`);
}
// Dependencies
if (task.depends_on && task.depends_on.length > 0) {
sections.push(`
<div class="drawer-section">
<h4 class="drawer-section-title">Dependencies</h4>
<div class="dependencies-list">
${task.depends_on.map(dep => `<span class="dep-badge">${escapeHtml(dep)}</span>`).join(' ')}
</div>
</div>
`);
}
// Reference
if (task.reference) {
sections.push(`
<div class="drawer-section">
<h4 class="drawer-section-title">Reference</h4>
${task.reference.pattern ? `<div class="ref-item"><strong>Pattern:</strong> ${escapeHtml(task.reference.pattern)}</div>` : ''}
${task.reference.files && task.reference.files.length > 0 ? `
<div class="ref-item">
<strong>Files:</strong>
<ul class="ref-files-list">
${task.reference.files.map(f => `<li><code>${escapeHtml(f)}</code></li>`).join('')}
</ul>
</div>
` : ''}
${task.reference.examples ? `<div class="ref-item"><strong>Examples:</strong> ${escapeHtml(task.reference.examples)}</div>` : ''}
</div>
`);
}
return sections.length > 0 ? sections.join('') : '<div class="empty-section">No overview data</div>';
}
// Render lite task implementation steps
function renderLiteTaskImplementation(task) {
let sections = [];
// Implementation Steps
if (task.implementation && task.implementation.length > 0) {
sections.push(`
<div class="drawer-section">
<h4 class="drawer-section-title">Implementation Steps</h4>
<ol class="impl-steps-list">
${task.implementation.map((step, idx) => `
<li class="impl-step-item">
<span class="step-number">${idx + 1}</span>
<span class="step-text">${escapeHtml(typeof step === 'string' ? step : step.step || JSON.stringify(step))}</span>
</li>
`).join('')}
</ol>
</div>
`);
}
// Modification Points
if (task.modification_points && task.modification_points.length > 0) {
sections.push(`
<div class="drawer-section">
<h4 class="drawer-section-title">Modification Points</h4>
<div class="mod-points-list">
${task.modification_points.map(mp => `
<div class="mod-point-card">
<div class="mod-file"><code>${escapeHtml(mp.file || '')}</code></div>
${mp.target ? `<div class="mod-target"><strong>Target:</strong> ${escapeHtml(mp.target)}</div>` : ''}
${mp.change ? `<div class="mod-change">${escapeHtml(mp.change)}</div>` : ''}
</div>
`).join('')}
</div>
</div>
`);
}
return sections.length > 0 ? sections.join('') : '<div class="empty-section">No implementation data</div>';
}
// Render lite task files
function renderLiteTaskFiles(task) {
const files = [];
// Collect from modification_points
if (task.modification_points) {
task.modification_points.forEach(mp => {
if (mp.file && !files.includes(mp.file)) files.push(mp.file);
});
}
// Collect from scope
if (task.scope && !files.includes(task.scope)) {
files.push(task.scope);
}
if (files.length === 0) {
return '<div class="empty-section">No files specified</div>';
}
return `
<div class="drawer-section">
<h4 class="drawer-section-title">Target Files</h4>
<ul class="target-files-list">
${files.map(f => `
<li class="file-item">
<span class="file-icon">📄</span>
<code>${escapeHtml(f)}</code>
</li>
`).join('')}
</ul>
</div>
`;
}
// ==========================================
// TASK DRAWER RENDERERS
// ==========================================
// Detailed content renderers and helper functions for task drawer
function renderPreAnalysisSteps(preAnalysis) {
if (!Array.isArray(preAnalysis) || preAnalysis.length === 0) {
return '<div class="empty-section">No pre-analysis steps</div>';
}
return `
<div class="drawer-section">
<h4 class="drawer-section-title">Pre-Analysis Steps</h4>
<div class="steps-list">
${preAnalysis.map((item, idx) => `
<div class="step-item">
<div class="step-badge">${idx + 1}</div>
<div class="step-content">
<div class="step-name">${escapeHtml(item.step || item.action || 'Step ' + (idx + 1))}</div>
${item.action && item.action !== item.step ? `<div class="step-action">${escapeHtml(item.action)}</div>` : ''}
${item.commands?.length ? `
<div class="step-commands">
${item.commands.map(c => `<code>${escapeHtml(typeof c === 'string' ? c : JSON.stringify(c))}</code>`).join('')}
</div>
` : ''}
${item.output_to ? `<div class="step-output">Output: <code>${escapeHtml(item.output_to)}</code></div>` : ''}
</div>
</div>
`).join('')}
</div>
</div>
`;
}
function renderImplementationStepsList(steps) {
if (!Array.isArray(steps) || steps.length === 0) {
return '<div class="empty-section">No implementation steps</div>';
}
return `
<div class="drawer-section">
<h4 class="drawer-section-title">Implementation Approach</h4>
<div class="impl-steps-list">
${steps.map((step, idx) => {
const hasMods = step.modification_points?.length;
const hasFlow = step.logic_flow?.length;
const hasColumns = hasMods || hasFlow;
return `
<div class="impl-step-item">
<div class="impl-step-header">
<span class="impl-step-number">Step ${step.step || idx + 1}</span>
<span class="impl-step-title">${escapeHtml(step.title || 'Untitled Step')}</span>
</div>
${step.description ? `<div class="impl-step-desc">${escapeHtml(step.description)}</div>` : ''}
${hasColumns ? `
<div class="impl-step-columns">
${hasMods ? `
<div class="impl-step-mods">
<strong>Modifications</strong>
<ul>
${step.modification_points.map(mp => `
<li>
${typeof mp === 'string' ? escapeHtml(mp) : `
<code>${escapeHtml(mp.file || mp.path || '')}</code>
${mp.changes ? ` - ${escapeHtml(mp.changes)}` : ''}
`}
</li>
`).join('')}
</ul>
</div>
` : '<div class="impl-step-mods"><strong>Modifications</strong><p>-</p></div>'}
${hasFlow ? `
<div class="impl-step-flow">
<strong>Logic Flow</strong>
<ol>
${step.logic_flow.map(lf => `<li>${escapeHtml(typeof lf === 'string' ? lf : lf.action || JSON.stringify(lf))}</li>`).join('')}
</ol>
</div>
` : '<div class="impl-step-flow"><strong>Logic Flow</strong><p>-</p></div>'}
</div>
` : ''}
${step.depends_on?.length ? `
<div class="impl-step-deps">
Depends on: ${step.depends_on.map(d => `<span class="dep-badge">${escapeHtml(d)}</span>`).join(' ')}
</div>
` : ''}
</div>
`}).join('')}
</div>
</div>
`;
}
function renderTargetFiles(files) {
if (!Array.isArray(files) || files.length === 0) {
return '<div class="empty-section">No target files</div>';
}
// Get current project path for building full paths
const projectPath = window.currentProjectPath || '';
return `
<div class="drawer-section">
<h4 class="drawer-section-title">Target Files</h4>
<ul class="target-files-list">
${files.map(f => {
const filePath = typeof f === 'string' ? f : (f.path || JSON.stringify(f));
// Build full path for vscode link
const fullPath = filePath.startsWith('/') || filePath.includes(':')
? filePath
: (projectPath ? `${projectPath}/${filePath}` : filePath);
const vscodeUri = `vscode://file/${fullPath.replace(/\\/g, '/')}`;
return `
<li class="file-item">
<a href="${vscodeUri}" class="file-link" title="Open in VS Code: ${escapeHtml(fullPath)}">
<span class="file-icon">📄</span>
<code>${escapeHtml(filePath)}</code>
<span class="file-action">↗</span>
</a>
</li>
`;
}).join('')}
</ul>
</div>
`;
}
function renderTestCommands(testCommands) {
if (!testCommands || typeof testCommands !== 'object') return '';
return `
<div class="drawer-section">
<h4 class="drawer-section-title">Test Commands</h4>
<div class="test-commands-list">
${Object.entries(testCommands).map(([key, val]) => `
<div class="test-command-item">
<strong>${escapeHtml(key)}:</strong>
<code>${escapeHtml(typeof val === 'string' ? val : JSON.stringify(val))}</code>
</div>
`).join('')}
</div>
</div>
`;
}
function renderTaskDetail(sessionId, task) {
// Get raw task data for JSON view
const rawTask = task._raw || task;
const taskJsonId = `task-json-${sessionId}-${task.id}`.replace(/[^a-zA-Z0-9-]/g, '-');
// Store JSON in memory instead of inline script tag
taskJsonStore[taskJsonId] = rawTask;
return `
<div class="task-detail" id="task-${sessionId}-${task.id}">
<div class="task-detail-header">
<span class="task-id-badge">${escapeHtml(task.id)}</span>
<span class="task-title">${escapeHtml(task.title || 'Untitled')}</span>
<span class="task-status-badge ${task.status}">${task.status}</span>
<div class="task-header-actions">
<button class="btn-view-json" onclick="showJsonModal('${taskJsonId}', '${escapeHtml(task.id)}')">{ } JSON</button>
</div>
</div>
<!-- Collapsible: Meta -->
<div class="collapsible-section">
<div class="collapsible-header">
<span class="collapse-icon">▶</span>
<span class="section-label">meta</span>
<span class="section-preview">${escapeHtml((task.meta?.type || task.meta?.action || '') + (task.meta?.scope ? ' | ' + task.meta.scope : ''))}</span>
</div>
<div class="collapsible-content collapsed">
${renderDynamicFields(task.meta || rawTask, ['type', 'action', 'agent', 'scope', 'module', 'execution_group'])}
</div>
</div>
<!-- Collapsible: Context -->
<div class="collapsible-section">
<div class="collapsible-header">
<span class="collapse-icon">▶</span>
<span class="section-label">context</span>
<span class="section-preview">${escapeHtml(getContextPreview(task.context, rawTask))}</span>
</div>
<div class="collapsible-content collapsed">
${renderContextFields(task.context, rawTask)}
</div>
</div>
<!-- Collapsible: Flow Control (with Flowchart) -->
<div class="collapsible-section">
<div class="collapsible-header">
<span class="collapse-icon">▶</span>
<span class="section-label">flow_control</span>
<span class="section-preview">${escapeHtml(getFlowControlPreview(task.flow_control, rawTask))}</span>
</div>
<div class="collapsible-content collapsed">
<div class="flowchart-container" id="flowchart-${sessionId}-${task.id}"></div>
${renderFlowControlDetails(task.flow_control, rawTask)}
</div>
</div>
</div>
`;
}
function getContextPreview(context, rawTask) {
const items = [];
if (context?.requirements?.length) items.push(`${context.requirements.length} reqs`);
if (context?.acceptance?.length) items.push(`${context.acceptance.length} acceptance`);
if (context?.focus_paths?.length) items.push(`${context.focus_paths.length} paths`);
if (rawTask?.modification_points?.length) items.push(`${rawTask.modification_points.length} mods`);
return items.join(' | ') || 'No context';
}
function getFlowControlPreview(flowControl, rawTask) {
const steps = flowControl?.implementation_approach?.length || rawTask?.implementation?.length || 0;
return steps > 0 ? `${steps} steps` : 'No steps';
}
function renderDynamicFields(obj, priorityKeys = []) {
if (!obj || typeof obj !== 'object') return '<div class="field-value json-value-null">null</div>';
const entries = Object.entries(obj).filter(([k, v]) => v !== null && v !== undefined && k !== '_raw');
if (entries.length === 0) return '<div class="field-value json-value-null">Empty</div>';
// Sort: priority keys first, then alphabetically
entries.sort(([a], [b]) => {
const aIdx = priorityKeys.indexOf(a);
const bIdx = priorityKeys.indexOf(b);
if (aIdx !== -1 && bIdx !== -1) return aIdx - bIdx;
if (aIdx !== -1) return -1;
if (bIdx !== -1) return 1;
return a.localeCompare(b);
});
return `<div class="field-group">${entries.map(([key, value]) => renderFieldRow(key, value)).join('')}</div>`;
}
function renderFieldRow(key, value) {
return `
<div class="field-row">
<span class="field-label">${escapeHtml(key)}:</span>
<div class="field-value">${renderFieldValue(key, value)}</div>
</div>
`;
}
function renderFieldValue(key, value) {
if (value === null || value === undefined) {
return '<span class="json-value-null">null</span>';
}
if (typeof value === 'boolean') {
return `<span class="json-value-boolean">${value}</span>`;
}
if (typeof value === 'number') {
return `<span class="json-value-number">${value}</span>`;
}
if (typeof value === 'string') {
// Check if it's a path
if (key.includes('path') || key.includes('file') || value.includes('/') || value.includes('\\')) {
return `<span class="array-item path-item">${escapeHtml(value)}</span>`;
}
return `<span class="json-value-string">${escapeHtml(value)}</span>`;
}
if (Array.isArray(value)) {
if (value.length === 0) return '<span class="json-value-null">[]</span>';
// Check if array contains objects or strings
if (typeof value[0] === 'object') {
return `<div class="nested-array">${value.map((item, i) => `
<div class="array-object">
<div class="array-object-header">[${i + 1}]</div>
${renderDynamicFields(item)}
</div>
`).join('')}</div>`;
}
// Array of strings/primitives
const isPathArray = key.includes('path') || key.includes('file');
return `<div class="array-value">${value.map(v =>
`<span class="array-item ${isPathArray ? 'path-item' : ''}">${escapeHtml(String(v))}</span>`
).join('')}</div>`;
}
if (typeof value === 'object') {
return renderDynamicFields(value);
}
return escapeHtml(String(value));
}
function renderContextFields(context, rawTask) {
const sections = [];
// Requirements / Description
const requirements = context?.requirements || [];
const description = rawTask?.description;
if (requirements.length > 0 || description) {
sections.push(`
<div class="context-field">
<label>requirements:</label>
${description ? `<p style="margin-bottom: 8px;">${escapeHtml(description)}</p>` : ''}
${requirements.length > 0 ? `<ul>${requirements.map(r => `<li>${escapeHtml(r)}</li>`).join('')}</ul>` : ''}
</div>
`);
}
// Focus paths / Modification points
const focusPaths = context?.focus_paths || [];
const modPoints = rawTask?.modification_points || [];
if (focusPaths.length > 0 || modPoints.length > 0) {
sections.push(`
<div class="context-field">
<label>${modPoints.length > 0 ? 'modification_points:' : 'focus_paths:'}</label>
${modPoints.length > 0 ? `
<div class="mod-points">
${modPoints.map(m => `
<div class="mod-point">
<span class="array-item path-item">${escapeHtml(m.file || m)}</span>
${m.target ? `<span class="mod-target">→ ${escapeHtml(m.target)}</span>` : ''}
${m.change ? `<p class="mod-change">${escapeHtml(m.change)}</p>` : ''}
</div>
`).join('')}
</div>
` : `
<div class="path-tags">${focusPaths.map(p => `<span class="path-tag">${escapeHtml(p)}</span>`).join('')}</div>
`}
</div>
`);
}
// Acceptance criteria
const acceptance = context?.acceptance || rawTask?.acceptance || [];
if (acceptance.length > 0) {
sections.push(`
<div class="context-field">
<label>acceptance:</label>
<ul>${acceptance.map(a => `<li>${escapeHtml(a)}</li>`).join('')}</ul>
</div>
`);
}
// Dependencies
const depends = context?.depends_on || rawTask?.depends_on || [];
if (depends.length > 0) {
sections.push(`
<div class="context-field">
<label>depends_on:</label>
<div class="path-tags">${depends.map(d => `<span class="array-item depends-badge">${escapeHtml(d)}</span>`).join('')}</div>
</div>
`);
}
// Reference
const reference = rawTask?.reference;
if (reference) {
sections.push(`
<div class="context-field">
<label>reference:</label>
${renderDynamicFields(reference)}
</div>
`);
}
return sections.length > 0
? `<div class="context-fields">${sections.join('')}</div>`
: '<div class="field-value json-value-null">No context data</div>';
}
function renderFlowControlDetails(flowControl, rawTask) {
const sections = [];
// Pre-analysis
const preAnalysis = flowControl?.pre_analysis || rawTask?.pre_analysis || [];
if (preAnalysis.length > 0) {
sections.push(`
<div class="context-field" style="margin-top: 16px;">
<label>pre_analysis:</label>
<ul>${preAnalysis.map(p => `<li>${escapeHtml(p)}</li>`).join('')}</ul>
</div>
`);
}
// Target files
const targetFiles = flowControl?.target_files || rawTask?.target_files || [];
if (targetFiles.length > 0) {
sections.push(`
<div class="context-field">
<label>target_files:</label>
<div class="path-tags">${targetFiles.map(f => `<span class="path-tag">${escapeHtml(f)}</span>`).join('')}</div>
</div>
`);
}
return sections.join('');
}
// ==========================================
// FLOWCHART RENDERING (D3.js)
// ==========================================
function renderFlowchartForTask(sessionId, task) {
// Will render on section expand
}
function renderFlowchart(containerId, steps) {
if (!steps || steps.length === 0) return;
if (typeof d3 === 'undefined') {
document.getElementById(containerId).innerHTML = '<div class="flowchart-fallback">D3.js not loaded</div>';
return;
}
const container = document.getElementById(containerId);
const width = container.clientWidth || 500;
const nodeHeight = 50;
const nodeWidth = Math.min(width - 40, 300);
const padding = 15;
const height = steps.length * (nodeHeight + padding) + padding * 2;
// Clear existing content
container.innerHTML = '';
const svg = d3.select('#' + containerId)
.append('svg')
.attr('width', width)
.attr('height', height)
.attr('class', 'flowchart-svg');
// Arrow marker
svg.append('defs').append('marker')
.attr('id', 'arrow-' + containerId)
.attr('viewBox', '0 -5 10 10')
.attr('refX', 8)
.attr('refY', 0)
.attr('markerWidth', 6)
.attr('markerHeight', 6)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('fill', 'hsl(var(--border))');
// Draw arrows
for (let i = 0; i < steps.length - 1; i++) {
const y1 = padding + i * (nodeHeight + padding) + nodeHeight;
const y2 = padding + (i + 1) * (nodeHeight + padding);
svg.append('line')
.attr('x1', width / 2)
.attr('y1', y1)
.attr('x2', width / 2)
.attr('y2', y2)
.attr('stroke', 'hsl(var(--border))')
.attr('stroke-width', 2)
.attr('marker-end', 'url(#arrow-' + containerId + ')');
}
// Draw nodes
const nodes = svg.selectAll('.node')
.data(steps)
.enter()
.append('g')
.attr('class', 'flowchart-node')
.attr('transform', (d, i) => `translate(${(width - nodeWidth) / 2}, ${padding + i * (nodeHeight + padding)})`);
// Node rectangles
nodes.append('rect')
.attr('width', nodeWidth)
.attr('height', nodeHeight)
.attr('rx', 6)
.attr('fill', (d, i) => i === 0 ? 'hsl(var(--primary))' : 'hsl(var(--card))')
.attr('stroke', 'hsl(var(--border))')
.attr('stroke-width', 1);
// Step number circle
nodes.append('circle')
.attr('cx', 20)
.attr('cy', nodeHeight / 2)
.attr('r', 12)
.attr('fill', (d, i) => i === 0 ? 'rgba(255,255,255,0.2)' : 'hsl(var(--muted))');
nodes.append('text')
.attr('x', 20)
.attr('y', nodeHeight / 2)
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'central')
.attr('font-size', '11px')
.attr('fill', (d, i) => i === 0 ? 'white' : 'hsl(var(--muted-foreground))')
.text((d, i) => i + 1);
// Node text (step name)
nodes.append('text')
.attr('x', 45)
.attr('y', nodeHeight / 2)
.attr('dominant-baseline', 'central')
.attr('fill', (d, i) => i === 0 ? 'white' : 'hsl(var(--foreground))')
.attr('font-size', '12px')
.text(d => {
const text = d.step || d.action || 'Step';
return text.length > 35 ? text.substring(0, 32) + '...' : text;
});
}
function renderFullFlowchart(flowControl) {
if (!flowControl) return;
const container = document.getElementById('flowchartContainer');
if (!container) return;
const preAnalysis = Array.isArray(flowControl.pre_analysis) ? flowControl.pre_analysis : [];
const implSteps = Array.isArray(flowControl.implementation_approach) ? flowControl.implementation_approach : [];
if (preAnalysis.length === 0 && implSteps.length === 0) {
container.innerHTML = '<div class="empty-section">No flowchart data available</div>';
return;
}
const width = container.clientWidth || 500;
const nodeHeight = 90;
const nodeWidth = Math.min(width - 40, 420);
const nodeGap = 45;
const sectionGap = 30;
// Calculate total nodes and height
const totalPreNodes = preAnalysis.length;
const totalImplNodes = implSteps.length;
const hasBothSections = totalPreNodes > 0 && totalImplNodes > 0;
const height = (totalPreNodes + totalImplNodes) * (nodeHeight + nodeGap) +
(hasBothSections ? sectionGap + 60 : 0) + 60;
// Clear existing
d3.select('#flowchartContainer').selectAll('*').remove();
const svg = d3.select('#flowchartContainer')
.append('svg')
.attr('width', '100%')
.attr('height', height)
.attr('viewBox', `0 0 ${width} ${height}`);
// Add arrow markers
const defs = svg.append('defs');
defs.append('marker')
.attr('id', 'arrowhead-pre')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 8)
.attr('refY', 0)
.attr('markerWidth', 6)
.attr('markerHeight', 6)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('fill', '#f59e0b');
defs.append('marker')
.attr('id', 'arrowhead-impl')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 8)
.attr('refY', 0)
.attr('markerWidth', 6)
.attr('markerHeight', 6)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('fill', 'hsl(var(--primary))');
let currentY = 20;
// Render Pre-Analysis section
if (totalPreNodes > 0) {
// Section label
svg.append('text')
.attr('x', 20)
.attr('y', currentY)
.attr('fill', '#f59e0b')
.attr('font-weight', 'bold')
.attr('font-size', '13px')
.text('📋 Pre-Analysis Steps');
currentY += 25;
preAnalysis.forEach((step, idx) => {
const x = (width - nodeWidth) / 2;
// Connection line to next node
if (idx < preAnalysis.length - 1) {
svg.append('line')
.attr('x1', width / 2)
.attr('y1', currentY + nodeHeight)
.attr('x2', width / 2)
.attr('y2', currentY + nodeHeight + nodeGap - 10)
.attr('stroke', '#f59e0b')
.attr('stroke-width', 2)
.attr('marker-end', 'url(#arrowhead-pre)');
}
// Node group
const nodeG = svg.append('g')
.attr('class', 'flowchart-node')
.attr('transform', `translate(${x}, ${currentY})`);
// Node rectangle (pre-analysis style - amber/orange)
nodeG.append('rect')
.attr('width', nodeWidth)
.attr('height', nodeHeight)
.attr('rx', 10)
.attr('fill', 'hsl(var(--card))')
.attr('stroke', '#f59e0b')
.attr('stroke-width', 2)
.attr('stroke-dasharray', '5,3');
// Step badge
nodeG.append('circle')
.attr('cx', 25)
.attr('cy', 25)
.attr('r', 15)
.attr('fill', '#f59e0b');
nodeG.append('text')
.attr('x', 25)
.attr('y', 30)
.attr('text-anchor', 'middle')
.attr('fill', 'white')
.attr('font-weight', 'bold')
.attr('font-size', '11px')
.text('P' + (idx + 1));
// Step name
const stepName = step.step || step.action || 'Pre-step ' + (idx + 1);
nodeG.append('text')
.attr('x', 50)
.attr('y', 28)
.attr('fill', 'hsl(var(--foreground))')
.attr('font-weight', '600')
.attr('font-size', '13px')
.text(truncateText(stepName, 40));
// Action description
if (step.action && step.action !== stepName) {
nodeG.append('text')
.attr('x', 15)
.attr('y', 52)
.attr('fill', 'hsl(var(--muted-foreground))')
.attr('font-size', '11px')
.text(truncateText(step.action, 50));
}
// Output indicator
if (step.output_to) {
nodeG.append('text')
.attr('x', 15)
.attr('y', 75)
.attr('fill', '#f59e0b')
.attr('font-size', '10px')
.text('→ ' + truncateText(step.output_to, 45));
}
currentY += nodeHeight + nodeGap;
});
}
// Section divider if both sections exist
if (hasBothSections) {
currentY += 10;
svg.append('line')
.attr('x1', 40)
.attr('y1', currentY)
.attr('x2', width - 40)
.attr('y2', currentY)
.attr('stroke', 'hsl(var(--border))')
.attr('stroke-width', 1)
.attr('stroke-dasharray', '4,4');
// Connecting arrow from pre-analysis to implementation
svg.append('line')
.attr('x1', width / 2)
.attr('y1', currentY - nodeGap + 5)
.attr('x2', width / 2)
.attr('y2', currentY + sectionGap - 5)
.attr('stroke', 'hsl(var(--primary))')
.attr('stroke-width', 2)
.attr('marker-end', 'url(#arrowhead-impl)');
currentY += sectionGap;
}
// Render Implementation section
if (totalImplNodes > 0) {
// Section label
svg.append('text')
.attr('x', 20)
.attr('y', currentY)
.attr('fill', 'hsl(var(--primary))')
.attr('font-weight', 'bold')
.attr('font-size', '13px')
.text('🔧 Implementation Steps');
currentY += 25;
implSteps.forEach((step, idx) => {
const x = (width - nodeWidth) / 2;
// Connection line to next node
if (idx < implSteps.length - 1) {
svg.append('line')
.attr('x1', width / 2)
.attr('y1', currentY + nodeHeight)
.attr('x2', width / 2)
.attr('y2', currentY + nodeHeight + nodeGap - 10)
.attr('stroke', 'hsl(var(--primary))')
.attr('stroke-width', 2)
.attr('marker-end', 'url(#arrowhead-impl)');
}
// Node group
const nodeG = svg.append('g')
.attr('class', 'flowchart-node')
.attr('transform', `translate(${x}, ${currentY})`);
// Node rectangle (implementation style - blue)
nodeG.append('rect')
.attr('width', nodeWidth)
.attr('height', nodeHeight)
.attr('rx', 10)
.attr('fill', 'hsl(var(--card))')
.attr('stroke', 'hsl(var(--primary))')
.attr('stroke-width', 2);
// Step badge
nodeG.append('circle')
.attr('cx', 25)
.attr('cy', 25)
.attr('r', 15)
.attr('fill', 'hsl(var(--primary))');
nodeG.append('text')
.attr('x', 25)
.attr('y', 30)
.attr('text-anchor', 'middle')
.attr('fill', 'white')
.attr('font-weight', 'bold')
.attr('font-size', '12px')
.text(step.step || idx + 1);
// Step title
nodeG.append('text')
.attr('x', 50)
.attr('y', 28)
.attr('fill', 'hsl(var(--foreground))')
.attr('font-weight', '600')
.attr('font-size', '13px')
.text(truncateText(step.title || 'Step ' + (idx + 1), 40));
// Description
if (step.description) {
nodeG.append('text')
.attr('x', 15)
.attr('y', 52)
.attr('fill', 'hsl(var(--muted-foreground))')
.attr('font-size', '11px')
.text(truncateText(step.description, 50));
}
// Output/depends indicator
if (step.depends_on?.length) {
nodeG.append('text')
.attr('x', 15)
.attr('y', 75)
.attr('fill', 'var(--warning-color)')
.attr('font-size', '10px')
.text('← Depends: ' + step.depends_on.join(', '));
}
currentY += nodeHeight + nodeGap;
});
}
}
// D3.js Vertical Flowchart for Implementation Approach (legacy)
function renderImplementationFlowchart(steps) {
if (!Array.isArray(steps) || steps.length === 0) return;
const container = document.getElementById('flowchartContainer');
if (!container) return;
const width = container.clientWidth || 500;
const nodeHeight = 100;
const nodeWidth = Math.min(width - 40, 400);
const nodeGap = 50;
const height = steps.length * (nodeHeight + nodeGap) + 40;
// Clear existing
d3.select('#flowchartContainer').selectAll('*').remove();
const svg = d3.select('#flowchartContainer')
.append('svg')
.attr('width', '100%')
.attr('height', height)
.attr('viewBox', `0 0 ${width} ${height}`);
// Add arrow marker
svg.append('defs').append('marker')
.attr('id', 'arrowhead')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 8)
.attr('refY', 0)
.attr('markerWidth', 6)
.attr('markerHeight', 6)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('fill', 'hsl(var(--primary))');
// Draw nodes and connections
steps.forEach((step, idx) => {
const y = idx * (nodeHeight + nodeGap) + 20;
const x = (width - nodeWidth) / 2;
// Connection line to next node
if (idx < steps.length - 1) {
svg.append('line')
.attr('x1', width / 2)
.attr('y1', y + nodeHeight)
.attr('x2', width / 2)
.attr('y2', y + nodeHeight + nodeGap - 10)
.attr('stroke', 'hsl(var(--primary))')
.attr('stroke-width', 2)
.attr('marker-end', 'url(#arrowhead)');
}
// Node group
const nodeG = svg.append('g')
.attr('class', 'flowchart-node')
.attr('transform', `translate(${x}, ${y})`);
// Node rectangle with gradient
nodeG.append('rect')
.attr('width', nodeWidth)
.attr('height', nodeHeight)
.attr('rx', 10)
.attr('fill', 'hsl(var(--card))')
.attr('stroke', 'hsl(var(--primary))')
.attr('stroke-width', 2)
.attr('filter', 'drop-shadow(0 2px 4px rgba(0,0,0,0.1))');
// Step number badge
nodeG.append('circle')
.attr('cx', 25)
.attr('cy', 25)
.attr('r', 15)
.attr('fill', 'hsl(var(--primary))');
nodeG.append('text')
.attr('x', 25)
.attr('y', 30)
.attr('text-anchor', 'middle')
.attr('fill', 'white')
.attr('font-weight', 'bold')
.attr('font-size', '12px')
.text(step.step || idx + 1);
// Step title
nodeG.append('text')
.attr('x', 50)
.attr('y', 30)
.attr('fill', 'hsl(var(--foreground))')
.attr('font-weight', '600')
.attr('font-size', '14px')
.text(truncateText(step.title || 'Step ' + (idx + 1), 35));
// Step description (if available)
if (step.description) {
nodeG.append('text')
.attr('x', 15)
.attr('y', 55)
.attr('fill', 'hsl(var(--muted-foreground))')
.attr('font-size', '12px')
.text(truncateText(step.description, 45));
}
// Output indicator
if (step.output) {
nodeG.append('text')
.attr('x', 15)
.attr('y', 80)
.attr('fill', 'var(--success-color)')
.attr('font-size', '11px')
.text('→ ' + truncateText(step.output, 40));
}
});
}
// ==========================================
// HOME VIEW - Dashboard Homepage
// ==========================================
function renderDashboard() {
updateStats();
updateBadges();
renderSessions();
document.getElementById('generatedAt').textContent = workflowData.generatedAt || new Date().toISOString();
}
function updateStats() {
const stats = workflowData.statistics || {};
document.getElementById('statTotalSessions').textContent = stats.totalSessions || 0;
document.getElementById('statActiveSessions').textContent = stats.activeSessions || 0;
document.getElementById('statTotalTasks').textContent = stats.totalTasks || 0;
document.getElementById('statCompletedTasks').textContent = stats.completedTasks || 0;
}
function updateBadges() {
const active = workflowData.activeSessions || [];
const archived = workflowData.archivedSessions || [];
document.getElementById('badgeAll').textContent = active.length + archived.length;
document.getElementById('badgeActive').textContent = active.length;
document.getElementById('badgeArchived').textContent = archived.length;
// Lite Tasks badges
const liteTasks = workflowData.liteTasks || {};
document.getElementById('badgeLitePlan').textContent = liteTasks.litePlan?.length || 0;
document.getElementById('badgeLiteFix').textContent = liteTasks.liteFix?.length || 0;
}
function renderSessions() {
const container = document.getElementById('mainContent');
let sessions = [];
if (currentFilter === 'all' || currentFilter === 'active') {
sessions = sessions.concat((workflowData.activeSessions || []).map(s => ({ ...s, _isActive: true })));
}
if (currentFilter === 'all' || currentFilter === 'archived') {
sessions = sessions.concat((workflowData.archivedSessions || []).map(s => ({ ...s, _isActive: false })));
}
if (sessions.length === 0) {
container.innerHTML = `
<div class="empty-state" style="grid-column: 1/-1;">
<div class="empty-icon">📭</div>
<div class="empty-title">No Sessions Found</div>
<div class="empty-text">No workflow sessions match your current filter.</div>
</div>
`;
return;
}
container.innerHTML = `<div class="sessions-grid">${sessions.map(session => renderSessionCard(session)).join('')}</div>`;
}
function renderSessionCard(session) {
const tasks = session.tasks || [];
const taskCount = session.taskCount || tasks.length;
const completed = tasks.filter(t => t.status === 'completed').length;
const progress = taskCount > 0 ? Math.round((completed / taskCount) * 100) : 0;
// Use _isActive flag set during rendering, default to true
const isActive = session._isActive !== false;
const date = session.created_at;
// Get session type badge
const sessionType = session.type || 'workflow';
const typeBadge = sessionType !== 'workflow' ? `<span class="session-type-badge ${sessionType}">${sessionType}</span>` : '';
// Store session data for modal
const sessionKey = `session-${session.session_id}`.replace(/[^a-zA-Z0-9-]/g, '-');
sessionDataStore[sessionKey] = session;
return `
<div class="session-card" onclick="showSessionDetailPage('${sessionKey}')">
<div class="session-header">
<div class="session-title">${escapeHtml(session.session_id || 'Unknown')}</div>
<div class="session-badges">
${typeBadge}
<span class="session-status ${isActive ? 'active' : 'archived'}">
${isActive ? 'ACTIVE' : 'ARCHIVED'}
</span>
</div>
</div>
<div class="session-body">
<div class="session-meta">
<span class="session-meta-item">📅 ${formatDate(date)}</span>
<span class="session-meta-item">📋 ${taskCount} tasks</span>
</div>
${taskCount > 0 ? `
<div class="progress-container">
<span class="progress-label">Progress</span>
<div class="progress-bar-wrapper">
<div class="progress-bar">
<div class="progress-fill" style="width: ${progress}%"></div>
</div>
<span class="progress-text">${completed}/${taskCount} (${progress}%)</span>
</div>
</div>
` : ''}
</div>
</div>
`;
}
// ==========================================
// PROJECT OVERVIEW VIEW
// ==========================================
function renderProjectOverview() {
const container = document.getElementById('mainContent');
const project = workflowData.projectOverview;
if (!project) {
container.innerHTML = `
<div class="flex flex-col items-center justify-center py-16 text-center">
<div class="text-6xl mb-4">📋</div>
<h3 class="text-xl font-semibold text-foreground mb-2">No Project Overview</h3>
<p class="text-muted-foreground mb-4">
Run <code class="px-2 py-1 bg-muted rounded text-sm font-mono">/workflow:init</code> to initialize project analysis
</p>
</div>
`;
return;
}
container.innerHTML = `
<!-- Project Header -->
<div class="bg-card border border-border rounded-lg p-6 mb-6">
<div class="flex items-start justify-between mb-4">
<div>
<h2 class="text-2xl font-bold text-foreground mb-2">${escapeHtml(project.projectName)}</h2>
<p class="text-muted-foreground">${escapeHtml(project.description || 'No description available')}</p>
</div>
<div class="text-sm text-muted-foreground text-right">
<div>Initialized: ${formatDate(project.initializedAt)}</div>
<div class="mt-1">Mode: <span class="font-mono text-xs px-2 py-0.5 bg-muted rounded">${escapeHtml(project.metadata?.analysis_mode || 'unknown')}</span></div>
</div>
</div>
</div>
<!-- Technology Stack -->
<div class="bg-card border border-border rounded-lg p-6 mb-6">
<h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
<span>💻</span> Technology Stack
</h3>
<!-- Languages -->
<div class="mb-5">
<h4 class="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">Languages</h4>
<div class="flex flex-wrap gap-3">
${project.technologyStack.languages.map(lang => `
<div class="flex items-center gap-2 px-3 py-2 bg-background border border-border rounded-lg ${lang.primary ? 'ring-2 ring-primary' : ''}">
<span class="font-semibold text-foreground">${escapeHtml(lang.name)}</span>
<span class="text-xs text-muted-foreground">${lang.file_count} files</span>
${lang.primary ? '<span class="text-xs px-1.5 py-0.5 bg-primary text-primary-foreground rounded">Primary</span>' : ''}
</div>
`).join('') || '<span class="text-muted-foreground text-sm">No languages detected</span>'}
</div>
</div>
<!-- Frameworks -->
<div class="mb-5">
<h4 class="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">Frameworks</h4>
<div class="flex flex-wrap gap-2">
${project.technologyStack.frameworks.map(fw => `
<span class="px-3 py-1.5 bg-success-light text-success rounded-lg text-sm font-medium">${escapeHtml(fw)}</span>
`).join('') || '<span class="text-muted-foreground text-sm">No frameworks detected</span>'}
</div>
</div>
<!-- Build Tools -->
<div class="mb-5">
<h4 class="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">Build Tools</h4>
<div class="flex flex-wrap gap-2">
${project.technologyStack.build_tools.map(tool => `
<span class="px-3 py-1.5 bg-warning-light text-warning rounded-lg text-sm font-medium">${escapeHtml(tool)}</span>
`).join('') || '<span class="text-muted-foreground text-sm">No build tools detected</span>'}
</div>
</div>
<!-- Test Frameworks -->
<div>
<h4 class="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">Test Frameworks</h4>
<div class="flex flex-wrap gap-2">
${project.technologyStack.test_frameworks.map(fw => `
<span class="px-3 py-1.5 bg-accent text-accent-foreground rounded-lg text-sm font-medium">${escapeHtml(fw)}</span>
`).join('') || '<span class="text-muted-foreground text-sm">No test frameworks detected</span>'}
</div>
</div>
</div>
<!-- Architecture -->
<div class="bg-card border border-border rounded-lg p-6 mb-6">
<h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
<span>🏗️</span> Architecture
</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-5">
<!-- Style -->
<div>
<h4 class="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2">Style</h4>
<div class="px-3 py-2 bg-background border border-border rounded-lg">
<span class="text-foreground font-medium">${escapeHtml(project.architecture.style)}</span>
</div>
</div>
<!-- Layers -->
<div>
<h4 class="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2">Layers</h4>
<div class="flex flex-wrap gap-2">
${project.architecture.layers.map(layer => `
<span class="px-2 py-1 bg-muted text-foreground rounded text-sm">${escapeHtml(layer)}</span>
`).join('') || '<span class="text-muted-foreground text-sm">None</span>'}
</div>
</div>
<!-- Patterns -->
<div>
<h4 class="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2">Patterns</h4>
<div class="flex flex-wrap gap-2">
${project.architecture.patterns.map(pattern => `
<span class="px-2 py-1 bg-muted text-foreground rounded text-sm">${escapeHtml(pattern)}</span>
`).join('') || '<span class="text-muted-foreground text-sm">None</span>'}
</div>
</div>
</div>
</div>
<!-- Key Components -->
<div class="bg-card border border-border rounded-lg p-6 mb-6">
<h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
<span>⚙️</span> Key Components
</h3>
${project.keyComponents.length > 0 ? `
<div class="space-y-3">
${project.keyComponents.map(comp => {
const importanceColors = {
high: 'border-l-4 border-l-destructive bg-destructive/5',
medium: 'border-l-4 border-l-warning bg-warning/5',
low: 'border-l-4 border-l-muted-foreground bg-muted'
};
const importanceBadges = {
high: '<span class="px-2 py-0.5 text-xs font-semibold bg-destructive text-destructive-foreground rounded">High</span>',
medium: '<span class="px-2 py-0.5 text-xs font-semibold bg-warning text-foreground rounded">Medium</span>',
low: '<span class="px-2 py-0.5 text-xs font-semibold bg-muted text-muted-foreground rounded">Low</span>'
};
return `
<div class="p-4 ${importanceColors[comp.importance] || importanceColors.low} rounded-lg">
<div class="flex items-start justify-between mb-2">
<h4 class="font-semibold text-foreground">${escapeHtml(comp.name)}</h4>
${importanceBadges[comp.importance] || ''}
</div>
<p class="text-sm text-muted-foreground mb-2">${escapeHtml(comp.description)}</p>
<code class="text-xs font-mono text-primary">${escapeHtml(comp.path)}</code>
</div>
`;
}).join('')}
</div>
` : '<p class="text-muted-foreground text-sm">No key components identified</p>'}
</div>
<!-- Development Index -->
<div class="bg-card border border-border rounded-lg p-6 mb-6">
<h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
<span>📝</span> Development History
</h3>
${renderDevelopmentIndex(project.developmentIndex)}
</div>
<!-- Statistics -->
<div class="bg-card border border-border rounded-lg p-6">
<h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
<span>📊</span> Statistics
</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="text-center p-4 bg-background rounded-lg">
<div class="text-3xl font-bold text-primary mb-1">${project.statistics.total_features || 0}</div>
<div class="text-sm text-muted-foreground">Total Features</div>
</div>
<div class="text-center p-4 bg-background rounded-lg">
<div class="text-3xl font-bold text-success mb-1">${project.statistics.total_sessions || 0}</div>
<div class="text-sm text-muted-foreground">Total Sessions</div>
</div>
<div class="text-center p-4 bg-background rounded-lg">
<div class="text-sm text-muted-foreground mb-1">Last Updated</div>
<div class="text-sm font-medium text-foreground">${formatDate(project.statistics.last_updated)}</div>
</div>
</div>
</div>
`;
}
function renderDevelopmentIndex(devIndex) {
if (!devIndex) return '<p class="text-muted-foreground text-sm">No development history available</p>';
const categories = [
{ key: 'feature', label: 'Features', icon: '✨', badgeClass: 'bg-primary-light text-primary' },
{ key: 'enhancement', label: 'Enhancements', icon: '⚡', badgeClass: 'bg-success-light text-success' },
{ key: 'bugfix', label: 'Bug Fixes', icon: '🐛', badgeClass: 'bg-destructive/10 text-destructive' },
{ key: 'refactor', label: 'Refactorings', icon: '🔧', badgeClass: 'bg-warning-light text-warning' },
{ key: 'docs', label: 'Documentation', icon: '📚', badgeClass: 'bg-muted text-muted-foreground' }
];
const totalEntries = categories.reduce((sum, cat) => sum + (devIndex[cat.key]?.length || 0), 0);
if (totalEntries === 0) {
return '<p class="text-muted-foreground text-sm">No development history entries</p>';
}
return `
<div class="space-y-4">
${categories.map(cat => {
const entries = devIndex[cat.key] || [];
if (entries.length === 0) return '';
return `
<div>
<h4 class="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
<span>${cat.icon}</span>
<span>${cat.label}</span>
<span class="text-xs px-2 py-0.5 ${cat.badgeClass} rounded-full">${entries.length}</span>
</h4>
<div class="space-y-2">
${entries.slice(0, 5).map(entry => `
<div class="p-3 bg-background border border-border rounded-lg hover:shadow-sm transition-shadow">
<div class="flex items-start justify-between mb-1">
<h5 class="font-medium text-foreground text-sm">${escapeHtml(entry.title)}</h5>
<span class="text-xs text-muted-foreground">${formatDate(entry.date)}</span>
</div>
${entry.description ? `<p class="text-sm text-muted-foreground mb-1">${escapeHtml(entry.description)}</p>` : ''}
<div class="flex items-center gap-2 text-xs">
${entry.sub_feature ? `<span class="px-2 py-0.5 bg-muted rounded">${escapeHtml(entry.sub_feature)}</span>` : ''}
${entry.status ? `<span class="px-2 py-0.5 ${entry.status === 'completed' ? 'bg-success-light text-success' : 'bg-warning-light text-warning'} rounded">${escapeHtml(entry.status)}</span>` : ''}
</div>
</div>
`).join('')}
${entries.length > 5 ? `<div class="text-sm text-muted-foreground text-center py-2">... and ${entries.length - 5} more</div>` : ''}
</div>
</div>
`;
}).join('')}
</div>
`;
}
// ============================================
// SESSION DETAIL VIEW
// ============================================
// Standard workflow session detail page rendering
function showSessionDetailPage(sessionKey) {
const session = sessionDataStore[sessionKey];
if (!session) return;
currentView = 'sessionDetail';
currentSessionDetailKey = sessionKey;
updateContentTitle();
const container = document.getElementById('mainContent');
const sessionType = session.type || 'workflow';
// Render specialized pages for review and test-fix sessions
if (sessionType === 'review' || sessionType === 'review-cycle') {
container.innerHTML = renderReviewSessionDetailPage(session);
initReviewSessionPage(session);
return;
}
if (sessionType === 'test-fix' || sessionType === 'fix') {
container.innerHTML = renderFixSessionDetailPage(session);
initFixSessionPage(session);
return;
}
// Default workflow session detail page
const tasks = session.tasks || [];
const completed = tasks.filter(t => t.status === 'completed').length;
const inProgress = tasks.filter(t => t.status === 'in_progress').length;
const pending = tasks.filter(t => t.status === 'pending').length;
const isActive = session._isActive !== false;
container.innerHTML = `
<div class="session-detail-page">
<!-- Header -->
<div class="detail-header">
<button class="btn-back" onclick="goBackToSessions()">
<span class="back-icon">←</span>
<span>Back to Sessions</span>
</button>
<div class="detail-title-row">
<h2 class="detail-session-id">${escapeHtml(session.session_id)}</h2>
<div class="detail-badges">
<span class="session-type-badge ${session.type || 'workflow'}">${session.type || 'workflow'}</span>
<span class="session-status ${isActive ? 'active' : 'archived'}">
${isActive ? 'ACTIVE' : 'ARCHIVED'}
</span>
</div>
</div>
</div>
<!-- Session Info Bar -->
<div class="detail-info-bar">
<div class="info-item">
<span class="info-label">Created:</span>
<span class="info-value">${formatDate(session.created_at)}</span>
</div>
${session.archived_at ? `
<div class="info-item">
<span class="info-label">Archived:</span>
<span class="info-value">${formatDate(session.archived_at)}</span>
</div>
` : ''}
<div class="info-item">
<span class="info-label">Project:</span>
<span class="info-value">${escapeHtml(session.project || '-')}</span>
</div>
<div class="info-item">
<span class="info-label">Tasks:</span>
<span class="info-value">${completed}/${tasks.length} completed</span>
</div>
</div>
<!-- Tab Navigation -->
<div class="detail-tabs">
<button class="detail-tab active" data-tab="tasks" onclick="switchDetailTab('tasks')">
<span class="tab-icon">📋</span>
<span class="tab-text">Tasks</span>
<span class="tab-count">${tasks.length}</span>
</button>
<button class="detail-tab" data-tab="context" onclick="switchDetailTab('context')">
<span class="tab-icon">📦</span>
<span class="tab-text">Context</span>
</button>
<button class="detail-tab" data-tab="summary" onclick="switchDetailTab('summary')">
<span class="tab-icon">📝</span>
<span class="tab-text">Summary</span>
</button>
<button class="detail-tab" data-tab="impl-plan" onclick="switchDetailTab('impl-plan')">
<span class="tab-icon">📐</span>
<span class="tab-text">IMPL Plan</span>
</button>
${session.hasReview ? `
<button class="detail-tab" data-tab="review" onclick="switchDetailTab('review')">
<span class="tab-icon">🔍</span>
<span class="tab-text">Review</span>
</button>
` : ''}
</div>
<!-- Tab Content -->
<div class="detail-tab-content" id="detailTabContent">
${renderTasksTab(session, tasks, completed, inProgress, pending)}
</div>
</div>
`;
}
function goBackToSessions() {
currentView = 'sessions';
currentSessionDetailKey = null;
updateContentTitle();
renderSessions();
}
function switchDetailTab(tabName) {
// Update active tab
document.querySelectorAll('.detail-tab').forEach(tab => {
tab.classList.toggle('active', tab.dataset.tab === tabName);
});
const session = sessionDataStore[currentSessionDetailKey];
if (!session) return;
const contentArea = document.getElementById('detailTabContent');
const tasks = session.tasks || [];
const completed = tasks.filter(t => t.status === 'completed').length;
const inProgress = tasks.filter(t => t.status === 'in_progress').length;
const pending = tasks.filter(t => t.status === 'pending').length;
switch (tabName) {
case 'tasks':
contentArea.innerHTML = renderTasksTab(session, tasks, completed, inProgress, pending);
break;
case 'context':
loadAndRenderContextTab(session, contentArea);
break;
case 'summary':
loadAndRenderSummaryTab(session, contentArea);
break;
case 'impl-plan':
loadAndRenderImplPlanTab(session, contentArea);
break;
case 'review':
loadAndRenderReviewTab(session, contentArea);
break;
}
}
function renderTasksTab(session, tasks, completed, inProgress, pending) {
// Populate drawer tasks for click-to-open functionality
currentDrawerTasks = tasks;
// Auto-load full task details in server mode
if (window.SERVER_MODE && session.path) {
// Schedule auto-load after DOM render
setTimeout(() => loadFullTaskDetails(), 50);
}
// Show task list with loading state or basic list
const showLoading = window.SERVER_MODE && session.path;
return `
<div class="tasks-tab-content">
<div class="task-stats-bar">
<span class="task-stat completed">✓ ${completed} completed</span>
<span class="task-stat in-progress">⟳ ${inProgress} in progress</span>
<span class="task-stat pending">○ ${pending} pending</span>
</div>
<div class="tasks-list" id="tasksListContent">
${showLoading ? `
<div class="tab-loading">Loading task details...</div>
` : (tasks.length === 0 ? `
<div class="tab-empty-state">
<div class="empty-icon">📋</div>
<div class="empty-title">No Tasks</div>
<div class="empty-text">This session has no tasks defined.</div>
</div>
` : tasks.map(task => renderDetailTaskItem(task)).join(''))}
</div>
</div>
`;
}
async function loadFullTaskDetails() {
const session = sessionDataStore[currentSessionDetailKey];
if (!session || !window.SERVER_MODE || !session.path) return;
const tasksContainer = document.getElementById('tasksListContent');
tasksContainer.innerHTML = '<div class="tab-loading">Loading full task details...</div>';
try {
const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=tasks`);
if (response.ok) {
const data = await response.json();
if (data.tasks && data.tasks.length > 0) {
// Populate drawer tasks for click-to-open functionality
currentDrawerTasks = data.tasks;
tasksContainer.innerHTML = data.tasks.map(task => renderDetailTaskItem(task)).join('');
} else {
tasksContainer.innerHTML = `
<div class="tab-empty-state">
<div class="empty-icon">📋</div>
<div class="empty-title">No Task Files</div>
<div class="empty-text">No IMPL-*.json files found in .task/</div>
</div>
`;
}
}
} catch (err) {
tasksContainer.innerHTML = `<div class="tab-error">Failed to load tasks: ${err.message}</div>`;
}
}
function renderDetailTaskItem(task) {
const taskId = task.task_id || task.id || 'Unknown';
const status = task.status || 'pending';
// Simplified task card with only essential elements: task ID badge, title, and status badge
// Includes status class for border-left color and status-${status} class for background color
return `
<div class="detail-task-item ${status} status-${status}" onclick="openTaskDrawer('${escapeHtml(taskId)}')" style="cursor: pointer;">
<div class="task-item-header">
<span class="task-id-badge">${escapeHtml(taskId)}</span>
<span class="task-title">${escapeHtml(task.title || task.meta?.title || 'Untitled')}</span>
<span class="task-status-badge ${status}">${status}</span>
</div>
</div>
`;
}
function getMetaPreview(task) {
const meta = task.meta || {};
const parts = [];
if (meta.type) parts.push(meta.type);
if (meta.action) parts.push(meta.action);
if (meta.scope) parts.push(meta.scope);
return parts.join(' | ') || 'No meta';
}
function getTaskContextPreview(task) {
const items = [];
const ctx = task.context || {};
if (ctx.requirements?.length) items.push(`${ctx.requirements.length} reqs`);
if (ctx.focus_paths?.length) items.push(`${ctx.focus_paths.length} paths`);
if (task.modification_points?.length) items.push(`${task.modification_points.length} mods`);
if (task.description) items.push('desc');
return items.join(' | ') || 'No context';
}
function getFlowPreview(task) {
const steps = task.flow_control?.implementation_approach?.length || task.implementation?.length || 0;
return steps > 0 ? `${steps} steps` : 'No steps';
}
function renderTaskContext(task) {
const sections = [];
const ctx = task.context || {};
// Description
if (task.description) {
sections.push(`
<div class="context-field">
<label>description:</label>
<p>${escapeHtml(task.description)}</p>
</div>
`);
}
// Requirements
if (ctx.requirements?.length) {
sections.push(`
<div class="context-field">
<label>requirements:</label>
<ul>${ctx.requirements.map(r => `<li>${escapeHtml(r)}</li>`).join('')}</ul>
</div>
`);
}
// Focus paths
if (ctx.focus_paths?.length) {
sections.push(`
<div class="context-field">
<label>focus_paths:</label>
<div class="path-tags">${ctx.focus_paths.map(p => `<span class="path-tag">${escapeHtml(p)}</span>`).join('')}</div>
</div>
`);
}
// Modification points
if (task.modification_points?.length) {
sections.push(`
<div class="context-field">
<label>modification_points:</label>
<div class="mod-points">
${task.modification_points.map(m => `
<div class="mod-point">
<span class="array-item path-item">${escapeHtml(m.file || m)}</span>
${m.target ? `<span class="mod-target">→ ${escapeHtml(m.target)}</span>` : ''}
${m.change ? `<p class="mod-change">${escapeHtml(m.change)}</p>` : ''}
</div>
`).join('')}
</div>
</div>
`);
}
// Acceptance criteria
const acceptance = ctx.acceptance || task.acceptance || [];
if (acceptance.length) {
sections.push(`
<div class="context-field">
<label>acceptance:</label>
<ul>${acceptance.map(a => `<li>${escapeHtml(a)}</li>`).join('')}</ul>
</div>
`);
}
return sections.length > 0
? `<div class="context-fields">${sections.join('')}</div>`
: '<div class="field-value json-value-null">No context data</div>';
}
function renderFlowControl(task) {
const sections = [];
const fc = task.flow_control || {};
// Implementation approach
const steps = fc.implementation_approach || task.implementation || [];
if (steps.length) {
sections.push(`
<div class="context-field">
<label>implementation_approach:</label>
<ol class="impl-steps">
${steps.map(s => `<li>${escapeHtml(typeof s === 'string' ? s : s.step || s.action || JSON.stringify(s))}</li>`).join('')}
</ol>
</div>
`);
}
// Pre-analysis
const preAnalysis = fc.pre_analysis || task.pre_analysis || [];
if (preAnalysis.length) {
sections.push(`
<div class="context-field">
<label>pre_analysis:</label>
<ul>${preAnalysis.map(p => `<li>${escapeHtml(p)}</li>`).join('')}</ul>
</div>
`);
}
// Target files
const targetFiles = fc.target_files || task.target_files || [];
if (targetFiles.length) {
sections.push(`
<div class="context-field">
<label>target_files:</label>
<div class="path-tags">${targetFiles.map(f => `<span class="path-tag">${escapeHtml(f)}</span>`).join('')}</div>
</div>
`);
}
return sections.length > 0
? `<div class="context-fields">${sections.join('')}</div>`
: '<div class="field-value json-value-null">No flow control data</div>';
}
async function loadAndRenderContextTab(session, contentArea) {
contentArea.innerHTML = '<div class="tab-loading">Loading context data...</div>';
try {
// Try to load context-package.json from server
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);
return;
}
}
// Fallback: show placeholder
contentArea.innerHTML = `
<div class="tab-empty-state">
<div class="empty-icon">📦</div>
<div class="empty-title">Context Data</div>
<div class="empty-text">Context data will be loaded from context-package.json</div>
</div>
`;
} catch (err) {
contentArea.innerHTML = `<div class="tab-error">Failed to load context: ${err.message}</div>`;
}
}
async function loadAndRenderSummaryTab(session, contentArea) {
contentArea.innerHTML = '<div class="tab-loading">Loading summaries...</div>';
try {
if (window.SERVER_MODE && session.path) {
const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=summary`);
if (response.ok) {
const data = await response.json();
contentArea.innerHTML = renderSummaryContent(data.summaries);
return;
}
}
contentArea.innerHTML = `
<div class="tab-empty-state">
<div class="empty-icon">📝</div>
<div class="empty-title">Summaries</div>
<div class="empty-text">Session summaries will be loaded from .summaries/</div>
</div>
`;
} catch (err) {
contentArea.innerHTML = `<div class="tab-error">Failed to load summaries: ${err.message}</div>`;
}
}
async function loadAndRenderImplPlanTab(session, contentArea) {
contentArea.innerHTML = '<div class="tab-loading">Loading IMPL plan...</div>';
try {
if (window.SERVER_MODE && session.path) {
const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=impl-plan`);
if (response.ok) {
const data = await response.json();
contentArea.innerHTML = renderImplPlanContent(data.implPlan);
return;
}
}
contentArea.innerHTML = `
<div class="tab-empty-state">
<div class="empty-icon">📐</div>
<div class="empty-title">IMPL Plan</div>
<div class="empty-text">IMPL plan will be loaded from IMPL_PLAN.md</div>
</div>
`;
} catch (err) {
contentArea.innerHTML = `<div class="tab-error">Failed to load IMPL plan: ${err.message}</div>`;
}
}
async function loadAndRenderReviewTab(session, contentArea) {
contentArea.innerHTML = '<div class="tab-loading">Loading review data...</div>';
try {
if (window.SERVER_MODE && session.path) {
const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=review`);
if (response.ok) {
const data = await response.json();
contentArea.innerHTML = renderReviewContent(data.review);
return;
}
}
contentArea.innerHTML = `
<div class="tab-empty-state">
<div class="empty-icon">🔍</div>
<div class="empty-title">Review Data</div>
<div class="empty-text">Review data will be loaded from review files</div>
</div>
`;
} catch (err) {
contentArea.innerHTML = `<div class="tab-error">Failed to load review: ${err.message}</div>`;
}
}
function showRawSessionJson(sessionKey) {
const session = sessionDataStore[sessionKey];
if (!session) return;
// Close current modal
const currentModal = document.querySelector('.session-modal-overlay');
if (currentModal) currentModal.remove();
// Show JSON modal
const overlay = document.createElement('div');
overlay.className = 'json-modal-overlay active';
overlay.innerHTML = `
<div class="json-modal">
<div class="json-modal-header">
<div class="json-modal-title">
<span class="session-id-badge">${escapeHtml(session.session_id)}</span>
<span>Session JSON</span>
</div>
<button class="json-modal-close" onclick="closeJsonModal(this)">&times;</button>
</div>
<div class="json-modal-body">
<pre class="json-modal-content">${escapeHtml(JSON.stringify(session, null, 2))}</pre>
</div>
<div class="json-modal-footer">
<button class="json-modal-copy" onclick="copyJsonToClipboard(this)">Copy to Clipboard</button>
</div>
</div>
`;
document.body.appendChild(overlay);
// Close on overlay click
overlay.addEventListener('click', (e) => {
if (e.target === overlay) closeJsonModal();
});
}
// ==========================================
// REVIEW SESSION DETAIL PAGE
// ==========================================
function renderReviewSessionDetailPage(session) {
const isActive = session._isActive !== false;
const tasks = session.tasks || [];
const dimensions = session.reviewDimensions || [];
// Calculate review statistics
const totalFindings = dimensions.reduce((sum, d) => sum + (d.findings?.length || 0), 0);
const criticalCount = dimensions.reduce((sum, d) =>
sum + (d.findings?.filter(f => f.severity === 'critical').length || 0), 0);
const highCount = dimensions.reduce((sum, d) =>
sum + (d.findings?.filter(f => f.severity === 'high').length || 0), 0);
return `
<div class="session-detail-page session-type-review">
<!-- Header -->
<div class="detail-header">
<button class="btn-back" onclick="goBackToSessions()">
<span class="back-icon">←</span>
<span>Back to Sessions</span>
</button>
<div class="detail-title-row">
<h2 class="detail-session-id">🔍 ${escapeHtml(session.session_id)}</h2>
<div class="detail-badges">
<span class="session-type-badge review">Review</span>
<span class="session-status ${isActive ? 'active' : 'archived'}">
${isActive ? 'ACTIVE' : 'ARCHIVED'}
</span>
</div>
</div>
</div>
<!-- Review Progress Section -->
<div class="review-progress-section">
<div class="review-progress-header">
<h3>📊 Review Progress</h3>
<span class="phase-badge ${session.phase || 'in-progress'}">${session.phase || 'In Progress'}</span>
</div>
<!-- Summary Cards -->
<div class="review-summary-grid">
<div class="summary-card">
<div class="summary-icon">📊</div>
<div class="summary-value">${totalFindings}</div>
<div class="summary-label">Total Findings</div>
</div>
<div class="summary-card critical">
<div class="summary-icon">🔴</div>
<div class="summary-value">${criticalCount}</div>
<div class="summary-label">Critical</div>
</div>
<div class="summary-card high">
<div class="summary-icon">🟠</div>
<div class="summary-value">${highCount}</div>
<div class="summary-label">High</div>
</div>
<div class="summary-card">
<div class="summary-icon">📋</div>
<div class="summary-value">${dimensions.length}</div>
<div class="summary-label">Dimensions</div>
</div>
</div>
<!-- Dimension Timeline -->
<div class="dimension-timeline" id="dimensionTimeline">
${dimensions.map((dim, idx) => `
<div class="dimension-item ${dim.status || 'pending'}" data-dimension="${dim.name}">
<div class="dimension-number">D${idx + 1}</div>
<div class="dimension-name">${escapeHtml(dim.name || 'Unknown')}</div>
<div class="dimension-stats">${dim.findings?.length || 0} findings</div>
</div>
`).join('')}
</div>
</div>
<!-- Findings Grid -->
<div class="review-findings-section">
<div class="findings-header">
<h3>🔍 Findings by Dimension</h3>
<div class="findings-filters">
<button class="filter-btn active" data-severity="all" onclick="filterReviewFindings('all')">All</button>
<button class="filter-btn" data-severity="critical" onclick="filterReviewFindings('critical')">Critical</button>
<button class="filter-btn" data-severity="high" onclick="filterReviewFindings('high')">High</button>
<button class="filter-btn" data-severity="medium" onclick="filterReviewFindings('medium')">Medium</button>
</div>
</div>
<div class="findings-grid" id="reviewFindingsGrid">
${renderReviewFindingsGrid(dimensions)}
</div>
</div>
<!-- Session Info -->
<div class="detail-info-bar">
<div class="info-item">
<span class="info-label">Created:</span>
<span class="info-value">${formatDate(session.created_at)}</span>
</div>
${session.archived_at ? `
<div class="info-item">
<span class="info-label">Archived:</span>
<span class="info-value">${formatDate(session.archived_at)}</span>
</div>
` : ''}
<div class="info-item">
<span class="info-label">Project:</span>
<span class="info-value">${escapeHtml(session.project || '-')}</span>
</div>
</div>
</div>
`;
}
function renderReviewFindingsGrid(dimensions) {
if (!dimensions || dimensions.length === 0) {
return `
<div class="empty-state">
<div class="empty-icon">🔍</div>
<div class="empty-text">No review dimensions found</div>
</div>
`;
}
let html = '';
dimensions.forEach(dim => {
const findings = dim.findings || [];
if (findings.length === 0) return;
html += `
<div class="dimension-findings-group" data-dimension="${dim.name}">
<div class="dimension-group-header">
<span class="dimension-badge">${escapeHtml(dim.name)}</span>
<span class="dimension-count">${findings.length} findings</span>
</div>
<div class="findings-cards">
${findings.map(f => `
<div class="finding-card severity-${f.severity || 'medium'}" data-severity="${f.severity || 'medium'}">
<div class="finding-card-header">
<span class="severity-badge ${f.severity || 'medium'}">${f.severity || 'medium'}</span>
${f.fix_status ? `<span class="fix-status-badge status-${f.fix_status}">${f.fix_status}</span>` : ''}
</div>
<div class="finding-card-title">${escapeHtml(f.title || 'Finding')}</div>
<div class="finding-card-desc">${escapeHtml((f.description || '').substring(0, 100))}${f.description?.length > 100 ? '...' : ''}</div>
${f.file ? `<div class="finding-card-file">📄 ${escapeHtml(f.file)}${f.line ? ':' + f.line : ''}</div>` : ''}
</div>
`).join('')}
</div>
</div>
`;
});
return html || '<div class="empty-state"><div class="empty-text">No findings</div></div>';
}
function initReviewSessionPage(session) {
// Initialize event handlers for review session page
// Filter handlers are inline onclick
}
function filterReviewFindings(severity) {
// Update filter buttons
document.querySelectorAll('.findings-filters .filter-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.severity === severity);
});
// Filter finding cards
document.querySelectorAll('.finding-card').forEach(card => {
if (severity === 'all' || card.dataset.severity === severity) {
card.style.display = '';
} else {
card.style.display = 'none';
}
});
}
// ============================================
// LITE TASKS VIEW
// ============================================
// Lite-plan and lite-fix task list and detail rendering
function renderLiteTasks() {
const container = document.getElementById('mainContent');
const liteTasks = workflowData.liteTasks || {};
const sessions = currentLiteType === 'lite-plan'
? liteTasks.litePlan || []
: liteTasks.liteFix || [];
if (sessions.length === 0) {
container.innerHTML = `
<div class="empty-state">
<div class="empty-icon">⚡</div>
<div class="empty-title">No ${currentLiteType} Sessions</div>
<div class="empty-text">No sessions found in .workflow/.${currentLiteType}/</div>
</div>
`;
return;
}
container.innerHTML = `<div class="sessions-grid">${sessions.map(session => renderLiteTaskCard(session)).join('')}</div>`;
// Initialize collapsible sections
document.querySelectorAll('.collapsible-header').forEach(header => {
header.addEventListener('click', () => toggleSection(header));
});
// Render flowcharts for expanded tasks
sessions.forEach(session => {
session.tasks?.forEach(task => {
if (task.flow_control?.implementation_approach) {
renderFlowchartForTask(session.id, task);
}
});
});
}
function renderLiteTaskCard(session) {
const tasks = session.tasks || [];
// Store session data for detail page
const sessionKey = `lite-${session.type}-${session.id}`.replace(/[^a-zA-Z0-9-]/g, '-');
liteTaskDataStore[sessionKey] = session;
return `
<div class="session-card lite-task-card" onclick="showLiteTaskDetailPage('${sessionKey}')" style="cursor: pointer;">
<div class="session-header">
<div class="session-title">${escapeHtml(session.id)}</div>
<span class="session-status ${session.type}">
${session.type === 'lite-plan' ? '📝 PLAN' : '🔧 FIX'}
</span>
</div>
<div class="session-body">
<div class="session-meta">
<span class="session-meta-item">📅 ${formatDate(session.createdAt)}</span>
<span class="session-meta-item">📋 ${tasks.length} tasks</span>
</div>
</div>
</div>
`;
}
// Lite Task Detail Page
function showLiteTaskDetailPage(sessionKey) {
const session = liteTaskDataStore[sessionKey];
if (!session) return;
currentView = 'liteTaskDetail';
currentSessionDetailKey = sessionKey;
// Also store in sessionDataStore for tab switching compatibility
sessionDataStore[sessionKey] = {
...session,
session_id: session.id,
created_at: session.createdAt,
path: session.path,
type: session.type
};
const container = document.getElementById('mainContent');
const tasks = session.tasks || [];
const plan = session.plan || {};
const progress = session.progress || { total: 0, completed: 0, percentage: 0 };
const completed = tasks.filter(t => t.status === 'completed').length;
const inProgress = tasks.filter(t => t.status === 'in_progress').length;
const pending = tasks.filter(t => t.status === 'pending').length;
container.innerHTML = `
<div class="session-detail-page lite-task-detail-page">
<!-- Header -->
<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>
</button>
<div class="detail-title-row">
<h2 class="detail-session-id">${session.type === 'lite-plan' ? '📝' : '🔧'} ${escapeHtml(session.id)}</h2>
<div class="detail-badges">
<span class="session-type-badge ${session.type}">${session.type}</span>
</div>
</div>
</div>
<!-- Session Info Bar -->
<div class="detail-info-bar">
<div class="info-item">
<span class="info-label">Created:</span>
<span class="info-value">${formatDate(session.createdAt)}</span>
</div>
<div class="info-item">
<span class="info-label">Tasks:</span>
<span class="info-value">${tasks.length} tasks</span>
</div>
</div>
<!-- Tab Navigation -->
<div class="detail-tabs">
<button class="detail-tab active" data-tab="tasks" onclick="switchLiteDetailTab('tasks')">
<span class="tab-icon">📋</span>
<span class="tab-text">Tasks</span>
<span class="tab-count">${tasks.length}</span>
</button>
<button class="detail-tab" data-tab="plan" onclick="switchLiteDetailTab('plan')">
<span class="tab-icon">📐</span>
<span class="tab-text">Plan</span>
</button>
<button class="detail-tab" data-tab="context" onclick="switchLiteDetailTab('context')">
<span class="tab-icon">📦</span>
<span class="tab-text">Context</span>
</button>
<button class="detail-tab" data-tab="summary" onclick="switchLiteDetailTab('summary')">
<span class="tab-icon">📝</span>
<span class="tab-text">Summary</span>
</button>
</div>
<!-- Tab Content -->
<div class="detail-tab-content" id="liteDetailTabContent">
${renderLiteTasksTab(session, tasks, completed, inProgress, pending)}
</div>
</div>
`;
// Initialize collapsible sections
setTimeout(() => {
document.querySelectorAll('.collapsible-header').forEach(header => {
header.addEventListener('click', () => toggleSection(header));
});
}, 50);
}
function goBackToLiteTasks() {
currentView = 'liteTasks';
currentSessionDetailKey = null;
updateContentTitle();
renderLiteTasks();
}
function switchLiteDetailTab(tabName) {
// Update active tab
document.querySelectorAll('.detail-tab').forEach(tab => {
tab.classList.toggle('active', tab.dataset.tab === tabName);
});
const session = liteTaskDataStore[currentSessionDetailKey];
if (!session) return;
const contentArea = document.getElementById('liteDetailTabContent');
const tasks = session.tasks || [];
const completed = tasks.filter(t => t.status === 'completed').length;
const inProgress = tasks.filter(t => t.status === 'in_progress').length;
const pending = tasks.filter(t => t.status === 'pending').length;
switch (tabName) {
case 'tasks':
contentArea.innerHTML = renderLiteTasksTab(session, tasks, completed, inProgress, pending);
// Re-initialize collapsible sections
setTimeout(() => {
document.querySelectorAll('.collapsible-header').forEach(header => {
header.addEventListener('click', () => toggleSection(header));
});
}, 50);
break;
case 'plan':
contentArea.innerHTML = renderLitePlanTab(session);
break;
case 'context':
loadAndRenderLiteContextTab(session, contentArea);
break;
case 'summary':
loadAndRenderLiteSummaryTab(session, contentArea);
break;
}
}
function renderLiteTasksTab(session, tasks, completed, inProgress, pending) {
// Populate drawer tasks for click-to-open functionality
currentDrawerTasks = tasks;
if (tasks.length === 0) {
return `
<div class="tab-empty-state">
<div class="empty-icon">📋</div>
<div class="empty-title">No Tasks</div>
<div class="empty-text">This session has no tasks defined.</div>
</div>
`;
}
return `
<div class="tasks-tab-content">
<div class="tasks-list" id="liteTasksListContent">
${tasks.map(task => renderLiteTaskDetailItem(session.id, task)).join('')}
</div>
</div>
`;
}
function renderLiteTaskDetailItem(sessionId, task) {
const rawTask = task._raw || task;
const taskJsonId = `task-json-${sessionId}-${task.id}`.replace(/[^a-zA-Z0-9-]/g, '-');
taskJsonStore[taskJsonId] = rawTask;
// Get preview info for lite tasks
const action = rawTask.action || '';
const scope = rawTask.scope || '';
const modCount = rawTask.modification_points?.length || 0;
const implCount = rawTask.implementation?.length || 0;
const acceptCount = rawTask.acceptance?.length || 0;
return `
<div class="detail-task-item-full lite-task-item" onclick="openTaskDrawerForLite('${sessionId}', '${escapeHtml(task.id)}')" style="cursor: pointer;" title="Click to view details">
<div class="task-item-header-lite">
<span class="task-id-badge">${escapeHtml(task.id)}</span>
<span class="task-title">${escapeHtml(task.title || 'Untitled')}</span>
<button class="btn-view-json" onclick="event.stopPropagation(); showJsonModal('${taskJsonId}', '${escapeHtml(task.id)}')">{ } JSON</button>
</div>
<div class="task-item-meta-lite">
${action ? `<span class="meta-badge action">${escapeHtml(action)}</span>` : ''}
${scope ? `<span class="meta-badge scope">${escapeHtml(scope)}</span>` : ''}
${modCount > 0 ? `<span class="meta-badge mods">${modCount} mods</span>` : ''}
${implCount > 0 ? `<span class="meta-badge impl">${implCount} steps</span>` : ''}
${acceptCount > 0 ? `<span class="meta-badge accept">${acceptCount} acceptance</span>` : ''}
</div>
</div>
`;
}
function getMetaPreviewForLite(task, rawTask) {
const meta = task.meta || {};
const parts = [];
if (meta.type || rawTask.action) parts.push(meta.type || rawTask.action);
if (meta.scope || rawTask.scope) parts.push(meta.scope || rawTask.scope);
return parts.join(' | ') || 'No meta';
}
function openTaskDrawerForLite(sessionId, taskId) {
const session = liteTaskDataStore[currentSessionDetailKey];
if (!session) return;
const task = session.tasks?.find(t => t.id === taskId);
if (!task) return;
// Set current drawer tasks and session context
currentDrawerTasks = session.tasks || [];
window._currentDrawerSession = session;
document.getElementById('drawerTaskTitle').textContent = task.title || taskId;
// Use dedicated lite task drawer renderer
document.getElementById('drawerContent').innerHTML = renderLiteTaskDrawerContent(task, session);
document.getElementById('taskDetailDrawer').classList.add('open');
document.getElementById('drawerOverlay').classList.add('active');
}
function renderLitePlanTab(session) {
const plan = session.plan;
if (!plan) {
return `
<div class="tab-empty-state">
<div class="empty-icon">📐</div>
<div class="empty-title">No Plan Data</div>
<div class="empty-text">No plan.json found for this session.</div>
</div>
`;
}
return `
<div class="plan-tab-content">
<!-- Summary -->
${plan.summary ? `
<div class="plan-section">
<h4 class="plan-section-title">📋 Summary</h4>
<p class="plan-summary-text">${escapeHtml(plan.summary)}</p>
</div>
` : ''}
<!-- Approach -->
${plan.approach ? `
<div class="plan-section">
<h4 class="plan-section-title">🎯 Approach</h4>
<p class="plan-approach-text">${escapeHtml(plan.approach)}</p>
</div>
` : ''}
<!-- Focus Paths -->
${plan.focus_paths?.length ? `
<div class="plan-section">
<h4 class="plan-section-title">📁 Focus Paths</h4>
<div class="path-tags">
${plan.focus_paths.map(p => `<span class="path-tag">${escapeHtml(p)}</span>`).join('')}
</div>
</div>
` : ''}
<!-- Metadata -->
<div class="plan-section">
<h4 class="plan-section-title"> Metadata</h4>
<div class="plan-meta-grid">
${plan.estimated_time ? `<div class="meta-item"><span class="meta-label">Estimated Time:</span> ${escapeHtml(plan.estimated_time)}</div>` : ''}
${plan.complexity ? `<div class="meta-item"><span class="meta-label">Complexity:</span> ${escapeHtml(plan.complexity)}</div>` : ''}
${plan.recommended_execution ? `<div class="meta-item"><span class="meta-label">Execution:</span> ${escapeHtml(plan.recommended_execution)}</div>` : ''}
</div>
</div>
<!-- Raw JSON -->
<div class="plan-section">
<h4 class="plan-section-title">{ } Raw JSON</h4>
<pre class="json-content">${escapeHtml(JSON.stringify(plan, null, 2))}</pre>
</div>
</div>
`;
}
async function loadAndRenderLiteContextTab(session, contentArea) {
contentArea.innerHTML = '<div class="tab-loading">Loading context data...</div>';
try {
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 = renderLiteContextContent(data.context, session);
return;
}
}
// Fallback: show plan context if available
contentArea.innerHTML = renderLiteContextContent(null, session);
} catch (err) {
contentArea.innerHTML = `<div class="tab-error">Failed to load context: ${err.message}</div>`;
}
}
async function loadAndRenderLiteSummaryTab(session, contentArea) {
contentArea.innerHTML = '<div class="tab-loading">Loading summaries...</div>';
try {
if (window.SERVER_MODE && session.path) {
const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=summary`);
if (response.ok) {
const data = await response.json();
contentArea.innerHTML = renderSummaryContent(data.summaries);
return;
}
}
// Fallback
contentArea.innerHTML = `
<div class="tab-empty-state">
<div class="empty-icon">📝</div>
<div class="empty-title">No Summaries</div>
<div class="empty-text">No summaries found in .summaries/</div>
</div>
`;
} catch (err) {
contentArea.innerHTML = `<div class="tab-error">Failed to load summaries: ${err.message}</div>`;
}
}
// ============================================
// FIX SESSION VIEW
// ============================================
// Fix session detail page rendering
function renderFixSessionDetailPage(session) {
const isActive = session._isActive !== false;
const tasks = session.tasks || [];
// Calculate fix statistics
const totalTasks = tasks.length;
const fixedCount = tasks.filter(t => t.status === 'completed' && t.result === 'fixed').length;
const failedCount = tasks.filter(t => t.status === 'completed' && t.result === 'failed').length;
const pendingCount = tasks.filter(t => t.status === 'pending').length;
const inProgressCount = tasks.filter(t => t.status === 'in_progress').length;
const percentComplete = totalTasks > 0 ? ((fixedCount + failedCount) / totalTasks * 100) : 0;
return `
<div class="session-detail-page session-type-fix">
<!-- Header -->
<div class="detail-header">
<button class="btn-back" onclick="goBackToSessions()">
<span class="back-icon">←</span>
<span>Back to Sessions</span>
</button>
<div class="detail-title-row">
<h2 class="detail-session-id">🔧 ${escapeHtml(session.session_id)}</h2>
<div class="detail-badges">
<span class="session-type-badge test-fix">Fix</span>
<span class="session-status ${isActive ? 'active' : 'archived'}">
${isActive ? 'ACTIVE' : 'ARCHIVED'}
</span>
</div>
</div>
</div>
<!-- Fix Progress Section -->
<div class="fix-progress-section">
<div class="fix-progress-header">
<h3>🔧 Fix Progress</h3>
<span class="phase-badge ${session.phase || 'execution'}">${session.phase || 'Execution'}</span>
</div>
<!-- Progress Bar -->
<div class="fix-progress-bar">
<div class="fix-progress-bar-fill" style="width: ${percentComplete}%"></div>
</div>
<div class="progress-text">
<strong>${fixedCount + failedCount}/${totalTasks}</strong> completed (${percentComplete.toFixed(1)}%)
</div>
<!-- Summary Cards -->
<div class="fix-summary-grid">
<div class="summary-card">
<div class="summary-icon">📊</div>
<div class="summary-value">${totalTasks}</div>
<div class="summary-label">Total Tasks</div>
</div>
<div class="summary-card fixed">
<div class="summary-icon">✅</div>
<div class="summary-value">${fixedCount}</div>
<div class="summary-label">Fixed</div>
</div>
<div class="summary-card failed">
<div class="summary-icon">❌</div>
<div class="summary-value">${failedCount}</div>
<div class="summary-label">Failed</div>
</div>
<div class="summary-card pending">
<div class="summary-icon">⏳</div>
<div class="summary-value">${pendingCount}</div>
<div class="summary-label">Pending</div>
</div>
</div>
<!-- Stage Timeline (if available) -->
${session.stages && session.stages.length > 0 ? `
<div class="stage-timeline">
${session.stages.map((stage, idx) => `
<div class="stage-item ${stage.status || 'pending'}">
<div class="stage-number">Stage ${idx + 1}</div>
<div class="stage-mode">${stage.execution_mode === 'parallel' ? '⚡ Parallel' : '➡️ Serial'}</div>
<div class="stage-groups">${stage.groups?.length || 0} groups</div>
</div>
`).join('')}
</div>
` : ''}
</div>
<!-- Fix Tasks Grid -->
<div class="fix-tasks-section">
<div class="tasks-header">
<h3>📋 Fix Tasks</h3>
<div class="task-filters">
<button class="filter-btn active" data-status="all" onclick="filterFixTasks('all')">All</button>
<button class="filter-btn" data-status="pending" onclick="filterFixTasks('pending')">Pending</button>
<button class="filter-btn" data-status="in_progress" onclick="filterFixTasks('in_progress')">In Progress</button>
<button class="filter-btn" data-status="fixed" onclick="filterFixTasks('fixed')">Fixed</button>
<button class="filter-btn" data-status="failed" onclick="filterFixTasks('failed')">Failed</button>
</div>
</div>
<div class="fix-tasks-grid" id="fixTasksGrid">
${renderFixTasksGrid(tasks)}
</div>
</div>
<!-- Session Info -->
<div class="detail-info-bar">
<div class="info-item">
<span class="info-label">Created:</span>
<span class="info-value">${formatDate(session.created_at)}</span>
</div>
${session.archived_at ? `
<div class="info-item">
<span class="info-label">Archived:</span>
<span class="info-value">${formatDate(session.archived_at)}</span>
</div>
` : ''}
<div class="info-item">
<span class="info-label">Project:</span>
<span class="info-value">${escapeHtml(session.project || '-')}</span>
</div>
</div>
</div>
`;
}
function renderFixTasksGrid(tasks) {
if (!tasks || tasks.length === 0) {
return `
<div class="empty-state">
<div class="empty-icon">📋</div>
<div class="empty-text">No fix tasks found</div>
</div>
`;
}
return tasks.map(task => {
const statusClass = task.status === 'completed' ? (task.result || 'completed') : task.status;
const statusText = task.status === 'completed' ? (task.result || 'completed') : task.status;
return `
<div class="fix-task-card status-${statusClass}" data-status="${statusClass}">
<div class="task-card-header">
<span class="task-id-badge">${escapeHtml(task.task_id || task.id || 'N/A')}</span>
<span class="task-status-badge ${statusClass}">${statusText}</span>
</div>
<div class="task-card-title">${escapeHtml(task.title || 'Untitled Task')}</div>
${task.finding_title ? `<div class="task-finding">${escapeHtml(task.finding_title)}</div>` : ''}
${task.file ? `<div class="task-file">📄 ${escapeHtml(task.file)}${task.line ? ':' + task.line : ''}</div>` : ''}
<div class="task-card-meta">
${task.dimension ? `<span class="task-dimension">${escapeHtml(task.dimension)}</span>` : ''}
${task.attempts && task.attempts > 1 ? `<span class="task-attempts">🔄 ${task.attempts} attempts</span>` : ''}
${task.commit_hash ? `<span class="task-commit">💾 ${task.commit_hash.substring(0, 7)}</span>` : ''}
</div>
</div>
`;
}).join('');
}
function initFixSessionPage(session) {
// Initialize event handlers for fix session page
// Filter handlers are inline onclick
}
function filterFixTasks(status) {
// Update filter buttons
document.querySelectorAll('.task-filters .filter-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.status === status);
});
// Filter task cards
document.querySelectorAll('.fix-task-card').forEach(card => {
if (status === 'all' || card.dataset.status === status) {
card.style.display = '';
} else {
card.style.display = 'none';
}
});
}
// Application Entry Point
// Initializes all components and sets up global event handlers
document.addEventListener('DOMContentLoaded', async () => {
// Initialize components
initTheme();
initSidebar();
initPathSelector();
initNavigation();
initSearch();
// Server mode: load data from API
if (window.SERVER_MODE) {
await switchToPath(window.INITIAL_PATH || projectPath);
} else {
renderDashboard();
}
// Global Escape key handler for modals
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeMarkdownModal();
// Close JSON modal if exists
const jsonModal = document.querySelector('.json-modal-overlay');
if (jsonModal) {
const closeBtn = jsonModal.querySelector('.json-modal-close');
if (closeBtn) closeJsonModal(closeBtn);
}
// Close path modal if exists
closePathModal();
}
});
});
</script>
</body>
</html>