diff --git a/apps/web/components/kanban-card.tsx b/apps/web/components/kanban-card.tsx index 1ed36e545c..a1f8051c18 100644 --- a/apps/web/components/kanban-card.tsx +++ b/apps/web/components/kanban-card.tsx @@ -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({
{repoName && ( -

- {repoName} +

+ {repoName} + {task.repositories && task.repositories.length > 1 && ( + + +{task.repositories.length - 1} + + )}

)}
diff --git a/apps/web/hooks/domains/kanban/use-kanban-data.ts b/apps/web/hooks/domains/kanban/use-kanban-data.ts index 8b37eee385..3922b8c023 100644 --- a/apps/web/hooks/domains/kanban/use-kanban-data.ts +++ b/apps/web/hooks/domains/kanban/use-kanban-data.ts @@ -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, diff --git a/apps/web/hooks/domains/kanban/use-task-repositories.ts b/apps/web/hooks/domains/kanban/use-task-repositories.ts index ba1bd807a8..b86ef74a86 100644 --- a/apps/web/hooks/domains/kanban/use-task-repositories.ts +++ b/apps/web/hooks/domains/kanban/use-task-repositories.ts @@ -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; } diff --git a/apps/web/lib/ssr/mapper.ts b/apps/web/lib/ssr/mapper.ts index f7efcae211..b66a94684b 100644 --- a/apps/web/lib/ssr/mapper.ts +++ b/apps/web/lib/ssr/mapper.ts @@ -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 { .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 { 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, diff --git a/apps/web/lib/state/slices/kanban/types.ts b/apps/web/lib/state/slices/kanban/types.ts index 25838bfc61..c82520853f 100644 --- a/apps/web/lib/state/slices/kanban/types.ts +++ b/apps/web/lib/state/slices/kanban/types.ts @@ -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;