@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
|
@@ -1,86 +1,158 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useCallback, useEffect } from 'react';
|
|
2
|
+
import { useLocation, useNavigate } from 'react-router-dom';
|
|
3
|
+
import {
|
|
4
|
+
Columns3, MessageSquare, Network, ShieldCheck, Palette, Users,
|
|
5
|
+
Maximize, Settings, Sun, Moon,
|
|
6
|
+
} from 'lucide-react';
|
|
7
|
+
import { NavBarSidekick } from './ds/NavBarSidekick';
|
|
8
|
+
import type { NavItem } from './ds/NavBarSidekick';
|
|
2
9
|
import { ProjectSelector } from './ProjectSelector';
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
projects: Project[];
|
|
17
|
-
selectedProjectId: string | null;
|
|
18
|
-
onProjectSelect: (id: string) => void;
|
|
19
|
-
onProjectCreate: (name: string) => Promise<any>;
|
|
20
|
-
onProjectRename: (id: string, name: string) => Promise<void>;
|
|
21
|
-
onProjectArchive: (id: string) => Promise<void>;
|
|
22
|
-
onOpenGitModal?: (project: Project) => void;
|
|
10
|
+
import { useProjectStore } from '../stores/useProjectStore';
|
|
11
|
+
import { useGraphStore } from '../stores/useGraphStore';
|
|
12
|
+
import { useGraphUIStore } from '../stores/useGraphUIStore';
|
|
13
|
+
import { useGitModalStore } from '../stores/useGitModalStore';
|
|
14
|
+
import { useTheme } from '../hooks/useTheme';
|
|
15
|
+
import { useAuth } from '../hooks/useAuth';
|
|
16
|
+
import { apiClient } from '../api/client';
|
|
17
|
+
|
|
18
|
+
const VALID_TABS = new Set(['graph', 'kanban', 'coherence', 'users', 'terminal', 'design-system']);
|
|
19
|
+
|
|
20
|
+
function tabFromPath(pathname: string): string {
|
|
21
|
+
const seg = pathname.replace(/^\//, '');
|
|
22
|
+
return VALID_TABS.has(seg) ? seg : 'kanban';
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
const ICON_PROPS = { size: 14, strokeWidth: 2 } as const;
|
|
26
|
+
|
|
27
|
+
const TAB_ITEMS: NavItem[] = [
|
|
28
|
+
{ id: 'kanban', label: 'Kanban', icon: <Columns3 {...ICON_PROPS} />, description: 'Board view of features' },
|
|
29
|
+
{ id: 'terminal', label: 'Chat', icon: <MessageSquare {...ICON_PROPS} />, description: 'AI assistant terminal' },
|
|
30
|
+
{ id: 'graph', label: 'Graph', icon: <Network {...ICON_PROPS} />, description: 'Knowledge graph visualization' },
|
|
31
|
+
{ id: 'coherence', label: 'Coherence', icon: <ShieldCheck {...ICON_PROPS} />, description: 'Coherence analysis' },
|
|
32
|
+
{ id: 'design-system', label: 'Design System', icon: <Palette {...ICON_PROPS} />, description: 'Living style guide' },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const ADMIN_ITEM: NavItem = { id: 'users', label: 'Users', icon: <Users {...ICON_PROPS} />, description: 'User management' };
|
|
36
|
+
|
|
37
|
+
const iconBtnClass = (active?: boolean) => [
|
|
38
|
+
'flex h-8 w-8 items-center justify-center rounded-lg border',
|
|
39
|
+
'transition-all duration-150 cursor-pointer outline-none',
|
|
40
|
+
active
|
|
41
|
+
? 'border-accent/40 bg-accent/10 text-accent'
|
|
42
|
+
: 'border-edge text-content-muted hover:border-content/20 hover:text-content',
|
|
43
|
+
].join(' ');
|
|
44
|
+
|
|
45
|
+
export function Toolbar() {
|
|
46
|
+
const navigate = useNavigate();
|
|
47
|
+
const location = useLocation();
|
|
48
|
+
const activeTab = tabFromPath(location.pathname);
|
|
49
|
+
|
|
50
|
+
const { theme, toggleTheme } = useTheme();
|
|
51
|
+
const { user } = useAuth();
|
|
52
|
+
const isAdmin = user?.role === 'admin';
|
|
53
|
+
|
|
54
|
+
const projects = useProjectStore((s) => s.projects);
|
|
55
|
+
const selectedProjectId = useProjectStore((s) => s.selectedProjectId);
|
|
56
|
+
const selectProject = useProjectStore((s) => s.selectProject);
|
|
57
|
+
const createProject = useProjectStore((s) => s.createProject);
|
|
58
|
+
const renameProject = useProjectStore((s) => s.renameProject);
|
|
59
|
+
const archiveProject = useProjectStore((s) => s.archiveProject);
|
|
60
|
+
|
|
61
|
+
const graphData = useGraphStore((s) => s.graphData);
|
|
62
|
+
const onFit = useGraphUIStore((s) => s.onFit);
|
|
63
|
+
const settingsOpen = useGraphUIStore((s) => s.settingsOpen);
|
|
64
|
+
const onSettingsToggle = useGraphUIStore((s) => s.onSettingsToggle);
|
|
65
|
+
|
|
66
|
+
const openGitModal = useGitModalStore((s) => s.open);
|
|
67
|
+
|
|
68
|
+
const completeness = graphData
|
|
69
|
+
? Math.round((graphData.nodes.reduce((acc: number, n: any) => acc + (n.completeness || 0), 0) / Math.max(graphData.nodes.length, 1)) * 100)
|
|
70
|
+
: 0;
|
|
71
|
+
|
|
72
|
+
const items = isAdmin ? [...TAB_ITEMS, ADMIN_ITEM] : TAB_ITEMS;
|
|
73
|
+
|
|
74
|
+
const handleNavigate = useCallback((id: string) => {
|
|
75
|
+
navigate(`/${id}`);
|
|
76
|
+
}, [navigate]);
|
|
77
|
+
|
|
78
|
+
const handleLogout = useCallback(async () => {
|
|
79
|
+
try {
|
|
80
|
+
await apiClient.logout();
|
|
81
|
+
} catch {
|
|
82
|
+
// ignore
|
|
83
|
+
}
|
|
84
|
+
navigate('/login', { replace: true });
|
|
85
|
+
}, [navigate]);
|
|
34
86
|
|
|
35
|
-
|
|
87
|
+
// Global '/' shortcut — the NavBarSidekick handles its own cmd palette UI,
|
|
88
|
+
// but we wire the keyboard shortcut at the toolbar level for page-wide reach
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
const handler = (e: KeyboardEvent) => {
|
|
91
|
+
if (e.key === '/' && !(e.target instanceof HTMLInputElement) && !(e.target instanceof HTMLTextAreaElement)) {
|
|
92
|
+
// Let the NavBarSidekick's own button handle it via a simulated click
|
|
93
|
+
const btn = document.querySelector('[data-cmd-trigger]') as HTMLButtonElement | null;
|
|
94
|
+
if (btn) { e.preventDefault(); btn.click(); }
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
document.addEventListener('keydown', handler);
|
|
98
|
+
return () => document.removeEventListener('keydown', handler);
|
|
99
|
+
}, []);
|
|
36
100
|
|
|
37
101
|
return (
|
|
38
|
-
<div className="
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
102
|
+
<div className="border-b border-edge">
|
|
103
|
+
<NavBarSidekick
|
|
104
|
+
items={items}
|
|
105
|
+
activeId={activeTab}
|
|
106
|
+
onNavigate={handleNavigate}
|
|
107
|
+
brand={
|
|
108
|
+
<ProjectSelector
|
|
109
|
+
projects={projects}
|
|
110
|
+
selectedProjectId={selectedProjectId}
|
|
111
|
+
onSelect={selectProject}
|
|
112
|
+
onCreate={createProject}
|
|
113
|
+
onRename={renameProject}
|
|
114
|
+
onArchive={archiveProject}
|
|
115
|
+
onOpenGitModal={openGitModal}
|
|
116
|
+
/>
|
|
117
|
+
}
|
|
118
|
+
center={
|
|
119
|
+
<div className="flex items-center justify-end gap-2 text-[11px] text-content-muted">
|
|
120
|
+
<span className="hidden lg:inline uppercase tracking-wider font-medium">Completeness</span>
|
|
121
|
+
<div className="w-20 h-1.5 rounded-full bg-completeness-bg overflow-hidden">
|
|
122
|
+
<div
|
|
123
|
+
className="h-full rounded-full bg-completeness-fill transition-all duration-300"
|
|
124
|
+
style={{ width: `${completeness}%` }}
|
|
125
|
+
/>
|
|
126
|
+
</div>
|
|
127
|
+
<span className="font-mono text-[11px] text-content-secondary w-8 text-right">{completeness}%</span>
|
|
128
|
+
</div>
|
|
129
|
+
}
|
|
130
|
+
actions={
|
|
131
|
+
<div className="flex items-center gap-1.5">
|
|
132
|
+
<button onClick={() => onFit?.()} title="Fit graph to viewport" className={iconBtnClass()}>
|
|
133
|
+
<Maximize size={14} strokeWidth={2} />
|
|
134
|
+
</button>
|
|
135
|
+
<button onClick={onSettingsToggle} title="Graph settings" className={iconBtnClass(settingsOpen)}>
|
|
136
|
+
<Settings size={14} strokeWidth={2} />
|
|
137
|
+
</button>
|
|
138
|
+
<div className="h-5 w-px bg-edge mx-1" />
|
|
139
|
+
</div>
|
|
140
|
+
}
|
|
141
|
+
trailing={
|
|
142
|
+
<div className="flex items-center gap-1.5">
|
|
143
|
+
<button onClick={toggleTheme} title="Toggle theme" className={iconBtnClass()}>
|
|
144
|
+
{theme === 'dark' ? <Sun size={14} strokeWidth={2} /> : <Moon size={14} strokeWidth={2} />}
|
|
145
|
+
</button>
|
|
146
|
+
<button
|
|
147
|
+
onClick={handleLogout}
|
|
148
|
+
title="Logout"
|
|
149
|
+
className="flex h-8 w-8 items-center justify-center rounded-full bg-accent text-[11px] font-bold text-surface cursor-pointer outline-none hover:opacity-90 transition-opacity"
|
|
150
|
+
>
|
|
151
|
+
{user?.email?.slice(0, 2).toUpperCase() || 'U'}
|
|
152
|
+
</button>
|
|
153
|
+
</div>
|
|
154
|
+
}
|
|
155
|
+
/>
|
|
84
156
|
</div>
|
|
85
157
|
);
|
|
86
158
|
}
|
package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCard.tsx
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Play, AlertTriangle, Lock, CircleDot, Copy, Check,
|
|
4
|
+
} from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
/* ── Types ── */
|
|
7
|
+
|
|
8
|
+
export interface KanbanCardProps {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
/** 0–100 */
|
|
12
|
+
pct: number;
|
|
13
|
+
kind?: string;
|
|
14
|
+
rejectionCount: number;
|
|
15
|
+
blocked: boolean;
|
|
16
|
+
/** Pipeline status string */
|
|
17
|
+
pipeline: string;
|
|
18
|
+
/** Pipeline badge label (e.g. "Retry 2", "Running...") */
|
|
19
|
+
pipelineLabel?: string;
|
|
20
|
+
/** Tool call counts — { Read: 6, Tools: 11, Write: 3, Edit: 2 } */
|
|
21
|
+
toolCalls?: Record<string, number>;
|
|
22
|
+
/** Session meta pills — e.g. ["Ctx 17%", "19t", "$0.3948", "opus-4"] */
|
|
23
|
+
meta?: string[];
|
|
24
|
+
/** Stop reason (e.g. "end_turn", "max_turns") */
|
|
25
|
+
stopReason?: string;
|
|
26
|
+
/** Number of issues / notes */
|
|
27
|
+
issueCount: number;
|
|
28
|
+
/** Issue button label override */
|
|
29
|
+
issueLabel?: string;
|
|
30
|
+
/** Show play button */
|
|
31
|
+
showPlay?: boolean;
|
|
32
|
+
/** Show resume button */
|
|
33
|
+
showResume?: boolean;
|
|
34
|
+
/** Whether copy was just triggered */
|
|
35
|
+
copied?: boolean;
|
|
36
|
+
/** Is this the currently-playing card in Play All */
|
|
37
|
+
playAllActive?: boolean;
|
|
38
|
+
|
|
39
|
+
/* Callbacks */
|
|
40
|
+
onClick?: () => void;
|
|
41
|
+
onCopy?: () => void;
|
|
42
|
+
onPlay?: () => void;
|
|
43
|
+
onResume?: () => void;
|
|
44
|
+
onUnblock?: () => void;
|
|
45
|
+
onIssuesClick?: () => void;
|
|
46
|
+
|
|
47
|
+
/* Drag */
|
|
48
|
+
draggable?: boolean;
|
|
49
|
+
onDragStart?: (e: React.DragEvent) => void;
|
|
50
|
+
onDragEnd?: (e: React.DragEvent) => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* ── Pipeline style helpers ── */
|
|
54
|
+
|
|
55
|
+
function pipelineCls(status: string): string {
|
|
56
|
+
if (['developing', 'reviewing', 'testing', 'committing'].includes(status)) return 'bg-accent/10 text-accent animate-pulse';
|
|
57
|
+
switch (status) {
|
|
58
|
+
case 'completed': return 'bg-emerald-500/10 text-emerald-400';
|
|
59
|
+
case 'failed': return 'bg-error/10 text-error';
|
|
60
|
+
case 'blocked': return 'bg-error/10 text-error';
|
|
61
|
+
case 'interrupted': return 'bg-amber-500/10 text-amber-400';
|
|
62
|
+
default: return 'bg-white/[0.06] text-content-muted';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function pipelineIcon(status: string) {
|
|
67
|
+
if (['developing', 'reviewing', 'testing', 'committing'].includes(status)) return <CircleDot size={12} strokeWidth={2.5} />;
|
|
68
|
+
if (status === 'failed') return <AlertTriangle size={12} strokeWidth={2.5} />;
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* ── Kind badge ── */
|
|
73
|
+
|
|
74
|
+
function KindBadge({ kind }: { kind: string }) {
|
|
75
|
+
if (!kind || kind === 'new') return null;
|
|
76
|
+
const cls = kind === 'improvement'
|
|
77
|
+
? 'text-blue-400 bg-blue-400/15'
|
|
78
|
+
: 'text-amber-400 bg-amber-400/15';
|
|
79
|
+
return (
|
|
80
|
+
<span className={`rounded px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide ${cls}`}>
|
|
81
|
+
{kind}
|
|
82
|
+
</span>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* ── Card component ── */
|
|
87
|
+
|
|
88
|
+
export function KanbanCard({
|
|
89
|
+
id, name, pct, kind, rejectionCount, blocked, pipeline, pipelineLabel: pipLabel,
|
|
90
|
+
toolCalls, meta, stopReason, issueCount, issueLabel, showPlay, showResume, copied,
|
|
91
|
+
playAllActive,
|
|
92
|
+
onClick, onCopy, onPlay, onResume, onUnblock, onIssuesClick,
|
|
93
|
+
draggable, onDragStart, onDragEnd,
|
|
94
|
+
}: KanbanCardProps) {
|
|
95
|
+
const hasPipeline = pipeline !== 'idle' && !!pipLabel;
|
|
96
|
+
const hasTools = toolCalls && Object.values(toolCalls).some(v => v > 0);
|
|
97
|
+
const hasStatusRow = rejectionCount > 0 || hasPipeline || !!stopReason;
|
|
98
|
+
|
|
99
|
+
const stripColor = blocked
|
|
100
|
+
? 'from-error/60 to-error/20'
|
|
101
|
+
: rejectionCount >= 3
|
|
102
|
+
? 'from-error/40 to-amber-500/20'
|
|
103
|
+
: ['developing', 'reviewing', 'testing', 'committing'].includes(pipeline)
|
|
104
|
+
? 'from-accent/60 to-accent/20'
|
|
105
|
+
: pipeline === 'completed'
|
|
106
|
+
? 'from-emerald-400/60 to-emerald-400/20'
|
|
107
|
+
: playAllActive
|
|
108
|
+
? 'from-accent/60 to-accent/20'
|
|
109
|
+
: 'from-edge to-transparent';
|
|
110
|
+
|
|
111
|
+
const handleCardClick = (e: React.MouseEvent) => {
|
|
112
|
+
if ((e.target as HTMLElement).closest('button')) return;
|
|
113
|
+
onClick?.();
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<div
|
|
118
|
+
className="group relative shrink-0 overflow-hidden rounded-2xl bg-surface border border-edge shadow-lg shadow-black/10 backdrop-blur-sm transition-all duration-200 hover:border-content/15 hover:shadow-xl hover:shadow-black/15 cursor-pointer"
|
|
119
|
+
draggable={draggable}
|
|
120
|
+
onDragStart={onDragStart}
|
|
121
|
+
onDragEnd={onDragEnd}
|
|
122
|
+
onClick={handleCardClick}
|
|
123
|
+
data-feature-id={id}
|
|
124
|
+
>
|
|
125
|
+
{/* Left accent strip */}
|
|
126
|
+
<div className={`absolute left-0 top-0 h-full w-1 bg-gradient-to-b ${stripColor}`} />
|
|
127
|
+
|
|
128
|
+
<div className="p-4 pl-5">
|
|
129
|
+
{/* Header */}
|
|
130
|
+
<div className="flex items-center gap-2">
|
|
131
|
+
<span className="font-mono text-[12px] text-content-secondary">{id}</span>
|
|
132
|
+
<KindBadge kind={kind || 'new'} />
|
|
133
|
+
<button
|
|
134
|
+
className="flex h-6 w-6 items-center justify-center rounded-md bg-white/[0.08] text-content-secondary transition-colors hover:bg-white/15 hover:text-content cursor-pointer"
|
|
135
|
+
title="Copy feature ID and name"
|
|
136
|
+
onClick={(e) => { e.stopPropagation(); onCopy?.(); }}
|
|
137
|
+
>
|
|
138
|
+
{copied ? <Check size={12} strokeWidth={2.5} className="text-emerald-400" /> : <Copy size={12} strokeWidth={2} />}
|
|
139
|
+
</button>
|
|
140
|
+
<div className="flex-1" />
|
|
141
|
+
{blocked ? (
|
|
142
|
+
<span className="flex items-center gap-1.5 rounded-full bg-error/15 px-2.5 py-1 text-[11px] font-bold text-error backdrop-blur">
|
|
143
|
+
<Lock size={11} strokeWidth={2.5} /> Blocked
|
|
144
|
+
</span>
|
|
145
|
+
) : showPlay ? (
|
|
146
|
+
<button
|
|
147
|
+
className="flex h-7 w-7 items-center justify-center rounded-full bg-accent/10 text-accent backdrop-blur transition-all hover:bg-accent hover:text-surface hover:shadow-[0_0_12px_-2px_var(--accent)] cursor-pointer"
|
|
148
|
+
title="Start automated development pipeline"
|
|
149
|
+
onClick={(e) => { e.stopPropagation(); onPlay?.(); }}
|
|
150
|
+
>
|
|
151
|
+
<Play size={12} strokeWidth={2.5} fill="currentColor" />
|
|
152
|
+
</button>
|
|
153
|
+
) : showResume ? (
|
|
154
|
+
<button
|
|
155
|
+
className="flex h-7 w-7 items-center justify-center rounded-full bg-accent/10 text-accent backdrop-blur transition-all hover:bg-accent hover:text-surface hover:shadow-[0_0_12px_-2px_var(--accent)] cursor-pointer"
|
|
156
|
+
title="Resume pipeline from last completed step"
|
|
157
|
+
onClick={(e) => { e.stopPropagation(); onResume?.(); }}
|
|
158
|
+
>
|
|
159
|
+
<Play size={12} strokeWidth={2.5} fill="currentColor" />
|
|
160
|
+
</button>
|
|
161
|
+
) : null}
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
{/* Name */}
|
|
165
|
+
<div className="mt-2.5 text-[14px] font-semibold leading-snug text-content">{name}</div>
|
|
166
|
+
|
|
167
|
+
{/* Spec disc */}
|
|
168
|
+
<div className="mt-3 flex items-center gap-2">
|
|
169
|
+
<svg width="22" height="22" viewBox="0 0 22 22" className="shrink-0 -rotate-90">
|
|
170
|
+
<circle cx="11" cy="11" r="9" fill="none" stroke="currentColor" strokeWidth="2.5" className="text-white/5" />
|
|
171
|
+
<circle
|
|
172
|
+
cx="11" cy="11" r="9" fill="none" strokeWidth="2.5"
|
|
173
|
+
stroke="url(#specGrad)" strokeLinecap="round"
|
|
174
|
+
strokeDasharray={`${(pct / 100) * 2 * Math.PI * 9} ${2 * Math.PI * 9}`}
|
|
175
|
+
/>
|
|
176
|
+
<defs>
|
|
177
|
+
<linearGradient id="specGrad" x1="0" y1="0" x2="1" y2="1">
|
|
178
|
+
<stop offset="0%" stopColor="var(--completeness-fill)" />
|
|
179
|
+
<stop offset="100%" stopColor="var(--accent)" stopOpacity="0.6" />
|
|
180
|
+
</linearGradient>
|
|
181
|
+
</defs>
|
|
182
|
+
</svg>
|
|
183
|
+
<span className="font-mono text-[11px] text-content-secondary">{pct}%</span>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
{/* Stats pills */}
|
|
187
|
+
{(hasTools || hasStatusRow) && (
|
|
188
|
+
<div className="mt-3 space-y-1.5" onClick={(e) => e.stopPropagation()}>
|
|
189
|
+
{/* Tool calls row */}
|
|
190
|
+
{hasTools && (
|
|
191
|
+
<div className="flex gap-1">
|
|
192
|
+
{Object.entries(toolCalls!).filter(([, v]) => v > 0).map(([k, v]) => (
|
|
193
|
+
<span key={k} className="flex-1 rounded-full bg-accent/10 py-1 text-center font-mono text-[10px] font-medium text-accent backdrop-blur">
|
|
194
|
+
{k}: {v}
|
|
195
|
+
</span>
|
|
196
|
+
))}
|
|
197
|
+
</div>
|
|
198
|
+
)}
|
|
199
|
+
{/* Meta row */}
|
|
200
|
+
{meta && meta.length > 0 && (
|
|
201
|
+
<div className="flex gap-1">
|
|
202
|
+
{meta.map(t => (
|
|
203
|
+
<span key={t} className="flex-1 rounded-full bg-white/[0.08] py-1 text-center font-mono text-[10px] text-content backdrop-blur">{t}</span>
|
|
204
|
+
))}
|
|
205
|
+
</div>
|
|
206
|
+
)}
|
|
207
|
+
{/* Status row: rejected + pipeline + stop reason */}
|
|
208
|
+
{hasStatusRow && (
|
|
209
|
+
<div className="flex gap-1">
|
|
210
|
+
{rejectionCount > 0 && (
|
|
211
|
+
<span className="flex-1 rounded-full bg-amber-400/15 py-1 text-center text-[10px] font-semibold text-amber-400 backdrop-blur">
|
|
212
|
+
{rejectionCount}x rejected
|
|
213
|
+
</span>
|
|
214
|
+
)}
|
|
215
|
+
{hasPipeline && (
|
|
216
|
+
<span className={`flex-1 inline-flex items-center justify-center gap-1.5 rounded-full py-1 text-[10px] font-semibold backdrop-blur ${pipelineCls(pipeline)}`}>
|
|
217
|
+
{pipelineIcon(pipeline)}
|
|
218
|
+
{pipLabel}
|
|
219
|
+
</span>
|
|
220
|
+
)}
|
|
221
|
+
{stopReason && (
|
|
222
|
+
<span className="flex-1 rounded-full bg-accent-secondary/10 py-1 text-center font-mono text-[10px] text-accent-secondary backdrop-blur">{stopReason}</span>
|
|
223
|
+
)}
|
|
224
|
+
</div>
|
|
225
|
+
)}
|
|
226
|
+
</div>
|
|
227
|
+
)}
|
|
228
|
+
|
|
229
|
+
{/* Unblock */}
|
|
230
|
+
{blocked && (
|
|
231
|
+
<button
|
|
232
|
+
className="mt-3 w-full rounded-full border border-error/30 py-1.5 text-center text-[11px] font-mono text-error backdrop-blur transition-all hover:bg-error/10 cursor-pointer"
|
|
233
|
+
onClick={(e) => { e.stopPropagation(); onUnblock?.(); }}
|
|
234
|
+
>
|
|
235
|
+
Unblock
|
|
236
|
+
</button>
|
|
237
|
+
)}
|
|
238
|
+
|
|
239
|
+
{/* Issues */}
|
|
240
|
+
<button
|
|
241
|
+
className={[
|
|
242
|
+
'mt-3 w-full rounded-full border py-1.5 text-center text-[11px] font-mono backdrop-blur transition-all cursor-pointer',
|
|
243
|
+
issueCount > 0
|
|
244
|
+
? 'border-white/10 text-content-secondary hover:border-accent/30 hover:text-accent'
|
|
245
|
+
: 'border-white/5 text-content-muted/50 hover:border-white/10 hover:text-content-muted',
|
|
246
|
+
].join(' ')}
|
|
247
|
+
onClick={(e) => { e.stopPropagation(); onIssuesClick?.(); }}
|
|
248
|
+
>
|
|
249
|
+
{issueLabel}
|
|
250
|
+
</button>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
);
|
|
254
|
+
}
|