@pennyfarthing/cyclist 9.2.0 → 9.4.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.
Files changed (58) hide show
  1. package/dist/api/hotspots.d.ts +3 -0
  2. package/dist/api/hotspots.d.ts.map +1 -0
  3. package/dist/api/hotspots.js +54 -0
  4. package/dist/api/hotspots.js.map +1 -0
  5. package/dist/api/index.d.ts +1 -0
  6. package/dist/api/index.d.ts.map +1 -1
  7. package/dist/api/index.js +1 -0
  8. package/dist/api/index.js.map +1 -1
  9. package/dist/api/settings.d.ts +1 -1
  10. package/dist/api/settings.d.ts.map +1 -1
  11. package/dist/api/settings.js +44 -17
  12. package/dist/api/settings.js.map +1 -1
  13. package/dist/main.d.ts +4 -0
  14. package/dist/main.d.ts.map +1 -1
  15. package/dist/main.js +7 -0
  16. package/dist/main.js.map +1 -1
  17. package/dist/public/css/react.css +1 -1
  18. package/dist/public/js/react/react.js +43 -39
  19. package/dist/server.d.ts.map +1 -1
  20. package/dist/server.js +3 -1
  21. package/dist/server.js.map +1 -1
  22. package/dist/story-parser.d.ts +17 -0
  23. package/dist/story-parser.d.ts.map +1 -1
  24. package/dist/story-parser.js +183 -13
  25. package/dist/story-parser.js.map +1 -1
  26. package/dist/websocket.d.ts.map +1 -1
  27. package/dist/websocket.js +5 -4
  28. package/dist/websocket.js.map +1 -1
  29. package/package.json +1 -1
  30. package/src/public/App.tsx +2 -0
  31. package/src/public/components/ControlBar.tsx +1 -1
  32. package/src/public/components/DockviewWorkspace.tsx +4 -0
  33. package/src/public/components/FontPicker/index.tsx +118 -33
  34. package/src/public/components/FullFileTree.tsx +223 -0
  35. package/src/public/components/Message.tsx +32 -10
  36. package/src/public/components/MessageView.tsx +176 -93
  37. package/src/public/components/PersonaHeader.tsx +45 -15
  38. package/src/public/components/SubagentSpan.tsx +15 -8
  39. package/src/public/components/ThemePalette/ThemePalette.css +2 -0
  40. package/src/public/components/ToolStack.tsx +23 -13
  41. package/src/public/components/panels/AuditLogPanel.tsx +140 -66
  42. package/src/public/components/panels/ChangedPanel.tsx +30 -44
  43. package/src/public/components/panels/HotspotsPanel.tsx +365 -0
  44. package/src/public/components/panels/MessagePanel.tsx +14 -2
  45. package/src/public/components/panels/SettingsPanel.tsx +10 -10
  46. package/src/public/components/panels/WorkflowPanel.tsx +85 -12
  47. package/src/public/components/panels/index.ts +1 -0
  48. package/src/public/components/ui/switch.tsx +2 -2
  49. package/src/public/css/theme-system.css +71 -43
  50. package/src/public/hooks/useFileBrowser.ts +71 -0
  51. package/src/public/hooks/useHotspots.ts +113 -0
  52. package/src/public/hooks/useStory.ts +12 -3
  53. package/src/public/images/cyclist-dark.png +0 -0
  54. package/src/public/images/cyclist-light.png +0 -0
  55. package/src/public/styles/tailwind.css +428 -69
  56. package/src/public/types/message.ts +4 -0
  57. package/src/public/utils/slash-commands.ts +1 -1
  58. package/src/public/utils/toolStackGrouper.ts +4 -5
