@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.
Files changed (38) hide show
  1. package/dist/bin/create.js +0 -0
  2. package/package.json +7 -9
  3. package/templates/assistkick-product-system/packages/frontend/index.html +3 -0
  4. package/templates/assistkick-product-system/packages/frontend/package.json +5 -1
  5. package/templates/assistkick-product-system/packages/frontend/src/App.tsx +16 -7
  6. package/templates/assistkick-product-system/packages/frontend/src/components/DesignSystemView.tsx +363 -0
  7. package/templates/assistkick-product-system/packages/frontend/src/components/GitRepoModal.tsx +6 -8
  8. package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +92 -188
  9. package/templates/assistkick-product-system/packages/frontend/src/components/QaIssueSheet.tsx +11 -20
  10. package/templates/assistkick-product-system/packages/frontend/src/components/SidePanel.tsx +15 -70
  11. package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +149 -77
  12. package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCard.tsx +254 -0
  13. package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCardShowcase.tsx +216 -0
  14. package/templates/assistkick-product-system/packages/frontend/src/components/ds/NavBarSidekick.tsx +163 -0
  15. package/templates/assistkick-product-system/packages/frontend/src/hooks/useGraph.ts +6 -21
  16. package/templates/assistkick-product-system/packages/frontend/src/hooks/useProjects.ts +15 -80
  17. package/templates/assistkick-product-system/packages/frontend/src/routes/CoherenceRoute.tsx +19 -0
  18. package/templates/assistkick-product-system/packages/frontend/src/routes/DashboardLayout.tsx +54 -0
  19. package/templates/assistkick-product-system/packages/frontend/src/routes/DesignSystemRoute.tsx +6 -0
  20. package/templates/assistkick-product-system/packages/frontend/src/routes/GraphRoute.tsx +93 -0
  21. package/templates/assistkick-product-system/packages/frontend/src/routes/KanbanRoute.tsx +30 -0
  22. package/templates/assistkick-product-system/packages/frontend/src/routes/TerminalRoute.tsx +9 -0
  23. package/templates/assistkick-product-system/packages/frontend/src/routes/UsersRoute.tsx +6 -0
  24. package/templates/assistkick-product-system/packages/frontend/src/stores/useGitModalStore.ts +14 -0
  25. package/templates/assistkick-product-system/packages/frontend/src/stores/useGraphStore.ts +36 -0
  26. package/templates/assistkick-product-system/packages/frontend/src/stores/useGraphUIStore.ts +25 -0
  27. package/templates/assistkick-product-system/packages/frontend/src/stores/useProjectStore.ts +87 -0
  28. package/templates/assistkick-product-system/packages/frontend/src/stores/useQaSheetStore.ts +27 -0
  29. package/templates/assistkick-product-system/packages/frontend/src/stores/useSidePanelStore.ts +76 -0
  30. package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +64 -100
  31. package/templates/assistkick-product-system/packages/frontend/vite.config.ts +2 -1
  32. package/templates/assistkick-product-system/packages/shared/lib/graph.ts +11 -5
  33. package/templates/skills/assistkick-bootstrap/SKILL.md +3 -3
  34. package/templates/skills/assistkick-code-reviewer/SKILL.md +2 -2
  35. package/templates/skills/assistkick-debugger/SKILL.md +2 -2
  36. package/templates/skills/assistkick-developer/SKILL.md +3 -3
  37. package/templates/skills/assistkick-interview/SKILL.md +2 -2
  38. 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 type { Project } from '../hooks/useProjects';
4
-
5
- interface ToolbarProps {
6
- activeTab: string;
7
- onTabChange: (tab: string) => void;
8
- completeness: number;
9
- onFit: () => void;
10
- onSettingsToggle: () => void;
11
- settingsOpen: boolean;
12
- theme: string;
13
- onThemeToggle: () => void;
14
- isAdmin?: boolean;
15
- onLogout: () => void;
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
- export function Toolbar({
26
- activeTab, onTabChange, completeness, onFit,
27
- onSettingsToggle, settingsOpen, theme, onThemeToggle, isAdmin, onLogout,
28
- projects, selectedProjectId, onProjectSelect, onProjectCreate, onProjectRename, onProjectArchive, onOpenGitModal,
29
- }: ToolbarProps) {
30
- const tabs = ['kanban', 'terminal', 'graph', 'coherence'];
31
- if (isAdmin) {
32
- tabs.push('users');
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
- const tabLabels: Record<string, string> = { terminal: 'Chat' };
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="toolbar">
39
- <div className="tab-bar">
40
- <ProjectSelector
41
- projects={projects}
42
- selectedProjectId={selectedProjectId}
43
- onSelect={onProjectSelect}
44
- onCreate={onProjectCreate}
45
- onRename={onProjectRename}
46
- onArchive={onProjectArchive}
47
- onOpenGitModal={onOpenGitModal}
48
- />
49
- {tabs.map(tab => (
50
- <button
51
- key={tab}
52
- className={`tab-btn${activeTab === tab ? ' active' : ''}`}
53
- onClick={() => onTabChange(tab)}
54
- >
55
- {tabLabels[tab] || tab.charAt(0).toUpperCase() + tab.slice(1)}
56
- </button>
57
- ))}
58
- </div>
59
-
60
- <div className="toolbar-spacer" />
61
-
62
- <div className="completeness-bar">
63
- <span>Completeness</span>
64
- <div className="completeness-track">
65
- <div className="completeness-fill" style={{ width: `${completeness}%` }} />
66
- </div>
67
- <span>{completeness}%</span>
68
- </div>
69
-
70
- <button className="toolbar-btn" onClick={onFit} title="Fit graph to viewport">Fit</button>
71
- <button
72
- className={`toolbar-btn settings-gear-btn${settingsOpen ? ' active' : ''}`}
73
- onClick={onSettingsToggle}
74
- title="Graph settings"
75
- >
76
- &#9881;
77
- </button>
78
- <button className="theme-toggle" onClick={onThemeToggle} title="Toggle theme">
79
- {theme === 'dark' ? '\u2600' : '\u263E'}
80
- </button>
81
- <button className="toolbar-btn logout-btn" onClick={onLogout} title="Logout">
82
- Logout
83
- </button>
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
  }
@@ -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
+ }