diff --git a/ui/src/app/task/[id]/page.tsx b/ui/src/app/task/[id]/page.tsx index 2878494..2bef373 100644 --- a/ui/src/app/task/[id]/page.tsx +++ b/ui/src/app/task/[id]/page.tsx @@ -337,6 +337,33 @@ export default function TaskDetailPage() { runParamHandled.current = true; } }, [runParam, runs]); + + // Auto-open kanban card from URL query param ?item= (once only) + const itemParam = searchParams.get("item"); + const itemParamHandled = useRef(false); + useEffect(() => { + if (itemParam && kanbanItems.length > 0 && !itemParamHandled.current) { + const card = kanbanItems.find((i) => i.id === itemParam); + if (card) { + setViewMode("kanban"); + setSelectedCard(card); + } + itemParamHandled.current = true; + } + }, [itemParam, kanbanItems]); + + // Feed → Kanban: switch to kanban tab and open the card + const handleItemClick = useCallback((itemId: string) => { + setViewMode("kanban"); + const card = kanbanItems.find((i) => i.id === itemId); + if (card) setSelectedCard(card); + }, [kanbanItems]); + + // Kanban card → RunDetail: open run detail modal + const handleCardRunClick = useCallback((runId: string) => { + const run = runs.find((r) => r.id === runId); + if (run) setSelectedRun(run); + }, [runs]); const [leaderboardView, setLeaderboardView] = useState("best_runs"); const [viewingFile, setViewingFile] = useState<{ path: string; content: string } | null>(null); const [fileLoading, setFileLoading] = useState(null); @@ -809,7 +836,7 @@ export default function TaskDetailPage() {
- +
@@ -840,7 +867,7 @@ export default function TaskDetailPage() {
- +
@@ -868,6 +895,7 @@ export default function TaskDetailPage() { activities={cardActivities} activitiesLoading={cardActivitiesLoading} onClose={() => setSelectedCard(null)} + onRunClick={handleCardRunClick} taskId={taskId} /> )} diff --git a/ui/src/components/feed.tsx b/ui/src/components/feed.tsx index 75f53ba..8d4c2c1 100644 --- a/ui/src/components/feed.tsx +++ b/ui/src/components/feed.tsx @@ -15,6 +15,7 @@ interface FeedProps { items: FeedItem[]; skills?: SkillSummary[]; onRunClick?: (runId: string) => void; + onItemClick?: (itemId: string) => void; compact?: boolean; taskId?: string; hasMore?: boolean; @@ -197,7 +198,20 @@ const CompactSkillItem = memo(function CompactSkillItem({ skill }: { skill: Skil ); }); -const CompactItem = memo(function CompactItem({ item, onRunClick, taskId }: { item: FeedItem; onRunClick?: (id: string) => void; taskId?: string }) { +function ItemBadge({ itemId, onClick }: { itemId: string; onClick?: (id: string) => void }) { + return ( + { e.preventDefault(); e.stopPropagation(); onClick?.(itemId); }} + style={{ background: "rgba(88,166,255,.08)", color: "var(--blue)", fontSize: 10, padding: "1px 5px", cursor: "pointer" }} + className="shrink-0 font-medium" + > + [{itemId}] + + ); +} + +const CompactItem = memo(function CompactItem({ item, onRunClick, onItemClick, taskId }: { item: FeedItem; onRunClick?: (id: string) => void; onItemClick?: (id: string) => void; taskId?: string }) { const postHref = taskId ? `/task/${taskId}/post/${item.id}` : undefined; if (item.type === "result") { @@ -215,6 +229,7 @@ const CompactItem = memo(function CompactItem({ item, onRunClick, taskId }: { it {relativeTime(item.created_at)} + {item.item_id && } ); @@ -235,6 +250,7 @@ const CompactItem = memo(function CompactItem({ item, onRunClick, taskId }: { it {relativeTime(item.created_at)} · ▲ {item.upvotes} + {item.item_id && <>·} @@ -262,7 +278,7 @@ const CompactItem = memo(function CompactItem({ item, onRunClick, taskId }: { it return null; }); -export function Feed({ items, skills = [], onRunClick, compact, taskId, hasMore, onLoadMore, loadingMore }: FeedProps) { +export function Feed({ items, skills = [], onRunClick, onItemClick, compact, taskId, hasMore, onLoadMore, loadingMore }: FeedProps) { const [filter, setFilter] = useState("all"); const filteredItems = filter === "all" ? items : filter === "skill" ? [] : items.filter((item) => item.type === filter); const counts: Record = { @@ -290,7 +306,7 @@ export function Feed({ items, skills = [], onRunClick, compact, taskId, hasMore, filteredItems.length === 0 ?
No items
: filteredItems.map((item) => ( - + )) )} {hasMore && onLoadMore && ( diff --git a/ui/src/components/kanban/kanban-card-modal.tsx b/ui/src/components/kanban/kanban-card-modal.tsx index 3d1b1ec..e801d40 100644 --- a/ui/src/components/kanban/kanban-card-modal.tsx +++ b/ui/src/components/kanban/kanban-card-modal.tsx @@ -33,10 +33,11 @@ interface ModalProps { activities: ItemActivity[]; activitiesLoading?: boolean; onClose: () => void; + onRunClick?: (runId: string) => void; taskId: string; } -export function KanbanCardModal({ item, activities, activitiesLoading, onClose, taskId }: ModalProps) { +export function KanbanCardModal({ item, activities, activitiesLoading, onClose, onRunClick, taskId }: ModalProps) { useEffect(() => { const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); @@ -150,7 +151,10 @@ export function KanbanCardModal({ item, activities, activitiesLoading, onClose, {relativeTime(a.created_at)} -
+
onRunClick?.(a.id) : undefined} + > {a.content}
{a.type === "run" && a.score != null && ( diff --git a/ui/src/types/api.ts b/ui/src/types/api.ts index 170007b..094c00f 100644 --- a/ui/src/types/api.ts +++ b/ui/src/types/api.ts @@ -59,6 +59,7 @@ export interface ResultFeedItem { downvotes: number; comments: Comment[]; comment_count?: number; + item_id?: string | null; created_at: string; } @@ -71,6 +72,7 @@ export interface PostFeedItem { downvotes: number; comments: Comment[]; comment_count?: number; + item_id?: string | null; created_at: string; }