import type { JSX } from "react"; import { translate } from "../../../i18n/index"; import type { AgentMessageContentVM } from "../contracts/agentMessageRowVM"; import { AgentMessageDetailsDisclosure } from "./AgentMessageDetailsDisclosure"; import agentSystemNoticeStyles from "./agentSystemNoticeStyles"; import { useElapsedSeconds } from "./useElapsedSeconds"; const TRANSPORT_RETRY_PROGRESS_PATTERN = /\b(reconnect(?:ing)?(?:\s*(?:\.\.\.|…|[.。]+|:|-))?\s*\(?\d+\s*\/\s*\d+\)?)/i; export function AgentSystemNoticeMessage({ message }: { message: AgentMessageContentVM; }): JSX.Element { "use memo"; const notice = message.systemNotice; const detail = notice?.detail?.trim() ?? ""; const title = systemNoticeTitle(message); if (notice?.noticeKind === "transport_retry") { const retryText = transportRetryNoticeText(message); return (
{retryText}
); } if (isContextCompactionProgressNotice(message)) { return ( ); } if (isContextCompactionNotice(message)) { return ( ); } if (isContextHandoffRequiredNotice(message)) { return (
{translate("agentHost.agentGui.contextHandoffRequired")}
{translate("agentHost.agentGui.contextHandoffRequiredDetail")}
{detail ? ( ) : null}
); } if (isContextCompactionInterruptedNotice(message)) { return ( ); } const isStatusNotice = systemNoticeIsStatus(message); return (
{title}
{detail ? ( ) : null}
); } function systemNoticeIsStatus(message: AgentMessageContentVM): boolean { const notice = message.systemNotice; return ( notice?.severity === "warning" || notice?.severity === "error" || notice?.noticeKind === "transport_fallback" || isTransportFallbackNotice(message) ); } function isTransportFallbackNotice(message: AgentMessageContentVM): boolean { const notice = message.systemNotice; const text = [notice?.title, notice?.detail, message.body] .filter(Boolean) .join("\n") .toLowerCase(); return ( text.includes("falling back from websockets") || text.includes("https transport") ); } function transportRetryNoticeText(message: AgentMessageContentVM): string { const notice = message.systemNotice; const detail = notice?.detail?.trim() ?? ""; const progressText = transportRetryProgressText(detail) ?? transportRetryProgressText(notice?.title ?? "") ?? transportRetryProgressText(message.body); if (progressText) { return progressText; } return ( notice?.title?.trim() || message.body.trim() || translate("agentHost.agentGui.systemNoticeTransportRetry") ); } function transportRetryProgressText(value: string): string | null { const match = TRANSPORT_RETRY_PROGRESS_PATTERN.exec(value.trim()); return match?.[1]?.replace(/\s+/g, " ").trim() || null; } function isContextCompactionNotice(message: AgentMessageContentVM): boolean { const notice = message.systemNotice; return notice?.command === "compact" && notice.commandStatus === "completed"; } function isContextCompactionProgressNotice( message: AgentMessageContentVM ): boolean { const notice = message.systemNotice; return notice?.command === "compact" && notice.commandStatus === "running"; } function isContextCompactionInterruptedNotice( message: AgentMessageContentVM ): boolean { const notice = message.systemNotice; return ( notice?.command === "compact" && (notice.commandStatus === "failed" || notice.commandStatus === "canceled") ); } function isContextHandoffRequiredNotice( message: AgentMessageContentVM ): boolean { return message.systemNotice?.semanticKind === "context-handoff-required"; } function ContextCompactionDivider({ text, detail = null }: { text: string; detail?: string | null; }): JSX.Element { "use memo"; return (
{detail ? (
{detail}
) : null}
); } // Live compaction banner: the daemon replaces this notice in place with the // "Context compacted." notice once the provider finishes, so the timer only // runs while compaction is actually in flight. function ContextCompactionProgressDivider({ startedAtUnixMs }: { startedAtUnixMs: number | null; }): JSX.Element { "use memo"; const elapsedSeconds = useElapsedSeconds(startedAtUnixMs); const label = translate("agentHost.agentGui.contextCompactionInProgress"); const text = elapsedSeconds === null ? label : `${label} · ${formatElapsedSeconds(elapsedSeconds)}`; return ; } function formatElapsedSeconds(seconds: number): string { if (seconds < 60) { return `${seconds}s`; } return `${Math.floor(seconds / 60)}m ${seconds % 60}s`; } function systemNoticeTitle(message: AgentMessageContentVM): string { const notice = message.systemNotice; switch (notice?.noticeKind) { case "transport_retry": return translate("agentHost.agentGui.systemNoticeTransportRetry"); case "transport_fallback": return translate("agentHost.agentGui.systemNoticeTransportFallback"); case "plan_implementation_pending_confirmation": return translate( "agentHost.agentGui.systemNoticePlanImplementationPendingConfirmation" ); case "plan_implementation_completed": return translate( "agentHost.agentGui.systemNoticePlanImplementationCompleted" ); case "warning": return ( notice.title || translate("agentHost.agentGui.systemNoticeWarning") ); default: return ( notice?.title || message.body || translate("agentHost.agentGui.systemNoticeDefault") ); } }