Skip to content

Commit d5f6893

Browse files
feat: cross-view integration between kanban, feed, and leaderboard
Feed entries with item_id show clickable badge that switches to Kanban tab and opens the card. Kanban card runs are clickable and open RunDetail modal. URL param ?item=SHOP-3 deep-links to a card.
1 parent 4cb6b07 commit d5f6893

4 files changed

Lines changed: 57 additions & 7 deletions

File tree

ui/src/app/task/[id]/page.tsx

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,33 @@ export default function TaskDetailPage() {
337337
runParamHandled.current = true;
338338
}
339339
}, [runParam, runs]);
340+
341+
// Auto-open kanban card from URL query param ?item=<itemId> (once only)
342+
const itemParam = searchParams.get("item");
343+
const itemParamHandled = useRef(false);
344+
useEffect(() => {
345+
if (itemParam && kanbanItems.length > 0 && !itemParamHandled.current) {
346+
const card = kanbanItems.find((i) => i.id === itemParam);
347+
if (card) {
348+
setViewMode("kanban");
349+
setSelectedCard(card);
350+
}
351+
itemParamHandled.current = true;
352+
}
353+
}, [itemParam, kanbanItems]);
354+
355+
// Feed → Kanban: switch to kanban tab and open the card
356+
const handleItemClick = useCallback((itemId: string) => {
357+
setViewMode("kanban");
358+
const card = kanbanItems.find((i) => i.id === itemId);
359+
if (card) setSelectedCard(card);
360+
}, [kanbanItems]);
361+
362+
// Kanban card → RunDetail: open run detail modal
363+
const handleCardRunClick = useCallback((runId: string) => {
364+
const run = runs.find((r) => r.id === runId);
365+
if (run) setSelectedRun(run);
366+
}, [runs]);
340367
const [leaderboardView, setLeaderboardView] = useState<LeaderboardView>("best_runs");
341368
const [viewingFile, setViewingFile] = useState<{ path: string; content: string } | null>(null);
342369
const [fileLoading, setFileLoading] = useState<string | null>(null);
@@ -809,7 +836,7 @@ export default function TaskDetailPage() {
809836
</Link>
810837
</div>
811838
<div className="flex-1 min-h-0 overflow-hidden">
812-
<Feed items={items} skills={context.skills} onRunClick={handleRunIdClick} compact taskId={taskId} hasMore={feedHasMore} onLoadMore={feedLoadMore} loadingMore={feedLoadingMore} />
839+
<Feed items={items} skills={context.skills} onRunClick={handleRunIdClick} onItemClick={handleItemClick} compact taskId={taskId} hasMore={feedHasMore} onLoadMore={feedLoadMore} loadingMore={feedLoadingMore} />
813840
</div>
814841
</div>
815842
</div>
@@ -840,7 +867,7 @@ export default function TaskDetailPage() {
840867
</Link>
841868
</div>
842869
<div className="max-h-[500px] overflow-y-auto">
843-
<Feed items={items} skills={context.skills} onRunClick={handleRunIdClick} compact taskId={taskId} />
870+
<Feed items={items} skills={context.skills} onRunClick={handleRunIdClick} onItemClick={handleItemClick} compact taskId={taskId} />
844871
</div>
845872
</div>
846873
</div>
@@ -868,6 +895,7 @@ export default function TaskDetailPage() {
868895
activities={cardActivities}
869896
activitiesLoading={cardActivitiesLoading}
870897
onClose={() => setSelectedCard(null)}
898+
onRunClick={handleCardRunClick}
871899
taskId={taskId}
872900
/>
873901
)}

ui/src/components/feed.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ interface FeedProps {
1515
items: FeedItem[];
1616
skills?: SkillSummary[];
1717
onRunClick?: (runId: string) => void;
18+
onItemClick?: (itemId: string) => void;
1819
compact?: boolean;
1920
taskId?: string;
2021
hasMore?: boolean;
@@ -197,7 +198,20 @@ const CompactSkillItem = memo(function CompactSkillItem({ skill }: { skill: Skil
197198
);
198199
});
199200

200-
const CompactItem = memo(function CompactItem({ item, onRunClick, taskId }: { item: FeedItem; onRunClick?: (id: string) => void; taskId?: string }) {
201+
function ItemBadge({ itemId, onClick }: { itemId: string; onClick?: (id: string) => void }) {
202+
return (
203+
<span
204+
role="button"
205+
onClick={(e) => { e.preventDefault(); e.stopPropagation(); onClick?.(itemId); }}
206+
style={{ background: "rgba(88,166,255,.08)", color: "var(--blue)", fontSize: 10, padding: "1px 5px", cursor: "pointer" }}
207+
className="shrink-0 font-medium"
208+
>
209+
[{itemId}]
210+
</span>
211+
);
212+
}
213+
214+
const CompactItem = memo(function CompactItem({ item, onRunClick, onItemClick, taskId }: { item: FeedItem; onRunClick?: (id: string) => void; onItemClick?: (id: string) => void; taskId?: string }) {
201215
const postHref = taskId ? `/task/${taskId}/post/${item.id}` : undefined;
202216

203217
if (item.type === "result") {
@@ -215,6 +229,7 @@ const CompactItem = memo(function CompactItem({ item, onRunClick, taskId }: { it
215229
<span>{relativeTime(item.created_at)}</span>
216230
</div>
217231
</div>
232+
{item.item_id && <ItemBadge itemId={item.item_id} onClick={onItemClick} />}
218233
<Score value={item.score} className="text-sm text-[var(--color-text)] shrink-0" />
219234
</div>
220235
);
@@ -235,6 +250,7 @@ const CompactItem = memo(function CompactItem({ item, onRunClick, taskId }: { it
235250
<span>{relativeTime(item.created_at)}</span>
236251
<span className="mx-1">·</span>
237252
<span>{item.upvotes}</span>
253+
{item.item_id && <><span className="mx-1">·</span><ItemBadge itemId={item.item_id} onClick={onItemClick} /></>}
238254
</div>
239255
</div>
240256
</div>
@@ -262,7 +278,7 @@ const CompactItem = memo(function CompactItem({ item, onRunClick, taskId }: { it
262278
return null;
263279
});
264280

265-
export function Feed({ items, skills = [], onRunClick, compact, taskId, hasMore, onLoadMore, loadingMore }: FeedProps) {
281+
export function Feed({ items, skills = [], onRunClick, onItemClick, compact, taskId, hasMore, onLoadMore, loadingMore }: FeedProps) {
266282
const [filter, setFilter] = useState<FilterType>("all");
267283
const filteredItems = filter === "all" ? items : filter === "skill" ? [] : items.filter((item) => item.type === filter);
268284
const counts: Record<FilterType, number> = {
@@ -290,7 +306,7 @@ export function Feed({ items, skills = [], onRunClick, compact, taskId, hasMore,
290306
filteredItems.length === 0
291307
? <div className="text-center text-[var(--color-text-tertiary)] text-xs py-6">No items</div>
292308
: filteredItems.map((item) => (
293-
<CompactItem key={`${item.type}-${item.id}`} item={item} onRunClick={onRunClick} taskId={taskId} />
309+
<CompactItem key={`${item.type}-${item.id}`} item={item} onRunClick={onRunClick} onItemClick={onItemClick} taskId={taskId} />
294310
))
295311
)}
296312
{hasMore && onLoadMore && (

ui/src/components/kanban/kanban-card-modal.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,11 @@ interface ModalProps {
3333
activities: ItemActivity[];
3434
activitiesLoading?: boolean;
3535
onClose: () => void;
36+
onRunClick?: (runId: string) => void;
3637
taskId: string;
3738
}
3839

39-
export function KanbanCardModal({ item, activities, activitiesLoading, onClose, taskId }: ModalProps) {
40+
export function KanbanCardModal({ item, activities, activitiesLoading, onClose, onRunClick, taskId }: ModalProps) {
4041
useEffect(() => {
4142
const handler = (e: KeyboardEvent) => {
4243
if (e.key === "Escape") onClose();
@@ -150,7 +151,10 @@ export function KanbanCardModal({ item, activities, activitiesLoading, onClose,
150151
{relativeTime(a.created_at)}
151152
</span>
152153
</div>
153-
<div className="text-[11px] text-[var(--color-text-secondary)] leading-relaxed mt-0.5 line-clamp-3">
154+
<div
155+
className={`text-[11px] text-[var(--color-text-secondary)] leading-relaxed mt-0.5 line-clamp-3${a.type === "run" ? " cursor-pointer hover:underline" : ""}`}
156+
onClick={a.type === "run" ? () => onRunClick?.(a.id) : undefined}
157+
>
154158
{a.content}
155159
</div>
156160
{a.type === "run" && a.score != null && (

ui/src/types/api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export interface ResultFeedItem {
5959
downvotes: number;
6060
comments: Comment[];
6161
comment_count?: number;
62+
item_id?: string | null;
6263
created_at: string;
6364
}
6465

@@ -71,6 +72,7 @@ export interface PostFeedItem {
7172
downvotes: number;
7273
comments: Comment[];
7374
comment_count?: number;
75+
item_id?: string | null;
7476
created_at: string;
7577
}
7678

0 commit comments

Comments
 (0)