import { useEffect, useRef, useState } from 'react'; import { Button } from '@renderer/components/ui/button'; import { cn } from '@renderer/lib/utils'; import { AlertTriangle, CheckCircle2, ChevronDown, ChevronRight, Loader2, X } from 'lucide-react'; import { MarkdownViewer } from '../chat/viewers/MarkdownViewer'; import { CliLogsRichView } from './CliLogsRichView'; import { DISPLAY_STEPS } from './provisioningSteps'; import { StepProgressBar } from './StepProgressBar'; import type { StepProgressBarStep } from './StepProgressBar'; /** Pre-built step definitions for the provisioning stepper. */ const PROVISIONING_STEPS: StepProgressBarStep[] = DISPLAY_STEPS.map((s) => ({ key: s.key, label: s.label, })); export interface ProvisioningProgressBlockProps { /** Title above the steps, e.g. "Launching team" */ title: string; /** Optional status message */ message?: string | null; /** Visual severity for the message subtitle */ messageSeverity?: 'error' | 'warning'; /** Visual tone (e.g. highlight errors) */ tone?: 'default' | 'error'; /** Whether Live output is expanded by default */ defaultLiveOutputOpen?: boolean; /** Display step index (0-3 for active steps, 4 for ready/all done, -1 for terminal) */ currentStepIndex: number; /** If set, this step index shows a red error indicator */ errorStepIndex?: number; /** Show spinner next to title */ loading?: boolean; /** Cancel button label and handler */ onCancel?: (() => void) | null; /** Success message shown inside the block header (e.g. "Team launched — all N teammates online") */ successMessage?: string | null; /** Visual tone for the status banner above the block. */ successMessageSeverity?: 'success' | 'warning'; /** Dismiss handler — renders an X button in the block header top-right */ onDismiss?: (() => void) | null; /** ISO timestamp when provisioning started */ startedAt?: string; /** PID of the CLI process */ pid?: number; /** CLI logs captured during launch */ cliLogsTail?: string; /** Accumulated assistant text output for live preview */ assistantOutput?: string; className?: string; } function formatElapsed(seconds: number): string { const m = Math.floor(seconds / 60); const s = seconds % 60; return `${m}:${String(s).padStart(2, '0')}`; } function useElapsedTimer(startedAt?: string, isRunning = true): string | null { const [elapsedSeconds, setElapsedSeconds] = useState(null); useEffect(() => { if (!startedAt) { // eslint-disable-next-line react-hooks/set-state-in-effect -- intentional sync on prop change setElapsedSeconds(null); return; } const startMs = Date.parse(startedAt); if (isNaN(startMs)) { setElapsedSeconds(null); return; } const computeElapsedSeconds = (): number => Math.max(0, Math.floor((Date.now() - startMs) / 1000)); if (!isRunning) { // Freeze timer on terminal states (failed/ready/cancelled) instead of continuing to tick. setElapsedSeconds((prev) => (prev === null ? computeElapsedSeconds() : prev)); return; } const tick = (): void => { setElapsedSeconds(computeElapsedSeconds()); }; tick(); const id = window.setInterval(tick, 1000); return () => { window.clearInterval(id); }; }, [startedAt, isRunning]); if (!startedAt) return null; if (elapsedSeconds === null) return null; return formatElapsed(elapsedSeconds); } function sanitizeAssistantOutput(raw?: string, isError = false): string | null { if (!raw) return null; if (!isError) return raw; const looksLikeRawApiEnvelope = raw.includes('API Error: 400') && (raw.includes('"_requests"') || raw.includes('"session_id"') || raw.includes('"parent_tool_use_id"') || raw.includes('\\u000')); if (!looksLikeRawApiEnvelope) { return raw; } return ( 'API Error: 400\n\n' + 'Raw payload from CLI stream hidden because it contains encoded/binary-like content.\n\n' + 'Open **CLI logs** below for readable diagnostics.' ); } export const ProvisioningProgressBlock = ({ title, message, messageSeverity, tone = 'default', defaultLiveOutputOpen = true, currentStepIndex, errorStepIndex, loading = false, onCancel, successMessage, successMessageSeverity = 'success', onDismiss, startedAt, pid, cliLogsTail, assistantOutput, className, }: ProvisioningProgressBlockProps): React.JSX.Element => { const elapsed = useElapsedTimer(startedAt, loading); const [logsOpen, setLogsOpen] = useState(() => Boolean(cliLogsTail) && loading); const [liveOutputOpen, setLiveOutputOpen] = useState(defaultLiveOutputOpen); const outputScrollRef = useRef(null); const isError = tone === 'error'; const displayAssistantOutput = sanitizeAssistantOutput(assistantOutput, isError); // Auto-scroll assistant output useEffect(() => { if (liveOutputOpen && outputScrollRef.current) { outputScrollRef.current.scrollTop = outputScrollRef.current.scrollHeight; } }, [assistantOutput, liveOutputOpen]); // If parent changes the default (e.g. transitioning to "ready"), respect it. useEffect(() => { setLiveOutputOpen(defaultLiveOutputOpen); }, [defaultLiveOutputOpen]); // On error with logs available, prioritize logs view over noisy live stream payload. useEffect(() => { if (isError && cliLogsTail) { setLogsOpen(true); setLiveOutputOpen(false); } }, [isError, cliLogsTail]); // Open CLI logs while loading, collapse when done (unless error). const prevLoadingRef = useRef(loading); const hadLogsRef = useRef(Boolean(cliLogsTail)); useEffect(() => { if (!isError) { const hasLogs = Boolean(cliLogsTail); if (loading && hasLogs && !hadLogsRef.current) { // Logs just appeared while loading → open setLogsOpen(true); } else if (loading && !prevLoadingRef.current && hasLogs) { // Started loading with logs already present → open setLogsOpen(true); } else if (!loading && prevLoadingRef.current) { // Finished loading → collapse setLogsOpen(false); } hadLogsRef.current = hasLogs; } prevLoadingRef.current = loading; }, [loading, cliLogsTail, isError]); return (
{successMessage ? (
{successMessageSeverity === 'warning' ? ( ) : ( )}

{successMessage}

{onDismiss ? ( ) : null}
) : onDismiss ? (
) : null}
{loading ? ( ) : null}

{title}

{elapsed !== null ? ( {elapsed} ) : null} {pid !== undefined ? ( PID {pid} ) : null}
{onCancel ? ( ) : null}
{message ? (

{message}

) : null}
{liveOutputOpen ? (
{displayAssistantOutput ? ( ) : (

No output captured yet.

)}
) : null}
{cliLogsTail ? (
{logsOpen ? ( ) : null}
) : null}
); };