"use client"; import { useMemo } from "react"; import { IconGitBranch, IconTerminal2 } from "@tabler/icons-react"; import { Badge } from "@kandev/ui/badge"; import { ScrollOnOverflow } from "@kandev/ui/scroll-on-overflow"; import type { LocalRepository, Repository, Branch, Executor, ExecutorProfile, } from "@/lib/types/http"; import type { AgentProfileOption } from "@/lib/state/slices"; import { formatUserHomePath, truncateRepoPath } from "@/lib/utils"; import { getExecutorIcon } from "@/lib/executor-icons"; import { AgentLogo } from "@/components/agent-logo"; import { getCapabilityWarning } from "@/lib/capability-warning"; import { buildBranchKeywords } from "./task-create-dialog-pill"; type OptionItem = { value: string; label: string; renderLabel: () => React.ReactNode; disabled?: boolean; disabledReason?: string; }; export function useRepositoryOptions( repositories: Repository[], discoveredRepositories: LocalRepository[], ) { const repositoryOptions = useMemo(() => { const normalizeRepoPath = (path: string) => path.replace(/\\/g, "/").replace(/\/+$/g, ""); const workspaceRepoPaths = new Set( repositories .map((repo: Repository) => repo.local_path) .filter(Boolean) .map((path: string) => normalizeRepoPath(path)), ); const localRepoOptions = discoveredRepositories.filter( (repo: LocalRepository) => !workspaceRepoPaths.has(normalizeRepoPath(repo.path)), ); return [ ...repositories.map((repo: Repository) => ({ value: repo.id, label: repo.name, renderLabel: () => ( {repo.name} {repo.local_path ? ( {truncateRepoPath(repo.local_path, 24)} ) : null} ), })), ...localRepoOptions.map((repo: LocalRepository) => ({ value: repo.path, label: truncateRepoPath(repo.path, 24), renderLabel: () => ( {truncateRepoPath(repo.path, 28)} ), })), ]; }, [repositories, discoveredRepositories]); const headerRepositoryOptions = useMemo(() => { return repositoryOptions.map((opt) => ({ ...opt, renderLabel: () => {opt.label}, })); }, [repositoryOptions]); return { repositoryOptions, headerRepositoryOptions }; } export function useBranchOptions(branchOptionsRaw: Branch[]) { return useMemo(() => { return branchOptionsRaw.map((branchObj: Branch) => { const displayName = branchObj.type === "remote" && branchObj.remote ? `${branchObj.remote}/${branchObj.name}` : branchObj.name; // Keywords give the scorer extra surfaces to match against: the leaf // branch name, every path segment, and (for remotes) the remote name. const keywords = buildBranchKeywords(branchObj.name, branchObj.remote); return { value: displayName, label: displayName, keywords, renderLabel: () => ( {displayName} {branchObj.type === "local" ? "local" : branchObj.remote || "remote"} ), }; }); }, [branchOptionsRaw]); } export function useAgentProfileOptions(agentProfiles: AgentProfileOption[]): OptionItem[] { return useMemo(() => { return agentProfiles.map((profile: AgentProfileOption) => { const parts = profile.label.split(" \u2022 "); const agentLabel = parts[0] ?? profile.label; const profileLabel = parts[1] ?? ""; const isPassthrough = profile.cli_passthrough === true; const warning = getCapabilityWarning(profile.capability_status, profile.capability_error); return { value: profile.id, label: profile.label, renderLabel: () => ( {agentLabel} {warning && ( )} {isPassthrough && ( )} {profileLabel ? ( {profileLabel} ) : null} ), }; }); }, [agentProfiles]); } export function useExecutorOptions(executors: Executor[]): OptionItem[] { return useMemo(() => { return executors.map((executor: Executor) => { const Icon = getExecutorIcon(executor.type); return { value: executor.id, label: executor.name, renderLabel: () => ( {executor.name} ), }; }); }, [executors]); } export function useIsLocalExecutor(executors: Executor[], executorId: string): boolean { return useMemo(() => { const selected = executors.find((e: Executor) => e.id === executorId); return selected?.type === "local" || selected?.type === "local_pc"; }, [executors, executorId]); } export function computeExecutorHint( executors: Executor[], executorId: string, repoCount: number, ): string | null { const selectedExecutor = executors.find((e: Executor) => e.id === executorId); if (selectedExecutor?.type === "worktree") { if (repoCount > 1) { return "A git worktree will be created for each repository in a parent folder. The agent runs in that parent folder so it can see every worktree side by side."; } return "A git worktree will be created from the base branch."; } if (selectedExecutor?.type === "local_docker" || selectedExecutor?.type === "remote_docker") { return "A Docker container will be created from the selected base branch and checked out on a task branch."; } if (selectedExecutor?.type === "local" || selectedExecutor?.type === "local_pc") return "The agent will run directly on the repository."; return null; } export function useExecutorHint( executors: Executor[], executorId: string, repoCount: number, ): string | null { return useMemo( () => computeExecutorHint(executors, executorId, repoCount), [executors, executorId, repoCount], ); } export type ExecutorProfileOptionItem = OptionItem & { executorType?: string; executorName?: string; }; export type ExecutorProfileOptionsConfig = { /** * Returns a tooltip string when the given profile should render disabled. * Used by the kanban task-create dialog to gate non-worktree executors * (only worktree-based execution is currently supported there). */ disabledReasonFor?: (profile: ExecutorProfile) => string | null; }; export function useExecutorProfileOptions( allProfiles: ExecutorProfile[], config?: ExecutorProfileOptionsConfig, ): ExecutorProfileOptionItem[] { const disabledReasonFor = config?.disabledReasonFor; return useMemo(() => { return allProfiles.map((profile) => { const Icon = getExecutorIcon(profile.executor_type ?? "local"); const disabledReason = disabledReasonFor?.(profile) ?? null; return { value: profile.id, label: profile.name, executorType: profile.executor_type, executorName: profile.executor_name, disabled: !!disabledReason, disabledReason: disabledReason ?? undefined, renderLabel: () => ( {profile.name} {profile.executor_name && ( {profile.executor_name} )} ), }; }); }, [allProfiles, disabledReasonFor]); }