Files
kandev/apps/web/components/task/ensure-session-error.tsx
T
2026-08-11 19:34:52 +02:00

129 lines
4.2 KiB
TypeScript

"use client";
import Link from "@/components/routing/app-link";
import { IconAlertTriangle, 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";
// 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.
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 BannerProps = {
error: Error | null;
onRetry: () => void;
workspaceId?: string | null;
};
/** Slim banner for the task page, rendered above the layout. */
export function EnsureSessionErrorBanner({ error, onRetry, workspaceId }: BannerProps) {
const { t } = useTranslation();
const info = describeEnsureError(error, workspaceId);
if (!info) return null;
return (
<div className="px-3 pt-2" data-testid="ensure-session-error-banner">
<Alert variant="destructive">
<IconAlertTriangle />
<AlertTitle>{info.title}</AlertTitle>
<AlertDescription>
<span>{info.detail}</span>
<span className="mt-1 flex flex-wrap items-center gap-2">
{info.action ? (
<Link
href={info.action.href}
className="cursor-pointer underline underline-offset-2 hover:text-foreground"
data-testid="ensure-session-error-action"
>
{info.action.label}
</Link>
) : null}
<Button
variant="outline"
size="sm"
className="h-6 cursor-pointer px-2 text-xs"
onClick={onRetry}
data-testid="ensure-session-error-retry"
>
<IconRefresh className="size-3" />
{t("task:retry")}
</Button>
</span>
</AlertDescription>
</Alert>
</div>
);
}
/** Full-panel centered state for the kanban preview's empty-sessions slot. */
export function EnsureSessionErrorEmptyState({ error, onRetry, workspaceId }: BannerProps) {
const { t } = useTranslation();
const info = describeEnsureError(error, workspaceId);
if (!info) return null;
return (
<div
className="flex h-full flex-col items-center justify-center gap-3 px-4 text-center text-sm"
data-testid="preview-ensure-error"
>
<span className="font-medium text-foreground">{info.title}</span>
<span className="max-w-xs text-muted-foreground">{info.detail}</span>
<span className="flex flex-wrap items-center justify-center gap-2">
{info.action ? (
<Link
href={info.action.href}
className="cursor-pointer underline underline-offset-2 hover:text-foreground"
data-testid="ensure-session-error-action"
>
{info.action.label}
</Link>
) : null}
<Button
variant="outline"
size="sm"
className="cursor-pointer"
onClick={onRetry}
data-testid="ensure-session-error-retry"
>
{t("task:retry")}
</Button>
</span>
</div>
);
}