"use client"; import { useId, useMemo, useState } from "react"; import { IconArrowLeft, IconCheck } from "@tabler/icons-react"; import { Badge } from "@kandev/ui/badge"; import { Button } from "@kandev/ui/button"; import { Checkbox } from "@kandev/ui/checkbox"; import { Input } from "@kandev/ui/input"; import { useTranslation } from "react-i18next"; import { PRTaskIcon } from "@/components/github/pr-task-icon"; import { getTaskStateIconLabelKey, TaskStateIcon, type TaskStateIconProps, } from "@/components/task/task-state-icon"; import type { ThreadCandidate } from "@/lib/threads/thread-view-query"; import { MAX_THREAD_VIEW_TASK_IDS, updateThreadTaskSelection, updateVisibleThreadTaskSelection, } from "./threads-view-editor-utils"; type Props = { candidates: ThreadCandidate[]; selectedTaskIds: string[]; onChange: (taskIds: string[]) => void; onBack: () => void; }; function selectAllValue( allVisibleSelected: boolean, someVisibleSelected: boolean, ): boolean | "indeterminate" { if (allVisibleSelected) return true; if (someVisibleSelected) return "indeterminate"; return false; } function candidateStateIconProps(candidate: ThreadCandidate): TaskStateIconProps { const pendingAction = candidate.taskPendingAction ?? candidate.pendingAction; return { state: candidate.taskState ?? undefined, sessionState: candidate.sessionState, foregroundActivity: candidate.foregroundActivity, hasPendingClarification: pendingAction === "clarification", hasPendingPermission: pendingAction === "permission", isOnLastWorkflowStep: candidate.isOnLastWorkflowStep, interrupted: candidate.interrupted, }; } function ThreadTaskPickerRow({ candidate, checked, onCheckedChange, }: { candidate: ThreadCandidate; checked: boolean; onCheckedChange: (checked: boolean) => void; }) { const { t } = useTranslation(); const checkboxId = useId(); const iconProps = candidateStateIconProps(candidate); return (
onCheckedChange(value === true)} aria-label={candidate.title} data-testid="threads-task-picker-checkbox" /> {checked && }
); } // eslint-disable-next-line max-lines-per-function -- Keeps search, bulk selection, and task-row updates in one picker boundary. export function ThreadsViewTaskPicker({ candidates, selectedTaskIds, onChange, onBack }: Props) { const { t } = useTranslation(); const [search, setSearch] = useState(""); const selected = useMemo(() => new Set(selectedTaskIds), [selectedTaskIds]); const visibleCandidates = useMemo(() => { const needle = search.trim().toLocaleLowerCase(); if (!needle) return candidates; return candidates.filter((candidate) => candidate.title.toLocaleLowerCase().includes(needle)); }, [candidates, search]); const visibleIds = visibleCandidates.map((candidate) => candidate.taskId); const selectedVisibleCount = visibleIds.filter((taskId) => selected.has(taskId)).length; const allVisibleSelected = visibleIds.length > 0 && selectedVisibleCount === visibleIds.length; const someVisibleSelected = selectedVisibleCount > 0 && !allVisibleSelected; function setSelected(taskId: string, checked: boolean) { onChange(updateThreadTaskSelection(selectedTaskIds, taskId, checked)); } function setAllVisible(checked: boolean) { onChange(updateVisibleThreadTaskSelection(selectedTaskIds, visibleIds, checked)); } return (

{t("threads:chooseTasks")}

{t("threads:selectedTaskCount", { count: selected.size })}

setSearch(event.target.value)} placeholder={t("threads:searchTasks")} aria-label={t("threads:searchTasks")} className="min-h-11" data-testid="threads-task-picker-search" />
{selected.size >= MAX_THREAD_VIEW_TASK_IDS && (

{t("threads:taskSelectionLimitReached", { count: MAX_THREAD_VIEW_TASK_IDS })}

)}
{visibleCandidates.length === 0 ? (

{t("threads:noTasksToChoose")}

) : ( visibleCandidates.map((candidate) => ( setSelected(candidate.taskId, checked)} /> )) )}
); }