feat(bd-1mh): Add DependencyDag component to web frontend

- Create DependencyDag.tsx with interactive task dependency visualization
- Add DAG types to web frontend types
- Add /api/dag endpoint to server.ts
- Add CSS styles for DAG panel
- Add unit tests

Co-Authored-By: Claude Worker <noreply@anthropic.com>
This commit is contained in:
jeda 2026-03-03 15:13:13 +00:00
parent 68104053a6
commit ccbe8e7a36
6 changed files with 1908 additions and 3 deletions

View file

@ -5,7 +5,7 @@
{"id":"bd-195","title":"ALT-007: SQLite direct query fallback","description":"For HUMAN bead bd-3sh. Query beads.db directly using sqlite3 or Node.js better-sqlite3. Bypasses br CLI entirely. Requires sqlite3 CLI or npm package. Fastest access but tight coupling to schema.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-03-03T08:39:58.775979286Z","created_by":"coder","updated_at":"2026-03-03T10:33:32.997760049Z","closed_at":"2026-03-03T10:33:31.799597115Z","close_reason":"done","source_repo":".","compaction_level":0,"original_size":0,"labels":["alternative","br","resilience","worker"],"comments":[{"id":32,"issue_id":"bd-195","author":"Jed Arden","text":"No longer needed - br v0.1.20 fixes the schema bug natively.","created_at":"2026-03-03T10:33:32Z"}]}
{"id":"bd-1a2","title":"P2: Add unit tests for parser.ts","description":"Add comprehensive unit tests for src/parser.ts covering: JSON parsing, formatEvent function, edge cases (malformed JSON, missing fields), and colorization options. Follow vitest patterns from tailer.test.ts.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-03-03T07:50:12.670624516Z","created_by":"coder","updated_at":"2026-03-03T10:45:42.655993370Z","closed_at":"2026-03-03T10:45:42.654557737Z","close_reason":"Parser tests already implemented in bd-5eh (36 tests covering parseLogLine, parseLogLines, formatEvent)","source_repo":".","compaction_level":0,"original_size":0,"labels":["parser","testing","unit-test"]}
{"id":"bd-1c6","title":"TEST-002: Add store integration tests","description":"Test Coverage: Add integration tests for EventStore - event indexing, LRU eviction, worker tracking, query performance.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-03-03T07:53:40.409846186Z","created_by":"coder","updated_at":"2026-03-03T07:53:40.409846186Z","closed_at":"2026-03-03T07:53:40.409846186Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["store","testing"]}
{"id":"bd-1cc","title":"Port FileHeatmap component to web dashboard","description":"Port the TUI FileHeatmap component (src/tui/components/FileHeatmap.ts) to React for the web dashboard. Add corresponding API endpoint if needed.","status":"in_progress","priority":3,"issue_type":"task","assignee":"coder","created_at":"2026-03-03T14:27:43.759301428Z","created_by":"coder","updated_at":"2026-03-03T14:58:30.513770688Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["frontend","phase-4","web"]}
{"id":"bd-1cc","title":"Port FileHeatmap component to web dashboard","description":"Port the TUI FileHeatmap component (src/tui/components/FileHeatmap.ts) to React for the web dashboard. Add corresponding API endpoint if needed.","status":"closed","priority":3,"issue_type":"task","assignee":"coder","created_at":"2026-03-03T14:27:43.759301428Z","created_by":"coder","updated_at":"2026-03-03T15:08:13.822190937Z","closed_at":"2026-03-03T15:08:13.792085291Z","close_reason":"done","source_repo":".","compaction_level":0,"original_size":0,"labels":["frontend","phase-4","web"]}
{"id":"bd-1e1","title":"P3-001: Setup Express HTTP server with static file serving","description":"Phase 3 Web Dashboard: Create Express server in src/web/server.ts that serves static files and handles WebSocket upgrade. Foundation for web dashboard.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-03-03T07:52:09.228852666Z","created_by":"coder","updated_at":"2026-03-03T10:05:21.171663977Z","closed_at":"2026-03-03T10:05:21.171457522Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["phase-3","server","web"]}
{"id":"bd-1fe","title":"Add RecoveryPanel component to web frontend","description":"Port TUI RecoveryPanel.ts to React web frontend. Create src/web/frontend/src/components/RecoveryPanel.tsx showing recovery suggestions for stuck workers.","status":"open","priority":2,"issue_type":"task","created_at":"2026-03-03T14:26:22.464306236Z","created_by":"coder","updated_at":"2026-03-03T14:26:22.464306236Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["frontend","phase-3","web"]}
{"id":"bd-1fk","title":"P1: Add Express HTTP server for web dashboard","description":"Set up Express.js HTTP server with basic routing for web dashboard. Should serve static files and provide API endpoints for worker data. Part of Phase 3 Web Dashboard.","status":"closed","priority":1,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-03-03T07:50:12.280655428Z","created_by":"coder","updated_at":"2026-03-03T09:37:50.271173474Z","closed_at":"2026-03-03T08:51:00.693337164Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["http","phase-3","server","web"],"comments":[{"id":10,"issue_id":"bd-1fk","author":"Jed Arden","text":"Express server implemented in src/web/server.ts","created_at":"2026-03-03T08:52:43Z"}]}
@ -18,7 +18,7 @@
{"id":"bd-1k7","title":"ALERT: Worker claude-code-glm-5-bravo has no work available","description":"# Worker Starvation Alert\n\nWorker **claude-code-glm-5-bravo** has exhausted all priorities and found zero work.\n\nThis is considered an error state - there should always be more work.\n\n## Worker State\n\n- **Executor:** claude-code-glm-5\n- **Model:** glm-5\n- **Workspace:** /home/coder/FABRIC\n- **Root Boundary:** /home/coder/FABRIC\n- **Last completion:** \n- **Beads completed:** 0\n- **Claim success rate:** %\n- **Uptime:** 23231s (h)\n- **Consecutive empty iterations:** 5\n\n## Priorities Exhausted\n\n1. ✗ Local workspace (bottoms-up): No beads in /home/coder/FABRIC or subfolders\n2. ✗ Parent exploration: No suitable workspaces found\n3. ✓ Maintenance: Completed (cleaned orphaned claims/locks)\n4. ✗ Gap analysis: false - No gaps found or created\n5. ✗ HUMAN alternatives: true - No HUMAN beads found to unblock\n\n## Discovered Workspaces\n\nTotal: 1\n\n- /home/coder/FABRIC\n\n## Required Actions\n\n1. Review discovery roots: Are all project folders being scanned?\n2. Check if projects need new features/tasks\n3. Review ROADMAP.md files across projects\n4. Enable gap analysis if disabled: `--enable-gap-analysis`\n5. Enable HUMAN alternatives if disabled\n6. Create manual beads to bootstrap work\n\n---\n*This alert was created automatically by Priority 6*","status":"closed","priority":0,"issue_type":"human","assignee":"coder","created_at":"2026-03-03T10:50:43.655296543Z","created_by":"coder","updated_at":"2026-03-03T10:54:08.648495859Z","closed_at":"2026-03-03T10:54:08.646738418Z","close_reason":"FALSE POSITIVE: Worker starvation alert triggered incorrectly. Ready queue has 22 beads available (ready-queue.json). The br ready command shows epic bd-2kf and other work exists. Worker discovery logic should check ready-queue.json directly before creating starvation alerts. This follows the established pattern for false-positive starvation alerts.","source_repo":".","compaction_level":0,"original_size":0}
{"id":"bd-1l5","title":"ALERT: Worker claude-code-glm-5-bravo has no work available","description":"# Worker Starvation Alert\n\nWorker **claude-code-glm-5-bravo** has exhausted all priorities and found zero work.\n\nThis is considered an error state - there should always be more work.\n\n## Worker State\n\n- **Executor:** claude-code-glm-5\n- **Model:** glm-5\n- **Workspace:** /home/coder/FABRIC\n- **Root Boundary:** /home/coder/FABRIC\n- **Last completion:** \n- **Beads completed:** 0\n- **Claim success rate:** %\n- **Uptime:** 30662s (h)\n- **Consecutive empty iterations:** 5\n\n## Priorities Exhausted\n\n1. ✗ Local workspace (bottoms-up): No beads in /home/coder/FABRIC or subfolders\n2. ✗ Parent exploration: No suitable workspaces found\n3. ✓ Maintenance: Completed (cleaned orphaned claims/locks)\n4. ✗ Gap analysis: false - No gaps found or created\n5. ✗ HUMAN alternatives: true - No HUMAN beads found to unblock\n\n## Discovered Workspaces\n\nTotal: 1\n\n- /home/coder/FABRIC\n\n## Required Actions\n\n1. Review discovery roots: Are all project folders being scanned?\n2. Check if projects need new features/tasks\n3. Review ROADMAP.md files across projects\n4. Enable gap analysis if disabled: `--enable-gap-analysis`\n5. Enable HUMAN alternatives if disabled\n6. Create manual beads to bootstrap work\n\n---\n*This alert was created automatically by Priority 6*","status":"closed","priority":0,"issue_type":"human","assignee":"coder","created_at":"2026-03-03T12:54:35.115007190Z","created_by":"coder","updated_at":"2026-03-03T12:55:34.048020006Z","closed_at":"2026-03-03T12:55:34.034977802Z","close_reason":"FALSE POSITIVE: Ready queue has 22 available beads. Worker did not check ready-queue.json before escalating starvation alert. See MEMORY.md pattern.","source_repo":".","compaction_level":0,"original_size":0}
{"id":"bd-1lc","title":"ALERT: Worker claude-code-glm-5-bravo has no work available","description":"# Worker Starvation Alert\n\nWorker **claude-code-glm-5-bravo** has exhausted all priorities and found zero work.\n\nThis is considered an error state - there should always be more work.\n\n## Worker State\n\n- **Executor:** claude-code-glm-5\n- **Model:** glm-5\n- **Workspace:** /home/coder/FABRIC\n- **Root Boundary:** /home/coder/FABRIC\n- **Last completion:** \n- **Beads completed:** 0\n- **Claim success rate:** %\n- **Uptime:** 19477s (h)\n- **Consecutive empty iterations:** 5\n\n## Priorities Exhausted\n\n1. ✗ Local workspace (bottoms-up): No beads in /home/coder/FABRIC or subfolders\n2. ✗ Parent exploration: No suitable workspaces found\n3. ✓ Maintenance: Completed (cleaned orphaned claims/locks)\n4. ✗ Gap analysis: false - No gaps found or created\n5. ✗ HUMAN alternatives: true - No HUMAN beads found to unblock\n\n## Discovered Workspaces\n\nTotal: 1\n\n- /home/coder/FABRIC\n\n## Required Actions\n\n1. Review discovery roots: Are all project folders being scanned?\n2. Check if projects need new features/tasks\n3. Review ROADMAP.md files across projects\n4. Enable gap analysis if disabled: `--enable-gap-analysis`\n5. Enable HUMAN alternatives if disabled\n6. Create manual beads to bootstrap work\n\n---\n*This alert was created automatically by Priority 6*","status":"closed","priority":0,"issue_type":"human","created_at":"2026-03-03T09:48:10.053171230Z","created_by":"coder","updated_at":"2026-03-03T09:55:17.989495128Z","closed_at":"2026-03-03T09:55:07.746952209Z","source_repo":".","compaction_level":0,"original_size":0,"comments":[{"id":20,"issue_id":"bd-1lc","author":"Jed Arden","text":"FALSE POSITIVE: ready-queue.json contains 22 available beads. Worker discovery failed to check ready queue before escalating. Closed per Worker Starvation Resolution pattern.","created_at":"2026-03-03T09:55:17Z"}]}
{"id":"bd-1mh","title":"Add DependencyDag component to web frontend","description":"Port TUI DependencyDag.ts to React web frontend. Create src/web/frontend/src/components/DependencyDag.tsx with interactive task dependency graph visualization.","status":"open","priority":2,"issue_type":"task","created_at":"2026-03-03T14:26:17.941694993Z","created_by":"coder","updated_at":"2026-03-03T14:26:17.941694993Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["frontend","phase-3","web"]}
{"id":"bd-1mh","title":"Add DependencyDag component to web frontend","description":"Port TUI DependencyDag.ts to React web frontend. Create src/web/frontend/src/components/DependencyDag.tsx with interactive task dependency graph visualization.","status":"in_progress","priority":2,"issue_type":"task","assignee":"coder","created_at":"2026-03-03T14:26:17.941694993Z","created_by":"coder","updated_at":"2026-03-03T15:00:57.556080545Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["frontend","phase-3","web"]}
{"id":"bd-1mq","title":"ALT-003: Use br list JSON instead of br ready","description":"For HUMAN bd-1sw. Create a wrapper script that uses br list --all --format json (which works) to find available work instead of br ready (which has schema bug). Script created at scripts/br-ready-jsonl.sh. Drop-in replacement for br ready command.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-03-03T08:30:23.972858496Z","created_by":"coder","updated_at":"2026-03-03T09:59:56.375702385Z","closed_at":"2026-03-03T09:59:56.375449151Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["alternative","discovery","worker"],"dependencies":[{"issue_id":"bd-1mq","depends_on_id":"bd-1sw","type":"blocks","created_at":"2026-03-03T08:30:48.335086821Z","created_by":"coder","metadata":"{}","thread_id":""}],"comments":[{"id":23,"issue_id":"bd-1mq","author":"Jed Arden","text":"Implementation verified working. Script outputs 17 available beads correctly in both JSON and table formats.","created_at":"2026-03-03T09:59:56Z"}]}
{"id":"bd-1ms","title":"ALERT: Worker claude-code-glm-5-bravo has no work available","description":"# Worker Starvation Alert\n\nWorker **claude-code-glm-5-bravo** has exhausted all priorities and found zero work.\n\nThis is considered an error state - there should always be more work.\n\n## Worker State\n\n- **Executor:** claude-code-glm-5\n- **Model:** glm-5\n- **Workspace:** /home/coder/FABRIC\n- **Root Boundary:** /home/coder/FABRIC\n- **Last completion:** \n- **Beads completed:** 0\n- **Claim success rate:** %\n- **Uptime:** 19113s (h)\n- **Consecutive empty iterations:** 5\n\n## Priorities Exhausted\n\n1. ✗ Local workspace (bottoms-up): No beads in /home/coder/FABRIC or subfolders\n2. ✗ Parent exploration: No suitable workspaces found\n3. ✓ Maintenance: Completed (cleaned orphaned claims/locks)\n4. ✗ Gap analysis: false - No gaps found or created\n5. ✗ HUMAN alternatives: true - No HUMAN beads found to unblock\n\n## Discovered Workspaces\n\nTotal: 1\n\n- /home/coder/FABRIC\n\n## Required Actions\n\n1. Review discovery roots: Are all project folders being scanned?\n2. Check if projects need new features/tasks\n3. Review ROADMAP.md files across projects\n4. Enable gap analysis if disabled: `--enable-gap-analysis`\n5. Enable HUMAN alternatives if disabled\n6. Create manual beads to bootstrap work\n\n---\n*This alert was created automatically by Priority 6*","status":"closed","priority":0,"issue_type":"human","created_at":"2026-03-03T09:42:05.483000034Z","created_by":"coder","updated_at":"2026-03-03T09:44:49.799179920Z","closed_at":"2026-03-03T09:44:49.798973717Z","source_repo":".","compaction_level":0,"original_size":0}
{"id":"bd-1of","title":"ALERT: Worker claude-code-glm-5-bravo has no work available","description":"# Worker Starvation Alert\n\nWorker **claude-code-glm-5-bravo** has exhausted all priorities and found zero work.\n\nThis is considered an error state - there should always be more work.\n\n## Worker State\n\n- **Executor:** claude-code-glm-5\n- **Model:** glm-5\n- **Workspace:** /home/coder/FABRIC\n- **Root Boundary:** /home/coder/FABRIC\n- **Last completion:** \n- **Beads completed:** 0\n- **Claim success rate:** %\n- **Uptime:** 17909s (h)\n- **Consecutive empty iterations:** 5\n\n## Priorities Exhausted\n\n1. ✗ Local workspace (bottoms-up): No beads in /home/coder/FABRIC or subfolders\n2. ✗ Parent exploration: No suitable workspaces found\n3. ✓ Maintenance: Completed (cleaned orphaned claims/locks)\n4. ✗ Gap analysis: false - No gaps found or created\n5. ✗ HUMAN alternatives: true - No HUMAN beads found to unblock\n\n## Discovered Workspaces\n\nTotal: 1\n\n- /home/coder/FABRIC\n\n## Required Actions\n\n1. Review discovery roots: Are all project folders being scanned?\n2. Check if projects need new features/tasks\n3. Review ROADMAP.md files across projects\n4. Enable gap analysis if disabled: `--enable-gap-analysis`\n5. Enable HUMAN alternatives if disabled\n6. Create manual beads to bootstrap work\n\n---\n*This alert was created automatically by Priority 6*","status":"closed","priority":0,"issue_type":"human","created_at":"2026-03-03T09:22:01.376456909Z","created_by":"coder","updated_at":"2026-03-03T09:23:18.070431328Z","closed_at":"2026-03-03T09:23:09.029612467Z","source_repo":".","compaction_level":0,"original_size":0,"comments":[{"id":18,"issue_id":"bd-1of","author":"Jed Arden","text":"FALSE POSITIVE - ready-queue.json has 22 available beads. Worker discovery is not checking ready-queue.json properly (known issue tracked in bd-b02). Available work: bd-2zt, bd-2ed, bd-1fk, bd-1sk, bd-2qr and 17 more.","created_at":"2026-03-03T09:23:18Z"}]}

View file

@ -0,0 +1,648 @@
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import {
DependencyGraph,
DagStats,
BeadNode,
BeadStatus,
DagOptions,
DagViewMode,
} from '../types';
interface DependencyDagProps {
visible: boolean;
onClose: () => void;
}
// Status icons and colors
const getStatusIcon = (status: BeadStatus): string => {
switch (status) {
case 'open': return '○';
case 'in_progress': return '◐';
case 'blocked': return '⛔';
case 'completed': return '●';
case 'closed': return '✓';
case 'deferred': return '⏸';
default: return '?';
}
};
const getStatusColor = (status: BeadStatus): string => {
switch (status) {
case 'open': return 'var(--text-secondary)';
case 'in_progress': return '#00bcd4';
case 'blocked': return 'var(--error)';
case 'completed': return 'var(--success)';
case 'closed': return 'var(--success)';
case 'deferred': return 'var(--warning)';
default: return 'var(--text-secondary)';
}
};
const getStatusClassName = (status: BeadStatus): string => {
switch (status) {
case 'open': return 'status-open';
case 'in_progress': return 'status-progress';
case 'blocked': return 'status-blocked';
case 'completed': return 'status-completed';
case 'closed': return 'status-closed';
case 'deferred': return 'status-deferred';
default: return 'status-unknown';
}
};
const getPriorityLabel = (priority: number): string => {
switch (priority) {
case 0: return 'P0';
case 1: return 'P1';
case 2: return 'P2';
case 3: return 'P3';
case 4: return 'P4';
default: return 'P?';
}
};
const getPriorityClassName = (priority: number): string => {
switch (priority) {
case 0: return 'priority-critical';
case 1: return 'priority-high';
case 2: return 'priority-normal';
case 3: return 'priority-low';
case 4: return 'priority-backlog';
default: return 'priority-unknown';
}
};
// Helper functions for graph analysis
const getTopBlockers = (graph: DependencyGraph | null, limit: number = 15): BeadNode[] => {
if (!graph) return [];
const allNodes: BeadNode[] = [];
for (const component of graph.components) {
allNodes.push(...component.nodes);
}
allNodes.sort((a, b) => b.dependentCount - a.dependentCount);
return allNodes.filter(n => n.dependentCount > 0).slice(0, limit);
};
const getReadyBeads = (graph: DependencyGraph | null): BeadNode[] => {
if (!graph) return [];
const ready: BeadNode[] = [];
for (const component of graph.components) {
for (const node of component.nodes) {
if (node.status === 'open' && node.dependencyCount === 0) {
ready.push(node);
}
}
}
ready.sort((a, b) => a.priority - b.priority);
return ready;
};
const DependencyDag: React.FC<DependencyDagProps> = ({ visible, onClose }) => {
const [graph, setGraph] = useState<DependencyGraph | null>(null);
const [stats, setStats] = useState<DagStats | null>(null);
const [viewMode, setViewMode] = useState<DagViewMode>('tree');
const [selectedIndex, setSelectedIndex] = useState(0);
const [filterOptions, setFilterOptions] = useState<DagOptions>({});
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [selectedBead, setSelectedBead] = useState<BeadNode | null>(null);
// Fetch dependency graph from API
const fetchGraph = useCallback(async () => {
setLoading(true);
setError(null);
try {
const params = new URLSearchParams();
if (filterOptions.status && filterOptions.status !== 'all') {
params.set('status', filterOptions.status);
}
if (filterOptions.criticalOnly) {
params.set('criticalOnly', 'true');
}
if (filterOptions.maxDepth !== undefined) {
params.set('maxDepth', filterOptions.maxDepth.toString());
}
if (filterOptions.includeClosed) {
params.set('includeClosed', 'true');
}
const response = await fetch(`/api/dag?${params.toString()}`);
if (!response.ok) {
throw new Error(`Failed to fetch DAG: ${response.statusText}`);
}
const data = await response.json();
setGraph(data.graph);
setStats(data.stats);
setSelectedIndex(0);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
}, [filterOptions]);
useEffect(() => {
if (visible) {
fetchGraph();
}
}, [visible, fetchGraph]);
// Get items based on view mode
const currentItems = useMemo(() => {
if (!graph) return [];
switch (viewMode) {
case 'blockers':
return getTopBlockers(graph);
case 'ready':
return getReadyBeads(graph);
default:
const allNodes: BeadNode[] = [];
for (const component of graph.components) {
allNodes.push(...component.nodes);
}
return allNodes;
}
}, [graph, viewMode]);
// Cycle filter options
const cycleFilter = useCallback(() => {
const filters: Array<{ key: keyof DagOptions; value: any }> = [
{ key: 'status', value: undefined },
{ key: 'status', value: 'blocked' as BeadStatus },
{ key: 'status', value: 'in_progress' as BeadStatus },
{ key: 'criticalOnly', value: true },
{ key: 'criticalOnly', value: false },
];
const currentIdx = filters.findIndex(
(f) =>
(f.key === 'status' && filterOptions.status === f.value) ||
(f.key === 'criticalOnly' && filterOptions.criticalOnly === f.value)
);
const nextIdx = (currentIdx + 1) % filters.length;
const nextFilter = filters[nextIdx];
setFilterOptions({ ...filterOptions, [nextFilter.key]: nextFilter.value });
}, [filterOptions]);
// Get filter description
const getFilterDescription = (): string => {
const parts: string[] = [];
if (filterOptions.status) {
parts.push(`status=${filterOptions.status}`);
}
if (filterOptions.criticalOnly) {
parts.push('critical-only');
}
if (filterOptions.maxDepth !== undefined) {
parts.push(`depth≤${filterOptions.maxDepth}`);
}
return parts.length > 0 ? ` [${parts.join(', ')}]` : '';
};
// Render tree node recursively
const renderTreeNode = (
node: BeadNode,
componentIndex: number,
depth: number,
isLast: boolean,
visited: Set<string>
): React.ReactNode => {
if (depth > 5) return null;
if (visited.has(node.id)) {
return (
<div key={`${node.id}-cycle`} className="dag-tree-node cycle" style={{ paddingLeft: depth * 16 }}>
{node.id} (cycle)
</div>
);
}
const component = graph?.components[componentIndex];
if (!component) return null;
const children = component.edges
.filter(e => e.from === node.id)
.map(e => component.nodes.find(n => n.id === e.to))
.filter((n): n is BeadNode => n !== undefined);
return (
<div key={node.id} className="dag-tree-node-wrapper">
<div
className={`dag-tree-node ${selectedBead?.id === node.id ? 'selected' : ''}`}
style={{ paddingLeft: depth * 16 }}
onClick={() => setSelectedBead(node)}
>
<span className="dag-tree-connector">{isLast ? '└─' : '├─'}</span>
<span className={`dag-status-icon ${getStatusClassName(node.status)}`}>
{getStatusIcon(node.status)}
</span>
<span className="dag-node-id" style={{ color: getStatusColor(node.status) }}>
{node.id}
</span>
<span className={`dag-priority ${getPriorityClassName(node.priority)}`}>
[{getPriorityLabel(node.priority)}]
</span>
{node.isCriticalPath && <span className="dag-critical-icon"></span>}
</div>
{children.map((child, i) =>
renderTreeNode(
child,
componentIndex,
depth + 1,
i === children.length - 1,
new Set([...visited, node.id])
)
)}
</div>
);
};
// Render tree view
const renderTreeView = (): React.ReactNode => {
if (!graph) return null;
if (graph.components.length === 0) {
return (
<div className="dag-empty">
<p>No dependencies found</p>
<p className="dag-empty-hint">Tasks with dependencies will appear here.</p>
</div>
);
}
return (
<div className="dag-tree-container">
{graph.components.map((component, componentIndex) => (
<div key={`component-${componentIndex}`} className="dag-component">
{component.hasCycle && (
<div className="dag-cycle-warning">
Cycle detected in this component!
</div>
)}
{component.criticalPath.length > 0 && (
<div className="dag-critical-path">
Critical path: {component.criticalPath.map((id, i) => (
<React.Fragment key={id}>
<span className="dag-critical-node">{id}</span>
{i < component.criticalPath.length - 1 && <span className="dag-arrow"></span>}
</React.Fragment>
))}
</div>
)}
<div className="dag-tree">
{component.roots.length > 0
? component.roots.map((rootId, i) => {
const rootNode = component.nodes.find(n => n.id === rootId);
if (!rootNode) return null;
return (
<React.Fragment key={rootId}>
{renderTreeNode(
rootNode,
componentIndex,
0,
i === component.roots.length - 1,
new Set()
)}
</React.Fragment>
);
})
: component.nodes.map((node, i) =>
renderTreeNode(node, componentIndex, 0, i === component.nodes.length - 1, new Set())
)}
</div>
</div>
))}
</div>
);
};
// Render blockers view
const renderBlockersView = (): React.ReactNode => {
const blockers = getTopBlockers(graph, 15);
if (blockers.length === 0) {
return (
<div className="dag-empty">
<p className="dag-success-text">No blockers found!</p>
<p>All tasks are unblocked.</p>
</div>
);
}
return (
<div className="dag-list-container">
<p className="dag-list-header">Tasks blocking the most other tasks:</p>
<div className="dag-list">
{blockers.map((node, i) => (
<div
key={node.id}
className={`dag-list-item ${selectedIndex === i ? 'selected' : ''}`}
onClick={() => {
setSelectedIndex(i);
setSelectedBead(node);
}}
>
<span className={`dag-status-icon ${getStatusClassName(node.status)}`}>
{getStatusIcon(node.status)}
</span>
<span className="dag-node-id" style={{ color: getStatusColor(node.status) }}>
{node.id}
</span>
<span className={`dag-priority ${getPriorityClassName(node.priority)}`}>
[{getPriorityLabel(node.priority)}]
</span>
<span className="dag-blocked-count">
<strong>{node.dependentCount}</strong> blocked
</span>
{node.isCriticalPath && <span className="dag-critical-icon"></span>}
<div className="dag-item-title">{node.title.slice(0, 50)}</div>
</div>
))}
</div>
</div>
);
};
// Render ready view
const renderReadyView = (): React.ReactNode => {
const ready = getReadyBeads(graph);
if (ready.length === 0) {
return (
<div className="dag-empty">
<p className="dag-warning-text">No ready tasks found.</p>
<p>All open tasks have blocking dependencies.</p>
<p>Complete blockers to unlock new work.</p>
</div>
);
}
return (
<div className="dag-list-container">
<p className="dag-list-header">{ready.length} tasks ready to work on:</p>
<div className="dag-list">
{ready.map((node, i) => (
<div
key={node.id}
className={`dag-list-item ${selectedIndex === i ? 'selected' : ''}`}
onClick={() => {
setSelectedIndex(i);
setSelectedBead(node);
}}
>
<span className={`dag-status-icon ${getStatusClassName(node.status)}`}>
{getStatusIcon(node.status)}
</span>
<span className="dag-node-id" style={{ color: getStatusColor(node.status) }}>
{node.id}
</span>
<span className={`dag-priority ${getPriorityClassName(node.priority)}`}>
[{getPriorityLabel(node.priority)}]
</span>
{node.isCriticalPath && <span className="dag-critical-icon"></span>}
<div className="dag-item-title">{node.title.slice(0, 50)}</div>
</div>
))}
</div>
</div>
);
};
// Render stats view
const renderStatsView = (): React.ReactNode => {
if (!stats || !graph) return null;
return (
<div className="dag-stats-container">
<div className="dag-stats-section">
<h3>Overview</h3>
<div className="dag-stats-row">
<span className="dag-stats-label">Total Beads:</span>
<span className="dag-stats-value">{stats.totalBeads}</span>
</div>
<div className="dag-stats-row">
<span className="dag-stats-label">Components:</span>
<span className="dag-stats-value">{graph.totalComponents}</span>
</div>
<div className="dag-stats-row">
<span className="dag-stats-label">Total Edges:</span>
<span className="dag-stats-value">{graph.totalEdges}</span>
</div>
</div>
<div className="dag-stats-section">
<h3>Status Breakdown</h3>
<div className="dag-stats-row">
<span className="dag-stats-label status-ready">Ready:</span>
<span className="dag-stats-value">{stats.readyCount}</span>
</div>
<div className="dag-stats-row">
<span className="dag-stats-label status-blocked">Blocked:</span>
<span className="dag-stats-value">{stats.blockedCount}</span>
</div>
</div>
<div className="dag-stats-section">
<h3>Graph Depth</h3>
<div className="dag-stats-row">
<span className="dag-stats-label">Maximum:</span>
<span className="dag-stats-value">{stats.maxDepth}</span>
</div>
</div>
<div className="dag-stats-section">
<h3>Critical Path</h3>
<div className="dag-stats-row">
<span className="dag-stats-label">Length:</span>
<span className="dag-stats-value">{stats.criticalPathLength}</span>
</div>
<div className="dag-stats-row">
<span className="dag-stats-label">Beads on path:</span>
<span className="dag-stats-value">{stats.criticalPathBeads}</span>
</div>
{graph.globalCriticalPath.length > 0 && (
<div className="dag-critical-path-preview">
<span className="dag-stats-label">Path:</span>
<div className="dag-path-nodes">
{graph.globalCriticalPath.slice(0, 5).map((id, i) => (
<React.Fragment key={id}>
<span className="dag-path-node"> {id}</span>
</React.Fragment>
))}
{graph.globalCriticalPath.length > 5 && (
<span className="dag-path-more">
... and {graph.globalCriticalPath.length - 5} more
</span>
)}
</div>
</div>
)}
</div>
<div className="dag-stats-section">
<h3>Averages</h3>
<div className="dag-stats-row">
<span className="dag-stats-label">Dependencies:</span>
<span className="dag-stats-value">{stats.avgDependencies.toFixed(1)}</span>
</div>
<div className="dag-stats-row">
<span className="dag-stats-label">Dependents:</span>
<span className="dag-stats-value">{stats.avgDependents.toFixed(1)}</span>
</div>
</div>
{stats.cycleCount > 0 && (
<div className="dag-cycle-warning-section">
{stats.cycleCount} cycle(s) detected!
<p>Circular dependencies prevent proper execution.</p>
</div>
)}
</div>
);
};
if (!visible) return null;
return (
<div className="dag-panel">
<div className="dag-header">
<h2>
<span className="dag-header-icon">🔗</span>
Task Dependency DAG
{graph && <span className="dag-count">{graph.totalNodes}</span>}
</h2>
<div className="dag-header-actions">
<button
className="dag-btn dag-btn-secondary"
onClick={cycleFilter}
title="Cycle filter options"
>
🔍 Filter
</button>
<button
className="dag-btn dag-btn-secondary"
onClick={fetchGraph}
disabled={loading}
title="Refresh"
>
{loading ? '⏳' : '🔄'} Refresh
</button>
<button className="dag-btn dag-btn-close" onClick={onClose}>
</button>
</div>
</div>
<div className="dag-view-modes">
<button
className={`dag-mode-btn ${viewMode === 'tree' ? 'active' : ''}`}
onClick={() => setViewMode('tree')}
>
🌳 Tree
</button>
<button
className={`dag-mode-btn ${viewMode === 'blockers' ? 'active' : ''}`}
onClick={() => setViewMode('blockers')}
>
🚫 Blockers
</button>
<button
className={`dag-mode-btn ${viewMode === 'ready' ? 'active' : ''}`}
onClick={() => setViewMode('ready')}
>
Ready
</button>
<button
className={`dag-mode-btn ${viewMode === 'stats' ? 'active' : ''}`}
onClick={() => setViewMode('stats')}
>
📊 Stats
</button>
</div>
<div className="dag-content">
{loading && !graph && (
<div className="dag-loading">Loading dependency graph...</div>
)}
{error && (
<div className="dag-error">
<p>Error loading dependency graph</p>
<p className="dag-error-message">{error}</p>
<button className="dag-btn dag-btn-primary" onClick={fetchGraph}>
Retry
</button>
</div>
)}
{!loading && !error && (
<>
<div className="dag-filter-info">
{viewMode.charAt(0).toUpperCase() + viewMode.slice(1)} View{getFilterDescription()}
</div>
<div className="dag-scroll-content">
{viewMode === 'tree' && renderTreeView()}
{viewMode === 'blockers' && renderBlockersView()}
{viewMode === 'ready' && renderReadyView()}
{viewMode === 'stats' && renderStatsView()}
</div>
</>
)}
</div>
{selectedBead && (
<div className="dag-detail-panel">
<div className="dag-detail-header">
<h3>{selectedBead.id}</h3>
<button
className="dag-detail-close"
onClick={() => setSelectedBead(null)}
>
</button>
</div>
<div className="dag-detail-content">
<div className="dag-detail-row">
<span className="dag-detail-label">Title:</span>
<span className="dag-detail-value">{selectedBead.title}</span>
</div>
<div className="dag-detail-row">
<span className="dag-detail-label">Status:</span>
<span
className={`dag-detail-value dag-status ${getStatusClassName(selectedBead.status)}`}
>
{getStatusIcon(selectedBead.status)} {selectedBead.status}
</span>
</div>
<div className="dag-detail-row">
<span className="dag-detail-label">Priority:</span>
<span className={`dag-detail-value ${getPriorityClassName(selectedBead.priority)}`}>
{getPriorityLabel(selectedBead.priority)}
</span>
</div>
<div className="dag-detail-row">
<span className="dag-detail-label">Depth:</span>
<span className="dag-detail-value">{selectedBead.depth}</span>
</div>
<div className="dag-detail-row">
<span className="dag-detail-label">Dependencies:</span>
<span className="dag-detail-value">{selectedBead.dependencyCount}</span>
</div>
<div className="dag-detail-row">
<span className="dag-detail-label">Blocking:</span>
<span className="dag-detail-value">{selectedBead.dependentCount}</span>
</div>
{selectedBead.isCriticalPath && (
<div className="dag-detail-critical">
On critical path
</div>
)}
</div>
</div>
)}
</div>
);
};
export default DependencyDag;

View file

@ -1005,3 +1005,873 @@ body {
flex-direction: column;
}
}
/* ============================================
File Heatmap Component Styles
============================================ */
.file-heatmap-panel {
display: flex;
flex-direction: column;
background: var(--bg-secondary);
border: 1px solid var(--bg-tertiary);
border-radius: 6px;
overflow: hidden;
min-width: 350px;
max-width: 450px;
max-height: 500px;
}
.file-heatmap-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
background: var(--bg-tertiary);
border-bottom: 1px solid var(--bg-primary);
}
.file-heatmap-header h2 {
font-size: 0.875rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0;
}
.file-heatmap-close {
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.875rem;
transition: all 0.2s;
}
.file-heatmap-close:hover {
background: var(--bg-primary);
color: var(--text-primary);
}
.file-heatmap-controls {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: var(--bg-primary);
border-bottom: 1px solid var(--bg-tertiary);
}
.file-heatmap-filter {
display: flex;
align-items: center;
gap: 0.5rem;
}
.file-heatmap-filter input {
background: var(--bg-tertiary);
border: 1px solid var(--bg-secondary);
border-radius: 4px;
color: var(--text-primary);
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
min-width: 150px;
}
.file-heatmap-filter input:focus {
outline: none;
border-color: var(--accent);
}
.file-heatmap-toggle {
display: flex;
align-items: center;
gap: 0.25rem;
background: var(--bg-tertiary);
border: 1px solid var(--bg-secondary);
color: var(--text-secondary);
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
cursor: pointer;
transition: all 0.2s;
}
.file-heatmap-toggle:hover {
background: var(--bg-primary);
}
.file-heatmap-toggle.active {
background: var(--accent);
color: #fff;
border-color: var(--accent);
}
.file-heatmap-stats {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 0.5rem;
padding: 0.5rem 1rem;
font-size: 0.75rem;
border-bottom: 1px solid var(--bg-tertiary);
}
.file-heatmap-stat {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.file-heatmap-stat-label {
color: var(--text-secondary);
font-size: 0.7rem;
}
.file-heatmap-stat-value {
font-weight: 600;
font-size: 0.875rem;
}
.file-heatmap-stat-value.critical {
color: var(--error);
}
.file-heatmap-stat-value.warning {
color: var(--warning);
}
.file-heatmap-content {
flex: 1;
overflow-y: auto;
padding: 0.5rem;
}
.file-heatmap-list {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.file-heatmap-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem;
color: var(--text-secondary);
gap: 0.5rem;
}
.file-heatmap-empty-icon {
font-size: 2rem;
opacity: 0.5;
}
.file-heatmap-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.375rem 0.5rem;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.file-heatmap-item:hover {
background: var(--bg-tertiary);
}
.file-heatmap-item.selected {
background: var(--bg-tertiary);
border: 1px solid var(--accent);
}
.file-heatmap-item.collision {
border-left: 3px solid var(--warning);
}
.file-heatmap-icon {
min-width: 20px;
text-align: center;
font-size: 1rem;
}
.file-heatmap-bar-container {
display: flex;
align-items: center;
gap: 0.25rem;
flex: 1;
}
.file-heatmap-bar {
height: 8px;
background: var(--bg-tertiary);
border-radius: 4px;
overflow: hidden;
flex: 1;
}
.file-heatmap-bar-fill {
height: 100%;
border-radius: 4px;
transition: width 0.2s ease;
}
.file-heatmap-bar-fill.cold { background: #4fc3f7; }
.file-heatmap-bar-fill.warm { background: #ffb74d; }
.file-heatmap-bar-fill.hot { background: #f06292; }
.file-heatmap-bar-fill.critical { background: #e53935; }
.file-heatmap-path {
font-family: 'SF Mono', Monaco, monospace;
font-size: 0.75rem;
color: var(--text-primary);
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-heatmap-mod-count {
color: var(--text-secondary);
font-size: 0.75rem;
min-width: 30px;
text-align: right;
}
.file-heatmap-workers {
color: #00bcd4;
font-size: 0.75rem;
max-width: 80px;
overflow: hidden;
text-overflow: ellipsis;
}
.file-heatmap-collision-indicator {
min-width: 20px;
text-align: center;
}
.file-heatmap-collision-indicator.has-collision {
color: var(--warning);
animation: pulse-warning 2s infinite;
}
.file-heatmap-collision-indicator.multiple-workers {
color: var(--warning);
}
.file-heatmap-detail {
padding: 0.75rem;
border-top: 1px solid var(--bg-tertiary);
margin-top: 0.5rem;
}
.file-heatmap-detail-header {
font-weight: 600;
font-size: 0.875rem;
margin-bottom: 0.5rem;
display: flex;
align-items: center;
gap: 0.25rem;
}
.file-heatmap-detail-path {
font-family: 'SF Mono', Monaco, monospace;
font-size: 0.8125rem;
color: var(--text-primary);
word-break: break-all;
}
.file-heatmap-detail-row {
display: flex;
justify-content: space-between;
padding: 0.25rem 0;
font-size: 0.8125rem;
}
.file-heatmap-detail-label {
color: var(--text-secondary);
}
.file-heatmap-detail-value {
font-weight: 500;
}
.file-heatmap-detail-workers {
margin-top: 0.5rem;
}
.file-heatmap-worker-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.25rem 0;
font-size: 0.75rem;
}
.file-heatmap-worker-bar {
height: 4px;
background: var(--bg-tertiary);
border-radius: 2px;
width: 60px;
overflow: hidden;
}
.file-heatmap-worker-bar-fill {
height: 100%;
background: var(--accent);
}
.file-heatmap-footer {
display: flex;
justify-content: space-between;
padding: 0.5rem 1rem;
background: var(--bg-primary);
border-top: 1px solid var(--bg-tertiary);
font-size: 0.75rem;
color: var(--text-secondary);
}
.file-heatmap-help {
display: flex;
gap: 1rem;
}
.file-heatmap-help span {
opacity: 0.7;
}
/* Responsive adjustments for */
@media (max-width: 768px) {
.file-heatmap-panel {
max-width: 100%;
max-height: 80vh;
}
.file-heatmap-stats {
grid-template-columns: repeat(3, 1fr);
}
}
/* ============================================
Dependency DAG Panel Styles
============================================ */
.dag-panel {
display: flex;
flex-direction: column;
background: var(--bg-secondary);
border: 1px solid var(--bg-tertiary);
border-radius: 6px;
overflow: hidden;
min-width: 400px;
max-width: 600px;
max-height: 80vh;
}
.dag-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
background: var(--bg-tertiary);
border-bottom: 1px solid var(--bg-primary);
}
.dag-header h2 {
font-size: 0.875rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0;
color: var(--text-primary);
}
.dag-header-icon {
font-size: 1rem;
}
.dag-count {
background: var(--accent);
color: #fff;
font-size: 0.7rem;
padding: 0.125rem 0.375rem;
border-radius: 10px;
margin-left: 0.25rem;
}
.dag-header-actions {
display: flex;
align-items: center;
gap: 0.5rem;
}
.dag-btn {
background: var(--bg-tertiary);
border: none;
color: var(--text-secondary);
padding: 0.375rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
cursor: pointer;
transition: all 0.2s;
}
.dag-btn:hover:not(:disabled) {
background: var(--bg-primary);
color: var(--accent);
}
.dag-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.dag-btn-secondary {
background: var(--bg-primary);
}
.dag-btn-close {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
.dag-view-modes {
display: flex;
gap: 0.25rem;
padding: 0.5rem;
background: var(--bg-secondary);
border-bottom: 1px solid var(--bg-tertiary);
}
.dag-mode-btn {
flex: 1;
background: var(--bg-tertiary);
border: none;
color: var(--text-secondary);
padding: 0.375rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
cursor: pointer;
transition: all 0.2s;
}
.dag-mode-btn:hover {
background: var(--bg-primary);
color: var(--text-primary);
}
.dag-mode-btn.active {
background: var(--accent);
color: #fff;
}
.dag-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.dag-filter-info {
padding: 0.5rem 0.75rem;
font-size: 0.75rem;
color: var(--text-secondary);
background: var(--bg-tertiary);
border-bottom: 1px solid var(--bg-primary);
}
.dag-scroll-content {
flex: 1;
overflow-y: auto;
padding: 0.5rem;
}
.dag-loading {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
color: var(--text-secondary);
}
.dag-error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
color: var(--error);
gap: 0.5rem;
}
.dag-error-message {
font-size: 0.75rem;
color: var(--text-secondary);
}
.dag-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem;
color: var(--text-secondary);
text-align: center;
}
.dag-empty-hint {
font-size: 0.75rem;
margin-top: 0.5rem;
}
.dag-success-text {
color: var(--success);
}
.dag-warning-text {
color: var(--warning);
}
/* Tree View Styles */
.dag-tree-container {
display: flex;
flex-direction: column;
gap: 1rem;
}
.dag-component {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.dag-cycle-warning {
padding: 0.5rem;
background: rgba(244, 67, 54, 0.1);
border: 1px solid rgba(244, 67, 54, 0.3);
border-radius: 4px;
color: var(--error);
font-size: 0.8125rem;
}
.dag-critical-path {
padding: 0.5rem;
background: rgba(255, 193, 7, 0.1);
border: 1px solid rgba(255, 193, 7, 0.3);
border-radius: 4px;
font-size: 0.8125rem;
color: var(--warning);
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.25rem;
}
.dag-critical-node {
font-weight: 600;
color: var(--warning);
}
.dag-arrow {
color: var(--text-secondary);
}
.dag-tree {
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
font-size: 0.8125rem;
}
.dag-tree-node-wrapper {
display: flex;
flex-direction: column;
}
.dag-tree-node {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.25rem 0.375rem;
border-radius: 3px;
cursor: pointer;
transition: all 0.2s;
}
.dag-tree-node:hover {
background: var(--bg-tertiary);
}
.dag-tree-node.selected {
background: var(--bg-tertiary);
border: 1px solid var(--accent);
}
.dag-tree-node.cycle {
color: var(--error);
font-style: italic;
}
.dag-tree-connector {
color: var(--text-secondary);
min-width: 16px;
}
.dag-status-icon {
font-size: 0.875rem;
min-width: 16px;
text-align: center;
}
.dag-status-icon.status-open { color: var(--text-secondary); }
.dag-status-icon.status-progress { color: #00bcd4; }
.dag-status-icon.status-blocked { color: var(--error); }
.dag-status-icon.status-completed { color: var(--success); }
.dag-status-icon.status-closed { color: var(--success); }
.dag-status-icon.status-deferred { color: var(--warning); }
.dag-node-id {
font-weight: 500;
}
.dag-priority {
font-size: 0.75rem;
color: var(--text-secondary);
}
.dag-priority.priority-critical { color: var(--error); font-weight: 600; }
.dag-priority.priority-high { color: var(--warning); }
.dag-priority.priority-normal { color: var(--text-primary); }
.dag-priority.priority-low { color: var(--text-secondary); }
.dag-priority.priority-backlog { color: var(--text-secondary); }
.dag-critical-icon {
color: var(--warning);
font-size: 0.875rem;
}
/* List View Styles */
.dag-list-container {
display: flex;
flex-direction: column;
}
.dag-list-header {
font-size: 0.8125rem;
font-weight: 600;
padding: 0.5rem;
color: var(--text-primary);
}
.dag-list {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.dag-list-item {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.375rem;
padding: 0.5rem;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.dag-list-item:hover {
background: var(--bg-tertiary);
}
.dag-list-item.selected {
background: var(--bg-tertiary);
border: 1px solid var(--accent);
}
.dag-blocked-count {
font-size: 0.75rem;
color: var(--error);
margin-left: auto;
}
.dag-item-title {
width: 100%;
font-size: 0.75rem;
color: var(--text-secondary);
padding-left: 1.5rem;
}
/* Stats View Styles */
.dag-stats-container {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 0.5rem;
}
.dag-stats-section {
background: var(--bg-tertiary);
border-radius: 4px;
padding: 0.75rem;
}
.dag-stats-section h3 {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-secondary);
margin: 0 0 0.5rem 0;
}
.dag-stats-row {
display: flex;
justify-content: space-between;
padding: 0.25rem 0;
font-size: 0.8125rem;
}
.dag-stats-label {
color: var(--text-secondary);
}
.dag-stats-label.status-ready { color: var(--success); }
.dag-stats-label.status-blocked { color: var(--error); }
.dag-stats-value {
font-weight: 500;
color: var(--text-primary);
}
.dag-critical-path-preview {
margin-top: 0.5rem;
padding-top: 0.5rem;
border-top: 1px solid var(--bg-primary);
}
.dag-path-nodes {
display: flex;
flex-direction: column;
gap: 0.125rem;
margin-top: 0.25rem;
}
.dag-path-node {
font-size: 0.75rem;
color: #9c27b0;
font-family: 'SF Mono', Monaco, monospace;
}
.dag-path-more {
font-size: 0.7rem;
color: var(--text-secondary);
font-style: italic;
}
.dag-cycle-warning-section {
padding: 0.75rem;
background: rgba(244, 67, 54, 0.1);
border: 1px solid rgba(244, 67, 54, 0.3);
border-radius: 4px;
color: var(--error);
font-size: 0.8125rem;
}
.dag-cycle-warning-section p {
font-size: 0.75rem;
margin-top: 0.25rem;
color: var(--text-secondary);
}
/* Detail Panel Styles */
.dag-detail-panel {
border-top: 1px solid var(--bg-tertiary);
background: var(--bg-primary);
}
.dag-detail-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem 0.75rem;
border-bottom: 1px solid var(--bg-tertiary);
}
.dag-detail-header h3 {
font-size: 0.875rem;
font-weight: 600;
margin: 0;
color: var(--accent);
}
.dag-detail-close {
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
padding: 0.25rem;
border-radius: 4px;
font-size: 0.875rem;
transition: all 0.2s;
}
.dag-detail-close:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.dag-detail-content {
padding: 0.5rem 0.75rem;
}
.dag-detail-row {
display: flex;
justify-content: space-between;
padding: 0.25rem 0;
font-size: 0.8125rem;
border-bottom: 1px solid var(--bg-tertiary);
}
.dag-detail-row:last-of-type {
border-bottom: none;
}
.dag-detail-label {
color: var(--text-secondary);
}
.dag-detail-value {
font-weight: 500;
color: var(--text-primary);
}
.dag-detail-critical {
margin-top: 0.5rem;
padding: 0.5rem;
background: rgba(255, 193, 7, 0.1);
border: 1px solid rgba(255, 193, 7, 0.3);
border-radius: 4px;
color: var(--warning);
font-size: 0.8125rem;
text-align: center;
}
/* Responsive adjustments for DAG */
@media (max-width: 768px) {
.dag-panel {
min-width: 100%;
max-width: 100%;
}
.dag-view-modes {
flex-wrap: wrap;
}
.dag-mode-btn {
flex: 1 1 45%;
}
}

View file

@ -174,3 +174,66 @@ export interface FileHeatmapStats {
}
export type HeatmapSortMode = 'modifications' | 'recent' | 'workers' | 'collisions';
// Dependency DAG Types
export type BeadStatus = 'open' | 'in_progress' | 'blocked' | 'completed' | 'closed' | 'deferred';
export interface BeadNode {
id: string;
title: string;
status: BeadStatus;
priority: number;
depth: number;
dependentCount: number;
dependencyCount: number;
isCriticalPath: boolean;
estimatedEffort?: number;
}
export interface DependencyEdge {
from: string;
to: string;
isCritical: boolean;
}
export interface DagComponent {
nodes: BeadNode[];
edges: DependencyEdge[];
roots: string[];
hasCycle: boolean;
criticalPath: string[];
maxDepth: number;
}
export interface DependencyGraph {
components: DagComponent[];
totalNodes: number;
totalEdges: number;
totalComponents: number;
globalCriticalPath: string[];
generatedAt: number;
}
export interface DagStats {
totalBeads: number;
blockedCount: number;
readyCount: number;
avgDependencies: number;
avgDependents: number;
maxDepth: number;
cycleCount: number;
criticalPathLength: number;
criticalPathBeads: number;
}
export interface DagOptions {
status?: BeadStatus | 'all';
minPriority?: number;
maxPriority?: number;
criticalOnly?: boolean;
maxDepth?: number;
sortBy?: 'priority' | 'depth' | 'dependents';
includeClosed?: boolean;
}
export type DagViewMode = 'tree' | 'blockers' | 'ready' | 'stats';

View file

@ -0,0 +1,257 @@
/**
* Tests for DependencyDag component
* @vitest-environment jsdom
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/react';
import DependencyDag from '../src/components/DependencyDag';
// Mock fetch globally
const mockFetch = vi.fn();
global.fetch = mockFetch;
// Mock graph data
const mockGraph = {
components: [
{
nodes: [
{ id: 'bd-abc', title: 'Test bead', status: 'open', priority: 1, depth: 0, dependentCount: 2, dependencyCount: 0, isCriticalPath: true },
{ id: 'bd-def', title: 'Dependent bead', status: 'blocked', priority: 2, depth: 1, dependentCount: 0, dependencyCount: 1, isCriticalPath: false },
],
edges: [{ from: 'bd-def', to: 'bd-abc', isCritical: true }],
roots: ['bd-abc'],
hasCycle: false,
criticalPath: ['bd-abc'],
maxDepth: 1,
},
],
totalNodes: 2,
totalEdges: 1,
totalComponents: 1,
globalCriticalPath: ['bd-abc'],
generatedAt: Date.now(),
};
const mockStats = {
totalBeads: 2,
blockedCount: 1,
readyCount: 1,
avgDependencies: 0.5,
avgDependents: 1,
maxDepth: 1,
cycleCount: 0,
criticalPathLength: 1,
criticalPathBeads: 1,
};
// Helper to set up successful mock
const setupSuccessMock = (times: number = 1) => {
for (let i = 0; i < times; i++) {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ graph: mockGraph, stats: mockStats }),
});
}
};
// Helper to set up error mock
const setupErrorMock = () => {
mockFetch.mockResolvedValueOnce({
ok: false,
statusText: 'Internal Server Error',
});
};
describe('DependencyDag Component', () => {
beforeEach(() => {
mockFetch.mockReset();
});
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
describe('Visibility', () => {
it('should not render when visible is false', () => {
render(<DependencyDag visible={false} onClose={() => {}} />);
expect(screen.queryByText('Task Dependency DAG')).not.toBeInTheDocument();
});
it('should render when visible is true', async () => {
setupSuccessMock();
render(<DependencyDag visible={true} onClose={() => {}} />);
await waitFor(() => {
expect(screen.getByText('Task Dependency DAG')).toBeInTheDocument();
});
});
});
describe('View Modes', () => {
it('should show tree view by default', async () => {
setupSuccessMock();
render(<DependencyDag visible={true} onClose={() => {}} />);
await waitFor(() => {
expect(screen.getByText(/Tree View/)).toBeInTheDocument();
});
});
it('should switch to blockers view when blockers button clicked', async () => {
setupSuccessMock();
render(<DependencyDag visible={true} onClose={() => {}} />);
await waitFor(() => {
expect(screen.getByRole('button', { name: /blockers/i })).toBeInTheDocument();
});
fireEvent.click(screen.getByRole('button', { name: /blockers/i }));
await waitFor(() => {
expect(screen.getByText(/Blockers View/)).toBeInTheDocument();
});
});
it('should switch to ready view when ready button clicked', async () => {
setupSuccessMock();
render(<DependencyDag visible={true} onClose={() => {}} />);
await waitFor(() => {
expect(screen.getByRole('button', { name: /ready/i })).toBeInTheDocument();
});
fireEvent.click(screen.getByRole('button', { name: /ready/i }));
await waitFor(() => {
expect(screen.getByText(/Ready View/)).toBeInTheDocument();
});
});
it('should switch to stats view when stats button clicked', async () => {
setupSuccessMock();
render(<DependencyDag visible={true} onClose={() => {}} />);
await waitFor(() => {
expect(screen.getByRole('button', { name: /stats/i })).toBeInTheDocument();
});
fireEvent.click(screen.getByRole('button', { name: /stats/i }));
await waitFor(() => {
expect(screen.getByText(/Stats View/)).toBeInTheDocument();
});
});
});
describe('Stats View', () => {
it('should display stats correctly', async () => {
setupSuccessMock();
render(<DependencyDag visible={true} onClose={() => {}} />);
await waitFor(() => {
expect(screen.getByRole('button', { name: /stats/i })).toBeInTheDocument();
});
fireEvent.click(screen.getByRole('button', { name: /stats/i }));
await waitFor(() => {
expect(screen.getByText('Overview')).toBeInTheDocument();
});
});
});
describe('Close Button', () => {
it('should call onClose when close button clicked', async () => {
setupSuccessMock();
const onClose = vi.fn();
render(<DependencyDag visible={true} onClose={onClose} />);
await waitFor(() => {
expect(screen.getByRole('button', { name: /✕/i })).toBeInTheDocument();
});
fireEvent.click(screen.getByRole('button', { name: /✕/i }));
expect(onClose).toHaveBeenCalled();
});
});
describe('Refresh', () => {
it('should call API on mount when visible', async () => {
setupSuccessMock();
render(<DependencyDag visible={true} onClose={() => {}} />);
await waitFor(() => {
expect(mockFetch).toHaveBeenCalledWith('/api/dag?');
});
});
it('should refresh when refresh button clicked', async () => {
setupSuccessMock(2); // Need 2 calls - initial + refresh
render(<DependencyDag visible={true} onClose={() => {}} />);
await waitFor(() => {
expect(screen.getByRole('button', { name: /refresh/i })).toBeInTheDocument();
});
fireEvent.click(screen.getByRole('button', { name: /refresh/i }));
await waitFor(() => {
expect(mockFetch).toHaveBeenCalledTimes(2);
});
});
});
describe('Error Handling', () => {
it('should show error message when API fails', async () => {
setupErrorMock();
render(<DependencyDag visible={true} onClose={() => {}} />);
await waitFor(() => {
expect(screen.getByText(/Error loading dependency graph/i)).toBeInTheDocument();
});
});
});
describe('Tree View', () => {
it('should display critical path', async () => {
setupSuccessMock();
render(<DependencyDag visible={true} onClose={() => {}} />);
await waitFor(() => {
expect(screen.getByText(/Critical path:/i)).toBeInTheDocument();
});
});
it('should display bead nodes', async () => {
setupSuccessMock();
render(<DependencyDag visible={true} onClose={() => {}} />);
// Look for any element containing the bead ID text
await waitFor(() => {
const beadElements = screen.getAllByText((content, element) => {
return element?.classList?.contains('dag-node-id') && content.includes('bd-abc');
});
expect(beadElements.length).toBeGreaterThan(0);
});
});
});
describe('Filter Options', () => {
it('should cycle filters when filter button clicked', async () => {
setupSuccessMock(2); // Need 2 calls - initial + filter refresh
render(<DependencyDag visible={true} onClose={() => {}} />);
await waitFor(() => {
expect(screen.getByRole('button', { name: /filter/i })).toBeInTheDocument();
});
fireEvent.click(screen.getByRole('button', { name: /filter/i }));
await waitFor(() => {
expect(mockFetch).toHaveBeenCalledTimes(2);
});
});
});
});

View file

@ -10,9 +10,10 @@ import { EventEmitter } from 'events';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { WebSocketServer, WebSocket } from 'ws';
import { LogEvent, EventFilter, CrossReferenceEntityType, CrossReferenceRelationship } from '../types.js';
import { LogEvent, EventFilter, CrossReferenceEntityType, CrossReferenceRelationship, DagOptions, BeadStatus } from '../types.js';
import { InMemoryEventStore } from '../store.js';
import { CrossReferenceManager, getCrossReferenceManager } from '../crossReferenceManager.js';
import { refreshDependencyGraph, getDagStats } from '../tui/dagUtils.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
@ -125,6 +126,72 @@ export function createWebServer(options: WebServerOptions): WebServer {
res.json(collisions);
});
// ============================================
// File Heatmap API Endpoints
// ============================================
// Get file heatmap entries
app.get('/api/heatmap', (req: Request, res: Response) => {
const sortBy = req.query.sortBy as 'modifications' | 'recent' | 'workers' | 'collisions' || undefined;
const maxEntries = req.query.maxEntries ? parseInt(req.query.maxEntries as string) : 100;
const collisionsOnly = req.query.collisionsOnly === 'true';
const directoryFilter = req.query.directoryFilter as string | undefined;
const entries = store.getFileHeatmap({
sortBy,
maxEntries,
collisionsOnly,
directoryFilter,
});
res.json(entries);
});
// Get file heatmap statistics
app.get('/api/heatmap/stats', (_req: Request, res: Response) => {
const stats = store.getFileHeatmapStats();
res.json(stats);
});
// ============================================
// Dependency DAG API Endpoints
// ============================================
// Get dependency graph
app.get('/api/dag', (req: Request, res: Response) => {
try {
const status = req.query.status as BeadStatus | 'all' | undefined;
const criticalOnly = req.query.criticalOnly === 'true';
const maxDepth = req.query.maxDepth ? parseInt(req.query.maxDepth as string) : undefined;
const includeClosed = req.query.includeClosed === 'true';
const options: DagOptions = {};
if (status && status !== 'all') {
options.status = status as BeadStatus;
}
if (criticalOnly) {
options.criticalOnly = true;
}
if (maxDepth !== undefined) {
options.maxDepth = maxDepth;
}
if (includeClosed) {
options.includeClosed = true;
}
const graph = refreshDependencyGraph(options);
const stats = getDagStats(graph);
res.json({ graph, stats });
} catch (error) {
console.error('Error generating dependency graph:', error);
res.status(500).json({
error: 'Failed to generate dependency graph',
message: error instanceof Error ? error.message : 'Unknown error'
});
}
});
// ============================================
// Cross-Reference API Endpoints
// ============================================