Merge origin/main into feature/assign-archetypes-to-tasks-ui

Resolved conflicts:
- .gitignore: kept both bd.sock.startlock and .beadboard/ entries
- package.json: kept feature branch test script (explicit enumeration)
- API routes: kept dynamic export + isValidProjectRoot from main
- globals.css: kept HEAD slideInFromRight animation
- use-beads-subscription.ts: kept HEAD onopen handler
- realtime.ts: kept main console.log in emit()
- snapshot-differ.ts: kept main type-aware dependency diff

Blue colors preserved from feature branch.
This commit is contained in:
openhands 2026-02-26 18:50:18 +00:00
commit a8079813b8
28 changed files with 931 additions and 70 deletions

View file

@ -1,5 +1,6 @@
'use client';
import { useMemo } from 'react';
import { motion } from 'framer-motion';
import type { KanbanFilterOptions, KanbanStats } from '../../lib/kanban';
@ -12,6 +13,7 @@ interface KanbanControlsProps {
filters: KanbanFilterOptions;
stats: KanbanStats;
epics: BeadIssue[];
issues: BeadIssue[];
onFiltersChange: (filters: KanbanFilterOptions) => void;
onNextActionable: () => void;
nextActionableFeedback?: string | null;
@ -21,6 +23,7 @@ export function KanbanControls({
filters,
stats,
epics,
issues,
onFiltersChange,
onNextActionable,
nextActionableFeedback = null,
@ -29,12 +32,24 @@ export function KanbanControls({
'ui-field rounded-xl px-3 py-2.5 text-sm outline-none transition';
// Build bead counts map for EpicChipStrip
const beadCounts = new Map<string, number>();
for (const epic of epics) {
// Count non-epic issues that belong to this epic
const count = epic.dependencies?.filter(d => d.type === 'parent' && d.target === epic.id).length ?? 0;
beadCounts.set(epic.id, count);
}
// Count non-epic issues that have this epic as their parent
const beadCounts = useMemo(() => {
const counts = new Map<string, number>();
for (const epic of epics) {
let count = 0;
for (const issue of issues) {
if (issue.issue_type === 'epic') continue;
const parentDep = issue.dependencies.find(d => d.type === 'parent');
const inferredParent = issue.id.includes('.') ? issue.id.split('.')[0] : null;
const parentEpicId = parentDep?.target ?? inferredParent;
if (parentEpicId === epic.id) {
count++;
}
}
counts.set(epic.id, count);
}
return counts;
}, [epics, issues]);
return (
<section className="grid gap-3">

View file

@ -230,6 +230,7 @@ export function KanbanPage({
filters={filters}
stats={stats}
epics={localIssues.filter((issue) => issue.issue_type === 'epic')}
issues={localIssues}
onFiltersChange={setFilters}
onNextActionable={handleNextActionable}
nextActionableFeedback={nextActionableFeedback}

View file

@ -1,7 +1,7 @@
import { Badge } from '@/components/ui/badge';
import { cn } from '@/lib/utils';
import type { BeadStatus } from '@/lib/types';
type BeadStatus = 'ready' | 'in_progress' | 'blocked' | 'closed';
type BadgeSize = 'sm' | 'md';
interface StatusBadgeProps {
@ -9,11 +9,14 @@ interface StatusBadgeProps {
size?: BadgeSize;
}
const STATUS_CLASSES: Record<BeadStatus, string> = {
ready: 'border-teal-500/30 bg-teal-500/15 text-teal-200',
const STATUS_CLASSES: Partial<Record<BeadStatus, string>> = {
open: 'border-teal-500/30 bg-teal-500/15 text-teal-200',
in_progress: 'border-green-500/30 bg-green-500/15 text-green-200',
blocked: 'border-amber-500/30 bg-amber-500/15 text-amber-200',
deferred: 'border-slate-500/30 bg-slate-500/15 text-slate-300',
closed: 'border-slate-500/30 bg-slate-500/15 text-slate-300',
pinned: 'border-purple-500/30 bg-purple-500/15 text-purple-200',
hooked: 'border-cyan-500/30 bg-cyan-500/15 text-cyan-200',
};
const SIZE_CLASSES: Record<BadgeSize, string> = {
@ -21,24 +24,30 @@ const SIZE_CLASSES: Record<BadgeSize, string> = {
md: 'text-xs px-2.5 py-0.5',
};
const STATUS_LABELS: Record<BeadStatus, string> = {
ready: 'Ready',
const STATUS_LABELS: Partial<Record<BeadStatus, string>> = {
open: 'Open',
in_progress: 'In Progress',
blocked: 'Blocked',
deferred: 'Deferred',
closed: 'Closed',
pinned: 'Pinned',
hooked: 'Hooked',
};
export function StatusBadge({ status, size = 'md' }: StatusBadgeProps) {
const statusClass = STATUS_CLASSES[status] || 'border-slate-500/30 bg-slate-500/15 text-slate-300';
const statusLabel = STATUS_LABELS[status] || status;
return (
<Badge
variant="outline"
className={cn(
'rounded-md border font-semibold',
STATUS_CLASSES[status],
statusClass,
SIZE_CLASSES[size]
)}
>
{STATUS_LABELS[status]}
{statusLabel}
</Badge>
);
}