import { Check, Loader2, Pause, Play, Square, X } from 'lucide-react'; import React, { useEffect, useEffectEvent, useMemo, useState } from 'react'; import { useI18n } from '@/application/i18n/I18nProvider'; import type { ScriptRun } from '@/types/global/netcatty-bridge-script.d.ts'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils.ts'; export interface ScriptExecutionOverlayProps { run: ScriptRun; onPause: () => void; onResume: () => void; onStop: () => void; onDismiss: () => void; /** * Host info bar is hidden: no full toolbar. Sit the banner higher and stack * above the compact speed-dial (cover it for the run duration). */ compactTopChrome?: boolean; } /** Default top offset under the full host-info toolbar. */ export const SCRIPT_OVERLAY_TOP_DEFAULT_PX = 34; /** Top offset when only the compact speed-dial is present. */ export const SCRIPT_OVERLAY_TOP_COMPACT_PX = 8; /** Completed script results remain visible briefly before dismissing themselves. */ export const SCRIPT_OVERLAY_FINISHED_DISMISS_DELAY_MS = 5_000; function formatElapsed(ms: number) { const seconds = Math.max(0, Math.floor(ms / 1000)); const minutes = Math.floor(seconds / 60); const rest = seconds % 60; if (minutes > 0) { return `${minutes}:${String(rest).padStart(2, '0')}`; } return `${rest}s`; } function resolveWaitingPattern( run: ScriptRun, t: (key: string, params?: Record) => string, ) { if (!run.waitingFor) return undefined; if (run.waitingFor === 'shell prompt' || run.waitingFor.includes(' | ')) { return t('scripts.running.waitingForShellPrompt'); } return run.waitingFor; } function isLowValueActivityLabel(label?: string) { if (!label) return true; const normalized = label.trim().toLowerCase(); return normalized === 'log' || normalized.startsWith('sleep '); } function DotSeparator() { return ·; } function Muted({ children }: { children: React.ReactNode }) { return {children}; } function Accent({ children, className }: { children: React.ReactNode; className?: string }) { return ( {children} ); } function ScriptStatusIcon({ status }: { status: ScriptRun['status'] }) { const iconClass = 'block'; const boxClass = 'inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center'; if (status === 'completed') { return ( ); } if (status === 'failed') { return ( ); } return ( ); } function ScriptStatusLine({ run, elapsedMs, lastSent, t, }: { run: ScriptRun; elapsedMs: number; lastSent?: string; t: (key: string, params?: Record) => string; }) { const label = run.scriptLabel || t('scripts.running.unnamed'); const opCount = run.stepIndex ?? 0; const elapsed = formatElapsed(elapsedMs); const waitingPattern = resolveWaitingPattern(run, t); const isFinished = run.status === 'completed' || run.status === 'failed'; const opsSegment = ( <> {t('scripts.running.opsPrefix')} {opCount} {t('scripts.running.opsSuffix')} ); const elapsedSegment = {elapsed}; const activitySegment = !isLowValueActivityLabel(run.activityLabel) ? ( <> {run.activityLabel} ) : null; const progressSegment = run.progressMode === 'determinate' && run.progressTotal ? ( <> {run.progressLabel || t('scripts.running.progressFallback')} {' '} {run.progressCurrent ?? 0} / {run.progressTotal} ) : null; const pausedSegment = run.status === 'paused' ? ( <> {t('scripts.running.status.paused')} ) : null; const waitingSegment = waitingPattern ? ( <> {t('scripts.running.waitingForLabel')} {' '} {waitingPattern} ) : null; const lastSentSegment = !waitingPattern && lastSent ? ( <> {t('scripts.running.lastSentLabel')} {' '} {lastSent} ) : null; return ( {label} {(isFinished || opCount > 0) ? ( <> {opsSegment} ) : null} {elapsedSegment} {!isFinished ? ( <> {progressSegment} {activitySegment} {pausedSegment} {waitingSegment} {lastSentSegment} ) : null} ); } export const ScriptExecutionOverlay: React.FC = ({ run, onPause, onResume, onStop, onDismiss, compactTopChrome = false, }) => { const { t } = useI18n(); const [tick, setTick] = useState(0); const isFinished = run.status === 'completed' || run.status === 'failed'; const dismissFinishedRun = useEffectEvent(onDismiss); useEffect(() => { if (isFinished) return undefined; const timer = window.setInterval(() => setTick((value) => value + 1), 1000); return () => window.clearInterval(timer); }, [isFinished, run.runId]); useEffect(() => { if (!isFinished) return undefined; const timer = setTimeout(dismissFinishedRun, SCRIPT_OVERLAY_FINISHED_DISMISS_DELAY_MS); return () => clearTimeout(timer); }, [isFinished, run.runId]); void tick; const elapsedMs = run.elapsedMs ?? (run.endedAt ? run.endedAt - run.startedAt : Date.now() - run.startedAt); const lastSent = [...(run.logs || [])].reverse().find((entry) => entry.message.startsWith('→ '))?.message.slice(2); const errorMessage = run.status === 'failed' ? run.error : undefined; const statusLine = useMemo( () => ( ), [run, elapsedMs, lastSent, t], ); return (
{statusLine}
{errorMessage ? (
{errorMessage}
) : null}
{isFinished ? ( ) : ( <> {run.status === 'running' ? ( ) : null} {run.status === 'paused' ? ( ) : null} )}
); };