import { useWorkspaceBaseRefRecovery, useWorkspaceRetry } from "@/components/WorkspaceBaseRefRecovery"; import { WorkspaceBaseRefRecoveryNotice } from "@/components/WorkspaceBaseRefRecoveryNotice"; import { DispositionRecoveryNotice, useDispositionRecoverySnapshot } from "@/components/DispositionRecoveryNotice"; import { useId, useState } from "react"; import { ChevronDown, CircleCheck, Info, OctagonAlert, TriangleAlert, type LucideIcon, } from "lucide-react"; import { cn } from "@/lib/utils"; import { useStreamlinedTaskChatPresentation } from "./presentation-mode"; import { MarkdownBody } from "@/components/MarkdownBody"; import { Button } from "@/components/ui/button"; import { SystemNoticeMetadataSections, type SystemNoticeTone, } from "@/components/SystemNotice"; import { humanizeSystemNotice } from "@/lib/system-notice-humanizer"; import { mapCommentMetadataToSystemNoticeSections } from "@/lib/system-notice-comment"; import { timeAgo } from "@/lib/timeAgo"; import type { TaskChatMessageItem } from "./task-chat-model"; import { isNoLiveExecutionPathNotice } from "./adjacent-errors"; const TONE_ICON: Record = { neutral: Info, info: Info, success: CircleCheck, warning: TriangleAlert, danger: OctagonAlert, }; const TONE_ICON_CLASS: Record = { neutral: "text-muted-foreground", info: "text-(--status-task-icon-in_progress)", success: "text-(--status-task-icon-done)", warning: "text-(--status-task-icon-todo)", danger: "text-(--status-task-icon-blocked)", }; /** * System comment row in the chat shell (PAP-443 / PAP-442 Phase 1): collapsed, * it reads as a quiet left-aligned one-liner in TaskChatMarker's register — tone * icon + humanized plain-English title + relative time + chevron — instead of * a large gray paragraph of raw text. Expanding reveals the full * markdown-rendered body plus any structured metadata sections. Workspace-ready * notices omit their Markdown fallback when the equivalent structured workspace * metadata is present, so each value appears once in the expanded panel. */ export function TaskChatSystemNotice({ item, onTryAgainNoLiveExecutionPath, tryAgainNoLiveExecutionPathPending = false, }: { item: TaskChatMessageItem; onTryAgainNoLiveExecutionPath?: () => Promise | void; tryAgainNoLiveExecutionPathPending?: boolean; }) { const recoverySnapshot = useDispositionRecoverySnapshot(item.metadata); const branchRecovery = useWorkspaceBaseRefRecovery(item.metadata); const workspaceRetry = useWorkspaceRetry(item.metadata); const streamlined = useStreamlinedTaskChatPresentation(); const [open, setOpen] = useState(Boolean(item.presentation?.detailsDefaultOpen)); const detailsId = useId(); const { title, tone, detail } = humanizeSystemNotice({ body: item.text, presentation: item.presentation, }); const sections = mapCommentMetadataToSystemNoticeSections(item.metadata, { runAgentId: item.runAgentId, }); const isStructuredWorkspaceReadyNotice = item.presentation?.kind === "system_notice" && item.presentation.title?.trim().toLowerCase().startsWith("workspace ready") === true && sections.some((section) => section.title?.trim().toLowerCase() === "workspace"); const showBody = !isStructuredWorkspaceReadyNotice; const ToneIcon = TONE_ICON[tone]; const relative = item.createdAtIso ? timeAgo(item.createdAtIso) : undefined; const showTryAgain = Boolean(onTryAgainNoLiveExecutionPath) && isNoLiveExecutionPathNotice(item); const handleTryAgain = () => { void Promise.resolve() .then(() => onTryAgainNoLiveExecutionPath?.()) // The parent mutation owns visible error feedback. .catch(() => undefined); }; if (item.author === "system" && workspaceRetry) return null; if (item.author === "system" && branchRecovery) return ; if (item.author === "system" && recoverySnapshot) { return ; } return (
{showTryAgain && !open ? ( ) : null}
{open ? (
{showBody ? (
{item.text}
) : null} {sections.length > 0 ? (
) : null} {showTryAgain ? (
) : null}
) : null}
); }