@@ -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-color: var(--bg-primary);
158
- border-radius: 6px;
159
- border: 1px solid var(--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 - the main bar */
173
+ /* Tool Header - compact single-line */
169
174
  .tool-header {
170
175
  display: flex;
171
176
  align-items: center;
172
- gap: 0.625rem;
173
- padding: 0.5rem 0.75rem;
174
- background-color: var(--bg-secondary);
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
- /* Keep left border for additional visual cue */
205
- .tool-call-block.tool-read { border-left: 3px solid var(--tool-read-color); }
206
- .tool-call-block.tool-write { border-left: 3px solid var(--tool-write-color); }
207
- .tool-call-block.tool-bash { border-left: 3px solid var(--tool-bash-color); }
208
- .tool-call-block.tool-glob { border-left: 3px solid var(--tool-glob-color); }
209
- .tool-call-block.tool-grep { border-left: 3px solid var(--tool-grep-color); }
210
- .tool-call-block.tool-edit { border-left: 3px solid var(--tool-edit-color); }
211
- .tool-call-block.tool-task { border-left: 3px solid var(--tool-task-color); }
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.375rem 0.75rem;
255
- background-color: var(--bg-tertiary);
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.75rem 0;
395
- border-radius: 8px;
396
- background-color: var(--bg-secondary);
397
- border: 1px solid var(--border);
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.75rem;
409
- padding: 0.625rem 0.875rem;
414
+ gap: 0.5rem;
415
+ padding: 0.25rem 0;
410
416
  cursor: pointer;
411
417
  user-select: none;
412
- background-color: var(--bg-secondary);
413
- border-bottom: 1px solid transparent;
414
- transition: background-color 0.15s ease, border-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: var(--border);
424
+ border-bottom-color: transparent;
419
425
  }
420
426
 
421
427
  .tool-stack-header:hover {
422
- background-color: var(--bg-tertiary);
428
+ background-color: var(--bg-secondary);
423
429
  }
424
430
 
425
431
  .tool-stack-header:focus {
@@ -438,11 +444,30 @@
438
444
  transition: transform 0.2s ease;
439
445
  }
440
446
 
441
- /* Tool count with status context */
442
- .tool-stack-count {
447
+ /* Tool count - small de-emphasized badge */
448
+ .tool-stack-count-badge {
443
449
  font-weight: 600;
450
+ font-size: 0.6875rem;
451
+ color: var(--text-muted);
452
+ background-color: var(--bg-tertiary);
453
+ padding: 0.0625rem 0.375rem;
454
+ border-radius: 9999px;
455
+ font-family: var(--font-mono);
456
+ min-width: 1.25rem;
457
+ text-align: center;
458
+ flex-shrink: 0;
459
+ }
460
+
461
+ /* Active tool summary - the main visual element while running */
462
+ .tool-stack-active-summary {
463
+ font-weight: 500;
444
464
  font-size: 0.8125rem;
445
465
  color: var(--text-primary);
466
+ flex: 1;
467
+ min-width: 0;
468
+ overflow: hidden;
469
+ text-overflow: ellipsis;
470
+ white-space: nowrap;
446
471
  }
447
472
 
448
473
  /* Mini tool badges in collapsed header */
@@ -472,10 +497,11 @@
472
497
  .tool-mini-badge.badge-edit { background-color: var(--tool-edit-color); }
473
498
  .tool-mini-badge.badge-task { background-color: var(--tool-task-color); }
474
499
 
475
- /* Summary in collapsed state */
500
+ /* Summary in collapsed state - still prominent */
476
501
  .tool-stack-summary {
477
- color: var(--text-muted);
478
- font-size: 0.75rem;
502
+ color: var(--text-secondary);
503
+ font-size: 0.8125rem;
504
+ font-weight: 500;
479
505
  overflow: hidden;
480
506
  text-overflow: ellipsis;
481
507
  white-space: nowrap;
@@ -495,14 +521,16 @@
495
521
  .tool-stack-content {
496
522
  display: flex;
497
523
  flex-direction: column;
498
- gap: 0.5rem;
499
- padding: 0.75rem;
500
- background-color: var(--bg-tertiary);
524
+ gap: 0.125rem;
525
+ padding: 0.25rem 0;
526
+ background: none;
501
527
  }
502
528
 
503
- /* Visual distinction for tools within a stack */
529
+ /* Within a stack, tool blocks don't need extra indent or left border */
504
530
  .tool-stack-content .tool-call-block {
505
- margin: 0;
531
+ margin-left: 0;
532
+ border-left: none;
533
+ padding-left: 0;
506
534
  }
507
535
 
508
536
  /* Tool state classes within stack */
@@ -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
+ }
@@ -11,7 +11,7 @@
11
11
  import { useState, useEffect, useRef } from 'react';
12
12
 
13
13
  // Import types from story-parser for criteria and workflow
14
- import type { CriteriaItem, WorkflowPhase } from '../../../story-parser.js';
14
+ import type { CriteriaItem, WorkflowPhase, AvailableWorkflow } from '../../../story-parser.js';
15
15
 
16
16
  export interface StoryData {
17
17
  id: string;
@@ -24,15 +24,19 @@ export interface StoryData {
24
24
  // MSSCI-12849: AC and BikeLane panel data
25
25
  criteria?: CriteriaItem[] | null;
26
26
  workflowPhases?: WorkflowPhase[] | null;
27
+ // MSSCI-14300: Distinguish phased vs stepped workflow rendering
28
+ workflowType?: string;
27
29
  }
28
30
 
29
31
  // Re-export types for panel components
30
- export type { CriteriaItem, WorkflowPhase };
32
+ export type { CriteriaItem, WorkflowPhase, AvailableWorkflow };
31
33
 
32
34
  interface UseStoryResult {
33
35
  story: StoryData | null;
34
36
  isLoading: boolean;
35
37
  error: Error | null;
38
+ // MSSCI-14301: Available workflows for discovery panel
39
+ availableWorkflows: AvailableWorkflow[] | null;
36
40
  }
37
41
 
38
42
  /** WebSocket message format from /ws/story */
@@ -44,7 +48,9 @@ interface StoryMessage {
44
48
  status?: string | null;
45
49
  points?: number | null;
46
50
  workflow?: WorkflowPhase[] | null;
51
+ workflowType?: string | null;
47
52
  criteria?: CriteriaItem[] | null;
53
+ availableWorkflows?: AvailableWorkflow[] | null;
48
54
  [key: string]: unknown;
49
55
  }
50
56
 
@@ -59,6 +65,7 @@ function transformMessage(msg: StoryMessage): StoryData | null {
59
65
  points: msg.points ?? undefined,
60
66
  criteria: msg.criteria,
61
67
  workflowPhases: msg.workflow,
68
+ workflowType: msg.workflowType ?? undefined,
62
69
  };
63
70
  }
64
71
 
@@ -66,6 +73,7 @@ export function useStory(): UseStoryResult {
66
73
  const [story, setStory] = useState<StoryData | null>(null);
67
74
  const [isLoading, setIsLoading] = useState(true);
68
75
  const [error, setError] = useState<Error | null>(null);
76
+ const [availableWorkflows, setAvailableWorkflows] = useState<AvailableWorkflow[] | null>(null);
69
77
  const wsRef = useRef<WebSocket | null>(null);
70
78
  const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
71
79
 
@@ -86,6 +94,7 @@ export function useStory(): UseStoryResult {
86
94
  const msg = JSON.parse(event.data) as StoryMessage;
87
95
  if (msg.type === 'init' || msg.type === 'update') {
88
96
  setStory(transformMessage(msg));
97
+ setAvailableWorkflows(msg.availableWorkflows ?? null);
89
98
  setIsLoading(false);
90
99
  setError(null);
91
100
  }
@@ -122,5 +131,5 @@ export function useStory(): UseStoryResult {
122
131
  };
123
132
  }, []);
124
133
 
125
- return { story, isLoading, error };
134
+ return { story, isLoading, error, availableWorkflows };
126
135
  }