feat(web): kanban card shows multi-repo "+N" chip and hydrates full repo list
Phase 6 (subset) of multi-repository task support. The kanban now exposes the per-task repository list to the UI and renders a compact "+N" badge on cards whose tasks span more than one repository. - KanbanState.tasks[].repositories slim shape (id, repository_id, base_branch, checkout_branch, position). - SSR mapper hydrates the full list and uses primaryTaskRepository() to pick the position-aware primary instead of [0]. - useKanbanData passes repositories through to the card props. - KanbanCard renders the primary repo name with a small "+N" chip when len(repositories) > 1; tooltip names the count. - useTaskRepositories / useTaskPrimaryRepository hooks return the slim KanbanTaskRepository shape so multi-repo consumers share one read path. Larger UI surfaces (task-create dialog, diff view, file browser, mobile sheets) still target the primary repo only — they will be ported as the multi-repo task creation flow lands. No visible change for single-repo tasks. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -42,6 +42,8 @@ export interface Task {
|
||||
description?: string;
|
||||
position?: number;
|
||||
repositoryId?: string;
|
||||
/** All repositories linked to the task; used to render a "+N" chip for multi-repo. */
|
||||
repositories?: Array<{ id: string; repository_id: string; position: number }>;
|
||||
// Workflow fields
|
||||
sessionCount?: number | null;
|
||||
primarySessionId?: string | null;
|
||||
@@ -93,8 +95,16 @@ function KanbanCardBody({
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
{repoName && (
|
||||
<p className="text-[10px] mb-1 text-muted-foreground leading-tight truncate">
|
||||
{repoName}
|
||||
<p className="text-[10px] mb-1 text-muted-foreground leading-tight truncate flex items-center gap-1">
|
||||
<span className="truncate">{repoName}</span>
|
||||
{task.repositories && task.repositories.length > 1 && (
|
||||
<span
|
||||
className="shrink-0 rounded-sm bg-muted px-1 py-px text-[9px] font-medium text-muted-foreground/80"
|
||||
title={`Task spans ${task.repositories.length} repositories`}
|
||||
>
|
||||
+{task.repositories.length - 1}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center gap-1 min-w-0">
|
||||
|
||||
@@ -71,6 +71,7 @@ export function useKanbanData({
|
||||
description: task.description,
|
||||
position: task.position,
|
||||
repositoryId: task.repositoryId,
|
||||
repositories: task.repositories,
|
||||
primarySessionId: task.primarySessionId,
|
||||
sessionCount: task.sessionCount,
|
||||
reviewStatus: task.reviewStatus,
|
||||
|
||||
@@ -3,7 +3,14 @@
|
||||
import { useMemo } from "react";
|
||||
import { useAppStore } from "@/components/state-provider";
|
||||
import type { KanbanState } from "@/lib/state/slices";
|
||||
import { primaryTaskRepository, type TaskRepository } from "@/lib/types/http";
|
||||
|
||||
/**
|
||||
* Slim per-task repository shape carried in the kanban store. Mirrors
|
||||
* KanbanState["tasks"][number]["repositories"][number].
|
||||
*/
|
||||
export type KanbanTaskRepository = NonNullable<
|
||||
KanbanState["tasks"][number]["repositories"]
|
||||
>[number];
|
||||
|
||||
/**
|
||||
* Returns the repositories linked to a task, ordered by Position. Empty
|
||||
@@ -11,7 +18,7 @@ import { primaryTaskRepository, type TaskRepository } from "@/lib/types/http";
|
||||
* use this instead of poking task.repositories directly so multi-repo
|
||||
* consumers all share one source of truth.
|
||||
*/
|
||||
export function useTaskRepositories(taskId: string | null | undefined): TaskRepository[] {
|
||||
export function useTaskRepositories(taskId: string | null | undefined): KanbanTaskRepository[] {
|
||||
const task = useAppStore((state) =>
|
||||
taskId
|
||||
? (state.kanban.tasks.find((t: KanbanState["tasks"][number]) => t.id === taskId) ?? null)
|
||||
@@ -28,7 +35,7 @@ export function useTaskRepositories(taskId: string | null | undefined): TaskRepo
|
||||
*/
|
||||
export function useTaskPrimaryRepository(
|
||||
taskId: string | null | undefined,
|
||||
): TaskRepository | null {
|
||||
): KanbanTaskRepository | null {
|
||||
const repos = useTaskRepositories(taskId);
|
||||
return primaryTaskRepository(repos) ?? null;
|
||||
return repos.length > 0 ? repos[0] : null;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AppState, KanbanState } from "@/lib/state/store";
|
||||
import { primaryTaskRepository } from "@/lib/types/http";
|
||||
import type { WorkflowSnapshot, Message, Task } from "@/lib/types/http";
|
||||
import {
|
||||
isPRReviewFromMetadata,
|
||||
@@ -26,6 +27,7 @@ export function snapshotToState(snapshot: WorkflowSnapshot): Partial<AppState> {
|
||||
.map((task) => {
|
||||
const workflowStepId = task.workflow_step_id;
|
||||
if (!workflowStepId) return null;
|
||||
const primary = primaryTaskRepository(task.repositories);
|
||||
return {
|
||||
id: task.id,
|
||||
workflowStepId,
|
||||
@@ -33,7 +35,14 @@ export function snapshotToState(snapshot: WorkflowSnapshot): Partial<AppState> {
|
||||
description: task.description ?? undefined,
|
||||
position: task.position ?? 0,
|
||||
state: task.state,
|
||||
repositoryId: task.repositories?.[0]?.repository_id ?? undefined,
|
||||
repositoryId: primary?.repository_id ?? undefined,
|
||||
repositories: task.repositories?.map((r) => ({
|
||||
id: r.id,
|
||||
repository_id: r.repository_id,
|
||||
base_branch: r.base_branch,
|
||||
checkout_branch: r.checkout_branch,
|
||||
position: r.position,
|
||||
})),
|
||||
primarySessionId: task.primary_session_id ?? undefined,
|
||||
primarySessionState: task.primary_session_state ?? undefined,
|
||||
sessionCount: task.session_count ?? undefined,
|
||||
|
||||
@@ -28,7 +28,20 @@ export type KanbanState = {
|
||||
description?: string;
|
||||
position: number;
|
||||
state?: TaskStatus;
|
||||
/** Primary repository id (lowest position). Kept for backwards compat. */
|
||||
repositoryId?: string;
|
||||
/**
|
||||
* All repositories linked to the task, ordered by Position. Optional so
|
||||
* legacy SSR payloads still parse; multi-repo UI consumers should prefer
|
||||
* this over repositoryId.
|
||||
*/
|
||||
repositories?: Array<{
|
||||
id: string;
|
||||
repository_id: string;
|
||||
base_branch: string;
|
||||
checkout_branch?: string;
|
||||
position: number;
|
||||
}>;
|
||||
primarySessionId?: string | null;
|
||||
primarySessionState?: string | null;
|
||||
sessionCount?: number | null;
|
||||
|
||||
Reference in New Issue
Block a user