170 lines
7.3 KiB
TypeScript
170 lines
7.3 KiB
TypeScript
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
|
|
import {
|
|
formatOpenCodeRuntimeDeliveryDebugDetails,
|
|
type OpenCodeRuntimeDeliveryDebugDetails,
|
|
} from '@renderer/utils/openCodeRuntimeDeliveryDiagnostics';
|
|
import { AlertCircle } from 'lucide-react';
|
|
|
|
import type { JSX } from 'react';
|
|
|
|
interface OpenCodeDeliveryWarningProps {
|
|
warning: string | null;
|
|
debugDetails?: OpenCodeRuntimeDeliveryDebugDetails | null;
|
|
pendingDelayMs?: number;
|
|
}
|
|
|
|
export const OpenCodeDeliveryWarning = ({
|
|
warning,
|
|
debugDetails,
|
|
pendingDelayMs = 10_000,
|
|
}: OpenCodeDeliveryWarningProps): JSX.Element | null => {
|
|
const detailsKey = `${warning ?? ''}:${debugDetails?.messageId ?? ''}:${debugDetails?.statusMessageId ?? ''}:${debugDetails?.userVisibleState ?? ''}`;
|
|
const delayPendingWarning =
|
|
debugDetails?.userVisibleState === 'checking' ||
|
|
(!debugDetails?.userVisibleState &&
|
|
debugDetails?.responsePending === true &&
|
|
debugDetails.delivered !== false);
|
|
const [expandedKey, setExpandedKey] = useState<string | null>(null);
|
|
const [copiedKey, setCopiedKey] = useState<string | null>(null);
|
|
const [pendingVisibleKey, setPendingVisibleKey] = useState<string | null>(() =>
|
|
delayPendingWarning ? null : detailsKey
|
|
);
|
|
const mountedRef = useRef(true);
|
|
const copiedResetTimerRef = useRef<number | null>(null);
|
|
const pendingTimerRef = useRef<number | null>(null);
|
|
const expanded = expandedKey === detailsKey;
|
|
const copied = copiedKey === detailsKey;
|
|
const copyText = useMemo(
|
|
() => (debugDetails ? formatOpenCodeRuntimeDeliveryDebugDetails(debugDetails) : ''),
|
|
[debugDetails]
|
|
);
|
|
|
|
useEffect(() => {
|
|
mountedRef.current = true;
|
|
return () => {
|
|
mountedRef.current = false;
|
|
if (copiedResetTimerRef.current !== null) {
|
|
window.clearTimeout(copiedResetTimerRef.current);
|
|
}
|
|
if (pendingTimerRef.current !== null) {
|
|
window.clearTimeout(pendingTimerRef.current);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (pendingTimerRef.current !== null) {
|
|
window.clearTimeout(pendingTimerRef.current);
|
|
pendingTimerRef.current = null;
|
|
}
|
|
if (!warning) {
|
|
setPendingVisibleKey(null);
|
|
return;
|
|
}
|
|
if (!delayPendingWarning || pendingDelayMs <= 0) {
|
|
setPendingVisibleKey(detailsKey);
|
|
return;
|
|
}
|
|
setPendingVisibleKey(null);
|
|
pendingTimerRef.current = window.setTimeout(() => {
|
|
pendingTimerRef.current = null;
|
|
if (mountedRef.current) {
|
|
setPendingVisibleKey(detailsKey);
|
|
}
|
|
}, pendingDelayMs);
|
|
}, [delayPendingWarning, detailsKey, pendingDelayMs, warning]);
|
|
|
|
if (!warning) return null;
|
|
if (delayPendingWarning && pendingVisibleKey !== detailsKey) return null;
|
|
|
|
const handleCopy = async (): Promise<void> => {
|
|
if (!copyText || !navigator.clipboard?.writeText) return;
|
|
try {
|
|
await navigator.clipboard.writeText(copyText);
|
|
} catch {
|
|
return;
|
|
}
|
|
if (!mountedRef.current) return;
|
|
setCopiedKey(detailsKey);
|
|
if (copiedResetTimerRef.current !== null) {
|
|
window.clearTimeout(copiedResetTimerRef.current);
|
|
}
|
|
copiedResetTimerRef.current = window.setTimeout(() => {
|
|
copiedResetTimerRef.current = null;
|
|
if (mountedRef.current) {
|
|
setCopiedKey(null);
|
|
}
|
|
}, 1500);
|
|
};
|
|
|
|
return (
|
|
<span className="relative inline-flex flex-col items-start gap-1">
|
|
<span className="inline-flex items-center gap-1 rounded bg-amber-500/10 px-1.5 py-0.5 text-[10px] text-amber-300">
|
|
<AlertCircle size={10} className="shrink-0" />
|
|
<span>{warning}</span>
|
|
{debugDetails ? (
|
|
<button
|
|
type="button"
|
|
className="ml-1 rounded px-1 text-[10px] font-medium text-amber-200 underline decoration-amber-300/50 underline-offset-2 hover:text-amber-100"
|
|
aria-expanded={expanded}
|
|
onClick={() =>
|
|
setExpandedKey((currentKey) => (currentKey === detailsKey ? null : detailsKey))
|
|
}
|
|
>
|
|
Details
|
|
</button>
|
|
) : null}
|
|
</span>
|
|
{expanded && debugDetails ? (
|
|
<span className="z-10 block max-w-[min(34rem,calc(100vw-3rem))] rounded border border-amber-500/20 bg-[var(--color-bg-primary)] p-2 text-left text-[10px] text-[var(--color-text-secondary)] shadow-xl">
|
|
<span className="grid grid-cols-[auto_1fr] gap-x-2 gap-y-1">
|
|
<span className="text-[var(--color-text-muted)]">messageId</span>
|
|
<span className="break-all">{debugDetails.messageId}</span>
|
|
<span className="text-[var(--color-text-muted)]">statusMessageId</span>
|
|
<span className="break-all">{debugDetails.statusMessageId}</span>
|
|
<span className="text-[var(--color-text-muted)]">providerId</span>
|
|
<span>{debugDetails.providerId}</span>
|
|
<span className="text-[var(--color-text-muted)]">delivered</span>
|
|
<span>{String(debugDetails.delivered)}</span>
|
|
<span className="text-[var(--color-text-muted)]">responsePending</span>
|
|
<span>{String(debugDetails.responsePending)}</span>
|
|
<span className="text-[var(--color-text-muted)]">responseState</span>
|
|
<span>{debugDetails.responseState ?? 'null'}</span>
|
|
<span className="text-[var(--color-text-muted)]">ledgerStatus</span>
|
|
<span>{debugDetails.ledgerStatus ?? 'null'}</span>
|
|
<span className="text-[var(--color-text-muted)]">visibleReplyMessageId</span>
|
|
<span className="break-all">{debugDetails.visibleReplyMessageId ?? 'null'}</span>
|
|
<span className="text-[var(--color-text-muted)]">visibleReplyCorrelation</span>
|
|
<span>{debugDetails.visibleReplyCorrelation ?? 'null'}</span>
|
|
<span className="text-[var(--color-text-muted)]">queuedBehindMessageId</span>
|
|
<span className="break-all">{debugDetails.queuedBehindMessageId ?? 'null'}</span>
|
|
<span className="text-[var(--color-text-muted)]">acceptanceUnknown</span>
|
|
<span>{String(debugDetails.acceptanceUnknown)}</span>
|
|
<span className="text-[var(--color-text-muted)]">userVisibleState</span>
|
|
<span>{debugDetails.userVisibleState ?? 'null'}</span>
|
|
<span className="text-[var(--color-text-muted)]">userVisibleReasonCode</span>
|
|
<span>{debugDetails.userVisibleReasonCode ?? 'null'}</span>
|
|
<span className="text-[var(--color-text-muted)]">userVisibleNextReviewAt</span>
|
|
<span>{debugDetails.userVisibleNextReviewAt ?? 'null'}</span>
|
|
<span className="text-[var(--color-text-muted)]">userVisibleMessage</span>
|
|
<span>{debugDetails.userVisibleMessage ?? 'null'}</span>
|
|
<span className="text-[var(--color-text-muted)]">reason</span>
|
|
<span>{debugDetails.reason ?? 'null'}</span>
|
|
<span className="text-[var(--color-text-muted)]">diagnostics</span>
|
|
<span>
|
|
{debugDetails.diagnostics.length ? debugDetails.diagnostics.join('; ') : '[]'}
|
|
</span>
|
|
</span>
|
|
<button
|
|
type="button"
|
|
className="mt-2 rounded border border-amber-500/20 px-2 py-1 text-[10px] text-amber-200 hover:border-amber-400/40 hover:text-amber-100"
|
|
onClick={() => void handleCopy()}
|
|
>
|
|
{copied ? 'Copied' : 'Copy debug details'}
|
|
</button>
|
|
</span>
|
|
) : null}
|
|
</span>
|
|
);
|
|
};
|