| 286 | } |
| 287 | |
| 288 | export function CommandOutputDialog({ title, logs, exitCode, running, onClose }) { |
| 289 | const outputRef = useRef(null); |
| 290 | |
| 291 | // Lock body scroll while dialog is open |
| 292 | useEffect(() => { |
| 293 | document.body.style.overflow = 'hidden'; |
| 294 | return () => { document.body.style.overflow = ''; }; |
| 295 | }, []); |
| 296 | |
| 297 | useEffect(() => { |
| 298 | if (outputRef.current) { |
| 299 | outputRef.current.scrollTop = outputRef.current.scrollHeight; |
| 300 | } |
| 301 | }, [logs?.length]); |
| 302 | |
| 303 | // Close on Escape — works whether running or not. |
| 304 | useEffect(() => { |
| 305 | const handler = (e) => { if (e.key === 'Escape') onClose(); }; |
| 306 | window.addEventListener('keydown', handler); |
| 307 | return () => window.removeEventListener('keydown', handler); |
| 308 | }, [onClose]); |
| 309 | |
| 310 | return createPortal( |
| 311 | <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={onClose}> |
| 312 | <div |
| 313 | className="bg-background border border-border rounded-lg shadow-lg w-full max-w-xl mx-4 flex flex-col max-h-[70vh]" |
| 314 | onClick={(e) => e.stopPropagation()} |
| 315 | > |
| 316 | {/* Header */} |
| 317 | <div className="flex items-center justify-between px-4 py-3 border-b border-border"> |
| 318 | <div className="flex items-center gap-2"> |
| 319 | <span className="text-sm font-semibold">{title}</span> |
| 320 | {running && ( |
| 321 | <span className="inline-block h-2 w-2 rounded-full bg-green-500 animate-pulse" /> |
| 322 | )} |
| 323 | </div> |
| 324 | <button |
| 325 | type="button" |
| 326 | onClick={onClose} |
| 327 | className="text-muted-foreground hover:text-foreground transition-colors p-0.5" |
| 328 | > |
| 329 | <XIcon size={16} /> |
| 330 | </button> |
| 331 | </div> |
| 332 | |
| 333 | {/* Body */} |
| 334 | <div ref={outputRef} className="flex-1 overflow-auto p-4 min-h-[120px] font-mono text-xs"> |
| 335 | {logs?.length > 0 ? ( |
| 336 | <CodeLogView logs={logs} /> |
| 337 | ) : running ? ( |
| 338 | <div className="flex items-center gap-2 text-sm text-muted-foreground"> |
| 339 | <SpinnerIcon size={14} className="animate-spin" /> |
| 340 | Starting... |
| 341 | </div> |
| 342 | ) : ( |
| 343 | <span className="text-sm text-muted-foreground">No output</span> |
| 344 | )} |
| 345 | </div> |