@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,105 @@
|
|
|
1
|
+
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface AgentLoadComponent {
|
|
4
|
+
name: string;
|
|
5
|
+
tokens: number;
|
|
6
|
+
source?: string | null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface AgentLoadEntry {
|
|
10
|
+
agent: string;
|
|
11
|
+
totalTokens: number | null;
|
|
12
|
+
tokenCounts?: Record<string, number>;
|
|
13
|
+
components?: AgentLoadComponent[];
|
|
14
|
+
error?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AgentLoadData {
|
|
18
|
+
agents: AgentLoadEntry[];
|
|
19
|
+
cachedAt: string;
|
|
20
|
+
totalAcrossAllAgents: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface PruneResult {
|
|
24
|
+
success: boolean;
|
|
25
|
+
tokensFreed?: number;
|
|
26
|
+
agent?: string;
|
|
27
|
+
file?: string;
|
|
28
|
+
error?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface UseAgentLoadReturn {
|
|
32
|
+
data: AgentLoadData | null;
|
|
33
|
+
isLoading: boolean;
|
|
34
|
+
error: Error | null;
|
|
35
|
+
refresh: () => void;
|
|
36
|
+
pruneSidecar: (agent: string, file: string) => Promise<void>;
|
|
37
|
+
pruneResult: PruneResult | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function useAgentLoad(): UseAgentLoadReturn {
|
|
41
|
+
const [data, setData] = useState<AgentLoadData | null>(null);
|
|
42
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
43
|
+
const [error, setError] = useState<Error | null>(null);
|
|
44
|
+
const [pruneResult, setPruneResult] = useState<PruneResult | null>(null);
|
|
45
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
46
|
+
|
|
47
|
+
const refresh = useCallback(() => {
|
|
48
|
+
if (abortRef.current) {
|
|
49
|
+
abortRef.current.abort();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const controller = new AbortController();
|
|
53
|
+
abortRef.current = controller;
|
|
54
|
+
|
|
55
|
+
setIsLoading(true);
|
|
56
|
+
setError(null);
|
|
57
|
+
|
|
58
|
+
fetch('/api/agent-load', { signal: controller.signal })
|
|
59
|
+
.then((res) => {
|
|
60
|
+
if (!res.ok) {
|
|
61
|
+
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
62
|
+
}
|
|
63
|
+
return res.json();
|
|
64
|
+
})
|
|
65
|
+
.then((json: AgentLoadData) => {
|
|
66
|
+
setData(json);
|
|
67
|
+
setIsLoading(false);
|
|
68
|
+
})
|
|
69
|
+
.catch((err) => {
|
|
70
|
+
if (err.name === 'AbortError') return;
|
|
71
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
72
|
+
setIsLoading(false);
|
|
73
|
+
});
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
76
|
+
const pruneSidecar = useCallback(async (agent: string, file: string) => {
|
|
77
|
+
const res = await fetch('/api/agent-load/prune-sidecar', {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: { 'Content-Type': 'application/json' },
|
|
80
|
+
body: JSON.stringify({ agent, file }),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (!res.ok) {
|
|
84
|
+
setPruneResult({ success: false, error: `HTTP ${res.status}: ${res.statusText}` });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const result: PruneResult = await res.json();
|
|
89
|
+
setPruneResult(result);
|
|
90
|
+
|
|
91
|
+
if (result.success) {
|
|
92
|
+
refresh();
|
|
93
|
+
}
|
|
94
|
+
}, [refresh]);
|
|
95
|
+
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
return () => {
|
|
98
|
+
if (abortRef.current) {
|
|
99
|
+
abortRef.current.abort();
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}, []);
|
|
103
|
+
|
|
104
|
+
return { data, isLoading, error, refresh, pruneSidecar, pruneResult };
|
|
105
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useCodeMarkers React hook — Story 80-3 (MSSCI-14456)
|
|
3
|
+
*
|
|
4
|
+
* Wraps /api/code-markers endpoint with AbortController,
|
|
5
|
+
* loading/error/data state management, and manual refresh.
|
|
6
|
+
*/
|
|
7
|
+
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
8
|
+
|
|
9
|
+
export interface CodeMarker {
|
|
10
|
+
path: string;
|
|
11
|
+
line: number;
|
|
12
|
+
marker_type: string;
|
|
13
|
+
text: string;
|
|
14
|
+
author: string;
|
|
15
|
+
date: string;
|
|
16
|
+
age_days: number;
|
|
17
|
+
is_stale: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface MarkerSummary {
|
|
21
|
+
total_markers: number;
|
|
22
|
+
stale_markers: number;
|
|
23
|
+
by_type: Record<string, number>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface CodeMarkersData {
|
|
27
|
+
success: boolean;
|
|
28
|
+
repo_name: string;
|
|
29
|
+
repo_path: string;
|
|
30
|
+
stale_threshold_days: number;
|
|
31
|
+
markers: CodeMarker[];
|
|
32
|
+
summary: MarkerSummary;
|
|
33
|
+
error: string | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface UseCodeMarkersOptions {
|
|
37
|
+
days: number;
|
|
38
|
+
repo?: string;
|
|
39
|
+
type?: 'all' | 'stale' | 'deprecated';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface UseCodeMarkersReturn {
|
|
43
|
+
data: CodeMarkersData | null;
|
|
44
|
+
isLoading: boolean;
|
|
45
|
+
error: Error | null;
|
|
46
|
+
refresh: () => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function useCodeMarkers(options: UseCodeMarkersOptions): UseCodeMarkersReturn {
|
|
50
|
+
const [data, setData] = useState<CodeMarkersData | null>(null);
|
|
51
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
52
|
+
const [error, setError] = useState<Error | null>(null);
|
|
53
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
54
|
+
|
|
55
|
+
const fetchCodeMarkers = useCallback(() => {
|
|
56
|
+
if (abortRef.current) {
|
|
57
|
+
abortRef.current.abort();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const controller = new AbortController();
|
|
61
|
+
abortRef.current = controller;
|
|
62
|
+
|
|
63
|
+
setIsLoading(true);
|
|
64
|
+
setError(null);
|
|
65
|
+
|
|
66
|
+
const params = new URLSearchParams({ days: String(options.days) });
|
|
67
|
+
if (options.repo) {
|
|
68
|
+
params.set('repo', options.repo);
|
|
69
|
+
}
|
|
70
|
+
if (options.type) {
|
|
71
|
+
params.set('type', options.type);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
fetch(`/api/code-markers?${params}`, { signal: controller.signal })
|
|
75
|
+
.then((res) => {
|
|
76
|
+
if (!res.ok) {
|
|
77
|
+
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
78
|
+
}
|
|
79
|
+
return res.json();
|
|
80
|
+
})
|
|
81
|
+
.then((json: CodeMarkersData) => {
|
|
82
|
+
setData(json);
|
|
83
|
+
setIsLoading(false);
|
|
84
|
+
})
|
|
85
|
+
.catch((err) => {
|
|
86
|
+
if (err.name === 'AbortError') return;
|
|
87
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
88
|
+
setIsLoading(false);
|
|
89
|
+
});
|
|
90
|
+
}, [options.days, options.repo, options.type]);
|
|
91
|
+
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
return () => {
|
|
94
|
+
if (abortRef.current) {
|
|
95
|
+
abortRef.current.abort();
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}, []);
|
|
99
|
+
|
|
100
|
+
return { data, isLoading, error, refresh: fetchCodeMarkers };
|
|
101
|
+
}
|
|
@@ -1,26 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useColorScheme Hook
|
|
3
3
|
*
|
|
4
|
-
* Tracks the
|
|
5
|
-
* the
|
|
4
|
+
* Tracks the active color scheme (light/dark) from the applied color preset's
|
|
5
|
+
* data-variant attribute on the document root. Falls back to OS preference.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { useState, useEffect } from 'react';
|
|
9
9
|
|
|
10
10
|
export type ColorScheme = 'light' | 'dark';
|
|
11
11
|
|
|
12
|
+
function getVariant(): ColorScheme {
|
|
13
|
+
const variant = document.documentElement.getAttribute('data-variant');
|
|
14
|
+
if (variant === 'light' || variant === 'dark') return variant;
|
|
15
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
16
|
+
}
|
|
17
|
+
|
|
12
18
|
export function useColorScheme(): ColorScheme {
|
|
13
|
-
const [scheme, setScheme] = useState<ColorScheme>(
|
|
14
|
-
window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
|
15
|
-
);
|
|
19
|
+
const [scheme, setScheme] = useState<ColorScheme>(getVariant);
|
|
16
20
|
|
|
17
21
|
useEffect(() => {
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
setScheme(
|
|
22
|
+
// Watch for preset changes via data-variant attribute
|
|
23
|
+
const observer = new MutationObserver(() => {
|
|
24
|
+
setScheme(getVariant());
|
|
25
|
+
});
|
|
26
|
+
observer.observe(document.documentElement, {
|
|
27
|
+
attributes: true,
|
|
28
|
+
attributeFilter: ['data-variant'],
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Also listen to presetChange events from applyPreset()
|
|
32
|
+
const handlePreset = () => setScheme(getVariant());
|
|
33
|
+
window.addEventListener('presetChange', handlePreset);
|
|
34
|
+
|
|
35
|
+
return () => {
|
|
36
|
+
observer.disconnect();
|
|
37
|
+
window.removeEventListener('presetChange', handlePreset);
|
|
21
38
|
};
|
|
22
|
-
mq.addEventListener('change', handler);
|
|
23
|
-
return () => mq.removeEventListener('change', handler);
|
|
24
39
|
}, []);
|
|
25
40
|
|
|
26
41
|
return scheme;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface FileComplexity {
|
|
4
|
+
path: string;
|
|
5
|
+
total_lines: number;
|
|
6
|
+
longest_function: number;
|
|
7
|
+
avg_cyclomatic_complexity: number;
|
|
8
|
+
max_nesting_depth: number;
|
|
9
|
+
function_count: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ComplexityData {
|
|
13
|
+
success: boolean;
|
|
14
|
+
target_path: string;
|
|
15
|
+
file_count: number;
|
|
16
|
+
files: FileComplexity[];
|
|
17
|
+
error?: string | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface UseComplexityOptions {
|
|
21
|
+
path?: string;
|
|
22
|
+
top?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface UseComplexityReturn {
|
|
26
|
+
data: ComplexityData | null;
|
|
27
|
+
isLoading: boolean;
|
|
28
|
+
error: Error | null;
|
|
29
|
+
refresh: () => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function useComplexity(options: UseComplexityOptions): UseComplexityReturn {
|
|
33
|
+
const [data, setData] = useState<ComplexityData | null>(null);
|
|
34
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
35
|
+
const [error, setError] = useState<Error | null>(null);
|
|
36
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
37
|
+
|
|
38
|
+
const fetchComplexity = useCallback(() => {
|
|
39
|
+
if (abortRef.current) {
|
|
40
|
+
abortRef.current.abort();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
abortRef.current = controller;
|
|
45
|
+
|
|
46
|
+
setIsLoading(true);
|
|
47
|
+
setError(null);
|
|
48
|
+
|
|
49
|
+
const params = new URLSearchParams();
|
|
50
|
+
if (options.path) params.set('path', options.path);
|
|
51
|
+
if (options.top) params.set('top', String(options.top));
|
|
52
|
+
|
|
53
|
+
fetch(`/api/complexity?${params}`, { signal: controller.signal })
|
|
54
|
+
.then((res) => {
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
57
|
+
}
|
|
58
|
+
return res.json();
|
|
59
|
+
})
|
|
60
|
+
.then((json: ComplexityData) => {
|
|
61
|
+
setData(json);
|
|
62
|
+
setIsLoading(false);
|
|
63
|
+
})
|
|
64
|
+
.catch((err) => {
|
|
65
|
+
if (err.name === 'AbortError') return;
|
|
66
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
67
|
+
setIsLoading(false);
|
|
68
|
+
});
|
|
69
|
+
}, [options.path, options.top]);
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
return () => {
|
|
73
|
+
if (abortRef.current) {
|
|
74
|
+
abortRef.current.abort();
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}, []);
|
|
78
|
+
|
|
79
|
+
return { data, isLoading, error, refresh: fetchComplexity };
|
|
80
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface StaleFile {
|
|
4
|
+
path: string;
|
|
5
|
+
last_commit_date: string;
|
|
6
|
+
days_since_last_commit: number;
|
|
7
|
+
size_bytes: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface UnusedExport {
|
|
11
|
+
symbol: string;
|
|
12
|
+
file: string;
|
|
13
|
+
line: number;
|
|
14
|
+
export_type: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DeadCodeData {
|
|
18
|
+
success: boolean;
|
|
19
|
+
repo_name?: string;
|
|
20
|
+
repo_path?: string;
|
|
21
|
+
time_window_days?: number;
|
|
22
|
+
stale_files?: StaleFile[];
|
|
23
|
+
unused_exports?: UnusedExport[];
|
|
24
|
+
stale_file_count?: number;
|
|
25
|
+
unused_export_count?: number;
|
|
26
|
+
total_files?: number;
|
|
27
|
+
total_exports_scanned?: number;
|
|
28
|
+
repo_results?: DeadCodeData[];
|
|
29
|
+
error?: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface UseDeadCodeOptions {
|
|
33
|
+
days: number;
|
|
34
|
+
repo?: string;
|
|
35
|
+
layer?: 'stale' | 'exports' | 'all';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface UseDeadCodeReturn {
|
|
39
|
+
data: DeadCodeData | null;
|
|
40
|
+
isLoading: boolean;
|
|
41
|
+
error: Error | null;
|
|
42
|
+
refresh: () => void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function useDeadCode(options: UseDeadCodeOptions): UseDeadCodeReturn {
|
|
46
|
+
const [data, setData] = useState<DeadCodeData | null>(null);
|
|
47
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
48
|
+
const [error, setError] = useState<Error | null>(null);
|
|
49
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
50
|
+
|
|
51
|
+
const fetchDeadCode = useCallback(() => {
|
|
52
|
+
// Cancel any in-flight request
|
|
53
|
+
if (abortRef.current) {
|
|
54
|
+
abortRef.current.abort();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
abortRef.current = controller;
|
|
59
|
+
|
|
60
|
+
setIsLoading(true);
|
|
61
|
+
setError(null);
|
|
62
|
+
|
|
63
|
+
const params = new URLSearchParams({
|
|
64
|
+
days: String(options.days),
|
|
65
|
+
layer: options.layer || 'all',
|
|
66
|
+
});
|
|
67
|
+
if (options.repo) {
|
|
68
|
+
params.set('repo', options.repo);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fetch(`/api/dead-code?${params}`, { signal: controller.signal })
|
|
72
|
+
.then((res) => {
|
|
73
|
+
if (!res.ok) {
|
|
74
|
+
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
75
|
+
}
|
|
76
|
+
return res.json();
|
|
77
|
+
})
|
|
78
|
+
.then((json: DeadCodeData) => {
|
|
79
|
+
setData(json);
|
|
80
|
+
setIsLoading(false);
|
|
81
|
+
})
|
|
82
|
+
.catch((err) => {
|
|
83
|
+
if (err.name === 'AbortError') return;
|
|
84
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
85
|
+
setIsLoading(false);
|
|
86
|
+
});
|
|
87
|
+
}, [options.days, options.repo, options.layer]);
|
|
88
|
+
|
|
89
|
+
// Cleanup abort controller on unmount
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
return () => {
|
|
92
|
+
if (abortRef.current) {
|
|
93
|
+
abortRef.current.abort();
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}, []);
|
|
97
|
+
|
|
98
|
+
return { data, isLoading, error, refresh: fetchDeadCode };
|
|
99
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface OutdatedPackage {
|
|
4
|
+
name: string;
|
|
5
|
+
current: string;
|
|
6
|
+
wanted: string;
|
|
7
|
+
latest: string;
|
|
8
|
+
type: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SecurityAdvisory {
|
|
12
|
+
severity: string;
|
|
13
|
+
count: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface DependenciesData {
|
|
17
|
+
success: boolean;
|
|
18
|
+
target_path: string;
|
|
19
|
+
outdated: OutdatedPackage[];
|
|
20
|
+
advisories: SecurityAdvisory[];
|
|
21
|
+
error?: string | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface UseDependenciesOptions {
|
|
25
|
+
path?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface UseDependenciesReturn {
|
|
29
|
+
data: DependenciesData | null;
|
|
30
|
+
isLoading: boolean;
|
|
31
|
+
error: Error | null;
|
|
32
|
+
refresh: () => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function useDependencies(options: UseDependenciesOptions): UseDependenciesReturn {
|
|
36
|
+
const [data, setData] = useState<DependenciesData | null>(null);
|
|
37
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
38
|
+
const [error, setError] = useState<Error | null>(null);
|
|
39
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
40
|
+
|
|
41
|
+
const fetchDependencies = useCallback(() => {
|
|
42
|
+
if (abortRef.current) {
|
|
43
|
+
abortRef.current.abort();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const controller = new AbortController();
|
|
47
|
+
abortRef.current = controller;
|
|
48
|
+
|
|
49
|
+
setIsLoading(true);
|
|
50
|
+
setError(null);
|
|
51
|
+
|
|
52
|
+
const params = new URLSearchParams();
|
|
53
|
+
if (options.path) params.set('path', options.path);
|
|
54
|
+
|
|
55
|
+
fetch(`/api/dependencies?${params}`, { signal: controller.signal })
|
|
56
|
+
.then((res) => {
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
59
|
+
}
|
|
60
|
+
return res.json();
|
|
61
|
+
})
|
|
62
|
+
.then((json: DependenciesData) => {
|
|
63
|
+
setData(json);
|
|
64
|
+
setIsLoading(false);
|
|
65
|
+
})
|
|
66
|
+
.catch((err) => {
|
|
67
|
+
if (err.name === 'AbortError') return;
|
|
68
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
69
|
+
setIsLoading(false);
|
|
70
|
+
});
|
|
71
|
+
}, [options.path]);
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
return () => {
|
|
75
|
+
if (abortRef.current) {
|
|
76
|
+
abortRef.current.abort();
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}, []);
|
|
80
|
+
|
|
81
|
+
return { data, isLoading, error, refresh: fetchDependencies };
|
|
82
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface HealthScoreDimension {
|
|
4
|
+
name: string;
|
|
5
|
+
score: number | null;
|
|
6
|
+
weight: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface HealthScoreData {
|
|
10
|
+
success: boolean;
|
|
11
|
+
composite_score: number;
|
|
12
|
+
dimensions: HealthScoreDimension[];
|
|
13
|
+
cached: boolean;
|
|
14
|
+
error?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface UseHealthScoreReturn {
|
|
18
|
+
data: HealthScoreData | null;
|
|
19
|
+
isLoading: boolean;
|
|
20
|
+
error: Error | null;
|
|
21
|
+
lastFetchedAt: number | null;
|
|
22
|
+
refresh: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function useHealthScore(): UseHealthScoreReturn {
|
|
26
|
+
const [data, setData] = useState<HealthScoreData | null>(null);
|
|
27
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
28
|
+
const [error, setError] = useState<Error | null>(null);
|
|
29
|
+
const [lastFetchedAt, setLastFetchedAt] = useState<number | null>(null);
|
|
30
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
31
|
+
|
|
32
|
+
const refresh = useCallback(() => {
|
|
33
|
+
if (abortRef.current) {
|
|
34
|
+
abortRef.current.abort();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
abortRef.current = controller;
|
|
39
|
+
|
|
40
|
+
setIsLoading(true);
|
|
41
|
+
setError(null);
|
|
42
|
+
|
|
43
|
+
fetch('/api/health-score', { signal: controller.signal })
|
|
44
|
+
.then((res) => {
|
|
45
|
+
if (!res.ok) {
|
|
46
|
+
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
47
|
+
}
|
|
48
|
+
return res.json();
|
|
49
|
+
})
|
|
50
|
+
.then((json: HealthScoreData) => {
|
|
51
|
+
setData(json);
|
|
52
|
+
setLastFetchedAt(Date.now());
|
|
53
|
+
setIsLoading(false);
|
|
54
|
+
})
|
|
55
|
+
.catch((err) => {
|
|
56
|
+
if (err.name === 'AbortError') return;
|
|
57
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
58
|
+
setIsLoading(false);
|
|
59
|
+
});
|
|
60
|
+
}, []);
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
return () => {
|
|
64
|
+
abortRef.current?.abort();
|
|
65
|
+
};
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
return { data, isLoading, error, lastFetchedAt, refresh };
|
|
69
|
+
}
|
|
@@ -50,6 +50,8 @@ export interface HotspotData {
|
|
|
50
50
|
export interface UseHotspotsOptions {
|
|
51
51
|
days: number;
|
|
52
52
|
repo?: string;
|
|
53
|
+
skipTypes?: string[];
|
|
54
|
+
includeOrchestrator?: boolean;
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
export interface UseHotspotsReturn {
|
|
@@ -82,6 +84,14 @@ export function useHotspots(options: UseHotspotsOptions): UseHotspotsReturn {
|
|
|
82
84
|
params.set('repo', options.repo);
|
|
83
85
|
}
|
|
84
86
|
|
|
87
|
+
// Determine skip_type values: use explicit skipTypes, or default to ['orchestrator']
|
|
88
|
+
// unless includeOrchestrator is true
|
|
89
|
+
const skipTypes = options.skipTypes ??
|
|
90
|
+
(options.includeOrchestrator ? [] : ['orchestrator']);
|
|
91
|
+
for (const st of skipTypes) {
|
|
92
|
+
params.append('skip_type', st);
|
|
93
|
+
}
|
|
94
|
+
|
|
85
95
|
fetch(`/api/hotspots?${params}`, { signal: controller.signal })
|
|
86
96
|
.then((res) => {
|
|
87
97
|
if (!res.ok) {
|
|
@@ -98,7 +108,7 @@ export function useHotspots(options: UseHotspotsOptions): UseHotspotsReturn {
|
|
|
98
108
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
99
109
|
setIsLoading(false);
|
|
100
110
|
});
|
|
101
|
-
}, [options.days, options.repo]);
|
|
111
|
+
}, [options.days, options.repo, options.skipTypes, options.includeOrchestrator]);
|
|
102
112
|
|
|
103
113
|
// Cleanup abort controller on unmount
|
|
104
114
|
useEffect(() => {
|
|
@@ -13,22 +13,33 @@
|
|
|
13
13
|
|
|
14
14
|
import { useState, useEffect, useRef } from 'react';
|
|
15
15
|
|
|
16
|
+
export interface TandemAgentData {
|
|
17
|
+
character: string;
|
|
18
|
+
role: string;
|
|
19
|
+
slug: string;
|
|
20
|
+
theme: string;
|
|
21
|
+
isThinking: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
16
24
|
export interface PersonaData {
|
|
17
25
|
character: string | null;
|
|
18
26
|
theme: string | null;
|
|
19
27
|
role: string | null;
|
|
20
28
|
slug: string | null;
|
|
21
29
|
quote: string | null; // Random catchphrase from theme
|
|
30
|
+
tandemAgent?: TandemAgentData | null;
|
|
22
31
|
}
|
|
23
32
|
|
|
24
33
|
interface UsePersonaResult {
|
|
25
34
|
persona: PersonaData | null;
|
|
35
|
+
isStreaming: boolean;
|
|
26
36
|
isLoading: boolean;
|
|
27
37
|
error: Error | null;
|
|
28
38
|
}
|
|
29
39
|
|
|
30
40
|
export function usePersona(): UsePersonaResult {
|
|
31
41
|
const [persona, setPersona] = useState<PersonaData | null>(null);
|
|
42
|
+
const [isStreaming, setIsStreaming] = useState(false);
|
|
32
43
|
const [isLoading, setIsLoading] = useState(true);
|
|
33
44
|
const [error, setError] = useState<Error | null>(null);
|
|
34
45
|
const wsRef = useRef<WebSocket | null>(null);
|
|
@@ -48,8 +59,19 @@ export function usePersona(): UsePersonaResult {
|
|
|
48
59
|
|
|
49
60
|
wsRef.current.onmessage = (event) => {
|
|
50
61
|
try {
|
|
51
|
-
const data = JSON.parse(event.data)
|
|
52
|
-
|
|
62
|
+
const data = JSON.parse(event.data);
|
|
63
|
+
|
|
64
|
+
// Story 94-1: Handle streaming state updates
|
|
65
|
+
if (data.type === 'streaming') {
|
|
66
|
+
setIsStreaming(data.isStreaming ?? false);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Persona data (initial or agent change) — extract isStreaming if present
|
|
71
|
+
if (data.isStreaming !== undefined) {
|
|
72
|
+
setIsStreaming(data.isStreaming);
|
|
73
|
+
}
|
|
74
|
+
setPersona(data as PersonaData);
|
|
53
75
|
setIsLoading(false);
|
|
54
76
|
setError(null);
|
|
55
77
|
} catch (err) {
|
|
@@ -59,6 +81,7 @@ export function usePersona(): UsePersonaResult {
|
|
|
59
81
|
|
|
60
82
|
wsRef.current.onclose = () => {
|
|
61
83
|
console.debug('[usePersona] WebSocket closed, reconnecting...');
|
|
84
|
+
setIsStreaming(false);
|
|
62
85
|
reconnectTimeoutRef.current = setTimeout(connect, 2000);
|
|
63
86
|
};
|
|
64
87
|
|
|
@@ -85,5 +108,5 @@ export function usePersona(): UsePersonaResult {
|
|
|
85
108
|
};
|
|
86
109
|
}, []);
|
|
87
110
|
|
|
88
|
-
return { persona, isLoading, error };
|
|
111
|
+
return { persona, isStreaming, isLoading, error };
|
|
89
112
|
}
|