@pennyfarthing/cyclist 9.3.0 → 10.0.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/hook-request.d.ts +11 -0
- package/dist/api/hook-request.d.ts.map +1 -1
- package/dist/api/hook-request.js +126 -28
- package/dist/api/hook-request.js.map +1 -1
- package/dist/api/hotspots.d.ts +3 -0
- package/dist/api/hotspots.d.ts.map +1 -0
- package/dist/api/hotspots.js +54 -0
- package/dist/api/hotspots.js.map +1 -0
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +3 -0
- package/dist/api/index.js.map +1 -1
- package/dist/api/permissions.d.ts +16 -0
- package/dist/api/permissions.d.ts.map +1 -0
- package/dist/api/permissions.js +67 -0
- package/dist/api/permissions.js.map +1 -0
- package/dist/api/settings.d.ts +1 -1
- package/dist/api/settings.d.ts.map +1 -1
- package/dist/api/settings.js +44 -17
- package/dist/api/settings.js.map +1 -1
- package/dist/api/theme-agents.d.ts +4 -0
- package/dist/api/theme-agents.d.ts.map +1 -1
- package/dist/api/theme-agents.js +3 -0
- package/dist/api/theme-agents.js.map +1 -1
- package/dist/approval-gate.d.ts +3 -75
- package/dist/approval-gate.d.ts.map +1 -1
- package/dist/approval-gate.js +4 -121
- package/dist/approval-gate.js.map +1 -1
- package/dist/hooks/cyclist-pretooluse-hook.d.ts +60 -0
- package/dist/hooks/cyclist-pretooluse-hook.d.ts.map +1 -0
- package/dist/hooks/cyclist-pretooluse-hook.js +57 -0
- package/dist/hooks/cyclist-pretooluse-hook.js.map +1 -0
- package/dist/hooks/pretooluse-hook.d.ts +89 -0
- package/dist/hooks/pretooluse-hook.d.ts.map +1 -0
- package/dist/hooks/pretooluse-hook.js +235 -0
- package/dist/hooks/pretooluse-hook.js.map +1 -0
- package/dist/main.d.ts +1 -134
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +42 -373
- package/dist/main.js.map +1 -1
- package/dist/menu-builder.d.ts +7 -1
- package/dist/menu-builder.d.ts.map +1 -1
- package/dist/menu-builder.js +36 -1
- package/dist/menu-builder.js.map +1 -1
- package/dist/otlp-receiver.d.ts.map +1 -1
- package/dist/otlp-receiver.js +6 -0
- package/dist/otlp-receiver.js.map +1 -1
- package/dist/public/css/react.css +1 -1
- package/dist/public/js/react/react.js +42 -42
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +16 -3
- package/dist/server.js.map +1 -1
- package/dist/settings-store.d.ts +3 -1
- package/dist/settings-store.d.ts.map +1 -1
- package/dist/settings-store.js +18 -9
- package/dist/settings-store.js.map +1 -1
- package/dist/story-parser.d.ts +17 -0
- package/dist/story-parser.d.ts.map +1 -1
- package/dist/story-parser.js +183 -13
- package/dist/story-parser.js.map +1 -1
- package/dist/websocket.d.ts +1 -0
- package/dist/websocket.d.ts.map +1 -1
- package/dist/websocket.js +48 -5
- package/dist/websocket.js.map +1 -1
- package/dist/workflow-presets.d.ts +72 -0
- package/dist/workflow-presets.d.ts.map +1 -0
- package/dist/workflow-presets.js +93 -0
- package/dist/workflow-presets.js.map +1 -0
- package/package.json +2 -2
- package/src/public/App.tsx +61 -1
- package/src/public/components/ApprovalModal/index.tsx +31 -1
- package/src/public/components/ControlBar.tsx +19 -20
- package/src/public/components/DockviewWorkspace.tsx +39 -5
- package/src/public/components/FontPicker/index.tsx +118 -33
- package/src/public/components/FullFileTree.tsx +223 -0
- package/src/public/components/Message.tsx +89 -11
- package/src/public/components/MessageView.tsx +206 -93
- package/src/public/components/PersonaHeader.tsx +47 -15
- package/src/public/components/SubagentSpan.tsx +15 -8
- package/src/public/components/panels/BackgroundPanel.tsx +1 -1
- package/src/public/components/panels/ChangedPanel.tsx +30 -44
- package/src/public/components/panels/HotspotsPanel.tsx +365 -0
- package/src/public/components/panels/MessagePanel.tsx +79 -5
- package/src/public/components/panels/SettingsPanel.tsx +3 -28
- package/src/public/components/panels/WorkflowPanel.tsx +108 -13
- package/src/public/components/panels/index.ts +1 -0
- package/src/public/contexts/ClaudeContext.tsx +16 -1
- package/src/public/css/theme-system.css +46 -38
- package/src/public/hooks/useColorScheme.ts +27 -0
- package/src/public/hooks/useFileBrowser.ts +71 -0
- package/src/public/hooks/useHotspots.ts +113 -0
- package/src/public/hooks/usePlanModeExit.ts +105 -0
- package/src/public/hooks/useStory.ts +12 -3
- package/src/public/images/cyclist-dark.png +0 -0
- package/src/public/images/cyclist-light.png +0 -0
- package/src/public/styles/dockview-theme.css +31 -33
- package/src/public/styles/tailwind.css +417 -58
- package/src/public/types/message.ts +6 -1
- package/src/public/utils/markdown.ts +2 -2
- package/src/public/utils/slash-commands.ts +1 -1
- package/src/public/utils/toolStackGrouper.ts +5 -6
|
@@ -3,16 +3,20 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Extracted from ProgressPanel as part of MSSCI-14188.
|
|
5
5
|
* Shows workflow type badge (TDD/BDD/Trivial) and phase progress.
|
|
6
|
+
* MSSCI-14300: Added stepped workflow "Step N of M" display.
|
|
7
|
+
* MSSCI-14301: Added available workflows discovery list.
|
|
6
8
|
*
|
|
7
9
|
* Story: MSSCI-14188 - Split Progress panel into Workflow, AC, and Todo panels
|
|
8
10
|
* Epic: epic-76 (Dockview Panel Migration)
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
|
-
import React from 'react';
|
|
13
|
+
import React, { useCallback } from 'react';
|
|
12
14
|
import { Badge } from '@/components/ui/badge';
|
|
15
|
+
import { Button } from '@/components/ui/button';
|
|
13
16
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
17
|
+
import { useClaudeContext } from '../../contexts/ClaudeContext';
|
|
14
18
|
import { useStory } from '../../hooks/useStory';
|
|
15
|
-
import type { WorkflowPhase } from '../../../story-parser.js';
|
|
19
|
+
import type { WorkflowPhase, AvailableWorkflow } from '../../../story-parser.js';
|
|
16
20
|
|
|
17
21
|
// =============================================================================
|
|
18
22
|
// Helper Functions
|
|
@@ -42,7 +46,7 @@ function formatWorkflowType(type: string | null): string {
|
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
// =============================================================================
|
|
45
|
-
// Phase Step Component
|
|
49
|
+
// Phase Step Component (for phased workflows)
|
|
46
50
|
// =============================================================================
|
|
47
51
|
|
|
48
52
|
function PhaseStep({ phase, isLast }: { phase: WorkflowPhase; isLast: boolean }): React.ReactElement {
|
|
@@ -60,12 +64,90 @@ function PhaseStep({ phase, isLast }: { phase: WorkflowPhase; isLast: boolean })
|
|
|
60
64
|
);
|
|
61
65
|
}
|
|
62
66
|
|
|
67
|
+
// =============================================================================
|
|
68
|
+
// Stepped Progress Component (for stepped workflows)
|
|
69
|
+
// =============================================================================
|
|
70
|
+
|
|
71
|
+
function SteppedProgress({ phases }: { phases: WorkflowPhase[] }): React.ReactElement {
|
|
72
|
+
const total = phases.length;
|
|
73
|
+
const currentIndex = phases.findIndex(p => p.status === 'current');
|
|
74
|
+
const doneCount = phases.filter(p => p.status === 'done').length;
|
|
75
|
+
|
|
76
|
+
// If all done, current step = total; otherwise use 1-based index of current
|
|
77
|
+
const currentStep = currentIndex >= 0 ? currentIndex + 1 : (doneCount === total ? total : 1);
|
|
78
|
+
const currentPhase = currentIndex >= 0 ? phases[currentIndex] : null;
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div className="stepped-progress">
|
|
82
|
+
<span className="stepped-counter">Step {currentStep} of {total}</span>
|
|
83
|
+
{currentPhase && (
|
|
84
|
+
<span className="stepped-current-label">{currentPhase.label}</span>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// =============================================================================
|
|
91
|
+
// Available Workflows List (MSSCI-14301)
|
|
92
|
+
// =============================================================================
|
|
93
|
+
|
|
94
|
+
function AvailableWorkflowsList({ workflows, onStart }: {
|
|
95
|
+
workflows: AvailableWorkflow[];
|
|
96
|
+
onStart?: (workflow: AvailableWorkflow) => void;
|
|
97
|
+
}): React.ReactElement {
|
|
98
|
+
return (
|
|
99
|
+
<div className="available-workflows">
|
|
100
|
+
<div className="available-workflows-header">
|
|
101
|
+
<span className="available-workflows-title">Available Workflows ({workflows.length})</span>
|
|
102
|
+
</div>
|
|
103
|
+
<div className="available-workflows-list">
|
|
104
|
+
{workflows.map((wf) => (
|
|
105
|
+
<div
|
|
106
|
+
key={wf.name}
|
|
107
|
+
className="workflow-entry"
|
|
108
|
+
data-testid="workflow-entry"
|
|
109
|
+
data-workflow-entry-type={wf.type}
|
|
110
|
+
>
|
|
111
|
+
<div className="workflow-entry-header">
|
|
112
|
+
<span className="workflow-entry-name">{wf.name}</span>
|
|
113
|
+
<Badge variant="outline" className="workflow-entry-type-badge">
|
|
114
|
+
{wf.type}
|
|
115
|
+
</Badge>
|
|
116
|
+
</div>
|
|
117
|
+
<div className="workflow-entry-description">{wf.description}</div>
|
|
118
|
+
{wf.type === 'stepped' && (
|
|
119
|
+
<div className="workflow-entry-hint">/workflow start {wf.name}</div>
|
|
120
|
+
)}
|
|
121
|
+
<div className="workflow-entry-footer">
|
|
122
|
+
<Button
|
|
123
|
+
variant="ghost"
|
|
124
|
+
size="sm"
|
|
125
|
+
className="workflow-start-button"
|
|
126
|
+
data-testid="workflow-start-button"
|
|
127
|
+
onClick={() => onStart?.(wf)}
|
|
128
|
+
>
|
|
129
|
+
Start
|
|
130
|
+
</Button>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
))}
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
63
139
|
// =============================================================================
|
|
64
140
|
// WorkflowPanel Component
|
|
65
141
|
// =============================================================================
|
|
66
142
|
|
|
67
143
|
export function WorkflowPanel(): React.ReactElement {
|
|
68
|
-
const { story, isLoading, error } = useStory();
|
|
144
|
+
const { story, isLoading, error, availableWorkflows } = useStory();
|
|
145
|
+
const { send, isConnected } = useClaudeContext();
|
|
146
|
+
|
|
147
|
+
const handleStartWorkflow = useCallback((wf: AvailableWorkflow) => {
|
|
148
|
+
if (!isConnected) return;
|
|
149
|
+
send(`/workflow start ${wf.name}`);
|
|
150
|
+
}, [send, isConnected]);
|
|
69
151
|
|
|
70
152
|
if (isLoading) {
|
|
71
153
|
return (
|
|
@@ -92,8 +174,17 @@ export function WorkflowPanel(): React.ReactElement {
|
|
|
92
174
|
|
|
93
175
|
const workflowType = story?.workflow ?? null;
|
|
94
176
|
const phases = story?.workflowPhases ?? null;
|
|
177
|
+
const isStepped = story?.workflowType === 'stepped';
|
|
95
178
|
|
|
96
179
|
if (!workflowType && (!phases || phases.length === 0)) {
|
|
180
|
+
// MSSCI-14301: Show available workflows when no active workflow
|
|
181
|
+
if (availableWorkflows && availableWorkflows.length > 0) {
|
|
182
|
+
return (
|
|
183
|
+
<div className="workflow-panel" data-testid="workflow-panel">
|
|
184
|
+
<AvailableWorkflowsList workflows={availableWorkflows} onStart={handleStartWorkflow} />
|
|
185
|
+
</div>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
97
188
|
return (
|
|
98
189
|
<div className="workflow-panel" data-testid="workflow-panel">
|
|
99
190
|
<div className="placeholder">No active workflow</div>
|
|
@@ -111,15 +202,19 @@ export function WorkflowPanel(): React.ReactElement {
|
|
|
111
202
|
</Badge>
|
|
112
203
|
|
|
113
204
|
{phases && phases.length > 0 && (
|
|
114
|
-
|
|
115
|
-
{phases
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
205
|
+
isStepped ? (
|
|
206
|
+
<SteppedProgress phases={phases} />
|
|
207
|
+
) : (
|
|
208
|
+
<div className="phase-progress">
|
|
209
|
+
{phases.map((phase, index) => (
|
|
210
|
+
<PhaseStep
|
|
211
|
+
key={phase.name}
|
|
212
|
+
phase={phase}
|
|
213
|
+
isLast={index === phases.length - 1}
|
|
214
|
+
/>
|
|
215
|
+
))}
|
|
216
|
+
</div>
|
|
217
|
+
)
|
|
123
218
|
)}
|
|
124
219
|
</div>
|
|
125
220
|
</div>
|
|
@@ -18,6 +18,7 @@ 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';
|
|
21
22
|
|
|
22
23
|
// Legacy exports - kept for backwards compatibility and tests
|
|
23
24
|
export { AcceptanceCriteriaPanel, ConnectedAcceptanceCriteriaPanel } from './AcceptanceCriteriaPanel';
|
|
@@ -24,6 +24,7 @@ interface WebSocketClaudeMessage {
|
|
|
24
24
|
type MessageCallback = (message: ClaudeMessage) => void;
|
|
25
25
|
type CompleteCallback = () => void;
|
|
26
26
|
type ErrorCallback = (error: string) => void;
|
|
27
|
+
type ClearCallback = () => void;
|
|
27
28
|
|
|
28
29
|
/** User message sent via send() - for display in MessagePanel */
|
|
29
30
|
interface UserMessageData {
|
|
@@ -56,6 +57,8 @@ interface ClaudeContextValue {
|
|
|
56
57
|
onError: (callback: ErrorCallback) => () => void;
|
|
57
58
|
/** Subscribe to user messages sent via send() - for display in MessagePanel */
|
|
58
59
|
onUserMessage: (callback: UserMessageCallback) => () => void;
|
|
60
|
+
/** Subscribe to clear/reset events */
|
|
61
|
+
onClear: (callback: ClearCallback) => () => void;
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
// =============================================================================
|
|
@@ -83,6 +86,7 @@ export function ClaudeProvider({ children }: ClaudeProviderProps): React.ReactEl
|
|
|
83
86
|
const completeCallbacksRef = useRef<Set<CompleteCallback>>(new Set());
|
|
84
87
|
const errorCallbacksRef = useRef<Set<ErrorCallback>>(new Set());
|
|
85
88
|
const userMessageCallbacksRef = useRef<Set<UserMessageCallback>>(new Set());
|
|
89
|
+
const clearCallbacksRef = useRef<Set<ClearCallback>>(new Set());
|
|
86
90
|
|
|
87
91
|
// Connect to WebSocket
|
|
88
92
|
const connect = useCallback(() => {
|
|
@@ -204,6 +208,7 @@ export function ClaudeProvider({ children }: ClaudeProviderProps): React.ReactEl
|
|
|
204
208
|
}
|
|
205
209
|
|
|
206
210
|
wsRef.current.send(JSON.stringify({ type: 'clear' }));
|
|
211
|
+
clearCallbacksRef.current.forEach(cb => cb());
|
|
207
212
|
}, []);
|
|
208
213
|
|
|
209
214
|
// Clear session and reload agent (TirePump)
|
|
@@ -215,6 +220,7 @@ export function ClaudeProvider({ children }: ClaudeProviderProps): React.ReactEl
|
|
|
215
220
|
|
|
216
221
|
console.log('[ClaudeContext] TirePump: clearAndReload agent:', agent);
|
|
217
222
|
wsRef.current.send(JSON.stringify({ type: 'clearAndReload', agent }));
|
|
223
|
+
clearCallbacksRef.current.forEach(cb => cb());
|
|
218
224
|
}, []);
|
|
219
225
|
|
|
220
226
|
// Set permission mode
|
|
@@ -260,6 +266,14 @@ export function ClaudeProvider({ children }: ClaudeProviderProps): React.ReactEl
|
|
|
260
266
|
};
|
|
261
267
|
}, []);
|
|
262
268
|
|
|
269
|
+
// Subscribe to clear/reset events
|
|
270
|
+
const onClear = useCallback((callback: ClearCallback) => {
|
|
271
|
+
clearCallbacksRef.current.add(callback);
|
|
272
|
+
return () => {
|
|
273
|
+
clearCallbacksRef.current.delete(callback);
|
|
274
|
+
};
|
|
275
|
+
}, []);
|
|
276
|
+
|
|
263
277
|
const value = useMemo(() => ({
|
|
264
278
|
send,
|
|
265
279
|
abort,
|
|
@@ -272,7 +286,8 @@ export function ClaudeProvider({ children }: ClaudeProviderProps): React.ReactEl
|
|
|
272
286
|
onComplete,
|
|
273
287
|
onError,
|
|
274
288
|
onUserMessage,
|
|
275
|
-
|
|
289
|
+
onClear,
|
|
290
|
+
}), [send, abort, clear, clearAndReload, setMode, isConnected, mode, onMessage, onComplete, onError, onUserMessage, onClear]);
|
|
276
291
|
|
|
277
292
|
return (
|
|
278
293
|
<ClaudeContext.Provider value={value}>
|
|
@@ -152,26 +152,31 @@
|
|
|
152
152
|
Tool Call Block Styling (MSSCI-13402) - Klinger Redesign
|
|
153
153
|
============================================================================= */
|
|
154
154
|
|
|
155
|
-
/* Base tool call block */
|
|
155
|
+
/* Base tool call block — Tufte: indented, no border box */
|
|
156
156
|
.tool-call-block {
|
|
157
|
-
background
|
|
158
|
-
border-radius:
|
|
159
|
-
border:
|
|
157
|
+
background: none;
|
|
158
|
+
border-radius: 0;
|
|
159
|
+
border: none;
|
|
160
|
+
border-left: 2px solid var(--border);
|
|
160
161
|
overflow: hidden;
|
|
162
|
+
margin-left: 2.75rem; /* align with message content (avatar + gap) */
|
|
163
|
+
margin-top: 0.5rem;
|
|
164
|
+
margin-bottom: 0.5rem;
|
|
165
|
+
padding-left: 0.5rem;
|
|
161
166
|
transition: border-color 0.15s ease;
|
|
162
167
|
}
|
|
163
168
|
|
|
164
169
|
.tool-call-block:hover {
|
|
165
|
-
border-color: var(--accent);
|
|
170
|
+
border-left-color: var(--accent);
|
|
166
171
|
}
|
|
167
172
|
|
|
168
|
-
/* Tool Header -
|
|
173
|
+
/* Tool Header - compact single-line */
|
|
169
174
|
.tool-header {
|
|
170
175
|
display: flex;
|
|
171
176
|
align-items: center;
|
|
172
|
-
gap: 0.
|
|
173
|
-
padding: 0.
|
|
174
|
-
background
|
|
177
|
+
gap: 0.5rem;
|
|
178
|
+
padding: 0.125rem 0;
|
|
179
|
+
background: none;
|
|
175
180
|
}
|
|
176
181
|
|
|
177
182
|
/* Tool Type Badge - colored pill for instant recognition */
|
|
@@ -201,14 +206,14 @@
|
|
|
201
206
|
.tool-call-block.tool-edit .tool-type-badge { background-color: var(--tool-edit-color); }
|
|
202
207
|
.tool-call-block.tool-task .tool-type-badge { background-color: var(--tool-task-color); }
|
|
203
208
|
|
|
204
|
-
/*
|
|
205
|
-
.tool-call-block.tool-read { border-left:
|
|
206
|
-
.tool-call-block.tool-write { border-left:
|
|
207
|
-
.tool-call-block.tool-bash { border-left:
|
|
208
|
-
.tool-call-block.tool-glob { border-left:
|
|
209
|
-
.tool-call-block.tool-grep { border-left:
|
|
210
|
-
.tool-call-block.tool-edit { border-left:
|
|
211
|
-
.tool-call-block.tool-task { border-left:
|
|
209
|
+
/* Color the left border by tool type */
|
|
210
|
+
.tool-call-block.tool-read { border-left-color: var(--tool-read-color); }
|
|
211
|
+
.tool-call-block.tool-write { border-left-color: var(--tool-write-color); }
|
|
212
|
+
.tool-call-block.tool-bash { border-left-color: var(--tool-bash-color); }
|
|
213
|
+
.tool-call-block.tool-glob { border-left-color: var(--tool-glob-color); }
|
|
214
|
+
.tool-call-block.tool-grep { border-left-color: var(--tool-grep-color); }
|
|
215
|
+
.tool-call-block.tool-edit { border-left-color: var(--tool-edit-color); }
|
|
216
|
+
.tool-call-block.tool-task { border-left-color: var(--tool-task-color); }
|
|
212
217
|
|
|
213
218
|
/* Intent summary - the human-readable description */
|
|
214
219
|
.tool-name {
|
|
@@ -238,8 +243,7 @@
|
|
|
238
243
|
|
|
239
244
|
/* Error state styling */
|
|
240
245
|
.tool-call-block.tool-error {
|
|
241
|
-
border-left-color: var(--status-error);
|
|
242
|
-
background-color: rgba(239, 68, 68, 0.05);
|
|
246
|
+
border-left-color: var(--status-error) !important;
|
|
243
247
|
}
|
|
244
248
|
|
|
245
249
|
.tool-call-block.tool-error .tool-type-badge {
|
|
@@ -251,9 +255,8 @@
|
|
|
251
255
|
display: flex;
|
|
252
256
|
align-items: center;
|
|
253
257
|
gap: 0.5rem;
|
|
254
|
-
padding: 0.
|
|
255
|
-
background
|
|
256
|
-
border-top: 1px solid var(--border);
|
|
258
|
+
padding: 0.125rem 0;
|
|
259
|
+
background: none;
|
|
257
260
|
}
|
|
258
261
|
|
|
259
262
|
.tool-result-toggle {
|
|
@@ -391,10 +394,13 @@
|
|
|
391
394
|
|
|
392
395
|
/* Container for grouped tool calls */
|
|
393
396
|
.tool-stack {
|
|
394
|
-
margin: 0.
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
397
|
+
margin: 0.5rem 0;
|
|
398
|
+
margin-left: 2.75rem; /* align with message content */
|
|
399
|
+
border-radius: 0;
|
|
400
|
+
background: none;
|
|
401
|
+
border: none;
|
|
402
|
+
border-left: 2px solid var(--border);
|
|
403
|
+
padding-left: 0.5rem;
|
|
398
404
|
}
|
|
399
405
|
|
|
400
406
|
.tool-stack.collapsed {
|
|
@@ -405,21 +411,21 @@
|
|
|
405
411
|
.tool-stack-header {
|
|
406
412
|
display: flex;
|
|
407
413
|
align-items: center;
|
|
408
|
-
gap: 0.
|
|
409
|
-
padding: 0.
|
|
414
|
+
gap: 0.5rem;
|
|
415
|
+
padding: 0.25rem 0;
|
|
410
416
|
cursor: pointer;
|
|
411
417
|
user-select: none;
|
|
412
|
-
background
|
|
413
|
-
border-bottom:
|
|
414
|
-
transition: background-color 0.15s ease
|
|
418
|
+
background: none;
|
|
419
|
+
border-bottom: none;
|
|
420
|
+
transition: background-color 0.15s ease;
|
|
415
421
|
}
|
|
416
422
|
|
|
417
423
|
.tool-stack:not(.collapsed) .tool-stack-header {
|
|
418
|
-
border-bottom-color:
|
|
424
|
+
border-bottom-color: transparent;
|
|
419
425
|
}
|
|
420
426
|
|
|
421
427
|
.tool-stack-header:hover {
|
|
422
|
-
background-color: var(--bg-
|
|
428
|
+
background-color: var(--bg-secondary);
|
|
423
429
|
}
|
|
424
430
|
|
|
425
431
|
.tool-stack-header:focus {
|
|
@@ -515,14 +521,16 @@
|
|
|
515
521
|
.tool-stack-content {
|
|
516
522
|
display: flex;
|
|
517
523
|
flex-direction: column;
|
|
518
|
-
gap: 0.
|
|
519
|
-
padding: 0.
|
|
520
|
-
background
|
|
524
|
+
gap: 0.125rem;
|
|
525
|
+
padding: 0.25rem 0;
|
|
526
|
+
background: none;
|
|
521
527
|
}
|
|
522
528
|
|
|
523
|
-
/*
|
|
529
|
+
/* Within a stack, tool blocks don't need extra indent or left border */
|
|
524
530
|
.tool-stack-content .tool-call-block {
|
|
525
|
-
margin: 0;
|
|
531
|
+
margin-left: 0;
|
|
532
|
+
border-left: none;
|
|
533
|
+
padding-left: 0;
|
|
526
534
|
}
|
|
527
535
|
|
|
528
536
|
/* Tool state classes within stack */
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useColorScheme Hook
|
|
3
|
+
*
|
|
4
|
+
* Tracks the user's preferred color scheme (light/dark) via
|
|
5
|
+
* the prefers-color-scheme media query.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState, useEffect } from 'react';
|
|
9
|
+
|
|
10
|
+
export type ColorScheme = 'light' | 'dark';
|
|
11
|
+
|
|
12
|
+
export function useColorScheme(): ColorScheme {
|
|
13
|
+
const [scheme, setScheme] = useState<ColorScheme>(() =>
|
|
14
|
+
window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const mq = window.matchMedia('(prefers-color-scheme: dark)');
|
|
19
|
+
const handler = (e: MediaQueryListEvent) => {
|
|
20
|
+
setScheme(e.matches ? 'dark' : 'light');
|
|
21
|
+
};
|
|
22
|
+
mq.addEventListener('change', handler);
|
|
23
|
+
return () => mq.removeEventListener('change', handler);
|
|
24
|
+
}, []);
|
|
25
|
+
|
|
26
|
+
return scheme;
|
|
27
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useFileBrowser Hook
|
|
3
|
+
*
|
|
4
|
+
* Fetches directory listings from /api/files for the full file tree.
|
|
5
|
+
* Lazy-loads subdirectories on demand.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState, useCallback } from 'react';
|
|
9
|
+
|
|
10
|
+
export interface DirectoryEntry {
|
|
11
|
+
name: string;
|
|
12
|
+
path: string;
|
|
13
|
+
type: 'file' | 'directory';
|
|
14
|
+
isModified?: boolean;
|
|
15
|
+
size?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface DirectoryListing {
|
|
19
|
+
path: string;
|
|
20
|
+
entries: DirectoryEntry[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface DirectoryCache {
|
|
24
|
+
[path: string]: DirectoryEntry[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface UseFileBrowserResult {
|
|
28
|
+
cache: DirectoryCache;
|
|
29
|
+
loading: Set<string>;
|
|
30
|
+
error: string | null;
|
|
31
|
+
fetchDirectory: (dirPath: string) => Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function useFileBrowser(): UseFileBrowserResult {
|
|
35
|
+
const [cache, setCache] = useState<DirectoryCache>({});
|
|
36
|
+
const [loading, setLoading] = useState<Set<string>>(new Set());
|
|
37
|
+
const [error, setError] = useState<string | null>(null);
|
|
38
|
+
|
|
39
|
+
const fetchDirectory = useCallback(async (dirPath: string) => {
|
|
40
|
+
// Already cached or loading
|
|
41
|
+
if (cache[dirPath] || loading.has(dirPath)) return;
|
|
42
|
+
|
|
43
|
+
setLoading(prev => new Set(prev).add(dirPath));
|
|
44
|
+
setError(null);
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const params = dirPath ? `?path=${encodeURIComponent(dirPath)}` : '';
|
|
48
|
+
const res = await fetch(`/api/files${params}`);
|
|
49
|
+
if (!res.ok) throw new Error(`Failed to list directory: ${res.statusText}`);
|
|
50
|
+
const listing: DirectoryListing = await res.json();
|
|
51
|
+
|
|
52
|
+
// Sort: directories first, then files, alphabetical within each
|
|
53
|
+
const sorted = listing.entries.sort((a, b) => {
|
|
54
|
+
if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
|
|
55
|
+
return a.name.localeCompare(b.name);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
setCache(prev => ({ ...prev, [dirPath || '__root__']: sorted }));
|
|
59
|
+
} catch (err) {
|
|
60
|
+
setError(err instanceof Error ? err.message : 'Failed to load directory');
|
|
61
|
+
} finally {
|
|
62
|
+
setLoading(prev => {
|
|
63
|
+
const next = new Set(prev);
|
|
64
|
+
next.delete(dirPath);
|
|
65
|
+
return next;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}, [cache, loading]);
|
|
69
|
+
|
|
70
|
+
return { cache, loading, error, fetchDirectory };
|
|
71
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
// Types matching Python HotspotResult / MultiRepoHotspotResult
|
|
4
|
+
export interface FileHotspot {
|
|
5
|
+
path: string;
|
|
6
|
+
change_count: number;
|
|
7
|
+
bug_fix_count: number;
|
|
8
|
+
author_count: number;
|
|
9
|
+
lines_added: number;
|
|
10
|
+
lines_deleted: number;
|
|
11
|
+
churn: number;
|
|
12
|
+
last_changed: string;
|
|
13
|
+
hotspot_score: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface DirectoryHotspot {
|
|
17
|
+
path: string;
|
|
18
|
+
file_count: number;
|
|
19
|
+
total_changes: number;
|
|
20
|
+
total_bug_fixes: number;
|
|
21
|
+
avg_author_count: number;
|
|
22
|
+
hotspot_score: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface HotspotRepoResult {
|
|
26
|
+
success: boolean;
|
|
27
|
+
repo_name: string;
|
|
28
|
+
repo_path: string;
|
|
29
|
+
time_window_days: number;
|
|
30
|
+
commit_count: number;
|
|
31
|
+
file_hotspots: FileHotspot[];
|
|
32
|
+
directory_hotspots: DirectoryHotspot[];
|
|
33
|
+
error?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface HotspotData {
|
|
37
|
+
success: boolean;
|
|
38
|
+
// Single-repo result fields (when --path is used)
|
|
39
|
+
repo_name?: string;
|
|
40
|
+
repo_path?: string;
|
|
41
|
+
time_window_days?: number;
|
|
42
|
+
commit_count?: number;
|
|
43
|
+
file_hotspots?: FileHotspot[];
|
|
44
|
+
directory_hotspots?: DirectoryHotspot[];
|
|
45
|
+
// Multi-repo result fields
|
|
46
|
+
repo_results?: HotspotRepoResult[];
|
|
47
|
+
error?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface UseHotspotsOptions {
|
|
51
|
+
days: number;
|
|
52
|
+
repo?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface UseHotspotsReturn {
|
|
56
|
+
data: HotspotData | null;
|
|
57
|
+
isLoading: boolean;
|
|
58
|
+
error: Error | null;
|
|
59
|
+
refresh: () => void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function useHotspots(options: UseHotspotsOptions): UseHotspotsReturn {
|
|
63
|
+
const [data, setData] = useState<HotspotData | null>(null);
|
|
64
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
65
|
+
const [error, setError] = useState<Error | null>(null);
|
|
66
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
67
|
+
|
|
68
|
+
const fetchHotspots = useCallback(() => {
|
|
69
|
+
// Cancel any in-flight request
|
|
70
|
+
if (abortRef.current) {
|
|
71
|
+
abortRef.current.abort();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const controller = new AbortController();
|
|
75
|
+
abortRef.current = controller;
|
|
76
|
+
|
|
77
|
+
setIsLoading(true);
|
|
78
|
+
setError(null);
|
|
79
|
+
|
|
80
|
+
const params = new URLSearchParams({ days: String(options.days) });
|
|
81
|
+
if (options.repo) {
|
|
82
|
+
params.set('repo', options.repo);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
fetch(`/api/hotspots?${params}`, { signal: controller.signal })
|
|
86
|
+
.then((res) => {
|
|
87
|
+
if (!res.ok) {
|
|
88
|
+
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
89
|
+
}
|
|
90
|
+
return res.json();
|
|
91
|
+
})
|
|
92
|
+
.then((json: HotspotData) => {
|
|
93
|
+
setData(json);
|
|
94
|
+
setIsLoading(false);
|
|
95
|
+
})
|
|
96
|
+
.catch((err) => {
|
|
97
|
+
if (err.name === 'AbortError') return;
|
|
98
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
99
|
+
setIsLoading(false);
|
|
100
|
+
});
|
|
101
|
+
}, [options.days, options.repo]);
|
|
102
|
+
|
|
103
|
+
// Cleanup abort controller on unmount
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
return () => {
|
|
106
|
+
if (abortRef.current) {
|
|
107
|
+
abortRef.current.abort();
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}, []);
|
|
111
|
+
|
|
112
|
+
return { data, isLoading, error, refresh: fetchHotspots };
|
|
113
|
+
}
|