@pennyfarthing/cyclist 10.0.3 → 10.1.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 +38 -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 +6 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +11 -0
- package/dist/api/index.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/preload.js +11 -0
- 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 +12 -1
- 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 +80 -66
- package/dist/sprint-data.js.map +1 -1
- package/dist/websocket.d.ts.map +1 -1
- package/dist/websocket.js +6 -5
- package/dist/websocket.js.map +1 -1
- package/package.json +1 -1
- package/src/public/App.tsx +0 -2
- package/src/public/components/AgentLoadDialog.tsx +202 -0
- package/src/public/components/ControlBar.tsx +4 -3
- 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 +144 -0
- package/src/public/components/MessageView.tsx +23 -6
- 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/AcceptanceCriteriaPanel.tsx +15 -30
- package/src/public/components/panels/DebugPanel.tsx +83 -0
- package/src/public/components/panels/GitPanel.tsx +12 -18
- package/src/public/components/panels/SprintPanel.tsx +84 -15
- 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 +5 -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 +77 -0
- package/src/public/hooks/useHotspots.ts +11 -1
- package/src/public/hooks/useSprint.ts +6 -0
- package/src/public/styles/tailwind.css +90 -78
- package/src/public/utils/messageFilters.ts +77 -6
- package/src/public/utils/slash-commands.ts +2 -18
|
@@ -12,6 +12,14 @@ import React, { useState, useEffect } from 'react';
|
|
|
12
12
|
import { Button } from '@/components/ui/button';
|
|
13
13
|
import { Badge } from '@/components/ui/badge';
|
|
14
14
|
import { Separator } from '@/components/ui/separator';
|
|
15
|
+
import { HotspotsDialog } from '../dialogs/HotspotsDialog';
|
|
16
|
+
import { CodeMarkersDialog } from '../dialogs/CodeMarkersDialog';
|
|
17
|
+
import { ComplexityDialog } from '../dialogs/ComplexityDialog';
|
|
18
|
+
import { DependenciesDialog } from '../dialogs/DependenciesDialog';
|
|
19
|
+
import { AgentLoadDialog } from '../AgentLoadDialog';
|
|
20
|
+
import { DeadCodeDialog } from '../DeadCodeDialog';
|
|
21
|
+
import { HealthGauge } from '../HealthGauge';
|
|
22
|
+
import { useHealthScore } from '../../hooks/useHealthScore';
|
|
15
23
|
|
|
16
24
|
/** Context tier type */
|
|
17
25
|
type ContextTier = 'FULL' | 'REFRESH' | 'HANDOFF' | 'MINIMAL';
|
|
@@ -94,6 +102,13 @@ export function DebugPanel(): React.ReactElement {
|
|
|
94
102
|
const [context, setContext] = useState<ContextData | null>(null);
|
|
95
103
|
const [tokenStats, setTokenStats] = useState<Record<string, unknown> | null>(null);
|
|
96
104
|
const [breakdownExpanded, setBreakdownExpanded] = useState(false);
|
|
105
|
+
const [hotspotsOpen, setHotspotsOpen] = useState(false);
|
|
106
|
+
const [codeMarkersOpen, setCodeMarkersOpen] = useState(false);
|
|
107
|
+
const [complexityOpen, setComplexityOpen] = useState(false);
|
|
108
|
+
const [dependenciesOpen, setDependenciesOpen] = useState(false);
|
|
109
|
+
const [agentLoadOpen, setAgentLoadOpen] = useState(false);
|
|
110
|
+
const [deadCodeOpen, setDeadCodeOpen] = useState(false);
|
|
111
|
+
const healthScore = useHealthScore();
|
|
97
112
|
|
|
98
113
|
useEffect(() => {
|
|
99
114
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
@@ -134,6 +149,14 @@ export function DebugPanel(): React.ReactElement {
|
|
|
134
149
|
|
|
135
150
|
return (
|
|
136
151
|
<div className="debug-panel" data-testid="debug-panel">
|
|
152
|
+
<HealthGauge
|
|
153
|
+
score={healthScore.data?.composite_score ?? null}
|
|
154
|
+
dimensions={healthScore.data?.dimensions ?? []}
|
|
155
|
+
totalDimensions={8}
|
|
156
|
+
/>
|
|
157
|
+
|
|
158
|
+
<Separator className="my-3" />
|
|
159
|
+
|
|
137
160
|
<h4>Context Usage</h4>
|
|
138
161
|
{context ? (
|
|
139
162
|
<div className="context-info">
|
|
@@ -261,6 +284,66 @@ export function DebugPanel(): React.ReactElement {
|
|
|
261
284
|
<div className="placeholder">No token stats</div>
|
|
262
285
|
)}
|
|
263
286
|
|
|
287
|
+
<Separator className="my-3" />
|
|
288
|
+
|
|
289
|
+
<h4>Tools</h4>
|
|
290
|
+
<div className="tool-launcher" data-testid="tool-launcher">
|
|
291
|
+
<Button
|
|
292
|
+
variant="outline"
|
|
293
|
+
size="sm"
|
|
294
|
+
onClick={() => setHotspotsOpen(true)}
|
|
295
|
+
data-testid="tool-launcher-hotspots"
|
|
296
|
+
>
|
|
297
|
+
Hotspots
|
|
298
|
+
</Button>
|
|
299
|
+
<Button
|
|
300
|
+
variant="outline"
|
|
301
|
+
size="sm"
|
|
302
|
+
onClick={() => setCodeMarkersOpen(true)}
|
|
303
|
+
data-testid="tool-launcher-codemarkers"
|
|
304
|
+
>
|
|
305
|
+
Code Markers
|
|
306
|
+
</Button>
|
|
307
|
+
<Button
|
|
308
|
+
variant="outline"
|
|
309
|
+
size="sm"
|
|
310
|
+
onClick={() => setDeadCodeOpen(true)}
|
|
311
|
+
data-testid="tool-launcher-deadcode"
|
|
312
|
+
>
|
|
313
|
+
Dead Code
|
|
314
|
+
</Button>
|
|
315
|
+
<Button
|
|
316
|
+
variant="outline"
|
|
317
|
+
size="sm"
|
|
318
|
+
onClick={() => setComplexityOpen(true)}
|
|
319
|
+
data-testid="tool-launcher-complexity"
|
|
320
|
+
>
|
|
321
|
+
Complexity
|
|
322
|
+
</Button>
|
|
323
|
+
<Button
|
|
324
|
+
variant="outline"
|
|
325
|
+
size="sm"
|
|
326
|
+
onClick={() => setDependenciesOpen(true)}
|
|
327
|
+
data-testid="tool-launcher-dependencies"
|
|
328
|
+
>
|
|
329
|
+
Dependencies
|
|
330
|
+
</Button>
|
|
331
|
+
<Button
|
|
332
|
+
variant="outline"
|
|
333
|
+
size="sm"
|
|
334
|
+
onClick={() => setAgentLoadOpen(true)}
|
|
335
|
+
data-testid="tool-launcher-agent-load"
|
|
336
|
+
>
|
|
337
|
+
Analyze All Agents
|
|
338
|
+
</Button>
|
|
339
|
+
</div>
|
|
340
|
+
|
|
341
|
+
<HotspotsDialog open={hotspotsOpen} onOpenChange={setHotspotsOpen} />
|
|
342
|
+
<CodeMarkersDialog open={codeMarkersOpen} onOpenChange={setCodeMarkersOpen} />
|
|
343
|
+
<ComplexityDialog open={complexityOpen} onOpenChange={setComplexityOpen} />
|
|
344
|
+
<DependenciesDialog open={dependenciesOpen} onOpenChange={setDependenciesOpen} />
|
|
345
|
+
<AgentLoadDialog isOpen={agentLoadOpen} onClose={() => setAgentLoadOpen(false)} />
|
|
346
|
+
<DeadCodeDialog isOpen={deadCodeOpen} onClose={() => setDeadCodeOpen(false)} />
|
|
264
347
|
</div>
|
|
265
348
|
);
|
|
266
349
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import React, { useState } from 'react';
|
|
9
|
+
import { RefreshCw } from 'lucide-react';
|
|
9
10
|
import { Button } from '@/components/ui/button';
|
|
10
11
|
import { Badge } from '@/components/ui/badge';
|
|
11
12
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
@@ -90,14 +91,14 @@ function RepoStatus({ repo, onPullDevelop }: RepoStatusProps): React.ReactElemen
|
|
|
90
91
|
<span className="branch-name">{branch}</span>
|
|
91
92
|
</div>
|
|
92
93
|
|
|
93
|
-
{(ahead !== undefined && ahead > 0) || (behind !== undefined && behind > 0)
|
|
94
|
+
{((ahead !== undefined && ahead > 0) || (behind !== undefined && behind > 0) || hasDevelopUpdates) && (
|
|
94
95
|
<div className="sync-status">
|
|
95
96
|
{ahead !== undefined && ahead > 0 && (
|
|
96
97
|
<Tooltip>
|
|
97
98
|
<TooltipTrigger asChild>
|
|
98
99
|
<span className="ahead">↑{ahead}</span>
|
|
99
100
|
</TooltipTrigger>
|
|
100
|
-
<TooltipContent>Commits ahead</TooltipContent>
|
|
101
|
+
<TooltipContent>Commits ahead of remote</TooltipContent>
|
|
101
102
|
</Tooltip>
|
|
102
103
|
)}
|
|
103
104
|
{behind !== undefined && behind > 0 && (
|
|
@@ -105,29 +106,22 @@ function RepoStatus({ repo, onPullDevelop }: RepoStatusProps): React.ReactElemen
|
|
|
105
106
|
<TooltipTrigger asChild>
|
|
106
107
|
<span className="behind">↓{behind}</span>
|
|
107
108
|
</TooltipTrigger>
|
|
108
|
-
<TooltipContent>Commits behind</TooltipContent>
|
|
109
|
+
<TooltipContent>Commits behind remote</TooltipContent>
|
|
109
110
|
</Tooltip>
|
|
110
111
|
)}
|
|
111
|
-
|
|
112
|
-
) : null}
|
|
113
|
-
|
|
114
|
-
{hasDevelopUpdates && (
|
|
115
|
-
<div className="develop-behind-warning">
|
|
116
|
-
<span className="warning-icon">⚠️</span>
|
|
117
|
-
<span className="warning-text">develop is {developBehind} commit{developBehind > 1 ? 's' : ''} ahead</span>
|
|
118
|
-
{onPullDevelop && (
|
|
112
|
+
{hasDevelopUpdates && onPullDevelop && (
|
|
119
113
|
<Tooltip>
|
|
120
114
|
<TooltipTrigger asChild>
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
size="sm"
|
|
124
|
-
className="pull-develop-btn"
|
|
115
|
+
<button
|
|
116
|
+
className="sync-develop-btn"
|
|
125
117
|
onClick={() => onPullDevelop(name, path)}
|
|
118
|
+
aria-label={`Pull ${developBehind} commits from develop`}
|
|
126
119
|
>
|
|
127
|
-
|
|
128
|
-
|
|
120
|
+
<RefreshCw size={12} />
|
|
121
|
+
<span>{developBehind}</span>
|
|
122
|
+
</button>
|
|
129
123
|
</TooltipTrigger>
|
|
130
|
-
<TooltipContent>
|
|
124
|
+
<TooltipContent>develop is {developBehind} commit{developBehind > 1 ? 's' : ''} ahead — click to pull</TooltipContent>
|
|
131
125
|
</Tooltip>
|
|
132
126
|
)}
|
|
133
127
|
</div>
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* Story MSSCI-14189 - Enhanced Sprint Panel with story management and epic actions
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import React, { useState, useEffect, useCallback } from 'react';
|
|
8
|
+
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
9
|
+
import { Check, Loader, Circle, AlertTriangle } from 'lucide-react';
|
|
9
10
|
import { Button } from '@/components/ui/button';
|
|
10
11
|
import { Badge } from '@/components/ui/badge';
|
|
11
12
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
@@ -85,6 +86,21 @@ export function SprintPanel(): React.ReactElement {
|
|
|
85
86
|
// Enhanced Sprint Panel (MSSCI-14189)
|
|
86
87
|
// =============================================================================
|
|
87
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Format email to short display name: "keith.avery@..." -> "K. Avery"
|
|
91
|
+
*/
|
|
92
|
+
function formatAssignee(email: string | null | undefined): string | null {
|
|
93
|
+
if (!email) return null;
|
|
94
|
+
const local = email.split('@')[0];
|
|
95
|
+
const parts = local.split('.');
|
|
96
|
+
if (parts.length >= 2) {
|
|
97
|
+
const first = parts[0].charAt(0).toUpperCase();
|
|
98
|
+
const last = parts[parts.length - 1].charAt(0).toUpperCase() + parts[parts.length - 1].slice(1);
|
|
99
|
+
return `${first}. ${last}`;
|
|
100
|
+
}
|
|
101
|
+
return local;
|
|
102
|
+
}
|
|
103
|
+
|
|
88
104
|
/**
|
|
89
105
|
* Calculate epic progress (done points / total points)
|
|
90
106
|
*/
|
|
@@ -106,17 +122,18 @@ function isEpicCompleted(epic: SprintEpic): boolean {
|
|
|
106
122
|
/**
|
|
107
123
|
* Get status badge content and class for a story status
|
|
108
124
|
*/
|
|
109
|
-
function getStatusBadgeInfo(status: SprintStory['status']): { icon:
|
|
125
|
+
function getStatusBadgeInfo(status: SprintStory['status']): { icon: React.ReactElement; className: string } {
|
|
126
|
+
const size = 12;
|
|
110
127
|
switch (status) {
|
|
111
128
|
case 'done':
|
|
112
|
-
return { icon:
|
|
129
|
+
return { icon: <Check size={size} />, className: 'status-done' };
|
|
113
130
|
case 'in_progress':
|
|
114
|
-
return { icon:
|
|
131
|
+
return { icon: <Loader size={size} />, className: 'status-in-progress' };
|
|
115
132
|
case 'blocked':
|
|
116
|
-
return { icon:
|
|
133
|
+
return { icon: <AlertTriangle size={size} />, className: 'status-blocked' };
|
|
117
134
|
case 'backlog':
|
|
118
135
|
default:
|
|
119
|
-
return { icon:
|
|
136
|
+
return { icon: <Circle size={size} />, className: 'status-backlog' };
|
|
120
137
|
}
|
|
121
138
|
}
|
|
122
139
|
|
|
@@ -157,6 +174,22 @@ function ContextIndicator({
|
|
|
157
174
|
);
|
|
158
175
|
}
|
|
159
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Priority dot component - small color-coded circle
|
|
179
|
+
*/
|
|
180
|
+
function PriorityDot({ priority, storyId }: { priority?: string | null; storyId: string }): React.ReactElement | null {
|
|
181
|
+
if (!priority) return null;
|
|
182
|
+
const colorClass = priority === 'P0' ? 'priority-p0' : priority === 'P1' ? 'priority-p1' : 'priority-p2';
|
|
183
|
+
return (
|
|
184
|
+
<span
|
|
185
|
+
className={`priority-dot ${colorClass}`}
|
|
186
|
+
data-testid={`story-priority-${storyId}`}
|
|
187
|
+
data-priority={priority}
|
|
188
|
+
title={priority}
|
|
189
|
+
/>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
160
193
|
/**
|
|
161
194
|
* Status badge component for stories
|
|
162
195
|
*/
|
|
@@ -182,12 +215,16 @@ function JiraLink({ jiraKey, storyId }: { jiraKey: string; storyId: string }): R
|
|
|
182
215
|
const handleClick = (e: React.MouseEvent) => {
|
|
183
216
|
e.preventDefault();
|
|
184
217
|
const url = getJiraUrl(jiraKey);
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
(
|
|
188
|
-
|
|
189
|
-
|
|
218
|
+
try {
|
|
219
|
+
const api = (window as any).electronAPI;
|
|
220
|
+
if (api?.shell?.openExternal) {
|
|
221
|
+
api.shell.openExternal(url);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
} catch {
|
|
225
|
+
// electronAPI not available or call failed
|
|
190
226
|
}
|
|
227
|
+
window.open(url, '_blank');
|
|
191
228
|
};
|
|
192
229
|
|
|
193
230
|
return (
|
|
@@ -213,12 +250,14 @@ export function EnhancedSprintPanel(): React.ReactElement {
|
|
|
213
250
|
const [confirmArchive, setConfirmArchive] = useState<string | null>(null);
|
|
214
251
|
const [actionError, setActionError] = useState<Error | null>(null);
|
|
215
252
|
|
|
216
|
-
// Expand all epics by default when data first loads
|
|
253
|
+
// Expand all epics by default when data first loads (once only)
|
|
254
|
+
const hasInitializedExpansion = useRef(false);
|
|
217
255
|
useEffect(() => {
|
|
218
|
-
if (data?.epics &&
|
|
256
|
+
if (data?.epics && !hasInitializedExpansion.current) {
|
|
257
|
+
hasInitializedExpansion.current = true;
|
|
219
258
|
setExpandedEpics(new Set(data.epics.map((e) => e.id)));
|
|
220
259
|
}
|
|
221
|
-
}, [data?.epics
|
|
260
|
+
}, [data?.epics]);
|
|
222
261
|
|
|
223
262
|
// Toggle epic expansion
|
|
224
263
|
const toggleEpic = useCallback((epicId: string) => {
|
|
@@ -464,6 +503,7 @@ export function EnhancedSprintPanel(): React.ReactElement {
|
|
|
464
503
|
{epic.stories.map((story) => {
|
|
465
504
|
const hasContext = story.hasContext ?? false;
|
|
466
505
|
const isBlocked = story.status === 'blocked';
|
|
506
|
+
const assigneeDisplay = formatAssignee(story.assignedTo);
|
|
467
507
|
return (
|
|
468
508
|
<div
|
|
469
509
|
key={story.id}
|
|
@@ -473,9 +513,38 @@ export function EnhancedSprintPanel(): React.ReactElement {
|
|
|
473
513
|
data-story-id={story.id}
|
|
474
514
|
aria-label={`${story.id}: ${story.title}`}
|
|
475
515
|
>
|
|
516
|
+
<PriorityDot priority={story.priority} storyId={story.id} />
|
|
476
517
|
<StatusBadge status={story.status} storyId={story.id} />
|
|
477
518
|
{story.jiraKey && <JiraLink jiraKey={story.jiraKey} storyId={story.id} />}
|
|
478
|
-
<
|
|
519
|
+
<div className="story-info">
|
|
520
|
+
<span className="story-title">{story.title}</span>
|
|
521
|
+
<span className="story-meta">
|
|
522
|
+
{assigneeDisplay && (
|
|
523
|
+
<span
|
|
524
|
+
className="story-assignee"
|
|
525
|
+
data-testid={`story-assignee-${story.id}`}
|
|
526
|
+
>
|
|
527
|
+
{assigneeDisplay}
|
|
528
|
+
</span>
|
|
529
|
+
)}
|
|
530
|
+
{story.workflow && (
|
|
531
|
+
<span
|
|
532
|
+
className="story-workflow-badge"
|
|
533
|
+
data-testid={`story-workflow-${story.id}`}
|
|
534
|
+
>
|
|
535
|
+
{story.workflow}
|
|
536
|
+
</span>
|
|
537
|
+
)}
|
|
538
|
+
{story.status === 'done' && story.completed && (
|
|
539
|
+
<span
|
|
540
|
+
className="story-completed-date"
|
|
541
|
+
data-testid={`story-completed-${story.id}`}
|
|
542
|
+
>
|
|
543
|
+
{story.completed}
|
|
544
|
+
</span>
|
|
545
|
+
)}
|
|
546
|
+
</span>
|
|
547
|
+
</div>
|
|
479
548
|
<ContextIndicator hasContext={hasContext} testIdPrefix="story" id={story.id} />
|
|
480
549
|
<span
|
|
481
550
|
className="story-points"
|
|
@@ -18,7 +18,6 @@ export { DebugPanel } from './DebugPanel';
|
|
|
18
18
|
export { SettingsPanel } from './SettingsPanel';
|
|
19
19
|
export { AuditLogPanel } from './AuditLogPanel';
|
|
20
20
|
export { TTYPanel } from './TTYPanel';
|
|
21
|
-
export { HotspotsPanel } from './HotspotsPanel';
|
|
22
21
|
|
|
23
22
|
// Legacy exports - kept for backwards compatibility and tests
|
|
24
23
|
export { AcceptanceCriteriaPanel, ConnectedAcceptanceCriteriaPanel } from './AcceptanceCriteriaPanel';
|
|
@@ -36,7 +36,7 @@ const DialogContent = React.forwardRef<
|
|
|
36
36
|
<DialogPrimitive.Content
|
|
37
37
|
ref={ref}
|
|
38
38
|
className={cn(
|
|
39
|
-
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-
|
|
39
|
+
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-[var(--border)] bg-card text-card-foreground p-6 shadow-2xl shadow-black/40 duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
|
40
40
|
className
|
|
41
41
|
)}
|
|
42
42
|
{...props}
|
|
@@ -86,7 +86,7 @@ const DialogTitle = React.forwardRef<
|
|
|
86
86
|
<DialogPrimitive.Title
|
|
87
87
|
ref={ref}
|
|
88
88
|
className={cn(
|
|
89
|
-
"text-
|
|
89
|
+
"text-base font-medium leading-none tracking-tight",
|
|
90
90
|
className
|
|
91
91
|
)}
|
|
92
92
|
{...props}
|
|
@@ -100,7 +100,7 @@ const DialogDescription = React.forwardRef<
|
|
|
100
100
|
>(({ className, ...props }, ref) => (
|
|
101
101
|
<DialogPrimitive.Description
|
|
102
102
|
ref={ref}
|
|
103
|
-
className={cn("text-sm text-
|
|
103
|
+
className={cn("text-sm text-[var(--text-secondary)]", className)}
|
|
104
104
|
{...props}
|
|
105
105
|
/>
|
|
106
106
|
))
|
|
@@ -108,18 +108,12 @@
|
|
|
108
108
|
|
|
109
109
|
/* =============================================================================
|
|
110
110
|
Utility Classes for Theme Colors
|
|
111
|
-
============================================================================= */
|
|
112
|
-
|
|
113
|
-
/* Backgrounds */
|
|
114
|
-
.bg-primary { background-color: var(--bg-primary); }
|
|
115
|
-
.bg-secondary { background-color: var(--bg-secondary); }
|
|
116
|
-
.bg-tertiary { background-color: var(--bg-tertiary); }
|
|
117
111
|
|
|
118
|
-
|
|
119
|
-
.
|
|
120
|
-
.
|
|
121
|
-
|
|
122
|
-
|
|
112
|
+
NOTE: .bg-primary, .text-secondary, .text-muted etc. are generated by
|
|
113
|
+
Tailwind from tailwind.config.js. Do NOT duplicate them here — specificity
|
|
114
|
+
conflicts cause unpredictable results. Only define classes that Tailwind
|
|
115
|
+
does NOT generate (status colors, border-default, border-focus).
|
|
116
|
+
============================================================================= */
|
|
123
117
|
|
|
124
118
|
/* Status */
|
|
125
119
|
.text-success { color: var(--status-success); }
|
|
@@ -43,3 +43,7 @@ export type { UseMarkdownParserResult } from './useMarkdownParser';
|
|
|
43
43
|
|
|
44
44
|
export { useSyntaxHighlighter } from './useSyntaxHighlighter';
|
|
45
45
|
export type { UseSyntaxHighlighterResult } from './useSyntaxHighlighter';
|
|
46
|
+
|
|
47
|
+
// Agent load analysis
|
|
48
|
+
export { useAgentLoad } from './useAgentLoad';
|
|
49
|
+
export type { AgentLoadData, AgentLoadEntry, PruneResult } from './useAgentLoad';
|
|
@@ -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;
|