"use client"; import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import { Alert, AlertDescription, AlertTitle } from "@kandev/ui/alert"; import { Button } from "@kandev/ui/button"; import { Checkbox } from "@kandev/ui/checkbox"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@kandev/ui/dialog"; import { Drawer, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle, } from "@kandev/ui/drawer"; import { Spinner } from "@kandev/ui/spinner"; import { IconExternalLink } from "@tabler/icons-react"; import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint"; import { linkToTask } from "@/lib/links"; import type { DiagnosticBundleCapabilities, DiagnosticBundleSource, DiagnosticSession, } from "@/lib/types/system"; type BundleCustomizerProps = { open: boolean; onOpenChange: (open: boolean) => void; capabilities: DiagnosticBundleCapabilities | null; sessions: DiagnosticSession[]; sessionsLoading: boolean; selectedSources: DiagnosticBundleSource[]; selectedSessionIDs: string[]; submitting: boolean; onSourcesChange: (sources: DiagnosticBundleSource[]) => void; onSessionChange: (sessionID: string, checked: boolean) => void; onSessionSelectionChange: (sessionIDs: string[]) => void; onSubmit: () => void; }; const SOURCE_ORDER: DiagnosticBundleSource[] = ["backend", "frontend", "runtime", "acp"]; const SOURCE_COPY: Record = { backend: { label: "settings:diagnosticSourceBackend", description: "settings:diagnosticSourceBackendDescription", }, frontend: { label: "settings:diagnosticSourceFrontend", description: "settings:diagnosticSourceFrontendDescription", }, runtime: { label: "settings:diagnosticSourceRuntime", description: "settings:diagnosticSourceRuntimeDescription", }, acp: { label: "settings:diagnosticSourceACP", description: "settings:diagnosticSourceACPDescription", }, }; export function BundleCustomizer(props: BundleCustomizerProps) { const { t } = useTranslation(); const { isMobile } = useResponsiveBreakpoint(); const sourceOptions = useMemo(() => { const available = new Set( props.capabilities?.sources ?? ["backend", "frontend", "runtime"], ); return SOURCE_ORDER.filter( (source) => available.has(source) && (source !== "acp" || props.capabilities?.acp_debug_enabled === true), ); }, [props.capabilities?.acp_debug_enabled, props.capabilities?.sources]); const acpSelected = props.selectedSources.includes("acp"); const selectableSessions = props.sessions.filter( (session) => session.acp_availability !== "unavailable", ); const validationKey = validationMessageKey( props.selectedSources, acpSelected, props.selectedSessionIDs.length, selectableSessions.length, props.capabilities?.acp_max_sessions ?? 10, ); const content = ( ); const footer = ( props.onOpenChange(false)} onSubmit={props.onSubmit} /> ); const validation = validationKey ? (

{validationMessage(validationKey, t)}

) : null; if (isMobile) { return ( {t("settings:diagnosticCustomizerTitle")} {t("settings:diagnosticCustomizerDescription")}
{content} {validation}
{footer}
); } return ( {t("settings:diagnosticCustomizerTitle")} {t("settings:diagnosticCustomizerDescription")}
{content} {validation}
{footer}
); } type CustomizerContentProps = { sourceOptions: DiagnosticBundleSource[]; selectedSources: DiagnosticBundleSource[]; selectedSessionIDs: string[]; sessions: DiagnosticSession[]; sessionsLoading: boolean; acpSelected: boolean; selectableSessions: DiagnosticSession[]; maxSessions: number; onSourcesChange: (sources: DiagnosticBundleSource[]) => void; onSessionChange: (sessionID: string, checked: boolean) => void; onSessionSelectionChange: (sessionIDs: string[]) => void; }; function CustomizerContent({ sourceOptions, selectedSources, selectedSessionIDs, sessions, sessionsLoading, acpSelected, selectableSessions, maxSessions, onSourcesChange, onSessionChange, onSessionSelectionChange, }: CustomizerContentProps) { const { t } = useTranslation(); return (
{acpSelected && ( {t("settings:diagnosticSourceACP")} {t("settings:diagnosticACPWarning")} )} {acpSelected && ( )}
); } type CustomizerFooterProps = { submitting: boolean; invalid: boolean; onCancel: () => void; onSubmit: () => void; }; function CustomizerFooter({ submitting, invalid, onCancel, onSubmit }: CustomizerFooterProps) { const { t } = useTranslation(); return (
); } type SourceSectionProps = { sourceOptions: DiagnosticBundleSource[]; selectedSources: DiagnosticBundleSource[]; onSourcesChange: (sources: DiagnosticBundleSource[]) => void; }; function SourceSection({ sourceOptions, selectedSources, onSourcesChange }: SourceSectionProps) { const { t } = useTranslation(); const setSource = (source: DiagnosticBundleSource, checked: boolean) => { const next = checked ? [...selectedSources, source] : selectedSources.filter((selected) => selected !== source); onSourcesChange(SOURCE_ORDER.filter((candidate) => next.includes(candidate))); }; return (

{t("settings:diagnosticSourcesTitle")}

{sourceOptions.map((source) => ( setSource(source, checked)} /> ))}
); } function SourceRow({ source, checked, onCheckedChange, }: { source: DiagnosticBundleSource; checked: boolean; onCheckedChange: (checked: boolean) => void; }) { const { t } = useTranslation(); const copy = SOURCE_COPY[source]; return ( ); } type SessionSectionProps = { sessions: DiagnosticSession[]; sessionsLoading: boolean; selectableSessions: DiagnosticSession[]; selectedSessionIDs: string[]; maxSessions: number; onSessionChange: (sessionID: string, checked: boolean) => void; onSessionSelectionChange: (sessionIDs: string[]) => void; }; function SessionSection({ sessions, sessionsLoading, selectableSessions, selectedSessionIDs, maxSessions, onSessionChange, onSessionSelectionChange, }: SessionSectionProps) { const { t } = useTranslation(); return (

{t("settings:diagnosticSelectSessions")}

{t("settings:diagnosticSessionsDescription", { count: maxSessions })}

); } function SessionResults({ sessions, sessionsLoading, selectableSessions, selectedSessionIDs, maxSessions, onSessionChange, onSessionSelectionChange, }: SessionSectionProps) { const { t } = useTranslation(); if (sessionsLoading) { return (
{t("settings:diagnosticSessionsLoading")}
); } if (sessions.length === 0) { return (

{t("settings:diagnosticACPNoSessions")}

); } const selectAll = () => { onSessionSelectionChange( selectableSessions.slice(0, maxSessions).map((session) => session.session_id), ); }; return (
0} hasSelectedSessions={selectedSessionIDs.length > 0} onSelectAll={selectAll} onClear={() => onSessionSelectionChange([])} /> {sessions.map((session) => ( = maxSessions} onSessionChange={onSessionChange} /> ))}

