diff --git a/components/ConfirmDialog.tsx b/components/ConfirmDialog.tsx new file mode 100644 index 0000000..563e6e7 --- /dev/null +++ b/components/ConfirmDialog.tsx @@ -0,0 +1,62 @@ +'use client' + +type Props = { + open: boolean + title?: string + commandPreview: string + onConfirm: () => void + onCancel: () => void + loading?: boolean +} + +export default function ConfirmDialog({ + open, + title = 'Confirm action', + commandPreview, + onConfirm, + onCancel, + loading = false, +}: Props) { + if (!open) return null + + return ( +
+ + ) +} diff --git a/components/StreamingTerminal.tsx b/components/StreamingTerminal.tsx new file mode 100644 index 0000000..856806e --- /dev/null +++ b/components/StreamingTerminal.tsx @@ -0,0 +1,89 @@ +'use client' + +import { useEffect, useRef } from 'react' + +export type TerminalLine = { + type: 'stdout' | 'stderr' + text: string +} + +export type TerminalStatus = 'idle' | 'running' | 'done' | 'failed' | 'error' + +type Props = { + lines: TerminalLine[] + status: TerminalStatus + error?: string | null + className?: string +} + +export default function StreamingTerminal({ lines, status, error, className = '' }: Props) { + const bottomRef = useRef(null) + + useEffect(() => { + bottomRef.current?.scrollIntoView({ behavior: 'smooth' }) + }, [lines]) + + const statusBar = () => { + if (status === 'running') { + return ( +
+ + Running… +
+ ) + } + if (status === 'done') { + return ( +
+ + Completed successfully +
+ ) + } + if (status === 'failed') { + return ( +
+ + Exited with error +
+ ) + } + if (status === 'error') { + return ( +
+ + {error ?? 'Unknown error'} +
+ ) + } + return null + } + + return ( +
+
+ {lines.length === 0 && status === 'running' && ( + Waiting for output… + )} + {lines.map((line, i) => ( +
+            {line.text}
+          
+ ))} +
+
+ {statusBar()} +
+ ) +}