@pennyfarthing/cyclist 10.0.3 → 10.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/agent-load.d.ts +3 -0
- package/dist/api/agent-load.d.ts.map +1 -0
- package/dist/api/agent-load.js +124 -0
- package/dist/api/agent-load.js.map +1 -0
- package/dist/api/code-markers.d.ts +9 -0
- package/dist/api/code-markers.d.ts.map +1 -0
- package/dist/api/code-markers.js +62 -0
- package/dist/api/code-markers.js.map +1 -0
- package/dist/api/complexity.d.ts +3 -0
- package/dist/api/complexity.d.ts.map +1 -0
- package/dist/api/complexity.js +47 -0
- package/dist/api/complexity.js.map +1 -0
- package/dist/api/dead-code.d.ts +3 -0
- package/dist/api/dead-code.d.ts.map +1 -0
- package/dist/api/dead-code.js +70 -0
- package/dist/api/dead-code.js.map +1 -0
- package/dist/api/dependencies.d.ts +3 -0
- package/dist/api/dependencies.d.ts.map +1 -0
- package/dist/api/dependencies.js +43 -0
- package/dist/api/dependencies.js.map +1 -0
- package/dist/api/git.d.ts +3 -2
- package/dist/api/git.d.ts.map +1 -1
- package/dist/api/git.js +11 -6
- package/dist/api/git.js.map +1 -1
- package/dist/api/health-score.d.ts +3 -0
- package/dist/api/health-score.d.ts.map +1 -0
- package/dist/api/health-score.js +47 -0
- package/dist/api/health-score.js.map +1 -0
- package/dist/api/hotspots.d.ts.map +1 -1
- package/dist/api/hotspots.js +9 -1
- package/dist/api/hotspots.js.map +1 -1
- package/dist/api/index.d.ts +7 -1
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +12 -2
- package/dist/api/index.js.map +1 -1
- package/dist/api/persona.d.ts +2 -0
- package/dist/api/persona.d.ts.map +1 -1
- package/dist/api/persona.js +19 -1
- package/dist/api/persona.js.map +1 -1
- package/dist/api/settings.js +1 -1
- package/dist/api/settings.js.map +1 -1
- package/dist/claude-service.d.ts +8 -2
- package/dist/claude-service.d.ts.map +1 -1
- package/dist/claude-service.js +21 -2
- package/dist/claude-service.js.map +1 -1
- package/dist/git-diff.d.ts.map +1 -1
- package/dist/git-diff.js +6 -5
- package/dist/git-diff.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +11 -2
- package/dist/main.js.map +1 -1
- package/dist/plugin-loader.d.ts +49 -0
- package/dist/plugin-loader.d.ts.map +1 -0
- package/dist/plugin-loader.js +92 -0
- package/dist/plugin-loader.js.map +1 -0
- package/dist/preload.js +12 -1
- package/dist/preload.js.map +1 -1
- package/dist/prime.d.ts +3 -2
- package/dist/prime.d.ts.map +1 -1
- package/dist/prime.js +25 -8
- package/dist/prime.js.map +1 -1
- package/dist/public/css/react.css +1 -1
- package/dist/public/js/react/react.js +50 -39
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +19 -16
- package/dist/server.js.map +1 -1
- package/dist/sprint-data.d.ts +6 -0
- package/dist/sprint-data.d.ts.map +1 -1
- package/dist/sprint-data.js +118 -67
- package/dist/sprint-data.js.map +1 -1
- package/dist/story-parser.js +1 -1
- package/dist/story-parser.js.map +1 -1
- package/dist/theme-metadata.js +2 -2
- package/dist/theme-metadata.js.map +1 -1
- package/dist/websocket.d.ts +0 -6
- package/dist/websocket.d.ts.map +1 -1
- package/dist/websocket.js +36 -40
- package/dist/websocket.js.map +1 -1
- package/package.json +2 -1
- package/portraits/fifth-element/large/cornelius-54343.png +0 -0
- package/portraits/fifth-element/large/diva-53453.png +0 -0
- package/portraits/fifth-element/large/korben-34232.png +0 -0
- package/portraits/fifth-element/large/leeloo-54333.png +0 -0
- package/portraits/fifth-element/large/lindberg-34432.png +0 -0
- package/portraits/fifth-element/large/mondoshawan-55131.png +0 -0
- package/portraits/fifth-element/large/munro-25321.png +0 -0
- package/portraits/fifth-element/large/pacoli-45232.png +0 -0
- package/portraits/fifth-element/large/ruby-53544.png +0 -0
- package/portraits/fifth-element/large/zorg-45312.png +0 -0
- package/portraits/fifth-element/medium/cornelius-54343.png +0 -0
- package/portraits/fifth-element/medium/diva-53453.png +0 -0
- package/portraits/fifth-element/medium/korben-34232.png +0 -0
- package/portraits/fifth-element/medium/leeloo-54333.png +0 -0
- package/portraits/fifth-element/medium/lindberg-34432.png +0 -0
- package/portraits/fifth-element/medium/mondoshawan-55131.png +0 -0
- package/portraits/fifth-element/medium/munro-25321.png +0 -0
- package/portraits/fifth-element/medium/pacoli-45232.png +0 -0
- package/portraits/fifth-element/medium/ruby-53544.png +0 -0
- package/portraits/fifth-element/medium/zorg-45312.png +0 -0
- package/src/public/App.tsx +0 -2
- package/src/public/components/AgentLoadDialog.tsx +202 -0
- package/src/public/components/AgentPopup.tsx +3 -5
- package/src/public/components/ContextSparkline.tsx +56 -0
- package/src/public/components/ControlBar.tsx +140 -6
- package/src/public/components/DeadCodeDialog.tsx +169 -0
- package/src/public/components/DockviewWorkspace.tsx +0 -3
- package/src/public/components/FullFileTree.tsx +18 -4
- package/src/public/components/HealthGauge.tsx +181 -0
- package/src/public/components/MessageView.tsx +23 -6
- package/src/public/components/PersonaHeader.tsx +46 -3
- package/src/public/components/TandemPortrait.tsx +71 -0
- package/src/public/components/ToolCallBlock.tsx +21 -6
- package/src/public/components/dialogs/CodeMarkersDialog.tsx +169 -0
- package/src/public/components/dialogs/ComplexityDialog.tsx +163 -0
- package/src/public/components/dialogs/DependenciesDialog.tsx +120 -0
- package/src/public/components/dialogs/HotspotsDialog.tsx +451 -0
- package/src/public/components/dialogs/ToolDialog.tsx +43 -0
- package/src/public/components/panels/ACPanel.tsx +1 -1
- package/src/public/components/panels/AcceptanceCriteriaPanel.tsx +15 -30
- package/src/public/components/panels/DebugPanel.tsx +79 -3
- package/src/public/components/panels/GitPanel.tsx +25 -30
- package/src/public/components/panels/MessagePanel.tsx +44 -2
- package/src/public/components/panels/SettingsPanel.tsx +4 -4
- package/src/public/components/panels/SprintPanel.tsx +247 -123
- package/src/public/components/panels/index.ts +0 -1
- package/src/public/components/ui/dialog.tsx +3 -3
- package/src/public/css/theme-system.css +98 -11
- package/src/public/hooks/index.ts +4 -0
- package/src/public/hooks/useAgentLoad.ts +105 -0
- package/src/public/hooks/useCodeMarkers.ts +101 -0
- package/src/public/hooks/useColorScheme.ts +25 -10
- package/src/public/hooks/useComplexity.ts +80 -0
- package/src/public/hooks/useDeadCode.ts +99 -0
- package/src/public/hooks/useDependencies.ts +82 -0
- package/src/public/hooks/useHealthScore.ts +69 -0
- package/src/public/hooks/useHotspots.ts +11 -1
- package/src/public/hooks/usePersona.ts +26 -3
- package/src/public/hooks/useSprint.ts +7 -1
- package/src/public/styles/tailwind.css +389 -83
- package/src/public/utils/messageFilters.ts +77 -6
- package/src/public/utils/slash-commands.ts +3 -35
- package/dist/hooks/cyclist-pretooluse-hook.d.ts +0 -60
- package/dist/hooks/cyclist-pretooluse-hook.d.ts.map +0 -1
- package/dist/hooks/cyclist-pretooluse-hook.js +0 -57
- package/dist/hooks/cyclist-pretooluse-hook.js.map +0 -1
- package/dist/hooks/pretooluse-hook.d.ts +0 -89
- package/dist/hooks/pretooluse-hook.d.ts.map +0 -1
- package/dist/hooks/pretooluse-hook.js +0 -235
- package/dist/hooks/pretooluse-hook.js.map +0 -1
- package/dist/notification-sound.d.ts +0 -59
- package/dist/notification-sound.d.ts.map +0 -1
- package/dist/notification-sound.js +0 -219
- package/dist/notification-sound.js.map +0 -1
- package/src/public/types/electron.d.ts +0 -18
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodeMarkersDialog — Story 80-3 (MSSCI-14456)
|
|
3
|
+
*
|
|
4
|
+
* Dialog displaying code markers (TODO, FIXME, HACK, XXX) with tabs,
|
|
5
|
+
* sortable table, staleness badges, and summary stats.
|
|
6
|
+
*/
|
|
7
|
+
import React, { useState, useMemo, useEffect } from 'react';
|
|
8
|
+
import { Badge } from '@/components/ui/badge';
|
|
9
|
+
import { Skeleton } from '@/components/ui/skeleton';
|
|
10
|
+
import { ToolDialog } from './ToolDialog';
|
|
11
|
+
import { useCodeMarkers } from '../../hooks/useCodeMarkers';
|
|
12
|
+
|
|
13
|
+
export interface CodeMarkersDialogProps {
|
|
14
|
+
open: boolean;
|
|
15
|
+
onOpenChange: (open: boolean) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type TabId = 'all' | 'stale' | 'deprecated';
|
|
19
|
+
type SortField = 'marker_type' | 'path' | 'line' | 'author' | 'age_days';
|
|
20
|
+
type SortDirection = 'asc' | 'desc';
|
|
21
|
+
|
|
22
|
+
export function CodeMarkersDialog({ open, onOpenChange }: CodeMarkersDialogProps): React.ReactElement {
|
|
23
|
+
const { data, isLoading, error, refresh } = useCodeMarkers({ days: 90, repo: 'pennyfarthing' });
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (open) refresh();
|
|
27
|
+
}, [open, refresh]);
|
|
28
|
+
|
|
29
|
+
const [activeTab, setActiveTab] = useState<TabId>('all');
|
|
30
|
+
const [sortField, setSortField] = useState<SortField>('age_days');
|
|
31
|
+
const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
|
|
32
|
+
|
|
33
|
+
const filteredMarkers = useMemo(() => {
|
|
34
|
+
if (!data?.markers) return [];
|
|
35
|
+
switch (activeTab) {
|
|
36
|
+
case 'stale':
|
|
37
|
+
return data.markers.filter((m) => m.is_stale);
|
|
38
|
+
case 'deprecated':
|
|
39
|
+
return [];
|
|
40
|
+
default:
|
|
41
|
+
return data.markers;
|
|
42
|
+
}
|
|
43
|
+
}, [data, activeTab]);
|
|
44
|
+
|
|
45
|
+
const sortedMarkers = useMemo(() => {
|
|
46
|
+
const sorted = [...filteredMarkers];
|
|
47
|
+
sorted.sort((a, b) => {
|
|
48
|
+
const aVal = a[sortField];
|
|
49
|
+
const bVal = b[sortField];
|
|
50
|
+
if (typeof aVal === 'string' && typeof bVal === 'string') {
|
|
51
|
+
return sortDirection === 'asc' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
|
|
52
|
+
}
|
|
53
|
+
const aNum = Number(aVal);
|
|
54
|
+
const bNum = Number(bVal);
|
|
55
|
+
return sortDirection === 'asc' ? aNum - bNum : bNum - aNum;
|
|
56
|
+
});
|
|
57
|
+
return sorted;
|
|
58
|
+
}, [filteredMarkers, sortField, sortDirection]);
|
|
59
|
+
|
|
60
|
+
const handleSort = (field: SortField) => {
|
|
61
|
+
if (sortField === field) {
|
|
62
|
+
setSortDirection((d) => (d === 'asc' ? 'desc' : 'asc'));
|
|
63
|
+
} else {
|
|
64
|
+
setSortField(field);
|
|
65
|
+
setSortDirection('desc');
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const sortArrow = (field: SortField) => {
|
|
70
|
+
if (sortField !== field) return '';
|
|
71
|
+
return sortDirection === 'desc' ? ' v' : ' ^';
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<ToolDialog
|
|
76
|
+
open={open}
|
|
77
|
+
onOpenChange={onOpenChange}
|
|
78
|
+
title="Code Markers"
|
|
79
|
+
description="TODO, FIXME, HACK, and XXX markers across the codebase"
|
|
80
|
+
>
|
|
81
|
+
{isLoading && (
|
|
82
|
+
<div className="space-y-2">
|
|
83
|
+
<Skeleton className="h-4 w-full" />
|
|
84
|
+
<Skeleton className="h-4 w-3/4" />
|
|
85
|
+
<Skeleton className="h-4 w-1/2" />
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
|
|
89
|
+
{error && (
|
|
90
|
+
<div className="p-4 rounded border border-[var(--status-error)]/20 bg-[var(--status-error)]/5 text-[var(--status-error)] text-sm">
|
|
91
|
+
{error.message}
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
|
|
95
|
+
{!isLoading && !error && data && (
|
|
96
|
+
<>
|
|
97
|
+
{/* Summary stats */}
|
|
98
|
+
<div className="flex gap-4 text-xs text-[var(--text-muted)]" data-testid="code-markers-summary">
|
|
99
|
+
<span data-testid="summary-total">Total: <span className="tabular-nums font-mono">{data.summary.total_markers}</span></span>
|
|
100
|
+
<span data-testid="summary-stale">Stale: <span className="tabular-nums font-mono">{data.summary.stale_markers}</span></span>
|
|
101
|
+
{Object.entries(data.summary.by_type).map(([type, count]) => (
|
|
102
|
+
<span key={type} data-testid={`summary-type-${type.toLowerCase()}`}>{type}: <span className="tabular-nums font-mono">{count}</span></span>
|
|
103
|
+
))}
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
{/* Tabs */}
|
|
107
|
+
<div role="tablist" className="flex gap-4 border-b border-[var(--border)] mb-3">
|
|
108
|
+
{(['all', 'stale', 'deprecated'] as const).map((tab) => (
|
|
109
|
+
<button
|
|
110
|
+
key={tab}
|
|
111
|
+
role="tab"
|
|
112
|
+
aria-selected={activeTab === tab}
|
|
113
|
+
onClick={() => setActiveTab(tab)}
|
|
114
|
+
className={`pb-2 text-sm capitalize ${activeTab === tab ? 'border-b-2 border-[var(--accent)] text-[var(--text-primary)] font-medium' : 'text-[var(--text-muted)]'}`}
|
|
115
|
+
>
|
|
116
|
+
{tab}
|
|
117
|
+
</button>
|
|
118
|
+
))}
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{/* Table or empty state */}
|
|
122
|
+
{sortedMarkers.length === 0 ? (
|
|
123
|
+
<div className="text-center py-12 text-[var(--text-muted)]">No markers found</div>
|
|
124
|
+
) : (
|
|
125
|
+
<table role="table" className="w-full text-sm">
|
|
126
|
+
<thead>
|
|
127
|
+
<tr className="border-b border-[var(--border)]">
|
|
128
|
+
<th className="cursor-pointer select-none text-left text-xs font-medium uppercase tracking-wider text-[var(--text-muted)] pb-2" onClick={() => handleSort('marker_type')}>
|
|
129
|
+
Type{sortArrow('marker_type')}
|
|
130
|
+
</th>
|
|
131
|
+
<th className="cursor-pointer select-none text-left text-xs font-medium uppercase tracking-wider text-[var(--text-muted)] pb-2" onClick={() => handleSort('path')}>
|
|
132
|
+
File{sortArrow('path')}
|
|
133
|
+
</th>
|
|
134
|
+
<th className="cursor-pointer select-none text-right text-xs font-medium uppercase tracking-wider text-[var(--text-muted)] pb-2" onClick={() => handleSort('line')}>
|
|
135
|
+
Line{sortArrow('line')}
|
|
136
|
+
</th>
|
|
137
|
+
<th className="text-left text-xs font-medium uppercase tracking-wider text-[var(--text-muted)] pb-2">Text</th>
|
|
138
|
+
<th className="cursor-pointer select-none text-left text-xs font-medium uppercase tracking-wider text-[var(--text-muted)] pb-2" onClick={() => handleSort('author')}>
|
|
139
|
+
Author{sortArrow('author')}
|
|
140
|
+
</th>
|
|
141
|
+
<th className="cursor-pointer select-none text-right text-xs font-medium uppercase tracking-wider text-[var(--text-muted)] pb-2" onClick={() => handleSort('age_days')}>
|
|
142
|
+
Age{sortArrow('age_days')}
|
|
143
|
+
</th>
|
|
144
|
+
</tr>
|
|
145
|
+
</thead>
|
|
146
|
+
<tbody>
|
|
147
|
+
{sortedMarkers.map((marker, i) => (
|
|
148
|
+
<tr key={`${marker.path}:${marker.line}:${i}`} className="text-[var(--text-primary)]">
|
|
149
|
+
<td className="py-1.5">
|
|
150
|
+
<Badge variant={marker.is_stale ? 'destructive' : 'secondary'}>
|
|
151
|
+
{marker.marker_type}
|
|
152
|
+
</Badge>
|
|
153
|
+
</td>
|
|
154
|
+
<td className="py-1.5 font-mono text-xs">{marker.path}</td>
|
|
155
|
+
<td className="text-right py-1.5 tabular-nums font-mono">{marker.line}</td>
|
|
156
|
+
<td className="py-1.5 truncate max-w-xs">{marker.text}</td>
|
|
157
|
+
<td className="py-1.5">{marker.author}</td>
|
|
158
|
+
<td className="text-right py-1.5 tabular-nums font-mono">{marker.age_days}d</td>
|
|
159
|
+
</tr>
|
|
160
|
+
))}
|
|
161
|
+
</tbody>
|
|
162
|
+
</table>
|
|
163
|
+
)}
|
|
164
|
+
</>
|
|
165
|
+
)}
|
|
166
|
+
|
|
167
|
+
</ToolDialog>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import React, { useState, useMemo, useCallback, useEffect } from 'react';
|
|
2
|
+
import { Badge } from '@/components/ui/badge';
|
|
3
|
+
import { Button } from '@/components/ui/button';
|
|
4
|
+
import { Skeleton } from '@/components/ui/skeleton';
|
|
5
|
+
import { ToolDialog } from './ToolDialog';
|
|
6
|
+
import { useComplexity, FileComplexity } from '../../hooks/useComplexity';
|
|
7
|
+
|
|
8
|
+
export interface ComplexityDialogProps {
|
|
9
|
+
open: boolean;
|
|
10
|
+
onOpenChange: (open: boolean) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type SortField = 'avg_cyclomatic_complexity' | 'max_nesting_depth' | 'longest_function' | 'total_lines' | 'function_count' | 'path';
|
|
14
|
+
type SortDirection = 'asc' | 'desc';
|
|
15
|
+
|
|
16
|
+
function SortableHeader({
|
|
17
|
+
label,
|
|
18
|
+
field,
|
|
19
|
+
currentSort,
|
|
20
|
+
currentDirection,
|
|
21
|
+
onSort,
|
|
22
|
+
align = 'right',
|
|
23
|
+
}: {
|
|
24
|
+
label: string;
|
|
25
|
+
field: SortField;
|
|
26
|
+
currentSort: SortField;
|
|
27
|
+
currentDirection: SortDirection;
|
|
28
|
+
onSort: (field: SortField) => void;
|
|
29
|
+
align?: 'left' | 'right';
|
|
30
|
+
}) {
|
|
31
|
+
const isActive = currentSort === field;
|
|
32
|
+
const arrow = isActive ? (currentDirection === 'desc' ? ' v' : ' ^') : '';
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<th
|
|
36
|
+
className={`text-${align} ${isActive ? 'active' : ''}`}
|
|
37
|
+
onClick={() => onSort(field)}
|
|
38
|
+
role="columnheader"
|
|
39
|
+
aria-sort={isActive ? (currentDirection === 'desc' ? 'descending' : 'ascending') : 'none'}
|
|
40
|
+
style={{ cursor: 'pointer', userSelect: 'none', padding: '4px 8px' }}
|
|
41
|
+
>
|
|
42
|
+
{label}{arrow}
|
|
43
|
+
</th>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function ComplexityDialog({ open, onOpenChange }: ComplexityDialogProps): React.ReactElement {
|
|
48
|
+
const [sortField, setSortField] = useState<SortField>('avg_cyclomatic_complexity');
|
|
49
|
+
const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
|
|
50
|
+
|
|
51
|
+
const { data, isLoading, error, refresh } = useComplexity({});
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (open) refresh();
|
|
55
|
+
}, [open, refresh]);
|
|
56
|
+
|
|
57
|
+
const handleSort = useCallback((field: SortField) => {
|
|
58
|
+
setSortField((prev) => {
|
|
59
|
+
if (prev === field) {
|
|
60
|
+
setSortDirection((d) => (d === 'desc' ? 'asc' : 'desc'));
|
|
61
|
+
return prev;
|
|
62
|
+
}
|
|
63
|
+
setSortDirection('desc');
|
|
64
|
+
return field;
|
|
65
|
+
});
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
const sorted = useMemo(() => {
|
|
69
|
+
if (!data?.files) return [];
|
|
70
|
+
const items = [...data.files];
|
|
71
|
+
items.sort((a, b) => {
|
|
72
|
+
const aVal = a[sortField as keyof FileComplexity];
|
|
73
|
+
const bVal = b[sortField as keyof FileComplexity];
|
|
74
|
+
if (typeof aVal === 'number' && typeof bVal === 'number') {
|
|
75
|
+
return sortDirection === 'desc' ? bVal - aVal : aVal - bVal;
|
|
76
|
+
}
|
|
77
|
+
return sortDirection === 'desc'
|
|
78
|
+
? String(bVal).localeCompare(String(aVal))
|
|
79
|
+
: String(aVal).localeCompare(String(bVal));
|
|
80
|
+
});
|
|
81
|
+
return items;
|
|
82
|
+
}, [data, sortField, sortDirection]);
|
|
83
|
+
|
|
84
|
+
const renderContent = () => {
|
|
85
|
+
if (isLoading) {
|
|
86
|
+
return (
|
|
87
|
+
<div className="complexity-panel loading" data-testid="complexity-panel">
|
|
88
|
+
<div className="space-y-3 p-2">
|
|
89
|
+
<Skeleton className="h-4 w-40" />
|
|
90
|
+
<Skeleton className="h-3 w-full" />
|
|
91
|
+
<Skeleton className="h-3 w-full" />
|
|
92
|
+
<Skeleton className="h-3 w-3/4" />
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (error) {
|
|
99
|
+
return (
|
|
100
|
+
<div className="complexity-panel error" data-testid="complexity-panel">
|
|
101
|
+
<div className="error-message">Error: {error.message}</div>
|
|
102
|
+
<Button variant="outline" size="sm" onClick={refresh}>Retry</Button>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!data) {
|
|
108
|
+
return (
|
|
109
|
+
<div className="complexity-panel" data-testid="complexity-panel">
|
|
110
|
+
<p>Click <strong>Analyze</strong> to run complexity analysis</p>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<div className="complexity-panel" data-testid="complexity-panel">
|
|
117
|
+
<div className="complexity-summary" style={{ marginBottom: '8px' }}>
|
|
118
|
+
<span>{data.file_count} files analyzed</span>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<table className="complexity-table" role="table" style={{ width: '100%', borderCollapse: 'collapse' }}>
|
|
122
|
+
<thead>
|
|
123
|
+
<tr>
|
|
124
|
+
<SortableHeader label="Complexity" field="avg_cyclomatic_complexity" currentSort={sortField} currentDirection={sortDirection} onSort={handleSort} />
|
|
125
|
+
<SortableHeader label="Nesting" field="max_nesting_depth" currentSort={sortField} currentDirection={sortDirection} onSort={handleSort} />
|
|
126
|
+
<SortableHeader label="Longest Fn" field="longest_function" currentSort={sortField} currentDirection={sortDirection} onSort={handleSort} />
|
|
127
|
+
<SortableHeader label="Lines" field="total_lines" currentSort={sortField} currentDirection={sortDirection} onSort={handleSort} />
|
|
128
|
+
<SortableHeader label="Functions" field="function_count" currentSort={sortField} currentDirection={sortDirection} onSort={handleSort} />
|
|
129
|
+
<SortableHeader label="File" field="path" currentSort={sortField} currentDirection={sortDirection} onSort={handleSort} align="left" />
|
|
130
|
+
</tr>
|
|
131
|
+
</thead>
|
|
132
|
+
<tbody>
|
|
133
|
+
{sorted.map((f) => (
|
|
134
|
+
<tr key={f.path}>
|
|
135
|
+
<td className="text-right" style={{ padding: '4px 8px' }}>
|
|
136
|
+
<Badge variant={f.avg_cyclomatic_complexity >= 7 ? 'destructive' : f.avg_cyclomatic_complexity >= 4 ? 'outline' : 'secondary'}>
|
|
137
|
+
{Number(f.avg_cyclomatic_complexity).toFixed(1)}
|
|
138
|
+
</Badge>
|
|
139
|
+
</td>
|
|
140
|
+
<td className="text-right" style={{ padding: '4px 8px' }}>{f.max_nesting_depth}</td>
|
|
141
|
+
<td className="text-right" style={{ padding: '4px 8px' }}>{f.longest_function}</td>
|
|
142
|
+
<td className="text-right" style={{ padding: '4px 8px' }}>{f.total_lines}</td>
|
|
143
|
+
<td className="text-right" style={{ padding: '4px 8px' }}>{f.function_count}</td>
|
|
144
|
+
<td className="text-left" style={{ padding: '4px 8px' }}>{f.path}</td>
|
|
145
|
+
</tr>
|
|
146
|
+
))}
|
|
147
|
+
</tbody>
|
|
148
|
+
</table>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<ToolDialog
|
|
155
|
+
open={open}
|
|
156
|
+
onOpenChange={onOpenChange}
|
|
157
|
+
title="Complexity"
|
|
158
|
+
description="Cyclomatic complexity analysis"
|
|
159
|
+
>
|
|
160
|
+
{renderContent()}
|
|
161
|
+
</ToolDialog>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { Badge } from '@/components/ui/badge';
|
|
3
|
+
import { Button } from '@/components/ui/button';
|
|
4
|
+
import { Skeleton } from '@/components/ui/skeleton';
|
|
5
|
+
import { ToolDialog } from './ToolDialog';
|
|
6
|
+
import { useDependencies } from '../../hooks/useDependencies';
|
|
7
|
+
|
|
8
|
+
export interface DependenciesDialogProps {
|
|
9
|
+
open: boolean;
|
|
10
|
+
onOpenChange: (open: boolean) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function severityVariant(severity: string): 'destructive' | 'outline' | 'secondary' {
|
|
14
|
+
if (severity === 'high' || severity === 'critical') return 'destructive';
|
|
15
|
+
if (severity === 'moderate') return 'outline';
|
|
16
|
+
return 'secondary';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function DependenciesDialog({ open, onOpenChange }: DependenciesDialogProps): React.ReactElement {
|
|
20
|
+
const { data, isLoading, error, refresh } = useDependencies({});
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (open) refresh();
|
|
24
|
+
}, [open, refresh]);
|
|
25
|
+
|
|
26
|
+
const renderContent = () => {
|
|
27
|
+
if (isLoading) {
|
|
28
|
+
return (
|
|
29
|
+
<div className="dependencies-panel loading" data-testid="dependencies-panel">
|
|
30
|
+
<div className="space-y-3 p-2">
|
|
31
|
+
<Skeleton className="h-4 w-40" />
|
|
32
|
+
<Skeleton className="h-3 w-full" />
|
|
33
|
+
<Skeleton className="h-3 w-full" />
|
|
34
|
+
<Skeleton className="h-3 w-3/4" />
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (error) {
|
|
41
|
+
return (
|
|
42
|
+
<div className="dependencies-panel error" data-testid="dependencies-panel">
|
|
43
|
+
<div className="error-message">Error: {error.message}</div>
|
|
44
|
+
<Button variant="outline" size="sm" onClick={refresh}>Retry</Button>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!data) {
|
|
50
|
+
return (
|
|
51
|
+
<div className="dependencies-panel" data-testid="dependencies-panel">
|
|
52
|
+
<p>Click <strong>Analyze</strong> to check dependencies</p>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="dependencies-panel" data-testid="dependencies-panel">
|
|
59
|
+
{data.outdated.length > 0 && (
|
|
60
|
+
<div className="outdated-section" style={{ marginBottom: '16px' }}>
|
|
61
|
+
<h4>Outdated Packages ({data.outdated.length})</h4>
|
|
62
|
+
<table role="table" style={{ width: '100%', borderCollapse: 'collapse' }}>
|
|
63
|
+
<thead>
|
|
64
|
+
<tr>
|
|
65
|
+
<th className="text-left" style={{ padding: '4px 8px' }}>Package</th>
|
|
66
|
+
<th className="text-left" style={{ padding: '4px 8px' }}>Current</th>
|
|
67
|
+
<th className="text-left" style={{ padding: '4px 8px' }}>Wanted</th>
|
|
68
|
+
<th className="text-left" style={{ padding: '4px 8px' }}>Latest</th>
|
|
69
|
+
<th className="text-left" style={{ padding: '4px 8px' }}>Type</th>
|
|
70
|
+
</tr>
|
|
71
|
+
</thead>
|
|
72
|
+
<tbody>
|
|
73
|
+
{data.outdated.map((pkg) => (
|
|
74
|
+
<tr key={pkg.name}>
|
|
75
|
+
<td style={{ padding: '4px 8px' }}>{pkg.name}</td>
|
|
76
|
+
<td style={{ padding: '4px 8px' }}>{pkg.current}</td>
|
|
77
|
+
<td style={{ padding: '4px 8px' }}>{pkg.wanted}</td>
|
|
78
|
+
<td style={{ padding: '4px 8px' }}>{pkg.latest}</td>
|
|
79
|
+
<td style={{ padding: '4px 8px' }}>{pkg.type}</td>
|
|
80
|
+
</tr>
|
|
81
|
+
))}
|
|
82
|
+
</tbody>
|
|
83
|
+
</table>
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
|
|
87
|
+
{data.advisories.length > 0 && (
|
|
88
|
+
<div className="security-section">
|
|
89
|
+
<h4>Security Advisories</h4>
|
|
90
|
+
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
|
91
|
+
{data.advisories.map((adv) => (
|
|
92
|
+
<div key={adv.severity} style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
|
93
|
+
<Badge variant={severityVariant(adv.severity)}>
|
|
94
|
+
{adv.severity}
|
|
95
|
+
</Badge>
|
|
96
|
+
<span>{adv.count}</span>
|
|
97
|
+
</div>
|
|
98
|
+
))}
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
|
|
103
|
+
{data.outdated.length === 0 && data.advisories.length === 0 && (
|
|
104
|
+
<p>All dependencies are up to date with no known vulnerabilities.</p>
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<ToolDialog
|
|
112
|
+
open={open}
|
|
113
|
+
onOpenChange={onOpenChange}
|
|
114
|
+
title="Dependencies"
|
|
115
|
+
description="Package staleness and security analysis"
|
|
116
|
+
>
|
|
117
|
+
{renderContent()}
|
|
118
|
+
</ToolDialog>
|
|
119
|
+
);
|
|
120
|
+
}
|