mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-01 15:03:57 +08:00
Add benchmark results for fast3 and fast4, implement KeepAliveLspBridge, and add tests for staged strategies
- Added new benchmark result files: compare_2026-02-09_score_fast3.json and compare_2026-02-09_score_fast4.json. - Implemented KeepAliveLspBridge to maintain a persistent LSP connection across multiple queries, improving performance. - Created unit tests for staged clustering strategies in test_staged_stage3_fast_strategies.py, ensuring correct behavior of score and dir_rr strategies.
This commit is contained in:
171
ccw/frontend/src/components/issue/queue/QueueBoard.tsx
Normal file
171
ccw/frontend/src/components/issue/queue/QueueBoard.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
// ========================================
|
||||
// QueueBoard
|
||||
// ========================================
|
||||
// Kanban-style view of queue execution groups with DnD reordering/moving.
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import type { DropResult } from '@hello-pangea/dnd';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { LayoutGrid } from 'lucide-react';
|
||||
import { KanbanBoard, type KanbanColumn, type KanbanItem } from '@/components/shared/KanbanBoard';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useQueueMutations } from '@/hooks';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
import type { IssueQueue, QueueItem } from '@/lib/api';
|
||||
|
||||
type QueueBoardItem = QueueItem & KanbanItem;
|
||||
|
||||
function groupSortKey(groupId: string): [number, string] {
|
||||
const n = parseInt(groupId.match(/\d+/)?.[0] || '999');
|
||||
return [Number.isFinite(n) ? n : 999, groupId];
|
||||
}
|
||||
|
||||
function buildColumns(queue: IssueQueue): KanbanColumn<QueueBoardItem>[] {
|
||||
const entries = Object.entries(queue.grouped_items || {});
|
||||
entries.sort(([a], [b]) => {
|
||||
const [an, aid] = groupSortKey(a);
|
||||
const [bn, bid] = groupSortKey(b);
|
||||
if (an !== bn) return an - bn;
|
||||
return aid.localeCompare(bid);
|
||||
});
|
||||
|
||||
return entries.map(([groupId, items]) => {
|
||||
const sorted = [...(items || [])].sort((a, b) => (a.execution_order || 0) - (b.execution_order || 0));
|
||||
const mapped = sorted.map((it) => ({
|
||||
...it,
|
||||
id: it.item_id,
|
||||
title: `${it.issue_id} · ${it.solution_id}`,
|
||||
status: it.status,
|
||||
}));
|
||||
return {
|
||||
id: groupId,
|
||||
title: groupId,
|
||||
items: mapped,
|
||||
icon: <LayoutGrid className="w-4 h-4" />,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function applyDrag(columns: KanbanColumn<QueueBoardItem>[], result: DropResult): KanbanColumn<QueueBoardItem>[] {
|
||||
if (!result.destination) return columns;
|
||||
const { source, destination, draggableId } = result;
|
||||
|
||||
const next = columns.map((c) => ({ ...c, items: [...c.items] }));
|
||||
const src = next.find((c) => c.id === source.droppableId);
|
||||
const dst = next.find((c) => c.id === destination.droppableId);
|
||||
if (!src || !dst) return columns;
|
||||
|
||||
const srcIndex = src.items.findIndex((i) => i.id === draggableId);
|
||||
if (srcIndex === -1) return columns;
|
||||
|
||||
const [moved] = src.items.splice(srcIndex, 1);
|
||||
if (!moved) return columns;
|
||||
|
||||
dst.items.splice(destination.index, 0, moved);
|
||||
return next;
|
||||
}
|
||||
|
||||
export function QueueBoard({
|
||||
queue,
|
||||
onItemClick,
|
||||
className,
|
||||
}: {
|
||||
queue: IssueQueue;
|
||||
onItemClick?: (item: QueueItem) => void;
|
||||
className?: string;
|
||||
}) {
|
||||
const { formatMessage } = useIntl();
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
const { reorderQueueGroup, moveQueueItem, isReordering, isMoving } = useQueueMutations();
|
||||
|
||||
const baseColumns = useMemo(() => buildColumns(queue), [queue]);
|
||||
const [columns, setColumns] = useState<KanbanColumn<QueueBoardItem>[]>(baseColumns);
|
||||
|
||||
useEffect(() => {
|
||||
setColumns(baseColumns);
|
||||
}, [baseColumns]);
|
||||
|
||||
const handleDragEnd = useCallback(
|
||||
async (result: DropResult, sourceColumn: string, destColumn: string) => {
|
||||
if (!projectPath) return;
|
||||
if (!result.destination) return;
|
||||
if (sourceColumn === destColumn && result.source.index === result.destination.index) return;
|
||||
|
||||
try {
|
||||
const nextColumns = applyDrag(columns, result);
|
||||
setColumns(nextColumns);
|
||||
|
||||
const itemId = result.draggableId;
|
||||
|
||||
if (sourceColumn === destColumn) {
|
||||
const column = nextColumns.find((c) => c.id === sourceColumn);
|
||||
const nextOrder = (column?.items ?? []).map((i) => i.item_id);
|
||||
await reorderQueueGroup(sourceColumn, nextOrder);
|
||||
} else {
|
||||
await moveQueueItem(itemId, destColumn, result.destination.index);
|
||||
}
|
||||
} catch (e) {
|
||||
// Revert by resetting to server-derived columns
|
||||
setColumns(baseColumns);
|
||||
}
|
||||
},
|
||||
[baseColumns, columns, moveQueueItem, projectPath, reorderQueueGroup]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cn('space-y-2', className)}>
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<Badge variant="secondary" className="gap-1">
|
||||
{formatMessage({ id: 'issues.queue.stats.executionGroups' })}
|
||||
</Badge>
|
||||
{(isReordering || isMoving) && (
|
||||
<span>
|
||||
{formatMessage({ id: 'common.status.running' })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<KanbanBoard<QueueBoardItem>
|
||||
columns={columns}
|
||||
onDragEnd={handleDragEnd}
|
||||
onItemClick={(item) => onItemClick?.(item as unknown as QueueItem)}
|
||||
emptyColumnMessage={formatMessage({ id: 'issues.queue.empty' })}
|
||||
renderItem={(item, provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
onClick={() => onItemClick?.(item as unknown as QueueItem)}
|
||||
className={cn(
|
||||
'p-3 bg-card border border-border rounded-lg shadow-sm cursor-pointer',
|
||||
'hover:shadow-md hover:border-primary/50 transition-all',
|
||||
item.status === 'blocked' && 'border-destructive/50 bg-destructive/5',
|
||||
item.status === 'failed' && 'border-destructive/50 bg-destructive/5',
|
||||
item.status === 'executing' && 'border-primary/40'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<div className="text-xs font-mono text-muted-foreground">{item.item_id}</div>
|
||||
<div className="text-sm font-medium text-foreground truncate">
|
||||
{item.issue_id} · {item.solution_id}
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-xs shrink-0">
|
||||
{formatMessage({ id: `issues.queue.status.${item.status}` })}
|
||||
</Badge>
|
||||
</div>
|
||||
{item.depends_on?.length ? (
|
||||
<div className="mt-2 text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'issues.solution.overview.dependencies' })}: {item.depends_on.length}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default QueueBoard;
|
||||
Reference in New Issue
Block a user