@pennyfarthing/cyclist 10.0.2 → 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/api/theme-agents.d.ts +1 -0
- package/dist/api/theme-agents.d.ts.map +1 -1
- package/dist/api/theme-agents.js +1 -1
- package/dist/api/theme-agents.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.js +9 -3
- package/dist/main.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 +16 -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 +79 -3
- 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 +32 -31
- 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 +25 -22
- package/src/public/components/SubagentSpan.tsx +2 -2
- 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
- package/src/public/utils/subagent-display.ts +6 -3
- package/LICENSE +0 -14
- package/src/public/components/AskUserQuestionBlock.tsx +0 -162
- package/src/public/utils/askUserQuestion.ts +0 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pennyfarthing/cyclist",
|
|
3
|
-
"version": "10.0
|
|
3
|
+
"version": "10.1.0",
|
|
4
4
|
"description": "Visual terminal interface for Claude Code",
|
|
5
5
|
"author": "1898andCo",
|
|
6
6
|
"type": "module",
|
|
@@ -19,7 +19,36 @@
|
|
|
19
19
|
"src/public/",
|
|
20
20
|
"portraits/"
|
|
21
21
|
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"dev": "npm run build && concurrently -k -n tsc,preload,vite,electron -c blue,cyan,magenta,green \"tsc --watch --preserveWatchOutput\" \"tsc -p tsconfig.preload.json --watch --preserveWatchOutput\" \"vite build --watch\" \"electron dist/main.js\"",
|
|
24
|
+
"dev:cdp": "npm run build && concurrently -k -n tsc,preload,vite,electron -c blue,cyan,magenta,green \"tsc --watch --preserveWatchOutput\" \"tsc -p tsconfig.preload.json --watch --preserveWatchOutput\" \"vite build --watch\" \"electron --remote-debugging-port=9222 dist/main.js\"",
|
|
25
|
+
"dev:once": "npm run build && electron dist/main.js",
|
|
26
|
+
"dev:web": "CYCLIST_ELECTRON_MODE= CYCLIST_DEV_WEB=1 CYCLIST_PROJECT_DIR=${CYCLIST_PROJECT_DIR:-$PWD} tsx watch src/server.ts",
|
|
27
|
+
"dev:server": "tsx watch src/server.ts",
|
|
28
|
+
"dev:debug": "CYCLIST_DEV_WEB=1 OTEL_DEBUG=true CYCLIST_PROJECT_DIR=${CYCLIST_PROJECT_DIR:-$PWD} node --inspect src/server.ts",
|
|
29
|
+
"dev:debug-brk": "CYCLIST_DEV_WEB=1 OTEL_DEBUG=true CYCLIST_PROJECT_DIR=${CYCLIST_PROJECT_DIR:-$PWD} node --inspect-brk src/server.ts",
|
|
30
|
+
"dev:electron-debug": "npm run build && concurrently -k -n tsc,preload,vite,electron -c blue,cyan,magenta,green \"tsc --watch --preserveWatchOutput\" \"tsc -p tsconfig.preload.json --watch --preserveWatchOutput\" \"vite build --watch\" \"electron --inspect=9229 dist/main.js\"",
|
|
31
|
+
"dev:vite": "vite --config vite.config.ts",
|
|
32
|
+
"build": "npm run build:commands && tsc --build && tsc -p tsconfig.preload.json && npm run build:react",
|
|
33
|
+
"build:react": "vite build",
|
|
34
|
+
"build:commands": "node scripts/generate-slash-commands.js",
|
|
35
|
+
"build:electron": "npm run build && electron-builder -c.npmRebuild=false",
|
|
36
|
+
"start": "node dist/server.js",
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"test:watch": "vitest",
|
|
39
|
+
"test:e2e": "playwright test",
|
|
40
|
+
"test:e2e:web": "playwright test --project=web-chromium",
|
|
41
|
+
"test:e2e:ui": "playwright test --ui",
|
|
42
|
+
"test:e2e:debug": "playwright test --debug",
|
|
43
|
+
"test:e2e:trace": "playwright show-trace",
|
|
44
|
+
"prepack": "./scripts/copy-portraits.sh",
|
|
45
|
+
"install:app": "./scripts/install-app.sh",
|
|
46
|
+
"install:cli": "./scripts/install-cli.sh",
|
|
47
|
+
"install:all": "npm run install:app && npm run install:cli"
|
|
48
|
+
},
|
|
22
49
|
"dependencies": {
|
|
50
|
+
"@pennyfarthing/core": "workspace:^",
|
|
51
|
+
"@pennyfarthing/shared": "workspace:^",
|
|
23
52
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
|
24
53
|
"@radix-ui/react-checkbox": "^1.3.3",
|
|
25
54
|
"@radix-ui/react-collapsible": "^1.1.12",
|
|
@@ -49,9 +78,7 @@
|
|
|
49
78
|
"ws": "^8.19.0",
|
|
50
79
|
"xterm": "^5.3.0",
|
|
51
80
|
"xterm-addon-fit": "^0.8.0",
|
|
52
|
-
"yaml": "^2.8.2"
|
|
53
|
-
"@pennyfarthing/core": "^10.0.2",
|
|
54
|
-
"@pennyfarthing/shared": "^10.0.2"
|
|
81
|
+
"yaml": "^2.8.2"
|
|
55
82
|
},
|
|
56
83
|
"devDependencies": {
|
|
57
84
|
"@electron/rebuild": "^3.6.0",
|
|
@@ -159,31 +186,5 @@
|
|
|
159
186
|
],
|
|
160
187
|
"category": "Development"
|
|
161
188
|
}
|
|
162
|
-
},
|
|
163
|
-
"scripts": {
|
|
164
|
-
"dev": "npm run build && concurrently -k -n tsc,preload,vite,electron -c blue,cyan,magenta,green \"tsc --watch --preserveWatchOutput\" \"tsc -p tsconfig.preload.json --watch --preserveWatchOutput\" \"vite build --watch\" \"electron dist/main.js\"",
|
|
165
|
-
"dev:cdp": "npm run build && concurrently -k -n tsc,preload,vite,electron -c blue,cyan,magenta,green \"tsc --watch --preserveWatchOutput\" \"tsc -p tsconfig.preload.json --watch --preserveWatchOutput\" \"vite build --watch\" \"electron --remote-debugging-port=9222 dist/main.js\"",
|
|
166
|
-
"dev:once": "npm run build && electron dist/main.js",
|
|
167
|
-
"dev:web": "CYCLIST_ELECTRON_MODE= CYCLIST_DEV_WEB=1 CYCLIST_PROJECT_DIR=${CYCLIST_PROJECT_DIR:-$PWD} tsx watch src/server.ts",
|
|
168
|
-
"dev:server": "tsx watch src/server.ts",
|
|
169
|
-
"dev:debug": "CYCLIST_DEV_WEB=1 OTEL_DEBUG=true CYCLIST_PROJECT_DIR=${CYCLIST_PROJECT_DIR:-$PWD} node --inspect src/server.ts",
|
|
170
|
-
"dev:debug-brk": "CYCLIST_DEV_WEB=1 OTEL_DEBUG=true CYCLIST_PROJECT_DIR=${CYCLIST_PROJECT_DIR:-$PWD} node --inspect-brk src/server.ts",
|
|
171
|
-
"dev:electron-debug": "npm run build && concurrently -k -n tsc,preload,vite,electron -c blue,cyan,magenta,green \"tsc --watch --preserveWatchOutput\" \"tsc -p tsconfig.preload.json --watch --preserveWatchOutput\" \"vite build --watch\" \"electron --inspect=9229 dist/main.js\"",
|
|
172
|
-
"dev:vite": "vite --config vite.config.ts",
|
|
173
|
-
"build": "npm run build:commands && tsc --build && tsc -p tsconfig.preload.json && npm run build:react",
|
|
174
|
-
"build:react": "vite build",
|
|
175
|
-
"build:commands": "node scripts/generate-slash-commands.js",
|
|
176
|
-
"build:electron": "npm run build && electron-builder -c.npmRebuild=false",
|
|
177
|
-
"start": "node dist/server.js",
|
|
178
|
-
"test": "vitest run",
|
|
179
|
-
"test:watch": "vitest",
|
|
180
|
-
"test:e2e": "playwright test",
|
|
181
|
-
"test:e2e:web": "playwright test --project=web-chromium",
|
|
182
|
-
"test:e2e:ui": "playwright test --ui",
|
|
183
|
-
"test:e2e:debug": "playwright test --debug",
|
|
184
|
-
"test:e2e:trace": "playwright show-trace",
|
|
185
|
-
"install:app": "./scripts/install-app.sh",
|
|
186
|
-
"install:cli": "./scripts/install-cli.sh",
|
|
187
|
-
"install:all": "npm run install:app && npm run install:cli"
|
|
188
189
|
}
|
|
189
|
-
}
|
|
190
|
+
}
|
package/src/public/App.tsx
CHANGED
|
@@ -42,7 +42,6 @@ import {
|
|
|
42
42
|
SettingsPanel,
|
|
43
43
|
AuditLogPanel,
|
|
44
44
|
TTYPanel,
|
|
45
|
-
HotspotsPanel,
|
|
46
45
|
} from './components/panels';
|
|
47
46
|
|
|
48
47
|
// =============================================================================
|
|
@@ -68,7 +67,6 @@ registerPanelComponent(PANEL_INVENTORY.AC, ACPanel);
|
|
|
68
67
|
registerPanelComponent(PANEL_INVENTORY.TODO, TodoPanel);
|
|
69
68
|
registerPanelComponent(PANEL_INVENTORY.BACKGROUND, BackgroundPanel);
|
|
70
69
|
registerPanelComponent(PANEL_INVENTORY.GIT, GitPanel);
|
|
71
|
-
registerPanelComponent(PANEL_INVENTORY.HOTSPOTS, HotspotsPanel);
|
|
72
70
|
registerPanelComponent(PANEL_INVENTORY.SETTINGS, SettingsPanel);
|
|
73
71
|
|
|
74
72
|
// =============================================================================
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import React, { useState, useEffect, useMemo } from 'react';
|
|
2
|
+
import { Button } from '@/components/ui/button';
|
|
3
|
+
import { Progress } from '@/components/ui/progress';
|
|
4
|
+
import { Skeleton } from '@/components/ui/skeleton';
|
|
5
|
+
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@/components/ui/collapsible';
|
|
6
|
+
import { ToolDialog } from './dialogs/ToolDialog';
|
|
7
|
+
import { ConfirmDialog, useConfirmDialog } from './ConfirmDialog';
|
|
8
|
+
import { useAgentLoad } from '../hooks/useAgentLoad.js';
|
|
9
|
+
import type { AgentLoadEntry } from '../hooks/useAgentLoad.js';
|
|
10
|
+
import { formatComponentName } from './panels/DebugPanel';
|
|
11
|
+
|
|
12
|
+
const SIDECAR_FILES = ['patterns.md', 'gotchas.md', 'decisions.md'] as const;
|
|
13
|
+
|
|
14
|
+
export interface AgentLoadDialogProps {
|
|
15
|
+
isOpen: boolean;
|
|
16
|
+
onClose: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function AgentLoadDialog({ isOpen, onClose }: AgentLoadDialogProps): React.ReactElement {
|
|
20
|
+
const { data, isLoading, error, refresh, pruneSidecar, pruneResult } = useAgentLoad();
|
|
21
|
+
const [expandedAgent, setExpandedAgent] = useState<string | null>(null);
|
|
22
|
+
const [pendingPrune, setPendingPrune] = useState<{ agent: string; file: string } | null>(null);
|
|
23
|
+
|
|
24
|
+
const { confirm, dialogProps } = useConfirmDialog({
|
|
25
|
+
title: 'Clear Sidecar',
|
|
26
|
+
message: pendingPrune
|
|
27
|
+
? `Reset ${pendingPrune.file} for ${pendingPrune.agent}? This will replace the sidecar with its default template.`
|
|
28
|
+
: '',
|
|
29
|
+
confirmLabel: 'Confirm',
|
|
30
|
+
isDanger: true,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (isOpen) {
|
|
35
|
+
refresh();
|
|
36
|
+
}
|
|
37
|
+
}, [isOpen, refresh]);
|
|
38
|
+
|
|
39
|
+
const sortedAgents = useMemo(() => {
|
|
40
|
+
if (!data) return [];
|
|
41
|
+
return [...data.agents].sort((a, b) => (b.totalTokens ?? 0) - (a.totalTokens ?? 0));
|
|
42
|
+
}, [data]);
|
|
43
|
+
|
|
44
|
+
const maxTokens = useMemo(() => {
|
|
45
|
+
if (!sortedAgents.length) return 0;
|
|
46
|
+
return sortedAgents[0]?.totalTokens ?? 0;
|
|
47
|
+
}, [sortedAgents]);
|
|
48
|
+
|
|
49
|
+
const handleRowClick = (agent: string) => {
|
|
50
|
+
setExpandedAgent((prev) => (prev === agent ? null : agent));
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const handleClear = async (agent: string, file: string) => {
|
|
54
|
+
setPendingPrune({ agent, file });
|
|
55
|
+
const confirmed = await confirm();
|
|
56
|
+
if (confirmed) {
|
|
57
|
+
await pruneSidecar(agent, file);
|
|
58
|
+
}
|
|
59
|
+
setPendingPrune(null);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const renderAgentRow = (entry: AgentLoadEntry) => {
|
|
63
|
+
const tokens = entry.totalTokens ?? 0;
|
|
64
|
+
const progressValue = maxTokens > 0 ? (tokens / maxTokens) * 100 : 0;
|
|
65
|
+
const isExpanded = expandedAgent === entry.agent;
|
|
66
|
+
// Color by token threshold: <3k green, <5k orange, >=5k red
|
|
67
|
+
const barColor = tokens >= 5000
|
|
68
|
+
? 'bg-[var(--status-error,#f14c4c)]'
|
|
69
|
+
: tokens >= 3000
|
|
70
|
+
? 'bg-[var(--status-warning,#cca700)]'
|
|
71
|
+
: 'bg-[var(--status-success,#4ec9b0)]';
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<Collapsible
|
|
75
|
+
key={entry.agent}
|
|
76
|
+
open={isExpanded}
|
|
77
|
+
onOpenChange={() => handleRowClick(entry.agent)}
|
|
78
|
+
>
|
|
79
|
+
<CollapsibleTrigger asChild>
|
|
80
|
+
<div
|
|
81
|
+
className="px-4 py-2 cursor-pointer hover:bg-muted/50 rounded-md transition-colors"
|
|
82
|
+
data-testid={`agent-row-${entry.agent}`}
|
|
83
|
+
>
|
|
84
|
+
<div className="flex items-center justify-between mb-1">
|
|
85
|
+
<span className="font-mono text-sm font-medium">{entry.agent}</span>
|
|
86
|
+
<span className="font-mono text-sm tabular-nums text-text-secondary">
|
|
87
|
+
{tokens.toLocaleString()}
|
|
88
|
+
</span>
|
|
89
|
+
</div>
|
|
90
|
+
<Progress
|
|
91
|
+
value={progressValue}
|
|
92
|
+
className="h-2.5 bg-[var(--border)]"
|
|
93
|
+
indicatorClassName={barColor}
|
|
94
|
+
/>
|
|
95
|
+
</div>
|
|
96
|
+
</CollapsibleTrigger>
|
|
97
|
+
<CollapsibleContent>
|
|
98
|
+
<div className="mx-4 mb-3 rounded-md bg-muted/30 p-3 space-y-1">
|
|
99
|
+
{entry.components && entry.components.length > 0 ? (
|
|
100
|
+
entry.components.map((comp) => (
|
|
101
|
+
<div key={comp.name} className="flex items-center justify-between text-xs text-text-secondary">
|
|
102
|
+
<span>{formatComponentName(comp.name)}</span>
|
|
103
|
+
<span className="tabular-nums">{comp.tokens.toLocaleString()}</span>
|
|
104
|
+
</div>
|
|
105
|
+
))
|
|
106
|
+
) : (
|
|
107
|
+
<div className="text-xs text-text-secondary">No component breakdown available</div>
|
|
108
|
+
)}
|
|
109
|
+
<div className="pt-2 border-t border-muted/50 mt-2">
|
|
110
|
+
<div className="text-xs font-medium mb-1.5 text-text-secondary">Sidecars</div>
|
|
111
|
+
<div className="flex gap-2">
|
|
112
|
+
{SIDECAR_FILES.map((file) => (
|
|
113
|
+
<Button
|
|
114
|
+
key={file}
|
|
115
|
+
variant="ghost"
|
|
116
|
+
size="sm"
|
|
117
|
+
className="text-xs h-6"
|
|
118
|
+
onClick={(e) => {
|
|
119
|
+
e.stopPropagation();
|
|
120
|
+
handleClear(entry.agent, file);
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
Clear {file}
|
|
124
|
+
</Button>
|
|
125
|
+
))}
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</CollapsibleContent>
|
|
130
|
+
</Collapsible>
|
|
131
|
+
);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const renderContent = () => {
|
|
135
|
+
if (isLoading) {
|
|
136
|
+
return (
|
|
137
|
+
<div className="space-y-3 p-4">
|
|
138
|
+
{Array.from({ length: 5 }).map((_, i) => (
|
|
139
|
+
<div key={i} data-testid={`skeleton-${i}`} className="space-y-1.5 px-4">
|
|
140
|
+
<div className="flex justify-between">
|
|
141
|
+
<Skeleton className="h-4 w-24" />
|
|
142
|
+
<Skeleton className="h-4 w-16" />
|
|
143
|
+
</div>
|
|
144
|
+
<Skeleton className="h-2.5 w-full rounded-full" />
|
|
145
|
+
</div>
|
|
146
|
+
))}
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (error) {
|
|
152
|
+
return (
|
|
153
|
+
<div className="p-4 space-y-3">
|
|
154
|
+
<div className="p-4 rounded border border-[var(--status-error)]/20 bg-[var(--status-error)]/5 text-[var(--status-error)] text-sm">
|
|
155
|
+
{error.message}
|
|
156
|
+
</div>
|
|
157
|
+
<div className="text-center">
|
|
158
|
+
<Button variant="outline" size="sm" onClick={refresh}>
|
|
159
|
+
Retry
|
|
160
|
+
</Button>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!data) return null;
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<div className="space-y-0.5">
|
|
170
|
+
{pruneResult?.success && pruneResult.tokensFreed != null && (
|
|
171
|
+
<div className="text-xs text-green-600 px-4 py-1">
|
|
172
|
+
Freed {pruneResult.tokensFreed.toLocaleString()} tokens from {pruneResult.agent}/{pruneResult.file}
|
|
173
|
+
</div>
|
|
174
|
+
)}
|
|
175
|
+
<div className="flex items-center justify-between px-4 py-2">
|
|
176
|
+
<span data-testid="cached-at" className="text-xs text-[var(--text-muted)]">
|
|
177
|
+
Cached: {new Date(data.cachedAt).toLocaleString()}
|
|
178
|
+
</span>
|
|
179
|
+
<Button variant="ghost" size="sm" className="text-xs h-6 text-[var(--text-muted)] hover:text-[var(--text-primary)]" onClick={refresh}>
|
|
180
|
+
Refresh
|
|
181
|
+
</Button>
|
|
182
|
+
</div>
|
|
183
|
+
{sortedAgents.map((entry) => renderAgentRow(entry))}
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<>
|
|
190
|
+
<ToolDialog
|
|
191
|
+
open={isOpen}
|
|
192
|
+
onOpenChange={(open) => { if (!open) onClose(); }}
|
|
193
|
+
title="Agent Load Analysis"
|
|
194
|
+
description="Token usage breakdown for all agents"
|
|
195
|
+
className="max-w-2xl"
|
|
196
|
+
>
|
|
197
|
+
{renderContent()}
|
|
198
|
+
</ToolDialog>
|
|
199
|
+
<ConfirmDialog {...dialogProps} />
|
|
200
|
+
</>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import React, { useEffect, useRef, useCallback, useState, FocusEvent } from 'react';
|
|
19
|
+
import { BellRing, Zap, RotateCcw } from 'lucide-react';
|
|
19
20
|
import { Button } from '@/components/ui/button';
|
|
20
21
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
21
22
|
import { useClaudeContext } from '../contexts/ClaudeContext';
|
|
@@ -149,7 +150,7 @@ export function ControlBar({
|
|
|
149
150
|
aria-pressed={bellMode}
|
|
150
151
|
aria-label="Bell mode - inject queued messages via hook"
|
|
151
152
|
>
|
|
152
|
-
<
|
|
153
|
+
<BellRing className="h-4 w-4" />
|
|
153
154
|
</Button>
|
|
154
155
|
</TooltipTrigger>
|
|
155
156
|
<TooltipContent>Bell Mode: Inject queued messages during tool use (Cmd+B)</TooltipContent>
|
|
@@ -168,7 +169,7 @@ export function ControlBar({
|
|
|
168
169
|
aria-pressed={relayMode}
|
|
169
170
|
aria-label="Relay mode - auto-handoff to next agent"
|
|
170
171
|
>
|
|
171
|
-
<
|
|
172
|
+
<Zap className="h-4 w-4" />
|
|
172
173
|
</Button>
|
|
173
174
|
</TooltipTrigger>
|
|
174
175
|
<TooltipContent>Relay Mode: Auto-handoff to next agent (Cmd+4)</TooltipContent>
|
|
@@ -187,7 +188,7 @@ export function ControlBar({
|
|
|
187
188
|
disabled={!currentAgent}
|
|
188
189
|
aria-label="TirePump: Clear context and reload agent"
|
|
189
190
|
>
|
|
190
|
-
<
|
|
191
|
+
<RotateCcw className="h-4 w-4" />
|
|
191
192
|
</Button>
|
|
192
193
|
</TooltipTrigger>
|
|
193
194
|
<TooltipContent>{currentAgent ? `TirePump: Clear context (${contextPercent}%) and reload ${currentAgent}` : 'TirePump: No agent loaded'}</TooltipContent>
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import React, { useState, useMemo, useCallback, useEffect } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription,
|
|
4
|
+
} from '@/components/ui/dialog';
|
|
5
|
+
import { Badge } from '@/components/ui/badge';
|
|
6
|
+
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
7
|
+
import { cn } from '@/lib/utils';
|
|
8
|
+
import { useDeadCode, type StaleFile, type UnusedExport } from '@/hooks/useDeadCode';
|
|
9
|
+
|
|
10
|
+
export interface DeadCodeDialogProps {
|
|
11
|
+
isOpen: boolean;
|
|
12
|
+
onClose: () => void;
|
|
13
|
+
days?: number;
|
|
14
|
+
repo?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type TabId = 'stale' | 'exports';
|
|
18
|
+
type SortDir = 'asc' | 'desc';
|
|
19
|
+
|
|
20
|
+
export function DeadCodeDialog({ isOpen, onClose, days = 180, repo }: DeadCodeDialogProps) {
|
|
21
|
+
const { data, isLoading, error, refresh } = useDeadCode({ days, repo, layer: 'all' });
|
|
22
|
+
const [activeTab, setActiveTab] = useState<TabId>('stale');
|
|
23
|
+
const [staleSortField, setStaleSortField] = useState<keyof StaleFile>('days_since_last_commit');
|
|
24
|
+
const [staleSortDir, setStaleSortDir] = useState<SortDir>('desc');
|
|
25
|
+
const [exportSortField, setExportSortField] = useState<keyof UnusedExport>('file');
|
|
26
|
+
const [exportSortDir, setExportSortDir] = useState<SortDir>('asc');
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (isOpen) refresh();
|
|
30
|
+
}, [isOpen, refresh]);
|
|
31
|
+
|
|
32
|
+
const handleStaleSort = useCallback((field: keyof StaleFile) => {
|
|
33
|
+
setStaleSortDir(prev => staleSortField === field && prev === 'desc' ? 'asc' : 'desc');
|
|
34
|
+
setStaleSortField(field);
|
|
35
|
+
}, [staleSortField]);
|
|
36
|
+
|
|
37
|
+
const handleExportSort = useCallback((field: keyof UnusedExport) => {
|
|
38
|
+
setExportSortDir(prev => exportSortField === field && prev === 'desc' ? 'asc' : 'desc');
|
|
39
|
+
setExportSortField(field);
|
|
40
|
+
}, [exportSortField]);
|
|
41
|
+
|
|
42
|
+
const staleCount = data?.stale_file_count ?? data?.stale_files?.length ?? 0;
|
|
43
|
+
const exportCount = data?.unused_export_count ?? data?.unused_exports?.length ?? 0;
|
|
44
|
+
|
|
45
|
+
const sortedStaleFiles = useMemo(() => {
|
|
46
|
+
const files = [...(data?.stale_files || [])];
|
|
47
|
+
files.sort((a, b) => {
|
|
48
|
+
const aVal = a[staleSortField];
|
|
49
|
+
const bVal = b[staleSortField];
|
|
50
|
+
if (typeof aVal === 'number' && typeof bVal === 'number') {
|
|
51
|
+
return staleSortDir === 'asc' ? aVal - bVal : bVal - aVal;
|
|
52
|
+
}
|
|
53
|
+
const aStr = String(aVal);
|
|
54
|
+
const bStr = String(bVal);
|
|
55
|
+
return staleSortDir === 'asc' ? aStr.localeCompare(bStr) : bStr.localeCompare(aStr);
|
|
56
|
+
});
|
|
57
|
+
return files;
|
|
58
|
+
}, [data?.stale_files, staleSortField, staleSortDir]);
|
|
59
|
+
|
|
60
|
+
const sortedExports = useMemo(() => {
|
|
61
|
+
const exports = [...(data?.unused_exports || [])];
|
|
62
|
+
exports.sort((a, b) => {
|
|
63
|
+
const aVal = a[exportSortField];
|
|
64
|
+
const bVal = b[exportSortField];
|
|
65
|
+
if (typeof aVal === 'number' && typeof bVal === 'number') {
|
|
66
|
+
return exportSortDir === 'asc' ? aVal - bVal : bVal - aVal;
|
|
67
|
+
}
|
|
68
|
+
const aStr = String(aVal);
|
|
69
|
+
const bStr = String(bVal);
|
|
70
|
+
return exportSortDir === 'asc' ? aStr.localeCompare(bStr) : bStr.localeCompare(aStr);
|
|
71
|
+
});
|
|
72
|
+
return exports;
|
|
73
|
+
}, [data?.unused_exports, exportSortField, exportSortDir]);
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<Dialog open={isOpen} onOpenChange={(open) => { if (!open) onClose(); }}>
|
|
77
|
+
<DialogContent className="max-w-4xl max-h-[80vh]">
|
|
78
|
+
<DialogHeader>
|
|
79
|
+
<DialogTitle>Dead Code Analysis</DialogTitle>
|
|
80
|
+
<DialogDescription>
|
|
81
|
+
Diagnostic report — files and exports with no recent activity.
|
|
82
|
+
</DialogDescription>
|
|
83
|
+
</DialogHeader>
|
|
84
|
+
|
|
85
|
+
{/* Tab bar */}
|
|
86
|
+
<div className="flex gap-4 border-b border-[var(--border)]">
|
|
87
|
+
<button
|
|
88
|
+
onClick={() => setActiveTab('stale')}
|
|
89
|
+
className={cn('pb-2 text-sm', activeTab === 'stale' ? 'border-b-2 border-[var(--accent)] text-[var(--text-primary)] font-medium' : 'text-[var(--text-muted)]')}
|
|
90
|
+
>
|
|
91
|
+
Stale Files <Badge variant="secondary" className="ml-1">{staleCount}</Badge>
|
|
92
|
+
</button>
|
|
93
|
+
<button
|
|
94
|
+
onClick={() => setActiveTab('exports')}
|
|
95
|
+
className={cn('pb-2 text-sm', activeTab === 'exports' ? 'border-b-2 border-[var(--accent)] text-[var(--text-primary)] font-medium' : 'text-[var(--text-muted)]')}
|
|
96
|
+
>
|
|
97
|
+
Unused Exports <Badge variant="secondary" className="ml-1">{exportCount}</Badge>
|
|
98
|
+
</button>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
{/* Content area */}
|
|
102
|
+
<ScrollArea className="h-[50vh]">
|
|
103
|
+
{isLoading && <div className="text-center py-12 text-[var(--text-muted)]">Analyzing...</div>}
|
|
104
|
+
{error && (
|
|
105
|
+
<div className="m-4 p-4 rounded border border-[var(--status-error)]/20 bg-[var(--status-error)]/5 text-[var(--status-error)] text-sm">
|
|
106
|
+
{error.message}
|
|
107
|
+
</div>
|
|
108
|
+
)}
|
|
109
|
+
{data && activeTab === 'stale' && (
|
|
110
|
+
sortedStaleFiles.length === 0 ? (
|
|
111
|
+
<div className="text-center py-12 text-[var(--text-muted)]">No stale files found</div>
|
|
112
|
+
) : (
|
|
113
|
+
<table className="w-full text-sm">
|
|
114
|
+
<thead>
|
|
115
|
+
<tr role="row" className="border-b border-[var(--border)]">
|
|
116
|
+
<th className="text-left pb-2 cursor-pointer select-none text-xs font-medium uppercase tracking-wider text-[var(--text-muted)]" onClick={() => handleStaleSort('path')}>File</th>
|
|
117
|
+
<th className="text-right pb-2 cursor-pointer select-none text-xs font-medium uppercase tracking-wider text-[var(--text-muted)]" onClick={() => handleStaleSort('days_since_last_commit')}>Days Stale</th>
|
|
118
|
+
<th className="text-right pb-2 cursor-pointer select-none text-xs font-medium uppercase tracking-wider text-[var(--text-muted)]" onClick={() => handleStaleSort('size_bytes')}>Size</th>
|
|
119
|
+
<th className="text-left pb-2 cursor-pointer select-none text-xs font-medium uppercase tracking-wider text-[var(--text-muted)]" onClick={() => handleStaleSort('last_commit_date')}>Last Commit</th>
|
|
120
|
+
</tr>
|
|
121
|
+
</thead>
|
|
122
|
+
<tbody>
|
|
123
|
+
{sortedStaleFiles.map((file) => (
|
|
124
|
+
<tr key={file.path} role="row" className="text-[var(--text-primary)]">
|
|
125
|
+
<td className="py-1.5 font-mono text-xs">{file.path}</td>
|
|
126
|
+
<td className="py-1.5 text-right tabular-nums font-mono">{file.days_since_last_commit}</td>
|
|
127
|
+
<td className="py-1.5 text-right tabular-nums font-mono">{formatBytes(file.size_bytes)}</td>
|
|
128
|
+
<td className="py-1.5">{file.last_commit_date ? new Date(file.last_commit_date).toLocaleDateString() : '—'}</td>
|
|
129
|
+
</tr>
|
|
130
|
+
))}
|
|
131
|
+
</tbody>
|
|
132
|
+
</table>
|
|
133
|
+
)
|
|
134
|
+
)}
|
|
135
|
+
{data && activeTab === 'exports' && (
|
|
136
|
+
sortedExports.length === 0 ? (
|
|
137
|
+
<div className="text-center py-12 text-[var(--text-muted)]">No unused exports found</div>
|
|
138
|
+
) : (
|
|
139
|
+
<table className="w-full text-sm">
|
|
140
|
+
<thead>
|
|
141
|
+
<tr role="row" className="border-b border-[var(--border)]">
|
|
142
|
+
<th className="text-left pb-2 cursor-pointer select-none text-xs font-medium uppercase tracking-wider text-[var(--text-muted)]" onClick={() => handleExportSort('file')}>File</th>
|
|
143
|
+
<th className="text-left pb-2 cursor-pointer select-none text-xs font-medium uppercase tracking-wider text-[var(--text-muted)]" onClick={() => handleExportSort('symbol')}>Export</th>
|
|
144
|
+
<th className="text-right pb-2 cursor-pointer select-none text-xs font-medium uppercase tracking-wider text-[var(--text-muted)]" onClick={() => handleExportSort('line')}>Line</th>
|
|
145
|
+
</tr>
|
|
146
|
+
</thead>
|
|
147
|
+
<tbody>
|
|
148
|
+
{sortedExports.map((exp) => (
|
|
149
|
+
<tr key={`${exp.file}:${exp.symbol}`} role="row" className="text-[var(--text-primary)]">
|
|
150
|
+
<td className="py-1.5 font-mono text-xs">{exp.file}</td>
|
|
151
|
+
<td className="py-1.5">{exp.symbol}</td>
|
|
152
|
+
<td className="py-1.5 text-right tabular-nums font-mono">{exp.line}</td>
|
|
153
|
+
</tr>
|
|
154
|
+
))}
|
|
155
|
+
</tbody>
|
|
156
|
+
</table>
|
|
157
|
+
)
|
|
158
|
+
)}
|
|
159
|
+
</ScrollArea>
|
|
160
|
+
</DialogContent>
|
|
161
|
+
</Dialog>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function formatBytes(bytes: number): string {
|
|
166
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
167
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
168
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
169
|
+
}
|
|
@@ -49,7 +49,6 @@ export const PANEL_INVENTORY = {
|
|
|
49
49
|
TODO: 'todo',
|
|
50
50
|
BACKGROUND: 'background',
|
|
51
51
|
GIT: 'git',
|
|
52
|
-
HOTSPOTS: 'hotspots',
|
|
53
52
|
SETTINGS: 'settings',
|
|
54
53
|
} as const;
|
|
55
54
|
|
|
@@ -91,7 +90,6 @@ export const RIGHT_SIDEBAR_PANELS = [
|
|
|
91
90
|
PANEL_INVENTORY.TODO,
|
|
92
91
|
PANEL_INVENTORY.BACKGROUND,
|
|
93
92
|
PANEL_INVENTORY.GIT,
|
|
94
|
-
PANEL_INVENTORY.HOTSPOTS,
|
|
95
93
|
PANEL_INVENTORY.SETTINGS,
|
|
96
94
|
] as const;
|
|
97
95
|
|
|
@@ -109,7 +107,6 @@ const PANEL_TITLES: Record<string, string> = {
|
|
|
109
107
|
todo: 'Todo',
|
|
110
108
|
background: 'Subagents',
|
|
111
109
|
git: 'Git',
|
|
112
|
-
hotspots: 'Hotspots',
|
|
113
110
|
settings: 'Settings',
|
|
114
111
|
};
|
|
115
112
|
|
|
@@ -93,15 +93,29 @@ function TreeDirectoryNode({
|
|
|
93
93
|
cache: Record<string, DirectoryEntry[]>;
|
|
94
94
|
loading: Set<string>;
|
|
95
95
|
}): React.ReactElement {
|
|
96
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
97
|
-
const children = cache[entry.path];
|
|
98
|
-
const isLoading = loading.has(entry.path);
|
|
99
|
-
|
|
100
96
|
// Check if this directory contains any changed files
|
|
101
97
|
const hasChanges = Array.from(changedFiles.keys()).some(
|
|
102
98
|
(filePath) => filePath.startsWith(entry.path + '/')
|
|
103
99
|
);
|
|
104
100
|
|
|
101
|
+
const [isOpen, setIsOpen] = useState(hasChanges);
|
|
102
|
+
const children = cache[entry.path];
|
|
103
|
+
const isLoading = loading.has(entry.path);
|
|
104
|
+
|
|
105
|
+
// Auto-fetch children when directory has changes and is opened by default
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (hasChanges && !children && !loading.has(entry.path)) {
|
|
108
|
+
fetchDirectory(entry.path);
|
|
109
|
+
}
|
|
110
|
+
}, [hasChanges, children, entry.path, fetchDirectory, loading]);
|
|
111
|
+
|
|
112
|
+
// Auto-open when changes appear in this directory
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (hasChanges) {
|
|
115
|
+
setIsOpen(true);
|
|
116
|
+
}
|
|
117
|
+
}, [hasChanges]);
|
|
118
|
+
|
|
105
119
|
const handleToggle = useCallback(() => {
|
|
106
120
|
const willOpen = !isOpen;
|
|
107
121
|
setIsOpen(willOpen);
|