mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
9087 lines
254 KiB
HTML
9087 lines
254 KiB
HTML
<!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()">×</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()">×</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, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
/**
|
||
* 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)">×</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)">×</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>
|