"use client"; import Link from "@/components/routing/app-link"; import { IconAlertTriangle, IconInfoCircle, IconRefresh } from "@tabler/icons-react"; import { Alert, AlertDescription, AlertTitle } from "@kandev/ui/alert"; import { Button } from "@kandev/ui/button"; import { useTranslation } from "react-i18next"; import { t } from "@/lib/i18n"; import { cn } from "@/lib/utils"; import type { SessionRecoveryFailure } from "@/hooks/domains/session/use-session-resumption"; // EnsureSessionErrorInfo wraps a parsed ensure error so UI can offer a targeted action for the missing-agent-profile case. export type EnsureSessionErrorInfo = { title: string; detail: string; isAgentProfileMissing: boolean; action: { label: string; href: string } | null; }; // Matches the backend's exact validation message in task_http_handlers.go / task_ws_handlers.go. // i18n-exempt: matches the backend's exact validation message. See the comment above. const AGENT_PROFILE_MISSING_HINT = "agent_profile_id is required"; export function describeEnsureError( error: Error | null, workspaceId?: string | null, ): EnsureSessionErrorInfo | null { if (!error) return null; const message = error.message ?? ""; const isAgentProfileMissing = message.toLowerCase().includes(AGENT_PROFILE_MISSING_HINT); if (isAgentProfileMissing) { return { title: t("task:noAgentProfileConfigured"), detail: t("task:noAgentProfileConfiguredDetail"), isAgentProfileMissing: true, action: workspaceId ? { label: t("task:openWorkspaceSettings"), href: `/settings/workspaces/${workspaceId}`, } : null, }; } return { title: t("task:couldnTStartASession"), detail: message || t("task:backendRejectedSessionRequest"), isAgentProfileMissing: false, action: null, }; } type RecoveryAction = { label: string; onClick: () => void; testId: string; disabled?: boolean; }; type BannerProps = { error: Error | null; onRetry: () => void; workspaceId?: string | null; action?: RecoveryAction; secondaryAction?: RecoveryAction; retryDisabled?: boolean; recoveryFailure?: SessionRecoveryFailure | null; testId?: string; compact?: boolean; }; function RecoveryFailureDetails({ failure }: { failure: SessionRecoveryFailure }) { const { t } = useTranslation(); return (
{t("task:sessionRecoveryDetails")}
{t("task:sessionRecoveryResumeAttempt")}
{failure.resumeError}
{failure.outcome === "recovery_failed" ? (
{t("task:sessionRecoveryRestoreAttempt")}
{failure.restoreError}
) : null}
); } /** Slim banner for the task page, rendered above the layout. */ export function EnsureSessionErrorBanner({ error, onRetry, workspaceId, action, secondaryAction, retryDisabled, recoveryFailure, testId = "ensure-session-error-banner", compact = false, }: BannerProps) { const { t } = useTranslation(); const info = describeEnsureError(error, workspaceId); if (!info) return null; const failedRecovery = recoveryFailure?.outcome === "recovery_failed" ? recoveryFailure : null; return (
{failedRecovery ? t("task:sessionRecoveryFailed") : info.title} {failedRecovery ? t("task:sessionRecoveryFailedDetail") : info.detail} {failedRecovery ? : null} {info.action ? ( {info.action.label} ) : null} {action ? ( ) : null} {secondaryAction ? ( ) : null}
); } /** Non-blocking result notice used when the workspace remains available read-only. */ export function SessionRecoveryNotice({ message, recoveryFailure, }: { message: string; recoveryFailure?: Extract | null; }) { return (
{message} {recoveryFailure ? : null}
); } /** Shared inline rendering for automatic resume failures and read-only notices. */ export function SessionRecoveryFeedback({ error, notice, onRetry, workspaceId, action, secondaryAction, retryDisabled, recoveryFailure, testId = "session-recovery-error", }: { error: string | null; notice: string | null; onRetry: () => void; workspaceId?: string | null; action?: RecoveryAction; secondaryAction?: RecoveryAction; retryDisabled?: boolean; recoveryFailure?: SessionRecoveryFailure | null; testId?: string; }) { const readOnlyRecovery = recoveryFailure?.outcome === "workspace_read_only" ? recoveryFailure : null; return ( <> {notice ? ( ) : null} ); } /** Full-panel centered state for the kanban preview's empty-sessions slot. */ export function EnsureSessionErrorEmptyState({ error, onRetry, workspaceId, retryDisabled, }: BannerProps) { const { t } = useTranslation(); const info = describeEnsureError(error, workspaceId); if (!info) return null; return (
{info.title} {info.detail} {info.action ? ( {info.action.label} ) : null}
); }