"use client"; import { Trans, useTranslation } from "react-i18next"; import type { RefObject } from "react"; import type { TFunction } from "i18next"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@kandev/ui/alert-dialog"; import { ActionConfirmPopover } from "@/components/confirmation/action-confirm-popover"; import { InlineConfirmActions } from "@/components/confirmation/inline-confirm-actions"; import { MobileActionConfirmation } from "@/components/confirmation/mobile-action-confirmation"; import { useAppStore } from "@/components/state-provider"; import type { ActiveSessionInfo, AutomationReference, RoutingTierReference, WatcherReference, UtilityAgentReference, } from "@/lib/types/agent-profile-errors"; import { settingsActionClassName } from "@/components/settings/settings-control"; // The watcher `kind` values are the wire enum and are never translated; only // their labels are copy, so they travel as catalog keys and resolve at render. const WATCHER_KIND_LABEL_KEYS: Record = { linear: "agents:watcherKindLinear", jira: "agents:watcherKindJira", github_issue: "agents:watcherKindGithubIssue", github_review: "agents:watcherKindGithubReview", }; const DELETE_PROFILE_TITLE_KEY = "agents:deleteAgentProfileTitle"; const DELETE_PROFILE_DESCRIPTION_KEY = "agents:deleteAgentProfileDescription"; const CANCEL_LABEL_KEY = "common:cancel"; type AgentProfileDeleteConfirmationProps = { profileId: string; profileName: string; open: boolean; isFinePointer: boolean; anchorRef: RefObject; onOpenChange: (open: boolean) => void; onCancel: () => void; onConfirm: () => void | Promise; }; /** * Local confirmation for a simple profile deletion. Conflict handling stays in * AgentProfileDeleteConflictDialog because its dependency lists need a modal. */ export function AgentProfileDeleteConfirmation({ profileId, profileName, open, isFinePointer, anchorRef, onOpenChange, onCancel, onConfirm, }: AgentProfileDeleteConfirmationProps) { const { t } = useTranslation(); const fallback = isFinePointer ? ( ) : ( onOpenChange(false)} onConfirm={onConfirm} /> ); return ( ); } export function AgentProfileDisableConflictDialog({ agents, onCancel, onConfirm, }: { agents: UtilityAgentReference[]; onCancel: () => void; onConfirm: () => void; }) { const { t } = useTranslation(); return ( 0} onOpenChange={(open) => { if (!open) onCancel(); }} > {t("agents:disableProfile")} {t("agents:disableProfileUtilityWarning")}
    {agents.map((agent) => (
  • {agent.name || agent.id}
  • ))}
{t(CANCEL_LABEL_KEY)} {t("agents:disableProfileAnyway")}
); } // AgentProfileDeleteConflict carries the structured 409 payload from the // backend. `open` is separate from the lists so a watcher-only conflict // (no active sessions) still pops the dialog. export type AgentProfileDeleteConflict = { activeSessions: ActiveSessionInfo[]; watchers: WatcherReference[]; routingTiers: RoutingTierReference[]; automations: AutomationReference[]; utilityAgents?: UtilityAgentReference[]; }; type AgentProfileDeleteConflictDialogProps = { conflict: AgentProfileDeleteConflict | null; onOpenChange: (open: boolean) => void; onConfirm: () => void; }; // eslint-disable-next-line complexity export function AgentProfileDeleteConflictDialog({ conflict, onOpenChange, onConfirm, }: AgentProfileDeleteConflictDialogProps) { const { t } = useTranslation(); const tasks = conflict?.activeSessions.filter((s) => !s.is_ephemeral) ?? []; const quickChats = conflict?.activeSessions.filter((s) => s.is_ephemeral) ?? []; const watchers = conflict?.watchers ?? []; const routingTiers = conflict?.routingTiers ?? []; const automations = conflict?.automations ?? []; const utilityAgents = conflict?.utilityAgents ?? []; const hasHardBlockers = routingTiers.length > 0; const watchersByKind = groupWatchersByKind(watchers); const workspaces = useAppStore((s) => s.workspaces.items); const providers = useAppStore((s) => s.settingsAgents.items); return ( {hasHardBlockers ? t("agents:cannotDeleteAgentProfile") : t(DELETE_PROFILE_TITLE_KEY)}

{t("agents:profileInUseIntro")}

[w.id, w.name]))} providerLabels={new Map(providers.map((p) => [p.id, p.name]))} /> [w.id, w.name]))} /> {hasHardBlockers ? (

{t("agents:changeTierMappingsFirst")}

) : (

{t("agents:deleteAnywayConsequences")}

)}
{t(CANCEL_LABEL_KEY)} {hasHardBlockers ? null : ( {t("agents:deleteAnyway")} )}
); } function UtilityAgentConflictSection({ utilityAgents, }: { utilityAgents: UtilityAgentReference[]; }) { const { t } = useTranslation(); if (utilityAgents.length === 0) return null; return (

{t("agents:conflictUtilityAgentsTitle")}

    {utilityAgents.map((agent) => (
  • {agent.name || agent.id}
  • ))}
); } function SessionConflictSection({ title, sessions, fallback, }: { title: string; sessions: ActiveSessionInfo[]; fallback: string; }) { if (sessions.length === 0) return null; return (

{title}

    {sessions.map((t) => (
  • {t.task_title || fallback}
  • ))}
); } function WatcherConflictSection({ watchersByKind, }: { watchersByKind: Record; }) { const { t } = useTranslation(); const entries = Object.entries(watchersByKind); if (entries.length === 0) return null; return (

{t("agents:conflictWatchersTitle")}

    {entries.map(([kind, items]) => (
  • {watcherKindLabel(t, kind as WatcherReference["kind"])}: {" "} {items.map((w) => w.label || w.id).join(", ")}
  • ))}
); } // Enabled automations bound to this profile. Listed separately from sessions // because nothing is running: an automation is a standing instruction, and the // damage shows up on its next firing rather than now. function AutomationConflictSection({ automations, workspaceLabels, }: { automations: AutomationReference[]; workspaceLabels: Map; }) { const { t } = useTranslation(); if (automations.length === 0) return null; return (

{t("agents:conflictAutomationsTitle")}

    {automations.map((ref) => (
  • ))}
); } function RoutingTierConflictSection({ routingTiers, workspaceLabels, providerLabels, }: { routingTiers: RoutingTierReference[]; workspaceLabels: Map; providerLabels: Map; }) { const { t } = useTranslation(); if (routingTiers.length === 0) return null; return (

{t("agents:conflictTierMappingsTitle")}

    {routingTiers.map((ref) => (
  • ))}
); } /** * The watcher `kind` is the wire enum; only its label is copy. An unknown kind * echoes the raw value rather than inventing one. */ function watcherKindLabel(t: TFunction, kind: WatcherReference["kind"]): string { const key = WATCHER_KIND_LABEL_KEYS[kind]; return key ? t(key) : kind; } /** * `tier` is a routing wire value (`fast`, `heavy`, …), not copy — it is * title-cased for display and never translated. */ function formatRoutingTier(tier: string): string { return tier.charAt(0).toUpperCase() + tier.slice(1); } function formatLookupLabel(labels: Map, id: string): string { const label = labels.get(id); return label && label !== id ? `${label} (${id})` : id; } function groupWatchersByKind(watchers: WatcherReference[]): Record { return watchers.reduce>((acc, w) => { (acc[w.kind] ??= []).push(w); return acc; }, {}); }