import { useEffect, useId, useState } from "react"; import { MotionDisclosure } from "./disclosure-motion"; import { canCancelJob, currentJobs, useJobs } from "./jobs"; import type { Job } from "@chopin/protocol"; import type { MotionDisclosureContract } from "./disclosure-motion"; import type { JobStore } from "./jobs"; export type BackgroundWorkProps = { store: JobStore; connected: boolean; canEdit: boolean; headingId?: string; motion: MotionDisclosureContract; motionImmediately?: () => boolean; }; const INTERRUPTION_REASONS: Record = { "attempt-error": "Worker failed unexpectedly", "attempt-timeout": "Attempt timed out", "credential-rotated": "Planner credentials changed", "heartbeat-lost": "Runner heartbeat was lost", "hosted-search-sources-unverifiable": "Hosted web search returned unverifiable sources", "owner-unavailable": "Planner owner became unavailable", "private-analysis-failed": "Private document analysis failed", "private-answer-failed": "Private research answer failed", "public-research-failed": "Public web research failed", "public-session-failed": "Copilot research session failed", "research-result-invalid": "Research returned an invalid result", "research-result-missing": "Research returned no structured result", "research-permission-denied": "Copilot web search permission was denied", "research-source-mismatch": "Submitted sources did not match search results", "research-sources-unverifiable": "Search results had no verifiable source metadata", "report-synthesis-failed": "Research report synthesis failed", "source-validation-failed": "Research sources could not be verified", "web-search-failed": "Copilot web search failed", "web-search-not-invoked": "Copilot did not invoke web search", "web-search-unavailable": "Copilot web search is unavailable", }; export function safeInterruptionReason(reason: string | undefined): string { let code = reason?.startsWith("attempts-exhausted:") ? reason.slice("attempts-exhausted:".length) : reason; return code && Object.hasOwn(INTERRUPTION_REASONS, code) ? INTERRUPTION_REASONS[code]! : "Worker stopped unexpectedly"; } export function visibleProgress( job: Job.View, ): Array { let latest = new Map(); for (let entry of job.progress) latest.set(`${entry.attempt}:${entry.stage}`, entry); let activeRevision = job.state === "running" ? Math.max( 0, ...[...latest.values()].filter(entry => entry.attempt === job.attempts && entry.state === "started" ).map(entry => entry.revision), ) : 0; return [...latest.values()].map(entry => ({ ...entry, status: entry.state === "completed" ? "Completed" : entry.revision === activeRevision ? "In progress" : "Interrupted", ...(entry.state === "interrupted" ? { detail: safeInterruptionReason(entry.reason) } : {}), })); } function record(value: unknown): Record | undefined { return value && typeof value === "object" && !Array.isArray(value) ? value as Record : undefined; } export function backgroundJobResult( detail: Job.Detail | undefined, ): { title: string; summary: string } | undefined { if ( !detail || detail.job.state !== "completed" || detail.job.targetGeneration !== detail.currentTargetGeneration ) return undefined; let value = detail?.artifact?.value; let artifact = record(value); if (!artifact) return undefined; if (detail?.job.type === "document-summary") { if (artifact.output === "description" && typeof artifact.description === "string") { return { title: "Document description", summary: artifact.description }; } if (artifact.output === undefined && typeof artifact.summary === "string") { return { title: "Document summary", summary: artifact.summary }; } return undefined; } if (detail?.job.type !== "research-answer") return undefined; if (artifact.kind === "initial") { let report = record(artifact.report); return typeof report?.title === "string" && typeof report.summary === "string" ? { title: report.title, summary: report.summary } : undefined; } if (artifact.kind === "follow-up" || artifact.kind === "search-more") { let answer = record(artifact.answer); return typeof answer?.text === "string" ? { title: artifact.kind === "search-more" ? "Research update" : "Research answer", summary: answer.text, } : undefined; } return undefined; } function safeSubject(value: string | undefined): string | undefined { if (!value) return undefined; let normalized = value.replace(/\s+/g, " ").trim(); return normalized ? [...normalized].slice(0, 200).join("") : undefined; } export function backgroundJobLabel(job: Job.View, detail?: Job.Detail): string { let subject = safeSubject(job.subject); if (job.type === "research-evidence") { return subject ? `Research evidence: ${subject}` : "Research evidence"; } if (job.type === "research-answer") { return subject ? `Research answer: ${subject}` : "Research answer"; } if (job.type === "document-summary") { return backgroundJobResult(detail)?.title ?? "Document description"; } return "Background work"; } function BackgroundJob( { canEdit, connected, job, motion, motionImmediately, store }: { canEdit: boolean; connected: boolean; job: Job.View; motion: MotionDisclosureContract; motionImmediately?: () => boolean; store: JobStore; }, ) { let snapshot = useJobs(store); let [disclosure, setDisclosure] = useState({ immediately: false, open: false }); let [error, setError] = useState(); let resultId = useId(); let detail = snapshot.details[job.id]; let resultOpen = disclosure.open && detail !== undefined; let resultPending = snapshot.pending[job.id] === "detail"; let artifact = backgroundJobResult(detail); let progress = visibleProgress(job); let subject = backgroundJobLabel(job, detail); let reason = job.reason ? safeInterruptionReason(job.reason) : undefined; let readable = job.type === "document-summary" || job.type === "research-answer"; useEffect(() => { if (!connected || detail || job.type !== "document-summary" || job.state !== "completed") { return; } void store.detail(job.id).catch(() => {}); }, [connected, detail, job.id, job.state, job.type, store]); let toggle = () => { let immediately = motionImmediately?.() ?? false; setDisclosure(value => ({ immediately, open: detail ? !value.open : true })); if (!detail) void store.detail(job.id).catch(() => {}); }; return (
{subject} {job.state}{reason ? ` · ${reason}` : ""}
{job.state === "completed" && readable && ( )} {canEdit && canCancelJob(job) && ( )}
{progress.length > 0 && (
Progress
    {progress.map(entry => (
  1. {entry.label} {job.attempts > 1 ? `Attempt ${entry.attempt} · ` : ""} {entry.status} {entry.detail ? ` · ${entry.detail}` : ""}
  2. ))}
)} {artifact ? (

{artifact.title}

{artifact.summary}

) :

Result is unavailable.

}
{error &&

{error}

}
); } export function BackgroundWork( { store, connected, canEdit, headingId, motion, motionImmediately }: BackgroundWorkProps, ) { let snapshot = useJobs(store); let jobs = currentJobs(snapshot.jobs); return (

Background Work

{!snapshot.ready &&

Loading background work…

} {snapshot.error &&

{snapshot.error}

} {snapshot.ready && jobs.length === 0 &&

No background work yet.

} {jobs.map(job => ( ))} {snapshot.truncated &&

Only the most recent background work is shown.

}
); }