@assistkick/create 1.6.0 → 1.7.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/bin/create.js +0 -0
- package/package.json +7 -9
- package/templates/assistkick-product-system/packages/frontend/index.html +3 -0
- package/templates/assistkick-product-system/packages/frontend/package.json +5 -1
- package/templates/assistkick-product-system/packages/frontend/src/App.tsx +16 -7
- package/templates/assistkick-product-system/packages/frontend/src/components/DesignSystemView.tsx +363 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/GitRepoModal.tsx +6 -8
- package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +92 -188
- package/templates/assistkick-product-system/packages/frontend/src/components/QaIssueSheet.tsx +11 -20
- package/templates/assistkick-product-system/packages/frontend/src/components/SidePanel.tsx +15 -70
- package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +149 -77
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCard.tsx +254 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCardShowcase.tsx +216 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/NavBarSidekick.tsx +163 -0
- package/templates/assistkick-product-system/packages/frontend/src/hooks/useGraph.ts +6 -21
- package/templates/assistkick-product-system/packages/frontend/src/hooks/useProjects.ts +15 -80
- package/templates/assistkick-product-system/packages/frontend/src/routes/CoherenceRoute.tsx +19 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/DashboardLayout.tsx +54 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/DesignSystemRoute.tsx +6 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/GraphRoute.tsx +93 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/KanbanRoute.tsx +30 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/TerminalRoute.tsx +9 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/UsersRoute.tsx +6 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useGitModalStore.ts +14 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useGraphStore.ts +36 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useGraphUIStore.ts +25 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useProjectStore.ts +87 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useQaSheetStore.ts +27 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useSidePanelStore.ts +76 -0
- package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +64 -100
- package/templates/assistkick-product-system/packages/frontend/vite.config.ts +2 -1
- package/templates/assistkick-product-system/packages/shared/lib/graph.ts +11 -5
- package/templates/skills/assistkick-bootstrap/SKILL.md +3 -3
- package/templates/skills/assistkick-code-reviewer/SKILL.md +2 -2
- package/templates/skills/assistkick-debugger/SKILL.md +2 -2
- package/templates/skills/assistkick-developer/SKILL.md +3 -3
- package/templates/skills/assistkick-interview/SKILL.md +2 -2
- package/templates/assistkick-product-system/packages/frontend/package-lock.json +0 -2666
|
@@ -2,6 +2,7 @@ import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
|
2
2
|
import { apiClient } from '../api/client';
|
|
3
3
|
import { COLUMNS, PIPELINE_STATUS_LABELS } from '../constants/graph';
|
|
4
4
|
import { useToast } from '../hooks/useToast';
|
|
5
|
+
import { KanbanCard } from './ds/KanbanCard';
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
interface KanbanViewProps {
|
|
@@ -344,38 +345,50 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
|
|
|
344
345
|
}
|
|
345
346
|
};
|
|
346
347
|
|
|
347
|
-
if (error) return <div className="error-
|
|
348
|
-
if (!kanbanData) return <div className="
|
|
348
|
+
if (error) return <div className="p-4 text-error text-[13px]">{error}</div>;
|
|
349
|
+
if (!kanbanData) return <div className="p-4 text-content-muted text-[13px]">Loading...</div>;
|
|
349
350
|
|
|
350
351
|
return (
|
|
351
|
-
<div className="
|
|
352
|
+
<div className="flex h-full w-full gap-2 overflow-x-auto">
|
|
352
353
|
{COLUMNS.map(col => {
|
|
353
354
|
const cards = getCardsForColumn(col.id);
|
|
354
355
|
const sourceColumn = draggedCard ? kanbanData?.[draggedCard]?.column : null;
|
|
355
|
-
const isDropTarget = draggedCard && col.id !== sourceColumn;
|
|
356
356
|
const isDragOver = dragOverColumn === col.id;
|
|
357
357
|
|
|
358
358
|
return (
|
|
359
|
-
<div key={col.id} className="
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
<span className="
|
|
359
|
+
<div key={col.id} className="flex shrink-0 basis-[calc(20%-0.4rem)] flex-col rounded-xl border border-edge bg-surface-alt overflow-hidden" data-column={col.id}>
|
|
360
|
+
{/* Column header */}
|
|
361
|
+
<div className="flex items-center justify-between px-3 py-2.5 border-b border-edge bg-surface-raised">
|
|
362
|
+
<div className="flex items-center gap-2">
|
|
363
|
+
<span className="text-[12px] font-bold uppercase tracking-wider text-content">{col.label}</span>
|
|
364
|
+
<span className="rounded-md bg-surface px-1.5 py-0.5 text-[11px] font-mono text-content-muted">{cards.length}</span>
|
|
364
365
|
</div>
|
|
365
366
|
{col.id === 'todo' && (
|
|
366
367
|
playAllRunning ? (
|
|
367
|
-
<button
|
|
368
|
+
<button
|
|
369
|
+
className="flex h-6 w-6 items-center justify-center rounded-full border border-error text-error text-[12px] hover:bg-error hover:text-surface transition-colors cursor-pointer"
|
|
370
|
+
title="Stop processing"
|
|
371
|
+
onClick={stopPlayAll}
|
|
372
|
+
>
|
|
368
373
|
{'\u25A0'}
|
|
369
374
|
</button>
|
|
370
375
|
) : (
|
|
371
|
-
<button
|
|
376
|
+
<button
|
|
377
|
+
className="flex h-6 w-6 items-center justify-center rounded-full border border-accent text-accent text-[9px] tracking-[-2px] pl-0.5 hover:bg-accent hover:text-surface transition-colors cursor-pointer"
|
|
378
|
+
title="Start automated development for all TODO features"
|
|
379
|
+
onClick={startPlayAll}
|
|
380
|
+
>
|
|
372
381
|
{'\u25B6\u25B6'}
|
|
373
382
|
</button>
|
|
374
383
|
)
|
|
375
384
|
)}
|
|
376
385
|
</div>
|
|
386
|
+
{/* Column body */}
|
|
377
387
|
<div
|
|
378
|
-
className={
|
|
388
|
+
className={[
|
|
389
|
+
'flex flex-1 flex-col gap-2 overflow-y-auto p-2',
|
|
390
|
+
isDragOver ? 'bg-accent/10' : '',
|
|
391
|
+
].join(' ')}
|
|
379
392
|
data-column={col.id}
|
|
380
393
|
onDragOver={(e) => handleDragOver(e, col.id)}
|
|
381
394
|
onDragLeave={handleDragLeave}
|
|
@@ -385,192 +398,83 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
|
|
|
385
398
|
const pct = Math.round((card.completeness || 0) * 100);
|
|
386
399
|
const pStatus = pipelineStatuses[card.id];
|
|
387
400
|
const isActive = pStatus && !['idle', 'completed', 'blocked', 'failed', 'interrupted'].includes(pStatus.status);
|
|
388
|
-
const isTerminal = pStatus && ['completed', 'failed', 'blocked', 'interrupted'].includes(pStatus.status);
|
|
389
401
|
const pipelineStatusValue = pStatus?.status ?? 'idle';
|
|
390
402
|
const showResumeBtn = ['in_progress', 'in_review'].includes(card.column)
|
|
391
403
|
&& !isActive
|
|
392
404
|
&& ['interrupted', 'failed', 'idle'].includes(pipelineStatusValue);
|
|
393
405
|
|
|
406
|
+
// Build tool calls + meta from stage stats or flat toolCalls
|
|
407
|
+
let toolCalls: Record<string, number> | undefined;
|
|
408
|
+
let metaPills: string[] | undefined;
|
|
409
|
+
let stopReason: string | undefined;
|
|
410
|
+
|
|
411
|
+
const ss = pStatus?.stageStats;
|
|
412
|
+
const availableTabs = ss
|
|
413
|
+
? STAGE_TABS.filter(t => hasStageData(ss[t.key]))
|
|
414
|
+
: [];
|
|
415
|
+
|
|
416
|
+
if (availableTabs.length > 0) {
|
|
417
|
+
const currentTab = activeStageTab[card.id] || availableTabs[availableTabs.length - 1].key;
|
|
418
|
+
const stage = ss[currentTab];
|
|
419
|
+
const tc = stage?.toolCalls;
|
|
420
|
+
if (tc && tc.total > 0) {
|
|
421
|
+
toolCalls = {};
|
|
422
|
+
if (tc.read > 0) toolCalls['Read'] = tc.read;
|
|
423
|
+
if (tc.bash > 0) toolCalls['Tools'] = tc.bash;
|
|
424
|
+
if (tc.write > 0) toolCalls['Write'] = tc.write;
|
|
425
|
+
if (tc.edit > 0) toolCalls['Edit'] = tc.edit;
|
|
426
|
+
}
|
|
427
|
+
const pills: string[] = [];
|
|
428
|
+
if (stage?.lastTurnUsage) pills.push(`Ctx ${formatContextFill(stage.lastTurnUsage, stage.contextWindow)}`);
|
|
429
|
+
if (stage?.numTurns != null) pills.push(`${stage.numTurns}t`);
|
|
430
|
+
if (stage?.costUsd != null) pills.push(formatCost(stage.costUsd));
|
|
431
|
+
if (stage?.model) pills.push(formatModel(stage.model));
|
|
432
|
+
if (pills.length > 0) metaPills = pills;
|
|
433
|
+
if (stage?.stopReason) stopReason = stage.stopReason;
|
|
434
|
+
} else if (pStatus?.toolCalls?.total > 0) {
|
|
435
|
+
toolCalls = {};
|
|
436
|
+
if (pStatus.toolCalls.read > 0) toolCalls['Read'] = pStatus.toolCalls.read;
|
|
437
|
+
if (pStatus.toolCalls.bash > 0) toolCalls['Tools'] = pStatus.toolCalls.bash;
|
|
438
|
+
if (pStatus.toolCalls.write > 0) toolCalls['Write'] = pStatus.toolCalls.write;
|
|
439
|
+
if (pStatus.toolCalls.edit > 0) toolCalls['Edit'] = pStatus.toolCalls.edit;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const issueLabel = card.notes.length > 0
|
|
443
|
+
? `${card.notes.length} issue${card.notes.length !== 1 ? 's' : ''} reported`
|
|
444
|
+
: card.column === 'qa'
|
|
445
|
+
? '+ Report Issue'
|
|
446
|
+
: 'No issues';
|
|
447
|
+
|
|
394
448
|
return (
|
|
395
|
-
<
|
|
449
|
+
<KanbanCard
|
|
396
450
|
key={card.id}
|
|
397
|
-
|
|
398
|
-
|
|
451
|
+
id={card.id}
|
|
452
|
+
name={card.name}
|
|
453
|
+
pct={pct}
|
|
454
|
+
kind={card.kind}
|
|
455
|
+
rejectionCount={card.rejectionCount}
|
|
456
|
+
blocked={card.devBlocked}
|
|
457
|
+
pipeline={pipelineStatusValue}
|
|
458
|
+
pipelineLabel={pStatus && pStatus.status !== 'idle' ? getPipelineBadgeText(pStatus.status, card.rejectionCount) : undefined}
|
|
459
|
+
toolCalls={toolCalls}
|
|
460
|
+
meta={metaPills}
|
|
461
|
+
stopReason={stopReason}
|
|
462
|
+
issueCount={card.notes.length}
|
|
463
|
+
issueLabel={issueLabel}
|
|
464
|
+
showPlay={card.column === 'todo' && !card.devBlocked}
|
|
465
|
+
showResume={showResumeBtn}
|
|
466
|
+
copied={copiedId === card.id}
|
|
467
|
+
playAllActive={playAllCurrentFeature === card.id}
|
|
468
|
+
onClick={() => onCardClick({ id: card.id, name: card.name })}
|
|
469
|
+
onCopy={() => handleCopy(card)}
|
|
470
|
+
onPlay={() => handleStartPipeline(card.id)}
|
|
471
|
+
onResume={() => handleResumePipeline(card.id)}
|
|
472
|
+
onUnblock={() => handleUnblock(card.id)}
|
|
473
|
+
onIssuesClick={() => onIssuesClick(card.id, card.name, card.column, card.notes, card.reviews)}
|
|
399
474
|
draggable
|
|
400
475
|
onDragStart={(e) => handleDragStart(e, card.id)}
|
|
401
476
|
onDragEnd={handleDragEnd}
|
|
402
|
-
|
|
403
|
-
if ((e.target as HTMLElement).closest('button, textarea, .kanban-note-form')) return;
|
|
404
|
-
onCardClick({ id: card.id, name: card.name });
|
|
405
|
-
}}
|
|
406
|
-
style={{ cursor: 'pointer' }}
|
|
407
|
-
>
|
|
408
|
-
<div className="kanban-card-header">
|
|
409
|
-
<span className="kanban-card-id">
|
|
410
|
-
{card.id}
|
|
411
|
-
{card.kind && card.kind !== 'new' && (
|
|
412
|
-
<> <span className={`kanban-card-kind kind-${card.kind}`}>{card.kind}</span></>
|
|
413
|
-
)}
|
|
414
|
-
</span>
|
|
415
|
-
<button
|
|
416
|
-
className={`kanban-copy-btn${copiedId === card.id ? ' copied' : ''}`}
|
|
417
|
-
title="Copy feature ID and name"
|
|
418
|
-
onClick={(e) => { e.stopPropagation(); handleCopy(card); }}
|
|
419
|
-
>
|
|
420
|
-
{copiedId === card.id ? '\u2713' : '\uD83D\uDCCB'}
|
|
421
|
-
</button>
|
|
422
|
-
<span className="kanban-card-header-right">
|
|
423
|
-
{card.rejectionCount > 0 && (
|
|
424
|
-
<span className={`kanban-card-rejections${card.rejectionCount >= 3 ? ' high' : ''}`}>
|
|
425
|
-
{card.rejectionCount}x rejected
|
|
426
|
-
</span>
|
|
427
|
-
)}
|
|
428
|
-
{card.column === 'todo' && !card.devBlocked && (
|
|
429
|
-
<button
|
|
430
|
-
className="kanban-play-btn"
|
|
431
|
-
title="Start automated development pipeline"
|
|
432
|
-
onClick={(e) => { e.stopPropagation(); handleStartPipeline(card.id); }}
|
|
433
|
-
>
|
|
434
|
-
{'\u25B6'}
|
|
435
|
-
</button>
|
|
436
|
-
)}
|
|
437
|
-
{showResumeBtn && (
|
|
438
|
-
<button
|
|
439
|
-
className="kanban-play-btn kanban-resume-btn"
|
|
440
|
-
title="Resume pipeline from last completed step"
|
|
441
|
-
onClick={(e) => { e.stopPropagation(); handleResumePipeline(card.id); }}
|
|
442
|
-
>
|
|
443
|
-
{'\u25B6'}
|
|
444
|
-
</button>
|
|
445
|
-
)}
|
|
446
|
-
{card.devBlocked && (
|
|
447
|
-
<span className="kanban-blocked-badge">Blocked</span>
|
|
448
|
-
)}
|
|
449
|
-
</span>
|
|
450
|
-
</div>
|
|
451
|
-
|
|
452
|
-
<div className="kanban-card-name">{card.name}</div>
|
|
453
|
-
|
|
454
|
-
<div className="kanban-card-completeness-row">
|
|
455
|
-
<span className="kanban-card-completeness-prefix">Spec</span>
|
|
456
|
-
<div className="kanban-card-completeness">
|
|
457
|
-
<div className="kanban-card-completeness-fill" style={{ width: `${pct}%` }} />
|
|
458
|
-
</div>
|
|
459
|
-
<span className="kanban-card-completeness-label">{pct}%</span>
|
|
460
|
-
</div>
|
|
461
|
-
|
|
462
|
-
{pStatus && pStatus.status !== 'idle' && (
|
|
463
|
-
<div className={`kanban-pipeline-status${isActive ? ' pipeline-active' : ''}${isTerminal ? ` pipeline-${pStatus.status}` : ''}`}>
|
|
464
|
-
{getPipelineBadgeText(pStatus.status, card.rejectionCount)}
|
|
465
|
-
</div>
|
|
466
|
-
)}
|
|
467
|
-
|
|
468
|
-
{(() => {
|
|
469
|
-
const ss = pStatus?.stageStats;
|
|
470
|
-
const availableTabs = ss
|
|
471
|
-
? STAGE_TABS.filter(t => hasStageData(ss[t.key]))
|
|
472
|
-
: [];
|
|
473
|
-
// Fall back to flat toolCalls for old data without stageStats
|
|
474
|
-
if (availableTabs.length === 0 && pStatus?.toolCalls?.total > 0) {
|
|
475
|
-
return (
|
|
476
|
-
<div className="kanban-tool-calls">
|
|
477
|
-
{[
|
|
478
|
-
{ label: 'Write', count: pStatus.toolCalls.write },
|
|
479
|
-
{ label: 'Edit', count: pStatus.toolCalls.edit },
|
|
480
|
-
{ label: 'Read', count: pStatus.toolCalls.read },
|
|
481
|
-
{ label: 'Bash', count: pStatus.toolCalls.bash },
|
|
482
|
-
].filter(i => i.count > 0).map(i => (
|
|
483
|
-
<span key={i.label} className="kanban-tool-badge">{i.label}: {i.count}</span>
|
|
484
|
-
))}
|
|
485
|
-
</div>
|
|
486
|
-
);
|
|
487
|
-
}
|
|
488
|
-
if (availableTabs.length === 0) return null;
|
|
489
|
-
|
|
490
|
-
const currentTab = activeStageTab[card.id] || availableTabs[availableTabs.length - 1].key;
|
|
491
|
-
const stage = ss[currentTab];
|
|
492
|
-
const tc = stage?.toolCalls;
|
|
493
|
-
|
|
494
|
-
return (
|
|
495
|
-
<div className="kanban-stage-stats" onClick={(e) => e.stopPropagation()}>
|
|
496
|
-
<div className="kanban-stage-tabs">
|
|
497
|
-
{availableTabs.map(t => (
|
|
498
|
-
<button
|
|
499
|
-
key={t.key}
|
|
500
|
-
className={`kanban-stage-tab${currentTab === t.key ? ' active' : ''}`}
|
|
501
|
-
onClick={(e) => {
|
|
502
|
-
e.stopPropagation();
|
|
503
|
-
setActiveStageTab(prev => ({ ...prev, [card.id]: t.key }));
|
|
504
|
-
}}
|
|
505
|
-
>
|
|
506
|
-
{t.label}
|
|
507
|
-
</button>
|
|
508
|
-
))}
|
|
509
|
-
</div>
|
|
510
|
-
<div className="kanban-stage-body">
|
|
511
|
-
{tc && tc.total > 0 && (
|
|
512
|
-
<div className="kanban-tool-calls">
|
|
513
|
-
{[
|
|
514
|
-
{ label: 'Write', count: tc.write },
|
|
515
|
-
{ label: 'Edit', count: tc.edit },
|
|
516
|
-
{ label: 'Read', count: tc.read },
|
|
517
|
-
{ label: 'Bash', count: tc.bash },
|
|
518
|
-
].filter(i => i.count > 0).map(i => (
|
|
519
|
-
<span key={i.label} className="kanban-tool-badge">{i.label}: {i.count}</span>
|
|
520
|
-
))}
|
|
521
|
-
</div>
|
|
522
|
-
)}
|
|
523
|
-
<div className="kanban-stage-meta">
|
|
524
|
-
{stage?.lastTurnUsage && (
|
|
525
|
-
<span className="kanban-stage-meta-item" title="Peak context window utilization">
|
|
526
|
-
Ctx: {formatContextFill(stage.lastTurnUsage, stage.contextWindow)}
|
|
527
|
-
</span>
|
|
528
|
-
)}
|
|
529
|
-
{stage?.numTurns != null && (
|
|
530
|
-
<span className="kanban-stage-meta-item" title="Agentic turns">
|
|
531
|
-
{stage.numTurns} turns
|
|
532
|
-
</span>
|
|
533
|
-
)}
|
|
534
|
-
{stage?.costUsd != null && (
|
|
535
|
-
<span className="kanban-stage-meta-item" title={`Model: ${stage.model || 'unknown'}`}>
|
|
536
|
-
{formatCost(stage.costUsd)}
|
|
537
|
-
{stage.model && <span className="kanban-stage-model"> {formatModel(stage.model)}</span>}
|
|
538
|
-
</span>
|
|
539
|
-
)}
|
|
540
|
-
{stage?.stopReason && (
|
|
541
|
-
<span className="kanban-stage-meta-item kanban-stop-reason" title="Stop reason">
|
|
542
|
-
{stage.stopReason}
|
|
543
|
-
</span>
|
|
544
|
-
)}
|
|
545
|
-
</div>
|
|
546
|
-
</div>
|
|
547
|
-
</div>
|
|
548
|
-
);
|
|
549
|
-
})()}
|
|
550
|
-
|
|
551
|
-
{card.devBlocked && (
|
|
552
|
-
<button
|
|
553
|
-
className="kanban-unblock-btn"
|
|
554
|
-
onClick={(e) => { e.stopPropagation(); handleUnblock(card.id); }}
|
|
555
|
-
>
|
|
556
|
-
Unblock
|
|
557
|
-
</button>
|
|
558
|
-
)}
|
|
559
|
-
|
|
560
|
-
<button
|
|
561
|
-
className={`kanban-issues-btn${card.notes.length > 0 ? ' has-issues' : ''}`}
|
|
562
|
-
onClick={(e) => {
|
|
563
|
-
e.stopPropagation();
|
|
564
|
-
onIssuesClick(card.id, card.name, card.column, card.notes, card.reviews);
|
|
565
|
-
}}
|
|
566
|
-
>
|
|
567
|
-
{card.notes.length > 0
|
|
568
|
-
? `${card.notes.length} issue${card.notes.length !== 1 ? 's' : ''} reported`
|
|
569
|
-
: card.column === 'qa'
|
|
570
|
-
? '+ Report Issue'
|
|
571
|
-
: 'No issues'}
|
|
572
|
-
</button>
|
|
573
|
-
</div>
|
|
477
|
+
/>
|
|
574
478
|
);
|
|
575
479
|
})}
|
|
576
480
|
</div>
|
package/templates/assistkick-product-system/packages/frontend/src/components/QaIssueSheet.tsx
CHANGED
|
@@ -1,21 +1,13 @@
|
|
|
1
1
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
2
2
|
import { apiClient } from '../api/client';
|
|
3
|
+
import { useQaSheetStore } from '../stores/useQaSheetStore';
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
notes: any[];
|
|
10
|
-
reviews: any[];
|
|
11
|
-
onClose: () => void;
|
|
12
|
-
onNotesChanged: () => void;
|
|
13
|
-
}
|
|
5
|
+
export function QaIssueSheet() {
|
|
6
|
+
const {
|
|
7
|
+
isOpen, featureId, featureName, column, notes: initialNotes,
|
|
8
|
+
reviews: initialReviews, close,
|
|
9
|
+
} = useQaSheetStore();
|
|
14
10
|
|
|
15
|
-
export function QaIssueSheet({
|
|
16
|
-
isOpen, featureId, featureName, column, notes: initialNotes, reviews: initialReviews,
|
|
17
|
-
onClose, onNotesChanged,
|
|
18
|
-
}: QaIssueSheetProps) {
|
|
19
11
|
const sheetRef = useRef<HTMLDivElement>(null);
|
|
20
12
|
const [notes, setNotes] = useState<any[]>(initialNotes);
|
|
21
13
|
const [reviews, setReviews] = useState<any[]>(initialReviews);
|
|
@@ -25,7 +17,7 @@ export function QaIssueSheet({
|
|
|
25
17
|
|
|
26
18
|
const isEditable = column === 'qa';
|
|
27
19
|
|
|
28
|
-
// Sync when
|
|
20
|
+
// Sync when store data changes
|
|
29
21
|
useEffect(() => {
|
|
30
22
|
setNotes(initialNotes);
|
|
31
23
|
setReviews(initialReviews);
|
|
@@ -39,12 +31,12 @@ export function QaIssueSheet({
|
|
|
39
31
|
const handleClick = (e: MouseEvent) => {
|
|
40
32
|
if (sheetRef.current && !sheetRef.current.contains(e.target as Node)) {
|
|
41
33
|
if ((e.target as HTMLElement).closest('.kanban-issues-btn')) return;
|
|
42
|
-
|
|
34
|
+
close();
|
|
43
35
|
}
|
|
44
36
|
};
|
|
45
37
|
document.addEventListener('click', handleClick);
|
|
46
38
|
return () => document.removeEventListener('click', handleClick);
|
|
47
|
-
}, [isOpen,
|
|
39
|
+
}, [isOpen, close]);
|
|
48
40
|
|
|
49
41
|
const refreshNotes = useCallback(async () => {
|
|
50
42
|
if (!featureId) return;
|
|
@@ -55,11 +47,10 @@ export function QaIssueSheet({
|
|
|
55
47
|
setNotes(entry.notes || []);
|
|
56
48
|
setReviews(entry.reviews || []);
|
|
57
49
|
}
|
|
58
|
-
onNotesChanged();
|
|
59
50
|
} catch (err) {
|
|
60
51
|
console.error('Failed to refresh notes:', err);
|
|
61
52
|
}
|
|
62
|
-
}, [featureId
|
|
53
|
+
}, [featureId]);
|
|
63
54
|
|
|
64
55
|
const handleAddNote = async () => {
|
|
65
56
|
const text = newNoteText.trim();
|
|
@@ -108,7 +99,7 @@ export function QaIssueSheet({
|
|
|
108
99
|
<div className={`qa-issue-sheet${isOpen ? ' open' : ''}`} ref={sheetRef}>
|
|
109
100
|
<div className="qa-sheet-header">
|
|
110
101
|
<span className="qa-sheet-title">{featureId} — {featureName}</span>
|
|
111
|
-
<button className="qa-sheet-close" onClick={
|
|
102
|
+
<button className="qa-sheet-close" onClick={close}>×</button>
|
|
112
103
|
</div>
|
|
113
104
|
<div className="qa-sheet-body">
|
|
114
105
|
<div className="qa-sheet-subtitle">
|
|
@@ -1,63 +1,17 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React from 'react';
|
|
2
2
|
import { marked } from 'marked';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { useSidePanelStore } from '../stores/useSidePanelStore';
|
|
4
|
+
import { useGraphStore } from '../stores/useGraphStore';
|
|
5
|
+
import { useGraphUIStore } from '../stores/useGraphUIStore';
|
|
6
|
+
import { getTaskCssClass, getTaskIcon, shouldShowTaskList } from '../utils/task_status';
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
timestamp: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
interface SidePanelProps {
|
|
17
|
-
graphData: any;
|
|
18
|
-
onEdgeClick: (neighborId: string) => void;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function SidePanel({ graphData, onEdgeClick }: SidePanelProps) {
|
|
22
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
23
|
-
const [node, setNode] = useState<any>(null);
|
|
24
|
-
const [content, setContent] = useState('');
|
|
25
|
-
const [workSummaries, setWorkSummaries] = useState<WorkSummary[]>([]);
|
|
26
|
-
const [expandedSummaries, setExpandedSummaries] = useState(false);
|
|
27
|
-
const [tasks, setTasks] = useState<{ total: number; completed: number; items: any[] } | null>(null);
|
|
28
|
-
const [expandedTasks, setExpandedTasks] = useState(false);
|
|
29
|
-
|
|
30
|
-
const open = useCallback(async (n: any) => {
|
|
31
|
-
setNode(n);
|
|
32
|
-
setWorkSummaries([]);
|
|
33
|
-
setExpandedSummaries(false);
|
|
34
|
-
setTasks(null);
|
|
35
|
-
setExpandedTasks(false);
|
|
36
|
-
try {
|
|
37
|
-
const detail = await apiClient.fetchNode(n.id);
|
|
38
|
-
setContent(detail.content);
|
|
39
|
-
setIsOpen(true);
|
|
40
|
-
|
|
41
|
-
// Fetch pipeline status for feature nodes to get work summaries
|
|
42
|
-
if (n.type === 'feature' || n.id?.startsWith('feat_')) {
|
|
43
|
-
try {
|
|
44
|
-
const pStatus = await apiClient.getPipelineStatus(n.id);
|
|
45
|
-
if (pStatus?.workSummaries?.length > 0) {
|
|
46
|
-
setWorkSummaries(pStatus.workSummaries);
|
|
47
|
-
}
|
|
48
|
-
if (pStatus?.tasks) {
|
|
49
|
-
setTasks(pStatus.tasks);
|
|
50
|
-
}
|
|
51
|
-
} catch {
|
|
52
|
-
// Pipeline status may not exist for all features
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
} catch (err) {
|
|
56
|
-
console.error('Failed to fetch node:', err);
|
|
57
|
-
}
|
|
58
|
-
}, []);
|
|
59
|
-
|
|
60
|
-
const close = useCallback(() => setIsOpen(false), []);
|
|
8
|
+
export function SidePanel() {
|
|
9
|
+
const {
|
|
10
|
+
isOpen, node, content, workSummaries, expandedSummaries,
|
|
11
|
+
tasks, expandedTasks, close, toggleSummaries, toggleTasks,
|
|
12
|
+
} = useSidePanelStore();
|
|
13
|
+
const graphData = useGraphStore((s) => s.graphData);
|
|
14
|
+
const onEdgeClick = useGraphUIStore((s) => s.onEdgeClick);
|
|
61
15
|
|
|
62
16
|
const renderMarkdown = (md: string) => {
|
|
63
17
|
const stripped = md.replace(/^---[\s\S]*?---\n*/m, '');
|
|
@@ -78,11 +32,6 @@ export function SidePanel({ graphData, onEdgeClick }: SidePanelProps) {
|
|
|
78
32
|
return edges;
|
|
79
33
|
};
|
|
80
34
|
|
|
81
|
-
// Expose open/close via ref-like approach on the component instance
|
|
82
|
-
// We'll use a callback pattern instead
|
|
83
|
-
(SidePanel as any).__open = open;
|
|
84
|
-
(SidePanel as any).__close = close;
|
|
85
|
-
|
|
86
35
|
if (!node) return (
|
|
87
36
|
<div className={`side-panel${isOpen ? ' open' : ''}`} id="side-panel">
|
|
88
37
|
<div className="panel-header">
|
|
@@ -114,7 +63,7 @@ export function SidePanel({ graphData, onEdgeClick }: SidePanelProps) {
|
|
|
114
63
|
<div className="panel-task-list">
|
|
115
64
|
<button
|
|
116
65
|
className="panel-task-list-toggle"
|
|
117
|
-
onClick={
|
|
66
|
+
onClick={toggleTasks}
|
|
118
67
|
>
|
|
119
68
|
<span className={`panel-task-list-chevron${expandedTasks ? ' expanded' : ''}`}>{'\u25B6'}</span>
|
|
120
69
|
Tasks ({tasks!.completed}/{tasks!.total})
|
|
@@ -138,7 +87,7 @@ export function SidePanel({ graphData, onEdgeClick }: SidePanelProps) {
|
|
|
138
87
|
<div className="panel-work-summary">
|
|
139
88
|
<button
|
|
140
89
|
className="panel-work-summary-toggle"
|
|
141
|
-
onClick={
|
|
90
|
+
onClick={toggleSummaries}
|
|
142
91
|
>
|
|
143
92
|
{expandedSummaries ? '\u25BC' : '\u25B6'} Work Summary ({workSummaries.length} cycle{workSummaries.length !== 1 ? 's' : ''})
|
|
144
93
|
</button>
|
|
@@ -211,7 +160,7 @@ export function SidePanel({ graphData, onEdgeClick }: SidePanelProps) {
|
|
|
211
160
|
<li key={i} className="panel-edge-item">
|
|
212
161
|
<span className="panel-edge-direction">{direction}</span>
|
|
213
162
|
<span className="panel-edge-relation">{edge.relation.replace(/_/g, ' ')}</span>
|
|
214
|
-
<a className="panel-edge-link" href="#" onClick={(e) => { e.preventDefault(); onEdgeClick(edge.neighborId); }}>
|
|
163
|
+
<a className="panel-edge-link" href="#" onClick={(e) => { e.preventDefault(); onEdgeClick?.(edge.neighborId); }}>
|
|
215
164
|
{name}
|
|
216
165
|
</a>
|
|
217
166
|
<span className="panel-edge-id">{edge.neighborId}</span>
|
|
@@ -225,7 +174,3 @@ export function SidePanel({ graphData, onEdgeClick }: SidePanelProps) {
|
|
|
225
174
|
</div>
|
|
226
175
|
);
|
|
227
176
|
}
|
|
228
|
-
|
|
229
|
-
// Export the static methods for imperative use
|
|
230
|
-
export const openSidePanel = (node: any) => (SidePanel as any).__open?.(node);
|
|
231
|
-
export const closeSidePanel = () => (SidePanel as any).__close?.();
|