Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions ui/src/app/task/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,33 @@ export default function TaskDetailPage() {
runParamHandled.current = true;
}
}, [runParam, runs]);

// Auto-open kanban card from URL query param ?item=<itemId> (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<LeaderboardView>("best_runs");
const [viewingFile, setViewingFile] = useState<{ path: string; content: string } | null>(null);
const [fileLoading, setFileLoading] = useState<string | null>(null);
Expand Down Expand Up @@ -809,7 +836,7 @@ export default function TaskDetailPage() {
</Link>
</div>
<div className="flex-1 min-h-0 overflow-hidden">
<Feed items={items} skills={context.skills} onRunClick={handleRunIdClick} compact taskId={taskId} hasMore={feedHasMore} onLoadMore={feedLoadMore} loadingMore={feedLoadingMore} />
<Feed items={items} skills={context.skills} onRunClick={handleRunIdClick} onItemClick={handleItemClick} compact taskId={taskId} hasMore={feedHasMore} onLoadMore={feedLoadMore} loadingMore={feedLoadingMore} />
</div>
</div>
</div>
Expand Down Expand Up @@ -840,7 +867,7 @@ export default function TaskDetailPage() {
</Link>
</div>
<div className="max-h-[500px] overflow-y-auto">
<Feed items={items} skills={context.skills} onRunClick={handleRunIdClick} compact taskId={taskId} />
<Feed items={items} skills={context.skills} onRunClick={handleRunIdClick} onItemClick={handleItemClick} compact taskId={taskId} />
</div>
</div>
</div>
Expand Down Expand Up @@ -868,6 +895,7 @@ export default function TaskDetailPage() {
activities={cardActivities}
activitiesLoading={cardActivitiesLoading}
onClose={() => setSelectedCard(null)}
onRunClick={handleCardRunClick}
taskId={taskId}
/>
)}
Expand Down
22 changes: 19 additions & 3 deletions ui/src/components/feed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface FeedProps {
items: FeedItem[];
skills?: SkillSummary[];
onRunClick?: (runId: string) => void;
onItemClick?: (itemId: string) => void;
compact?: boolean;
taskId?: string;
hasMore?: boolean;
Expand Down Expand Up @@ -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 (
<span
role="button"
onClick={(e) => { 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}]
</span>
);
}

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") {
Expand All @@ -215,6 +229,7 @@ const CompactItem = memo(function CompactItem({ item, onRunClick, taskId }: { it
<span>{relativeTime(item.created_at)}</span>
</div>
</div>
{item.item_id && <ItemBadge itemId={item.item_id} onClick={onItemClick} />}
<Score value={item.score} className="text-sm text-[var(--color-text)] shrink-0" />
</div>
);
Expand All @@ -235,6 +250,7 @@ const CompactItem = memo(function CompactItem({ item, onRunClick, taskId }: { it
<span>{relativeTime(item.created_at)}</span>
<span className="mx-1">·</span>
<span>▲ {item.upvotes}</span>
{item.item_id && <><span className="mx-1">·</span><ItemBadge itemId={item.item_id} onClick={onItemClick} /></>}
</div>
</div>
</div>
Expand Down Expand Up @@ -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<FilterType>("all");
const filteredItems = filter === "all" ? items : filter === "skill" ? [] : items.filter((item) => item.type === filter);
const counts: Record<FilterType, number> = {
Expand Down Expand Up @@ -290,7 +306,7 @@ export function Feed({ items, skills = [], onRunClick, compact, taskId, hasMore,
filteredItems.length === 0
? <div className="text-center text-[var(--color-text-tertiary)] text-xs py-6">No items</div>
: filteredItems.map((item) => (
<CompactItem key={`${item.type}-${item.id}`} item={item} onRunClick={onRunClick} taskId={taskId} />
<CompactItem key={`${item.type}-${item.id}`} item={item} onRunClick={onRunClick} onItemClick={onItemClick} taskId={taskId} />
))
)}
{hasMore && onLoadMore && (
Expand Down
8 changes: 6 additions & 2 deletions ui/src/components/kanban/kanban-card-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -150,7 +151,10 @@ export function KanbanCardModal({ item, activities, activitiesLoading, onClose,
{relativeTime(a.created_at)}
</span>
</div>
<div className="text-[11px] text-[var(--color-text-secondary)] leading-relaxed mt-0.5 line-clamp-3">
<div
className={`text-[11px] text-[var(--color-text-secondary)] leading-relaxed mt-0.5 line-clamp-3${a.type === "run" ? " cursor-pointer hover:underline" : ""}`}
onClick={a.type === "run" ? () => onRunClick?.(a.id) : undefined}
>
{a.content}
</div>
{a.type === "run" && a.score != null && (
Expand Down
2 changes: 2 additions & 0 deletions ui/src/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export interface ResultFeedItem {
downvotes: number;
comments: Comment[];
comment_count?: number;
item_id?: string | null;
created_at: string;
}

Expand All @@ -71,6 +72,7 @@ export interface PostFeedItem {
downvotes: number;
comments: Comment[];
comment_count?: number;
item_id?: string | null;
created_at: string;
}

Expand Down
Loading