@pennyfarthing/cyclist 10.1.0 → 10.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/health-score.d.ts.map +1 -1
- package/dist/api/health-score.js +12 -3
- package/dist/api/health-score.js.map +1 -1
- package/dist/api/index.d.ts +1 -1
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +1 -2
- package/dist/api/index.js.map +1 -1
- package/dist/api/persona.d.ts +2 -0
- package/dist/api/persona.d.ts.map +1 -1
- package/dist/api/persona.js +19 -1
- package/dist/api/persona.js.map +1 -1
- package/dist/api/settings.js +1 -1
- package/dist/api/settings.js.map +1 -1
- package/dist/claude-service.d.ts +8 -2
- package/dist/claude-service.d.ts.map +1 -1
- package/dist/claude-service.js +21 -2
- package/dist/claude-service.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +11 -2
- package/dist/main.js.map +1 -1
- package/dist/plugin-loader.d.ts +49 -0
- package/dist/plugin-loader.d.ts.map +1 -0
- package/dist/plugin-loader.js +92 -0
- package/dist/plugin-loader.js.map +1 -0
- package/dist/preload.js +1 -1
- package/dist/preload.js.map +1 -1
- package/dist/public/css/react.css +1 -1
- package/dist/public/js/react/react.js +35 -35
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +7 -15
- package/dist/server.js.map +1 -1
- package/dist/sprint-data.d.ts.map +1 -1
- package/dist/sprint-data.js +38 -1
- package/dist/sprint-data.js.map +1 -1
- package/dist/story-parser.js +1 -1
- package/dist/story-parser.js.map +1 -1
- package/dist/theme-metadata.js +2 -2
- package/dist/theme-metadata.js.map +1 -1
- package/dist/websocket.d.ts +0 -6
- package/dist/websocket.d.ts.map +1 -1
- package/dist/websocket.js +30 -35
- package/dist/websocket.js.map +1 -1
- package/package.json +2 -1
- package/portraits/fifth-element/large/cornelius-54343.png +0 -0
- package/portraits/fifth-element/large/diva-53453.png +0 -0
- package/portraits/fifth-element/large/korben-34232.png +0 -0
- package/portraits/fifth-element/large/leeloo-54333.png +0 -0
- package/portraits/fifth-element/large/lindberg-34432.png +0 -0
- package/portraits/fifth-element/large/mondoshawan-55131.png +0 -0
- package/portraits/fifth-element/large/munro-25321.png +0 -0
- package/portraits/fifth-element/large/pacoli-45232.png +0 -0
- package/portraits/fifth-element/large/ruby-53544.png +0 -0
- package/portraits/fifth-element/large/zorg-45312.png +0 -0
- package/portraits/fifth-element/medium/cornelius-54343.png +0 -0
- package/portraits/fifth-element/medium/diva-53453.png +0 -0
- package/portraits/fifth-element/medium/korben-34232.png +0 -0
- package/portraits/fifth-element/medium/leeloo-54333.png +0 -0
- package/portraits/fifth-element/medium/lindberg-34432.png +0 -0
- package/portraits/fifth-element/medium/mondoshawan-55131.png +0 -0
- package/portraits/fifth-element/medium/munro-25321.png +0 -0
- package/portraits/fifth-element/medium/pacoli-45232.png +0 -0
- package/portraits/fifth-element/medium/ruby-53544.png +0 -0
- package/portraits/fifth-element/medium/zorg-45312.png +0 -0
- package/src/public/components/AgentPopup.tsx +3 -5
- package/src/public/components/ContextSparkline.tsx +56 -0
- package/src/public/components/ControlBar.tsx +137 -4
- package/src/public/components/HealthGauge.tsx +64 -27
- package/src/public/components/PersonaHeader.tsx +46 -3
- package/src/public/components/TandemPortrait.tsx +71 -0
- package/src/public/components/panels/ACPanel.tsx +1 -1
- package/src/public/components/panels/DebugPanel.tsx +50 -57
- package/src/public/components/panels/GitPanel.tsx +22 -21
- package/src/public/components/panels/MessagePanel.tsx +44 -2
- package/src/public/components/panels/SettingsPanel.tsx +4 -4
- package/src/public/components/panels/SprintPanel.tsx +199 -144
- package/src/public/css/theme-system.css +93 -0
- package/src/public/hooks/useHealthScore.ts +6 -14
- package/src/public/hooks/usePersona.ts +26 -3
- package/src/public/hooks/useSprint.ts +1 -1
- package/src/public/styles/tailwind.css +337 -43
- package/src/public/utils/slash-commands.ts +1 -17
- package/dist/hooks/cyclist-pretooluse-hook.d.ts +0 -60
- package/dist/hooks/cyclist-pretooluse-hook.d.ts.map +0 -1
- package/dist/hooks/cyclist-pretooluse-hook.js +0 -57
- package/dist/hooks/cyclist-pretooluse-hook.js.map +0 -1
- package/dist/hooks/pretooluse-hook.d.ts +0 -89
- package/dist/hooks/pretooluse-hook.d.ts.map +0 -1
- package/dist/hooks/pretooluse-hook.js +0 -235
- package/dist/hooks/pretooluse-hook.js.map +0 -1
- package/dist/notification-sound.d.ts +0 -59
- package/dist/notification-sound.d.ts.map +0 -1
- package/dist/notification-sound.js +0 -219
- package/dist/notification-sound.js.map +0 -1
- package/src/public/types/electron.d.ts +0 -18
|
@@ -175,18 +175,19 @@ function ContextIndicator({
|
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
/**
|
|
178
|
-
* Priority
|
|
178
|
+
* Priority label component - muted text abbreviation
|
|
179
179
|
*/
|
|
180
180
|
function PriorityDot({ priority, storyId }: { priority?: string | null; storyId: string }): React.ReactElement | null {
|
|
181
181
|
if (!priority) return null;
|
|
182
|
-
const colorClass = priority === 'P0' ? 'priority-p0' : priority === 'P1' ? 'priority-p1' : 'priority-p2';
|
|
183
182
|
return (
|
|
184
183
|
<span
|
|
185
|
-
className=
|
|
184
|
+
className="priority-label"
|
|
186
185
|
data-testid={`story-priority-${storyId}`}
|
|
187
186
|
data-priority={priority}
|
|
188
187
|
title={priority}
|
|
189
|
-
|
|
188
|
+
>
|
|
189
|
+
{priority}
|
|
190
|
+
</span>
|
|
190
191
|
);
|
|
191
192
|
}
|
|
192
193
|
|
|
@@ -239,6 +240,158 @@ function JiraLink({ jiraKey, storyId }: { jiraKey: string; storyId: string }): R
|
|
|
239
240
|
);
|
|
240
241
|
}
|
|
241
242
|
|
|
243
|
+
/**
|
|
244
|
+
* EpicGroup - Renders a single epic with its stories
|
|
245
|
+
*/
|
|
246
|
+
function EpicGroup({
|
|
247
|
+
epic,
|
|
248
|
+
isExpanded,
|
|
249
|
+
isArchiving,
|
|
250
|
+
onToggle,
|
|
251
|
+
onKeyDown,
|
|
252
|
+
onArchive,
|
|
253
|
+
}: {
|
|
254
|
+
epic: SprintEpic;
|
|
255
|
+
isExpanded: boolean;
|
|
256
|
+
isArchiving: boolean;
|
|
257
|
+
onToggle: (id: string) => void;
|
|
258
|
+
onKeyDown: (id: string, e: React.KeyboardEvent) => void;
|
|
259
|
+
onArchive: (id: string) => void;
|
|
260
|
+
}): React.ReactElement {
|
|
261
|
+
const { done, total } = calculateEpicProgress(epic);
|
|
262
|
+
const completed = isEpicCompleted(epic);
|
|
263
|
+
|
|
264
|
+
return (
|
|
265
|
+
<div
|
|
266
|
+
className={`epic-group ${completed ? 'epic-completed' : ''}`}
|
|
267
|
+
data-testid={`epic-group-${epic.id}`}
|
|
268
|
+
>
|
|
269
|
+
{/* Epic Header */}
|
|
270
|
+
<div className="epic-header">
|
|
271
|
+
<Button
|
|
272
|
+
variant="ghost"
|
|
273
|
+
size="icon"
|
|
274
|
+
className="epic-toggle"
|
|
275
|
+
data-testid={`epic-toggle-${epic.id}`}
|
|
276
|
+
onClick={() => onToggle(epic.id)}
|
|
277
|
+
onKeyDown={(e) => onKeyDown(epic.id, e)}
|
|
278
|
+
aria-expanded={isExpanded}
|
|
279
|
+
>
|
|
280
|
+
{isExpanded ? '▼' : '▶'}
|
|
281
|
+
</Button>
|
|
282
|
+
<span className="epic-title">{epic.title}</span>
|
|
283
|
+
{epic.jiraKey && <span className="epic-jira">{epic.jiraKey}</span>}
|
|
284
|
+
<ContextIndicator hasContext={epic.hasContext ?? false} testIdPrefix="epic" id={epic.id} />
|
|
285
|
+
{completed && epic.hasContext && (
|
|
286
|
+
<Badge variant="default" className="epic-ready-badge" data-testid={`epic-ready-badge-${epic.id}`}>
|
|
287
|
+
Ready
|
|
288
|
+
</Badge>
|
|
289
|
+
)}
|
|
290
|
+
|
|
291
|
+
{/* Progress bar */}
|
|
292
|
+
<div
|
|
293
|
+
className="epic-progress"
|
|
294
|
+
data-testid={`epic-progress-${epic.id}`}
|
|
295
|
+
data-done={String(done)}
|
|
296
|
+
data-total={String(total)}
|
|
297
|
+
>
|
|
298
|
+
<div
|
|
299
|
+
className="progress-bar"
|
|
300
|
+
style={{ width: `${total > 0 ? (done / total) * 100 : 0}%` }}
|
|
301
|
+
/>
|
|
302
|
+
</div>
|
|
303
|
+
<span
|
|
304
|
+
className="epic-progress-label"
|
|
305
|
+
data-testid={`epic-progress-label-${epic.id}`}
|
|
306
|
+
>
|
|
307
|
+
{done}/{total} pts
|
|
308
|
+
</span>
|
|
309
|
+
|
|
310
|
+
{/* Archive button for completed epics */}
|
|
311
|
+
{completed && (
|
|
312
|
+
<>
|
|
313
|
+
{isArchiving && (
|
|
314
|
+
<span data-testid={`archive-loading-${epic.id}`}>...</span>
|
|
315
|
+
)}
|
|
316
|
+
<Button
|
|
317
|
+
variant="outline"
|
|
318
|
+
size="sm"
|
|
319
|
+
className="archive-button"
|
|
320
|
+
data-testid={`archive-button-${epic.id}`}
|
|
321
|
+
aria-label={`Archive ${epic.id}`}
|
|
322
|
+
disabled={isArchiving}
|
|
323
|
+
onClick={() => onArchive(epic.id)}
|
|
324
|
+
>
|
|
325
|
+
Archive
|
|
326
|
+
</Button>
|
|
327
|
+
</>
|
|
328
|
+
)}
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
{/* Stories list (collapsible) */}
|
|
332
|
+
{isExpanded && (
|
|
333
|
+
<div className="epic-stories">
|
|
334
|
+
{epic.stories.map((story) => {
|
|
335
|
+
const hasContext = story.hasContext ?? false;
|
|
336
|
+
const isBlocked = story.status === 'blocked';
|
|
337
|
+
const assigneeDisplay = formatAssignee(story.assignedTo);
|
|
338
|
+
return (
|
|
339
|
+
<div
|
|
340
|
+
key={story.id}
|
|
341
|
+
className={`story-item ${!hasContext ? 'missing-context' : ''} ${isBlocked ? 'story-blocked' : ''}`}
|
|
342
|
+
data-testid={`story-item-${story.id}`}
|
|
343
|
+
data-status={story.status}
|
|
344
|
+
data-story-id={story.id}
|
|
345
|
+
aria-label={`${story.id}: ${story.title}`}
|
|
346
|
+
>
|
|
347
|
+
<PriorityDot priority={story.priority} storyId={story.id} />
|
|
348
|
+
<StatusBadge status={story.status} storyId={story.id} />
|
|
349
|
+
{story.jiraKey && <JiraLink jiraKey={story.jiraKey} storyId={story.id} />}
|
|
350
|
+
<div className="story-info">
|
|
351
|
+
<span className="story-title">{story.title}</span>
|
|
352
|
+
<span className="story-meta">
|
|
353
|
+
{assigneeDisplay && (
|
|
354
|
+
<span
|
|
355
|
+
className="story-assignee"
|
|
356
|
+
data-testid={`story-assignee-${story.id}`}
|
|
357
|
+
>
|
|
358
|
+
{assigneeDisplay}
|
|
359
|
+
</span>
|
|
360
|
+
)}
|
|
361
|
+
{story.workflow && (
|
|
362
|
+
<span
|
|
363
|
+
className="story-workflow-badge"
|
|
364
|
+
data-testid={`story-workflow-${story.id}`}
|
|
365
|
+
>
|
|
366
|
+
{story.workflow}
|
|
367
|
+
</span>
|
|
368
|
+
)}
|
|
369
|
+
{story.status === 'done' && story.completed && (
|
|
370
|
+
<span
|
|
371
|
+
className="story-completed-date"
|
|
372
|
+
data-testid={`story-completed-${story.id}`}
|
|
373
|
+
>
|
|
374
|
+
{story.completed}
|
|
375
|
+
</span>
|
|
376
|
+
)}
|
|
377
|
+
</span>
|
|
378
|
+
</div>
|
|
379
|
+
<ContextIndicator hasContext={hasContext} testIdPrefix="story" id={story.id} />
|
|
380
|
+
<span
|
|
381
|
+
className="story-points"
|
|
382
|
+
data-testid={`story-points-${story.id}`}
|
|
383
|
+
>
|
|
384
|
+
{story.points}
|
|
385
|
+
</span>
|
|
386
|
+
</div>
|
|
387
|
+
);
|
|
388
|
+
})}
|
|
389
|
+
</div>
|
|
390
|
+
)}
|
|
391
|
+
</div>
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
242
395
|
/**
|
|
243
396
|
* EnhancedSprintPanel - Full sprint management with epic actions
|
|
244
397
|
*/
|
|
@@ -250,12 +403,17 @@ export function EnhancedSprintPanel(): React.ReactElement {
|
|
|
250
403
|
const [confirmArchive, setConfirmArchive] = useState<string | null>(null);
|
|
251
404
|
const [actionError, setActionError] = useState<Error | null>(null);
|
|
252
405
|
|
|
253
|
-
//
|
|
406
|
+
// Split epics into active (has non-done stories) vs completed (all stories done)
|
|
407
|
+
const activeEpics = data?.epics.filter((e) => !isEpicCompleted(e)) ?? [];
|
|
408
|
+
const completedEpics = data?.epics.filter((e) => isEpicCompleted(e)) ?? [];
|
|
409
|
+
|
|
410
|
+
// Expand only active epics by default when data first loads (once only)
|
|
411
|
+
// Completed epics start collapsed
|
|
254
412
|
const hasInitializedExpansion = useRef(false);
|
|
255
413
|
useEffect(() => {
|
|
256
414
|
if (data?.epics && !hasInitializedExpansion.current) {
|
|
257
415
|
hasInitializedExpansion.current = true;
|
|
258
|
-
setExpandedEpics(new Set(
|
|
416
|
+
setExpandedEpics(new Set(activeEpics.map((e) => e.id)));
|
|
259
417
|
}
|
|
260
418
|
}, [data?.epics]);
|
|
261
419
|
|
|
@@ -413,7 +571,7 @@ export function EnhancedSprintPanel(): React.ReactElement {
|
|
|
413
571
|
|
|
414
572
|
<Separator className="my-2" />
|
|
415
573
|
|
|
416
|
-
{/* Section 2:
|
|
574
|
+
{/* Section 2: Active Epics */}
|
|
417
575
|
<section data-section="epics">
|
|
418
576
|
<h2>Current Epics</h2>
|
|
419
577
|
<div data-testid="epic-tree-view">
|
|
@@ -423,146 +581,43 @@ export function EnhancedSprintPanel(): React.ReactElement {
|
|
|
423
581
|
<p className="hint">Promote an epic from Future Initiatives to get started</p>
|
|
424
582
|
</div>
|
|
425
583
|
)}
|
|
426
|
-
{
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
>
|
|
438
|
-
{/* Epic Header */}
|
|
439
|
-
<div className="epic-header">
|
|
440
|
-
<Button
|
|
441
|
-
variant="ghost"
|
|
442
|
-
size="icon"
|
|
443
|
-
className="epic-toggle"
|
|
444
|
-
data-testid={`epic-toggle-${epic.id}`}
|
|
445
|
-
onClick={() => toggleEpic(epic.id)}
|
|
446
|
-
onKeyDown={(e) => handleEpicKeyDown(epic.id, e)}
|
|
447
|
-
aria-expanded={isExpanded}
|
|
448
|
-
>
|
|
449
|
-
{isExpanded ? '▼' : '▶'}
|
|
450
|
-
</Button>
|
|
451
|
-
<span className="epic-title">{epic.title}</span>
|
|
452
|
-
{epic.jiraKey && <span className="epic-jira">{epic.jiraKey}</span>}
|
|
453
|
-
<ContextIndicator hasContext={epic.hasContext ?? false} testIdPrefix="epic" id={epic.id} />
|
|
454
|
-
{completed && epic.hasContext && (
|
|
455
|
-
<Badge variant="default" className="epic-ready-badge" data-testid={`epic-ready-badge-${epic.id}`}>
|
|
456
|
-
Ready
|
|
457
|
-
</Badge>
|
|
458
|
-
)}
|
|
459
|
-
|
|
460
|
-
{/* Progress bar */}
|
|
461
|
-
<div
|
|
462
|
-
className="epic-progress"
|
|
463
|
-
data-testid={`epic-progress-${epic.id}`}
|
|
464
|
-
data-done={String(done)}
|
|
465
|
-
data-total={String(total)}
|
|
466
|
-
>
|
|
467
|
-
<div
|
|
468
|
-
className="progress-bar"
|
|
469
|
-
style={{ width: `${total > 0 ? (done / total) * 100 : 0}%` }}
|
|
470
|
-
/>
|
|
471
|
-
</div>
|
|
472
|
-
<span
|
|
473
|
-
className="epic-progress-label"
|
|
474
|
-
data-testid={`epic-progress-label-${epic.id}`}
|
|
475
|
-
>
|
|
476
|
-
{done}/{total} pts
|
|
477
|
-
</span>
|
|
478
|
-
|
|
479
|
-
{/* Archive button for completed epics */}
|
|
480
|
-
{completed && (
|
|
481
|
-
<>
|
|
482
|
-
{isArchiving && (
|
|
483
|
-
<span data-testid={`archive-loading-${epic.id}`}>...</span>
|
|
484
|
-
)}
|
|
485
|
-
<Button
|
|
486
|
-
variant="outline"
|
|
487
|
-
size="sm"
|
|
488
|
-
className="archive-button"
|
|
489
|
-
data-testid={`archive-button-${epic.id}`}
|
|
490
|
-
aria-label={`Archive ${epic.id}`}
|
|
491
|
-
disabled={isArchiving}
|
|
492
|
-
onClick={() => setConfirmArchive(epic.id)}
|
|
493
|
-
>
|
|
494
|
-
Archive
|
|
495
|
-
</Button>
|
|
496
|
-
</>
|
|
497
|
-
)}
|
|
498
|
-
</div>
|
|
499
|
-
|
|
500
|
-
{/* Stories list (collapsible) */}
|
|
501
|
-
{isExpanded && (
|
|
502
|
-
<div className="epic-stories">
|
|
503
|
-
{epic.stories.map((story) => {
|
|
504
|
-
const hasContext = story.hasContext ?? false;
|
|
505
|
-
const isBlocked = story.status === 'blocked';
|
|
506
|
-
const assigneeDisplay = formatAssignee(story.assignedTo);
|
|
507
|
-
return (
|
|
508
|
-
<div
|
|
509
|
-
key={story.id}
|
|
510
|
-
className={`story-item ${!hasContext ? 'missing-context' : ''} ${isBlocked ? 'story-blocked' : ''}`}
|
|
511
|
-
data-testid={`story-item-${story.id}`}
|
|
512
|
-
data-status={story.status}
|
|
513
|
-
data-story-id={story.id}
|
|
514
|
-
aria-label={`${story.id}: ${story.title}`}
|
|
515
|
-
>
|
|
516
|
-
<PriorityDot priority={story.priority} storyId={story.id} />
|
|
517
|
-
<StatusBadge status={story.status} storyId={story.id} />
|
|
518
|
-
{story.jiraKey && <JiraLink jiraKey={story.jiraKey} storyId={story.id} />}
|
|
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>
|
|
548
|
-
<ContextIndicator hasContext={hasContext} testIdPrefix="story" id={story.id} />
|
|
549
|
-
<span
|
|
550
|
-
className="story-points"
|
|
551
|
-
data-testid={`story-points-${story.id}`}
|
|
552
|
-
>
|
|
553
|
-
{story.points}
|
|
554
|
-
</span>
|
|
555
|
-
</div>
|
|
556
|
-
);
|
|
557
|
-
})}
|
|
558
|
-
</div>
|
|
559
|
-
)}
|
|
560
|
-
</div>
|
|
561
|
-
);
|
|
562
|
-
})}
|
|
584
|
+
{activeEpics.map((epic) => (
|
|
585
|
+
<EpicGroup
|
|
586
|
+
key={epic.id}
|
|
587
|
+
epic={epic}
|
|
588
|
+
isExpanded={expandedEpics.has(epic.id)}
|
|
589
|
+
isArchiving={loadingActions.has(`archive-${epic.id}`)}
|
|
590
|
+
onToggle={toggleEpic}
|
|
591
|
+
onKeyDown={handleEpicKeyDown}
|
|
592
|
+
onArchive={setConfirmArchive}
|
|
593
|
+
/>
|
|
594
|
+
))}
|
|
563
595
|
</div>
|
|
564
596
|
</section>
|
|
565
597
|
|
|
598
|
+
{/* Section 2b: Completed Epics */}
|
|
599
|
+
{completedEpics.length > 0 && (
|
|
600
|
+
<>
|
|
601
|
+
<Separator className="my-2" />
|
|
602
|
+
<section data-section="completed-epics">
|
|
603
|
+
<h2>Completed Epics</h2>
|
|
604
|
+
<div data-testid="completed-epics-section">
|
|
605
|
+
{completedEpics.map((epic) => (
|
|
606
|
+
<EpicGroup
|
|
607
|
+
key={epic.id}
|
|
608
|
+
epic={epic}
|
|
609
|
+
isExpanded={expandedEpics.has(epic.id)}
|
|
610
|
+
isArchiving={loadingActions.has(`archive-${epic.id}`)}
|
|
611
|
+
onToggle={toggleEpic}
|
|
612
|
+
onKeyDown={handleEpicKeyDown}
|
|
613
|
+
onArchive={setConfirmArchive}
|
|
614
|
+
/>
|
|
615
|
+
))}
|
|
616
|
+
</div>
|
|
617
|
+
</section>
|
|
618
|
+
</>
|
|
619
|
+
)}
|
|
620
|
+
|
|
566
621
|
<Separator className="my-2" />
|
|
567
622
|
|
|
568
623
|
{/* Section 3: Future Initiatives */}
|
|
@@ -535,3 +535,96 @@
|
|
|
535
535
|
.tool-stack-content .tool-historical {
|
|
536
536
|
opacity: 0.85;
|
|
537
537
|
}
|
|
538
|
+
|
|
539
|
+
/* =============================================================================
|
|
540
|
+
ACPanel Styling (MSSCI-14763) - Tufte Treatment
|
|
541
|
+
============================================================================= */
|
|
542
|
+
|
|
543
|
+
/* Panel container */
|
|
544
|
+
.ac-panel {
|
|
545
|
+
padding: 0.5rem;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/* Content wrapper — Tufte left border accent */
|
|
549
|
+
.ac-content {
|
|
550
|
+
border-left: 2px solid var(--border);
|
|
551
|
+
padding-left: 0.5rem;
|
|
552
|
+
transition: border-color 0.15s ease;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
.ac-content:hover {
|
|
556
|
+
border-left-color: var(--accent);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/* Progress counter — above the bar, not overlaid */
|
|
560
|
+
.ac-panel .progress-text {
|
|
561
|
+
font-size: 0.6875rem;
|
|
562
|
+
font-family: var(--font-mono);
|
|
563
|
+
font-variant-numeric: tabular-nums;
|
|
564
|
+
color: var(--text-muted);
|
|
565
|
+
margin-bottom: 0.25rem;
|
|
566
|
+
display: block;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/* Progress bar — Tufte: thin line, no rounded corners */
|
|
570
|
+
.ac-panel .progress-bar-container {
|
|
571
|
+
position: relative;
|
|
572
|
+
height: 4px;
|
|
573
|
+
background: var(--border);
|
|
574
|
+
border-radius: 0;
|
|
575
|
+
margin-bottom: 0.5rem;
|
|
576
|
+
overflow: hidden;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
.ac-panel .progress-bar {
|
|
580
|
+
height: 100%;
|
|
581
|
+
background: var(--accent);
|
|
582
|
+
border-radius: 0;
|
|
583
|
+
transition: width 0.3s ease;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/* Criteria list */
|
|
587
|
+
.ac-list {
|
|
588
|
+
display: flex;
|
|
589
|
+
flex-direction: column;
|
|
590
|
+
gap: 0.125rem;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/* Individual criterion */
|
|
594
|
+
.ac-item {
|
|
595
|
+
display: flex;
|
|
596
|
+
align-items: baseline;
|
|
597
|
+
gap: 0.375rem;
|
|
598
|
+
padding: 0.125rem 0;
|
|
599
|
+
font-size: 0.8125rem;
|
|
600
|
+
color: var(--text-primary);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
.ac-item.ac-done {
|
|
604
|
+
color: var(--text-muted);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/* Status icon */
|
|
608
|
+
.ac-icon {
|
|
609
|
+
width: 1rem;
|
|
610
|
+
text-align: center;
|
|
611
|
+
flex-shrink: 0;
|
|
612
|
+
font-size: 0.75rem;
|
|
613
|
+
color: var(--text-secondary);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
.ac-done .ac-icon {
|
|
617
|
+
color: var(--status-success);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/* Criterion text */
|
|
621
|
+
.ac-text {
|
|
622
|
+
flex: 1;
|
|
623
|
+
min-width: 0;
|
|
624
|
+
line-height: 1.4;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
.ac-done .ac-text {
|
|
628
|
+
text-decoration: line-through;
|
|
629
|
+
text-decoration-color: var(--text-muted);
|
|
630
|
+
}
|
|
@@ -18,17 +18,16 @@ export interface UseHealthScoreReturn {
|
|
|
18
18
|
data: HealthScoreData | null;
|
|
19
19
|
isLoading: boolean;
|
|
20
20
|
error: Error | null;
|
|
21
|
+
lastFetchedAt: number | null;
|
|
21
22
|
refresh: () => void;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
const POLL_INTERVAL = 60_000;
|
|
25
|
-
|
|
26
25
|
export function useHealthScore(): UseHealthScoreReturn {
|
|
27
26
|
const [data, setData] = useState<HealthScoreData | null>(null);
|
|
28
27
|
const [isLoading, setIsLoading] = useState(false);
|
|
29
28
|
const [error, setError] = useState<Error | null>(null);
|
|
29
|
+
const [lastFetchedAt, setLastFetchedAt] = useState<number | null>(null);
|
|
30
30
|
const abortRef = useRef<AbortController | null>(null);
|
|
31
|
-
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
32
31
|
|
|
33
32
|
const refresh = useCallback(() => {
|
|
34
33
|
if (abortRef.current) {
|
|
@@ -50,6 +49,7 @@ export function useHealthScore(): UseHealthScoreReturn {
|
|
|
50
49
|
})
|
|
51
50
|
.then((json: HealthScoreData) => {
|
|
52
51
|
setData(json);
|
|
52
|
+
setLastFetchedAt(Date.now());
|
|
53
53
|
setIsLoading(false);
|
|
54
54
|
})
|
|
55
55
|
.catch((err) => {
|
|
@@ -59,19 +59,11 @@ export function useHealthScore(): UseHealthScoreReturn {
|
|
|
59
59
|
});
|
|
60
60
|
}, []);
|
|
61
61
|
|
|
62
|
-
// Auto-poll on mount
|
|
63
62
|
useEffect(() => {
|
|
64
|
-
intervalRef.current = setInterval(refresh, POLL_INTERVAL);
|
|
65
|
-
|
|
66
63
|
return () => {
|
|
67
|
-
|
|
68
|
-
abortRef.current.abort();
|
|
69
|
-
}
|
|
70
|
-
if (intervalRef.current) {
|
|
71
|
-
clearInterval(intervalRef.current);
|
|
72
|
-
}
|
|
64
|
+
abortRef.current?.abort();
|
|
73
65
|
};
|
|
74
|
-
}, [
|
|
66
|
+
}, []);
|
|
75
67
|
|
|
76
|
-
return { data, isLoading, error, refresh };
|
|
68
|
+
return { data, isLoading, error, lastFetchedAt, refresh };
|
|
77
69
|
}
|
|
@@ -13,22 +13,33 @@
|
|
|
13
13
|
|
|
14
14
|
import { useState, useEffect, useRef } from 'react';
|
|
15
15
|
|
|
16
|
+
export interface TandemAgentData {
|
|
17
|
+
character: string;
|
|
18
|
+
role: string;
|
|
19
|
+
slug: string;
|
|
20
|
+
theme: string;
|
|
21
|
+
isThinking: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
16
24
|
export interface PersonaData {
|
|
17
25
|
character: string | null;
|
|
18
26
|
theme: string | null;
|
|
19
27
|
role: string | null;
|
|
20
28
|
slug: string | null;
|
|
21
29
|
quote: string | null; // Random catchphrase from theme
|
|
30
|
+
tandemAgent?: TandemAgentData | null;
|
|
22
31
|
}
|
|
23
32
|
|
|
24
33
|
interface UsePersonaResult {
|
|
25
34
|
persona: PersonaData | null;
|
|
35
|
+
isStreaming: boolean;
|
|
26
36
|
isLoading: boolean;
|
|
27
37
|
error: Error | null;
|
|
28
38
|
}
|
|
29
39
|
|
|
30
40
|
export function usePersona(): UsePersonaResult {
|
|
31
41
|
const [persona, setPersona] = useState<PersonaData | null>(null);
|
|
42
|
+
const [isStreaming, setIsStreaming] = useState(false);
|
|
32
43
|
const [isLoading, setIsLoading] = useState(true);
|
|
33
44
|
const [error, setError] = useState<Error | null>(null);
|
|
34
45
|
const wsRef = useRef<WebSocket | null>(null);
|
|
@@ -48,8 +59,19 @@ export function usePersona(): UsePersonaResult {
|
|
|
48
59
|
|
|
49
60
|
wsRef.current.onmessage = (event) => {
|
|
50
61
|
try {
|
|
51
|
-
const data = JSON.parse(event.data)
|
|
52
|
-
|
|
62
|
+
const data = JSON.parse(event.data);
|
|
63
|
+
|
|
64
|
+
// Story 94-1: Handle streaming state updates
|
|
65
|
+
if (data.type === 'streaming') {
|
|
66
|
+
setIsStreaming(data.isStreaming ?? false);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Persona data (initial or agent change) — extract isStreaming if present
|
|
71
|
+
if (data.isStreaming !== undefined) {
|
|
72
|
+
setIsStreaming(data.isStreaming);
|
|
73
|
+
}
|
|
74
|
+
setPersona(data as PersonaData);
|
|
53
75
|
setIsLoading(false);
|
|
54
76
|
setError(null);
|
|
55
77
|
} catch (err) {
|
|
@@ -59,6 +81,7 @@ export function usePersona(): UsePersonaResult {
|
|
|
59
81
|
|
|
60
82
|
wsRef.current.onclose = () => {
|
|
61
83
|
console.debug('[usePersona] WebSocket closed, reconnecting...');
|
|
84
|
+
setIsStreaming(false);
|
|
62
85
|
reconnectTimeoutRef.current = setTimeout(connect, 2000);
|
|
63
86
|
};
|
|
64
87
|
|
|
@@ -85,5 +108,5 @@ export function usePersona(): UsePersonaResult {
|
|
|
85
108
|
};
|
|
86
109
|
}, []);
|
|
87
110
|
|
|
88
|
-
return { persona, isLoading, error };
|
|
111
|
+
return { persona, isStreaming, isLoading, error };
|
|
89
112
|
}
|
|
@@ -100,7 +100,7 @@ export function useSprint(): UseSprintResult {
|
|
|
100
100
|
const msg = JSON.parse(event.data) as SprintMessage;
|
|
101
101
|
if (msg.type === 'init' || msg.type === 'update') {
|
|
102
102
|
// Extract data, excluding type field
|
|
103
|
-
const { type, ...sprintData } = msg;
|
|
103
|
+
const { type: _type, ...sprintData } = msg;
|
|
104
104
|
setData((prev) => {
|
|
105
105
|
if (!prev) return sprintData as SprintData;
|
|
106
106
|
// Merge partial updates
|