olly-molly 0.1.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/LICENSE +21 -0
- package/README.md +182 -0
- package/app/api/agent/execute/route.ts +157 -0
- package/app/api/agent/status/route.ts +38 -0
- package/app/api/check-api-key/route.ts +12 -0
- package/app/api/conversations/[id]/route.ts +35 -0
- package/app/api/conversations/route.ts +24 -0
- package/app/api/members/[id]/route.ts +37 -0
- package/app/api/members/route.ts +12 -0
- package/app/api/pm/breakdown/route.ts +142 -0
- package/app/api/pm/tickets/route.ts +147 -0
- package/app/api/projects/[id]/route.ts +56 -0
- package/app/api/projects/active/route.ts +15 -0
- package/app/api/projects/route.ts +53 -0
- package/app/api/tickets/[id]/logs/route.ts +16 -0
- package/app/api/tickets/[id]/route.ts +60 -0
- package/app/api/tickets/[id]/work-logs/route.ts +16 -0
- package/app/api/tickets/route.ts +37 -0
- package/app/design-system/page.tsx +242 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +318 -0
- package/app/layout.tsx +37 -0
- package/app/page.tsx +331 -0
- package/bin/cli.js +66 -0
- package/components/ThemeProvider.tsx +56 -0
- package/components/ThemeToggle.tsx +31 -0
- package/components/activity/ActivityLog.tsx +96 -0
- package/components/activity/index.ts +1 -0
- package/components/kanban/ConversationList.tsx +75 -0
- package/components/kanban/ConversationView.tsx +132 -0
- package/components/kanban/KanbanBoard.tsx +179 -0
- package/components/kanban/KanbanColumn.tsx +80 -0
- package/components/kanban/SortableTicket.tsx +58 -0
- package/components/kanban/TicketCard.tsx +98 -0
- package/components/kanban/TicketModal.tsx +510 -0
- package/components/kanban/TicketSidebar.tsx +448 -0
- package/components/kanban/index.ts +8 -0
- package/components/pm/PMRequestModal.tsx +196 -0
- package/components/pm/index.ts +1 -0
- package/components/project/ProjectSelector.tsx +211 -0
- package/components/project/index.ts +1 -0
- package/components/team/MemberCard.tsx +147 -0
- package/components/team/TeamPanel.tsx +57 -0
- package/components/team/index.ts +2 -0
- package/components/ui/ApiKeyModal.tsx +101 -0
- package/components/ui/Avatar.tsx +95 -0
- package/components/ui/Badge.tsx +59 -0
- package/components/ui/Button.tsx +60 -0
- package/components/ui/Card.tsx +64 -0
- package/components/ui/Input.tsx +41 -0
- package/components/ui/Modal.tsx +76 -0
- package/components/ui/ResizablePane.tsx +97 -0
- package/components/ui/Select.tsx +45 -0
- package/components/ui/Textarea.tsx +41 -0
- package/components/ui/index.ts +8 -0
- package/db/dev.sqlite +0 -0
- package/db/dev.sqlite-shm +0 -0
- package/db/dev.sqlite-wal +0 -0
- package/db/schema-conversations.sql +26 -0
- package/db/schema-projects.sql +29 -0
- package/db/schema.sql +94 -0
- package/lib/agent-jobs.ts +232 -0
- package/lib/db.ts +564 -0
- package/next.config.ts +10 -0
- package/package.json +80 -0
- package/postcss.config.mjs +7 -0
- package/public/app-icon.png +0 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/profiles/designer.png +0 -0
- package/public/profiles/dev-backend.png +0 -0
- package/public/profiles/dev-frontend.png +0 -0
- package/public/profiles/pm.png +0 -0
- package/public/profiles/qa.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ButtonHTMLAttributes, forwardRef } from 'react';
|
|
4
|
+
|
|
5
|
+
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
6
|
+
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
|
|
7
|
+
size?: 'sm' | 'md' | 'lg';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
11
|
+
({ className = '', variant = 'primary', size = 'md', children, ...props }, ref) => {
|
|
12
|
+
const baseStyles = `
|
|
13
|
+
inline-flex items-center justify-center font-medium
|
|
14
|
+
transition-colors duration-150
|
|
15
|
+
focus:outline-none focus:ring-1 focus:ring-[var(--accent-primary)] focus:ring-offset-1 focus:ring-offset-[var(--bg-primary)]
|
|
16
|
+
disabled:opacity-40 disabled:cursor-not-allowed
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
const variants = {
|
|
20
|
+
primary: `
|
|
21
|
+
bg-[var(--accent-primary)] text-[var(--bg-primary)]
|
|
22
|
+
hover:bg-[var(--accent-secondary)]
|
|
23
|
+
border border-[var(--accent-primary)]
|
|
24
|
+
`,
|
|
25
|
+
secondary: `
|
|
26
|
+
bg-transparent text-[var(--text-primary)]
|
|
27
|
+
border border-[var(--border-primary)]
|
|
28
|
+
hover:border-[var(--text-primary)]
|
|
29
|
+
`,
|
|
30
|
+
ghost: `
|
|
31
|
+
bg-transparent text-[var(--text-secondary)]
|
|
32
|
+
border border-transparent
|
|
33
|
+
hover:text-[var(--text-primary)]
|
|
34
|
+
`,
|
|
35
|
+
danger: `
|
|
36
|
+
bg-transparent text-[var(--priority-high-text)]
|
|
37
|
+
border border-[var(--priority-high-text)]
|
|
38
|
+
hover:bg-[var(--priority-high)]
|
|
39
|
+
`,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const sizes = {
|
|
43
|
+
sm: 'px-3 py-1 text-xs gap-1.5',
|
|
44
|
+
md: 'px-4 py-1.5 text-sm gap-2',
|
|
45
|
+
lg: 'px-5 py-2 text-sm gap-2',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<button
|
|
50
|
+
ref={ref}
|
|
51
|
+
className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${className}`}
|
|
52
|
+
{...props}
|
|
53
|
+
>
|
|
54
|
+
{children}
|
|
55
|
+
</button>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
Button.displayName = 'Button';
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
interface CardProps {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
className?: string;
|
|
8
|
+
onClick?: () => void;
|
|
9
|
+
hoverable?: boolean;
|
|
10
|
+
variant?: 'default' | 'bordered' | 'flat';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function Card({
|
|
14
|
+
children,
|
|
15
|
+
className = '',
|
|
16
|
+
onClick,
|
|
17
|
+
hoverable = false,
|
|
18
|
+
variant = 'default'
|
|
19
|
+
}: CardProps) {
|
|
20
|
+
const variants = {
|
|
21
|
+
default: `bg-[var(--bg-card)] border-b border-[var(--border-primary)]`,
|
|
22
|
+
bordered: `bg-[var(--bg-card)] border border-[var(--border-primary)]`,
|
|
23
|
+
flat: `bg-transparent`,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const hoverStyles = hoverable ? `
|
|
27
|
+
hover:bg-[var(--bg-secondary)]
|
|
28
|
+
cursor-pointer
|
|
29
|
+
transition-colors duration-150
|
|
30
|
+
` : '';
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div
|
|
34
|
+
className={`${variants[variant]} ${hoverStyles} ${className}`}
|
|
35
|
+
onClick={onClick}
|
|
36
|
+
>
|
|
37
|
+
{children}
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function CardHeader({ children, className = '' }: { children: ReactNode; className?: string }) {
|
|
43
|
+
return (
|
|
44
|
+
<div className={`px-4 py-3 border-b border-[var(--border-primary)] ${className}`}>
|
|
45
|
+
{children}
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function CardContent({ children, className = '' }: { children: ReactNode; className?: string }) {
|
|
51
|
+
return (
|
|
52
|
+
<div className={`p-4 ${className}`}>
|
|
53
|
+
{children}
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function CardFooter({ children, className = '' }: { children: ReactNode; className?: string }) {
|
|
59
|
+
return (
|
|
60
|
+
<div className={`px-4 py-3 border-t border-[var(--border-primary)] ${className}`}>
|
|
61
|
+
{children}
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { InputHTMLAttributes, forwardRef } from 'react';
|
|
4
|
+
|
|
5
|
+
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
|
6
|
+
label?: string;
|
|
7
|
+
error?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
11
|
+
({ className = '', label, error, ...props }, ref) => {
|
|
12
|
+
return (
|
|
13
|
+
<div className="w-full">
|
|
14
|
+
{label && (
|
|
15
|
+
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1.5 uppercase tracking-wide">
|
|
16
|
+
{label}
|
|
17
|
+
</label>
|
|
18
|
+
)}
|
|
19
|
+
<input
|
|
20
|
+
ref={ref}
|
|
21
|
+
className={`
|
|
22
|
+
w-full px-3 py-2 text-sm
|
|
23
|
+
bg-transparent text-[var(--text-primary)]
|
|
24
|
+
border-b border-[var(--border-primary)]
|
|
25
|
+
placeholder:text-[var(--text-muted)]
|
|
26
|
+
transition-colors duration-150
|
|
27
|
+
focus:outline-none focus:border-[var(--text-primary)]
|
|
28
|
+
${error ? 'border-[var(--priority-high-text)]' : ''}
|
|
29
|
+
${className}
|
|
30
|
+
`}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
{error && (
|
|
34
|
+
<p className="mt-1 text-xs text-[var(--priority-high-text)]">{error}</p>
|
|
35
|
+
)}
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
Input.displayName = 'Input';
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
interface ModalProps {
|
|
6
|
+
isOpen: boolean;
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
title?: string;
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function Modal({ isOpen, onClose, title, children, size = 'md' }: ModalProps) {
|
|
14
|
+
const overlayRef = useRef<HTMLDivElement>(null);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
18
|
+
if (e.key === 'Escape') onClose();
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
if (isOpen) {
|
|
22
|
+
document.addEventListener('keydown', handleEscape);
|
|
23
|
+
document.body.style.overflow = 'hidden';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return () => {
|
|
27
|
+
document.removeEventListener('keydown', handleEscape);
|
|
28
|
+
document.body.style.overflow = '';
|
|
29
|
+
};
|
|
30
|
+
}, [isOpen, onClose]);
|
|
31
|
+
|
|
32
|
+
if (!isOpen) return null;
|
|
33
|
+
|
|
34
|
+
const sizes = {
|
|
35
|
+
sm: 'max-w-sm',
|
|
36
|
+
md: 'max-w-md',
|
|
37
|
+
lg: 'max-w-lg',
|
|
38
|
+
xl: 'max-w-xl',
|
|
39
|
+
'2xl': 'max-w-2xl',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
ref={overlayRef}
|
|
45
|
+
className="fixed inset-0 z-50 flex items-center justify-center p-4 animate-in fade-in"
|
|
46
|
+
onClick={(e) => e.target === overlayRef.current && onClose()}
|
|
47
|
+
>
|
|
48
|
+
{/* Minimal backdrop */}
|
|
49
|
+
<div className="absolute inset-0 bg-[var(--bg-primary)]/90" />
|
|
50
|
+
|
|
51
|
+
{/* Modal container */}
|
|
52
|
+
<div className={`
|
|
53
|
+
relative w-full ${sizes[size]}
|
|
54
|
+
bg-[var(--bg-card)]
|
|
55
|
+
border border-[var(--border-primary)]
|
|
56
|
+
animate-in zoom-in-95
|
|
57
|
+
max-h-[calc(100vh-4rem)] overflow-y-auto
|
|
58
|
+
`}>
|
|
59
|
+
{title && (
|
|
60
|
+
<div className="flex items-center justify-between px-6 py-4 border-b border-[var(--border-primary)]">
|
|
61
|
+
<h2 className="text-sm font-medium text-[var(--text-primary)] uppercase tracking-wide">{title}</h2>
|
|
62
|
+
<button
|
|
63
|
+
onClick={onClose}
|
|
64
|
+
className="p-1 text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors"
|
|
65
|
+
>
|
|
66
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
67
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
68
|
+
</svg>
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
<div className="p-6">{children}</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
4
|
+
|
|
5
|
+
interface ResizablePaneProps {
|
|
6
|
+
left: React.ReactNode;
|
|
7
|
+
right: React.ReactNode;
|
|
8
|
+
defaultLeftWidth?: number;
|
|
9
|
+
minLeftWidth?: number;
|
|
10
|
+
minRightWidth?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ResizablePane({
|
|
14
|
+
left,
|
|
15
|
+
right,
|
|
16
|
+
defaultLeftWidth = 60, // percentage
|
|
17
|
+
minLeftWidth = 30,
|
|
18
|
+
minRightWidth = 25,
|
|
19
|
+
}: ResizablePaneProps) {
|
|
20
|
+
const [leftWidth, setLeftWidth] = useState(defaultLeftWidth);
|
|
21
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
22
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
23
|
+
|
|
24
|
+
// Update leftWidth when defaultLeftWidth changes
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
setLeftWidth(defaultLeftWidth);
|
|
27
|
+
}, [defaultLeftWidth]);
|
|
28
|
+
|
|
29
|
+
const handleMouseDown = () => {
|
|
30
|
+
setIsDragging(true);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const handleMouseMove = useCallback(
|
|
34
|
+
(e: MouseEvent) => {
|
|
35
|
+
if (!isDragging || !containerRef.current) return;
|
|
36
|
+
|
|
37
|
+
const containerRect = containerRef.current.getBoundingClientRect();
|
|
38
|
+
const newLeftWidth = ((e.clientX - containerRect.left) / containerRect.width) * 100;
|
|
39
|
+
|
|
40
|
+
// Apply constraints
|
|
41
|
+
const constrainedWidth = Math.max(
|
|
42
|
+
minLeftWidth,
|
|
43
|
+
Math.min(100 - minRightWidth, newLeftWidth)
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
setLeftWidth(constrainedWidth);
|
|
47
|
+
},
|
|
48
|
+
[isDragging, minLeftWidth, minRightWidth]
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const handleMouseUp = useCallback(() => {
|
|
52
|
+
setIsDragging(false);
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (isDragging) {
|
|
57
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
58
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
59
|
+
document.body.style.cursor = 'col-resize';
|
|
60
|
+
document.body.style.userSelect = 'none';
|
|
61
|
+
|
|
62
|
+
return () => {
|
|
63
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
64
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
65
|
+
document.body.style.cursor = '';
|
|
66
|
+
document.body.style.userSelect = '';
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}, [isDragging, handleMouseMove, handleMouseUp]);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div ref={containerRef} className="flex h-full overflow-hidden">
|
|
73
|
+
{/* Left Pane */}
|
|
74
|
+
<div
|
|
75
|
+
style={{ width: `${leftWidth}%` }}
|
|
76
|
+
className="overflow-auto flex-shrink-0"
|
|
77
|
+
>
|
|
78
|
+
{left}
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
{/* Resizer */}
|
|
82
|
+
<div
|
|
83
|
+
onMouseDown={handleMouseDown}
|
|
84
|
+
className={`w-1 bg-primary hover:bg-indigo-500 cursor-col-resize flex-shrink-0 transition-colors ${isDragging ? 'bg-indigo-500' : ''
|
|
85
|
+
}`}
|
|
86
|
+
/>
|
|
87
|
+
|
|
88
|
+
{/* Right Pane */}
|
|
89
|
+
<div
|
|
90
|
+
style={{ width: `${100 - leftWidth}%` }}
|
|
91
|
+
className="overflow-hidden flex-shrink-0"
|
|
92
|
+
>
|
|
93
|
+
{right}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
interface SelectProps {
|
|
4
|
+
label?: string;
|
|
5
|
+
value: string;
|
|
6
|
+
onChange: (value: string) => void;
|
|
7
|
+
options: { value: string; label: string }[];
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function Select({ label, value, onChange, options, placeholder, className = '' }: SelectProps) {
|
|
13
|
+
return (
|
|
14
|
+
<div className="w-full">
|
|
15
|
+
{label && (
|
|
16
|
+
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1.5 uppercase tracking-wide">
|
|
17
|
+
{label}
|
|
18
|
+
</label>
|
|
19
|
+
)}
|
|
20
|
+
<select
|
|
21
|
+
value={value}
|
|
22
|
+
onChange={(e) => onChange(e.target.value)}
|
|
23
|
+
className={`
|
|
24
|
+
w-full px-3 py-2 text-sm
|
|
25
|
+
bg-transparent text-[var(--text-primary)]
|
|
26
|
+
border-b border-[var(--border-primary)]
|
|
27
|
+
transition-colors duration-150 cursor-pointer
|
|
28
|
+
focus:outline-none focus:border-[var(--text-primary)]
|
|
29
|
+
${className}
|
|
30
|
+
`}
|
|
31
|
+
>
|
|
32
|
+
{placeholder && (
|
|
33
|
+
<option value="" disabled className="text-[var(--text-muted)]">
|
|
34
|
+
{placeholder}
|
|
35
|
+
</option>
|
|
36
|
+
)}
|
|
37
|
+
{options.map((option) => (
|
|
38
|
+
<option key={option.value} value={option.value} className="bg-[var(--bg-card)]">
|
|
39
|
+
{option.label}
|
|
40
|
+
</option>
|
|
41
|
+
))}
|
|
42
|
+
</select>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { TextareaHTMLAttributes, forwardRef } from 'react';
|
|
4
|
+
|
|
5
|
+
interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|
6
|
+
label?: string;
|
|
7
|
+
error?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
11
|
+
({ className = '', label, error, ...props }, ref) => {
|
|
12
|
+
return (
|
|
13
|
+
<div className="w-full">
|
|
14
|
+
{label && (
|
|
15
|
+
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1.5 uppercase tracking-wide">
|
|
16
|
+
{label}
|
|
17
|
+
</label>
|
|
18
|
+
)}
|
|
19
|
+
<textarea
|
|
20
|
+
ref={ref}
|
|
21
|
+
className={`
|
|
22
|
+
w-full px-3 py-2 text-sm
|
|
23
|
+
bg-transparent text-[var(--text-primary)]
|
|
24
|
+
border border-[var(--border-primary)]
|
|
25
|
+
placeholder:text-[var(--text-muted)]
|
|
26
|
+
transition-colors duration-150 resize-none
|
|
27
|
+
focus:outline-none focus:border-[var(--text-primary)]
|
|
28
|
+
${error ? 'border-[var(--priority-high-text)]' : ''}
|
|
29
|
+
${className}
|
|
30
|
+
`}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
{error && (
|
|
34
|
+
<p className="mt-1 text-xs text-[var(--priority-high-text)]">{error}</p>
|
|
35
|
+
)}
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
Textarea.displayName = 'Textarea';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { Button } from './Button';
|
|
2
|
+
export { Card, CardHeader, CardContent, CardFooter } from './Card';
|
|
3
|
+
export { Modal } from './Modal';
|
|
4
|
+
export { Badge, StatusBadge, PriorityBadge } from './Badge';
|
|
5
|
+
export { Avatar } from './Avatar';
|
|
6
|
+
export { Input } from './Input';
|
|
7
|
+
export { Textarea } from './Textarea';
|
|
8
|
+
export { Select } from './Select';
|
package/db/dev.sqlite
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
-- Conversations: Each execution of AI agent for a ticket
|
|
2
|
+
CREATE TABLE IF NOT EXISTS conversations (
|
|
3
|
+
id TEXT PRIMARY KEY,
|
|
4
|
+
ticket_id TEXT NOT NULL REFERENCES tickets(id) ON DELETE CASCADE,
|
|
5
|
+
agent_id TEXT NOT NULL REFERENCES members(id),
|
|
6
|
+
provider TEXT NOT NULL CHECK(provider IN ('claude', 'opencode')),
|
|
7
|
+
prompt TEXT,
|
|
8
|
+
feedback TEXT,
|
|
9
|
+
status TEXT DEFAULT 'running' CHECK(status IN ('running', 'completed', 'failed', 'cancelled')),
|
|
10
|
+
git_commit_hash TEXT,
|
|
11
|
+
started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
12
|
+
completed_at DATETIME,
|
|
13
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
-- Conversation Messages: Streaming log entries for each conversation
|
|
17
|
+
CREATE TABLE IF NOT EXISTS conversation_messages (
|
|
18
|
+
id TEXT PRIMARY KEY,
|
|
19
|
+
conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
|
20
|
+
content TEXT NOT NULL,
|
|
21
|
+
message_type TEXT DEFAULT 'log' CHECK(message_type IN ('log', 'error', 'success', 'system')),
|
|
22
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
CREATE INDEX IF NOT EXISTS idx_conversations_ticket ON conversations(ticket_id);
|
|
26
|
+
CREATE INDEX IF NOT EXISTS idx_conversation_messages_conversation ON conversation_messages(conversation_id);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
-- Projects table for storing target project configurations
|
|
2
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
3
|
+
id TEXT PRIMARY KEY,
|
|
4
|
+
name TEXT NOT NULL,
|
|
5
|
+
path TEXT NOT NULL UNIQUE,
|
|
6
|
+
description TEXT,
|
|
7
|
+
is_active INTEGER DEFAULT 0,
|
|
8
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
9
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
-- Agent work logs - detailed execution history
|
|
13
|
+
CREATE TABLE IF NOT EXISTS agent_work_logs (
|
|
14
|
+
id TEXT PRIMARY KEY,
|
|
15
|
+
ticket_id TEXT REFERENCES tickets(id) ON DELETE CASCADE,
|
|
16
|
+
agent_id TEXT REFERENCES members(id),
|
|
17
|
+
project_id TEXT REFERENCES projects(id),
|
|
18
|
+
command TEXT NOT NULL,
|
|
19
|
+
prompt TEXT,
|
|
20
|
+
output TEXT,
|
|
21
|
+
status TEXT DEFAULT 'RUNNING' CHECK(status IN ('RUNNING', 'SUCCESS', 'FAILED', 'CANCELLED')),
|
|
22
|
+
git_commit_hash TEXT,
|
|
23
|
+
started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
24
|
+
completed_at DATETIME,
|
|
25
|
+
duration_ms INTEGER
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
CREATE INDEX IF NOT EXISTS idx_agent_work_logs_ticket ON agent_work_logs(ticket_id);
|
|
29
|
+
CREATE INDEX IF NOT EXISTS idx_agent_work_logs_agent ON agent_work_logs(agent_id);
|
package/db/schema.sql
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
-- Team Members
|
|
2
|
+
CREATE TABLE IF NOT EXISTS members (
|
|
3
|
+
id TEXT PRIMARY KEY,
|
|
4
|
+
role TEXT NOT NULL CHECK(role IN ('PM', 'FE_DEV', 'BACKEND_DEV', 'QA', 'DEVOPS')),
|
|
5
|
+
name TEXT NOT NULL,
|
|
6
|
+
avatar TEXT,
|
|
7
|
+
system_prompt TEXT NOT NULL,
|
|
8
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
9
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
-- Tickets
|
|
13
|
+
CREATE TABLE IF NOT EXISTS tickets (
|
|
14
|
+
id TEXT PRIMARY KEY,
|
|
15
|
+
title TEXT NOT NULL,
|
|
16
|
+
description TEXT,
|
|
17
|
+
status TEXT DEFAULT 'TODO' CHECK(status IN ('TODO', 'IN_PROGRESS', 'IN_REVIEW', 'NEED_FIX', 'COMPLETE', 'ON_HOLD')),
|
|
18
|
+
priority TEXT DEFAULT 'MEDIUM' CHECK(priority IN ('LOW', 'MEDIUM', 'HIGH', 'CRITICAL')),
|
|
19
|
+
assignee_id TEXT REFERENCES members(id),
|
|
20
|
+
project_id TEXT REFERENCES projects(id),
|
|
21
|
+
created_by TEXT,
|
|
22
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
23
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
-- Activity Logs
|
|
27
|
+
CREATE TABLE IF NOT EXISTS activity_logs (
|
|
28
|
+
id TEXT PRIMARY KEY,
|
|
29
|
+
ticket_id TEXT REFERENCES tickets(id) ON DELETE CASCADE,
|
|
30
|
+
member_id TEXT REFERENCES members(id),
|
|
31
|
+
action TEXT NOT NULL,
|
|
32
|
+
old_value TEXT,
|
|
33
|
+
new_value TEXT,
|
|
34
|
+
details TEXT,
|
|
35
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
-- Create indexes for better query performance
|
|
39
|
+
CREATE INDEX IF NOT EXISTS idx_tickets_status ON tickets(status);
|
|
40
|
+
CREATE INDEX IF NOT EXISTS idx_tickets_assignee ON tickets(assignee_id);
|
|
41
|
+
CREATE INDEX IF NOT EXISTS idx_tickets_project ON tickets(project_id);
|
|
42
|
+
CREATE INDEX IF NOT EXISTS idx_activity_logs_ticket ON activity_logs(ticket_id);
|
|
43
|
+
|
|
44
|
+
-- Insert default team members
|
|
45
|
+
INSERT OR IGNORE INTO members (id, role, name, avatar, system_prompt) VALUES
|
|
46
|
+
('pm-001', 'PM', 'PM Agent', '👔', 'You are a Project Manager AI agent. Your responsibilities include:
|
|
47
|
+
- Creating and managing project tickets
|
|
48
|
+
- Assigning tasks to appropriate team members based on their expertise
|
|
49
|
+
- Setting priorities and deadlines
|
|
50
|
+
- Tracking project progress
|
|
51
|
+
- Facilitating communication between team members
|
|
52
|
+
- Making decisions about project scope and timeline
|
|
53
|
+
|
|
54
|
+
When creating tickets, analyze the task requirements and automatically assign them to the most suitable team member.'),
|
|
55
|
+
|
|
56
|
+
('fe-001', 'FE_DEV', 'Frontend Developer', '🎨', 'You are a Frontend Developer AI agent. Your responsibilities include:
|
|
57
|
+
- Implementing user interfaces using React and Next.js
|
|
58
|
+
- Writing clean, maintainable TypeScript/JavaScript code
|
|
59
|
+
- Creating responsive and accessible designs
|
|
60
|
+
- Integrating with backend APIs
|
|
61
|
+
- Optimizing frontend performance
|
|
62
|
+
- Following best practices for component architecture
|
|
63
|
+
|
|
64
|
+
Focus on creating beautiful, user-friendly interfaces with excellent UX.'),
|
|
65
|
+
|
|
66
|
+
('be-001', 'BACKEND_DEV', 'Backend Developer', '⚙️', 'You are a Backend Developer AI agent. Your responsibilities include:
|
|
67
|
+
- Designing and implementing REST APIs
|
|
68
|
+
- Working with databases (SQLite, PostgreSQL, etc.)
|
|
69
|
+
- Writing server-side logic and business rules
|
|
70
|
+
- Ensuring API security and performance
|
|
71
|
+
- Creating efficient data models
|
|
72
|
+
- Writing unit and integration tests
|
|
73
|
+
|
|
74
|
+
Focus on building robust, scalable backend systems.'),
|
|
75
|
+
|
|
76
|
+
('qa-001', 'QA', 'QA Engineer', '🔍', 'You are a QA Engineer AI agent. Your responsibilities include:
|
|
77
|
+
- Testing features moved to "In Review" status
|
|
78
|
+
- Using Chrome DevTools MCP or Playwright MCP for automated testing
|
|
79
|
+
- Writing and executing test cases
|
|
80
|
+
- Reporting bugs and issues
|
|
81
|
+
- Verifying bug fixes
|
|
82
|
+
- Ensuring quality standards are met
|
|
83
|
+
|
|
84
|
+
When a ticket moves to "In Review", thoroughly test the implementation and provide detailed feedback.'),
|
|
85
|
+
|
|
86
|
+
('devops-001', 'DEVOPS', 'DevOps Engineer', '🚀', 'You are a DevOps Engineer AI agent. Your responsibilities include:
|
|
87
|
+
- Setting up CI/CD pipelines
|
|
88
|
+
- Managing deployment processes
|
|
89
|
+
- Configuring infrastructure and environments
|
|
90
|
+
- Monitoring application performance
|
|
91
|
+
- Handling security and compliance
|
|
92
|
+
- Automating operational tasks
|
|
93
|
+
|
|
94
|
+
Focus on ensuring smooth deployments and reliable infrastructure.');
|