diff --git a/.beads/bd.sock.startlock b/.beads/bd.sock.startlock index 6bc3942..0066761 100644 --- a/.beads/bd.sock.startlock +++ b/.beads/bd.sock.startlock @@ -1 +1 @@ -97492 +117304 diff --git a/src/components/shared/left-panel.tsx b/src/components/shared/left-panel.tsx index b7a5d81..428b109 100644 --- a/src/components/shared/left-panel.tsx +++ b/src/components/shared/left-panel.tsx @@ -14,6 +14,15 @@ export interface LeftPanelProps { interface EpicNode { epic: BeadIssue; children: BeadIssue[]; + stats: { + total: number; + closed: number; + in_progress: number; + blocked: number; + ready: number; + lastActivity: number; + }; + status: 'blocked' | 'in_progress' | 'ready' | 'done' | 'empty'; } function buildEpicTree(issues: BeadIssue[]): EpicNode[] { @@ -21,7 +30,12 @@ function buildEpicTree(issues: BeadIssue[]): EpicNode[] { const epicMap = new Map(); for (const epic of epics) { - epicMap.set(epic.id, { epic, children: [] }); + epicMap.set(epic.id, { + epic, + children: [], + stats: { total: 0, closed: 0, in_progress: 0, blocked: 0, ready: 0, lastActivity: new Date(epic.updated_at).getTime() }, + status: 'empty' + }); } for (const issue of issues) { @@ -29,13 +43,48 @@ function buildEpicTree(issues: BeadIssue[]): EpicNode[] { const parentDep = issue.dependencies.find(dep => dep.type === 'parent'); if (parentDep && epicMap.has(parentDep.target)) { - epicMap.get(parentDep.target)!.children.push(issue); + const node = epicMap.get(parentDep.target)!; + node.children.push(issue); + + node.stats.total++; + if (issue.status === 'closed') node.stats.closed++; + else if (issue.status === 'blocked') node.stats.blocked++; + else if (issue.status === 'in_progress') node.stats.in_progress++; + else node.stats.ready++; // open/ready + + const issueTime = new Date(issue.updated_at).getTime(); + if (issueTime > node.stats.lastActivity) node.stats.lastActivity = issueTime; } } - return Array.from(epicMap.values()).sort((a, b) => - a.epic.id.localeCompare(b.epic.id) - ); + // Determine Aggregate Status + for (const node of epicMap.values()) { + if (node.stats.blocked > 0) node.status = 'blocked'; + else if (node.stats.in_progress > 0) node.status = 'in_progress'; + else if (node.stats.ready > 0) node.status = 'ready'; + else if (node.stats.total > 0 && node.stats.closed === node.stats.total) node.status = 'done'; + else node.status = 'empty'; + } + + return Array.from(epicMap.values()).sort((a, b) => { + // Sort by status priority (Blocked > In Progress > Ready > Done) then Recency + const statusScore = { blocked: 4, in_progress: 3, ready: 2, done: 1, empty: 0 }; + const scoreDiff = statusScore[b.status] - statusScore[a.status]; + if (scoreDiff !== 0) return scoreDiff; + return b.stats.lastActivity - a.stats.lastActivity; + }); +} + +function StatusIndicator({ status }: { status: string }) { + const styles = { + blocked: 'bg-rose-500 shadow-[0_0_8px_#f43f5e]', + in_progress: 'bg-amber-500 shadow-[0_0_8px_#f59e0b]', + ready: 'bg-teal-500 shadow-[0_0_8px_#14b8a6]', + done: 'bg-slate-500', + empty: 'bg-white/10' + }[status] || 'bg-slate-500'; + + return
; } export function LeftPanel({ @@ -61,28 +110,25 @@ export function LeftPanel({ }; const handleEpicClick = (epicId: string) => { - onEpicSelect?.(epicId === selectedEpicId ? null : epicId); // Toggle selection + onEpicSelect?.(epicId === selectedEpicId ? null : epicId); toggleEpic(epicId); }; if (isTablet) { return ( -
- {epicTree.map(({ epic }) => ( +
+ {epicTree.map(({ epic, status }) => ( @@ -97,103 +143,117 @@ export function LeftPanel({ 'flex flex-col h-full overflow-hidden transition-all duration-300', !isDesktop && 'hidden lg:flex' )} - style={{ - width: '18rem', // Wider panel - }} + style={{ width: '20rem' }} data-testid="left-panel" > -
+
{/* Header */} -
- Channels - {epicTree.length} +
+ Workstreams +
+ {epicTree.length} ACTIVE +
{/* Tree */} -
- {epicTree.map(({ epic, children }) => { +
+ {epicTree.map(({ epic, children, stats, status }) => { const isExpanded = expandedEpics.has(epic.id); const isSelected = selectedEpicId === epic.id; - const childCount = children.length; + + // Dynamic Styling based on Status + const statusStyle = { + blocked: 'border-rose-500/30 bg-rose-500/5 hover:bg-rose-500/10', + in_progress: 'border-amber-500/30 bg-amber-500/5 hover:bg-amber-500/10', + ready: 'border-teal-500/30 bg-teal-500/5 hover:bg-teal-500/10', + done: 'border-white/5 bg-white/[0.02] opacity-60', + empty: 'border-white/5 bg-transparent opacity-40' + }[status]; + + const activeStyle = isSelected ? 'ring-1 ring-white/20 shadow-lg scale-[1.02]' : ''; return ( -
+
- {/* Sub-items (Agents/Tasks usually, but here listed as children) */} - {isExpanded && childCount > 0 && ( -
+ {/* Sub-items (Tasks) */} + {isExpanded && children.length > 0 && ( +
{children.slice(0, 5).map(child => (
- {child.title} + {child.id} +
+ {child.title} + +
))} - {childCount > 5 && ( -
- +{childCount - 5} more -
+ {children.length > 5 && ( +
+{children.length - 5} more
)}
)}
); })} - - {epicTree.length === 0 && ( -
-
📡
-

NO_CHANNELS

-
- )}
- {/* Footer Scope */} + {/* Footer */}
-