{t("settings:diagnosticSelectedSessions", { count: selectedSessionIDs.length })}

); } function SessionSelectionActions({ hasSelectableSessions, hasSelectedSessions, onSelectAll, onClear, }: { hasSelectableSessions: boolean; hasSelectedSessions: boolean; onSelectAll: () => void; onClear: () => void; }) { const { t } = useTranslation(); return (
); } function SessionRow({ session, selectable, checked, maxReached, onSessionChange, }: { session: DiagnosticSession; selectable: boolean; checked: boolean; maxReached: boolean; onSessionChange: (sessionID: string, checked: boolean) => void; }) { const { t } = useTranslation(); const title = session.agent || session.provider || session.session_id; const taskTitle = session.task_title || session.task_id; const details = [session.model, session.status, session.executor_type].filter(Boolean).join(" ยท ") || session.session_id; const checkboxID = `diagnostic-acp-session-${session.session_id}`; return (
onSessionChange(session.session_id, value === true)} className="mt-0.5" aria-label={session.session_id} />
{taskTitle}
{availabilityLabel(session.acp_availability, t)}
); } function validationMessageKey( sources: DiagnosticBundleSource[], acpSelected: boolean, selectedSessionCount: number, selectableSessionCount: number, maxSessions: number, ): string | null { if (sources.length === 0) return "diagnosticSourceRequired"; if (acpSelected && selectedSessionCount > maxSessions) return "diagnosticACPSelectionLimit"; if (acpSelected && (selectableSessionCount === 0 || selectedSessionCount === 0)) { return "diagnosticACPSelectionRequired"; } return null; } function availabilityLabel( availability: DiagnosticSession["acp_availability"], t: (key: string) => string, ): string { if (availability === "reachable") return t("settings:diagnosticSessionReachable"); if (availability === "host_retained") return t("settings:diagnosticSessionRetained"); return t("settings:diagnosticSessionUnavailable"); } function validationMessage(key: string, t: (key: string) => string): string { if (key === "diagnosticSourceRequired") return t("settings:diagnosticSourceRequired"); if (key === "diagnosticACPSelectionRequired") { return t("settings:diagnosticACPSelectionRequired"); } return t("settings:diagnosticACPSelectionLimit"); }