import { useState, useEffect, useRef, useCallback } from 'react' import { Copy, Check, SkipForward, Terminal, ChevronDown, ChevronUp, Send, Clipboard, Loader2, X, MessageCircleQuestion, Wrench, Eye, } from 'lucide-react' import { cn } from '@/lib/utils' import { toast } from '@/lib/toast' import type { ActionItem, QuestionItem } from '@/types/ai-session' // ── Types ── type TaskState = 'pending' | 'active' | 'done' | 'skipped' interface QuestionResponse { type: 'question' text: string context?: string state: TaskState value: string } interface ActionResponse { type: 'action' label: string command?: string | null description: string state: TaskState value: string } type TaskResponse = QuestionResponse | ActionResponse interface TaskLaneProps { questions: QuestionItem[] actions: ActionItem[] onSubmit: (responses: TaskResponse[]) => void onClose: () => void loading?: boolean } // ── Component ── export function TaskLane({ questions, actions, onSubmit, onClose, loading }: TaskLaneProps) { const [tasks, setTasks] = useState(() => [ ...questions.map((q): QuestionResponse => ({ type: 'question', text: q.text, context: q.context, state: 'pending', value: '', })), ...actions.map((a): ActionResponse => ({ type: 'action', label: a.label, command: a.command, description: a.description, state: 'pending', value: '', })), ]) const [submitting, setSubmitting] = useState(false) const [submitted, setSubmitted] = useState(false) const [showRunAll, setShowRunAll] = useState(false) const [showPreview, setShowPreview] = useState(false) // ── Resize state ── const DEFAULT_WIDTH = 340 const MIN_WIDTH = 280 const MAX_WIDTH_RATIO = 0.5 // 50vw const [panelWidth, setPanelWidth] = useState(() => { const stored = localStorage.getItem('rf-tasklane-width') return stored ? Math.max(MIN_WIDTH, parseInt(stored, 10) || DEFAULT_WIDTH) : DEFAULT_WIDTH }) const isDragging = useRef(false) const startX = useRef(0) const startWidth = useRef(0) const handleMouseMove = useCallback((e: MouseEvent) => { if (!isDragging.current) return const maxWidth = window.innerWidth * MAX_WIDTH_RATIO const deltaX = startX.current - e.clientX const newWidth = Math.min(maxWidth, Math.max(MIN_WIDTH, startWidth.current + deltaX)) setPanelWidth(newWidth) }, []) const handleMouseUp = useCallback(() => { if (!isDragging.current) return isDragging.current = false document.body.style.cursor = '' document.body.style.userSelect = '' localStorage.setItem('rf-tasklane-width', String(Math.round(panelWidth))) }, [panelWidth]) const handleMouseDown = useCallback((e: React.MouseEvent) => { e.preventDefault() isDragging.current = true startX.current = e.clientX startWidth.current = panelWidth document.body.style.cursor = 'col-resize' document.body.style.userSelect = 'none' }, [panelWidth]) useEffect(() => { document.addEventListener('mousemove', handleMouseMove) document.addEventListener('mouseup', handleMouseUp) return () => { document.removeEventListener('mousemove', handleMouseMove) document.removeEventListener('mouseup', handleMouseUp) } }, [handleMouseMove, handleMouseUp]) // Reset when new tasks come in useEffect(() => { setTasks([ ...questions.map((q): QuestionResponse => ({ type: 'question', text: q.text, context: q.context, state: 'pending', value: '', })), ...actions.map((a): ActionResponse => ({ type: 'action', label: a.label, command: a.command, description: a.description, state: 'pending', value: '', })), ]) setSubmitted(false) }, [questions, actions]) const updateTask = (idx: number, updates: Partial) => { setTasks(prev => prev.map((t, i) => i === idx ? { ...t, ...updates } as TaskResponse : t)) } const questionTasks = tasks.filter(t => t.type === 'question') const actionTasks = tasks.filter(t => t.type === 'action') as ActionResponse[] const allHandled = tasks.every(t => t.state === 'done' || t.state === 'skipped') const anyHandled = tasks.some(t => t.state === 'done' || t.state === 'skipped') const handledCount = tasks.filter(t => t.state === 'done' || t.state === 'skipped').length const doneCount = tasks.filter(t => t.state === 'done').length const totalCount = tasks.length const commandActions = actionTasks.filter(a => a.command) const combinedScript = commandActions.map((a, i) => ( `# ── ${i + 1}. ${a.label} ──\n${a.command}` )).join('\n\n') const handleCopy = (text: string) => { navigator.clipboard.writeText(text) toast.success('Copied to clipboard') } const buildPreviewText = (): string => { const parts: string[] = [] for (const t of tasks) { if (t.type === 'question') { const q = t as QuestionResponse const name = `Q: ${q.text}` if (q.state === 'done' && q.value.trim()) { parts.push(`**${name}:**\n\`\`\`\n${q.value.trim()}\n\`\`\``) } else if (q.state === 'skipped') { parts.push(`**${name}:** _(skipped)_`) } } else { const a = t as ActionResponse const name = a.label || 'Check' if (a.state === 'done' && a.value.trim()) { parts.push(`**${name}:**\n\`\`\`\n${a.value.trim()}\n\`\`\``) } else if (a.state === 'skipped') { parts.push(`**${name}:** _(skipped)_`) } } } return parts.join('\n\n') || '(No responses yet)' } const handleSubmit = () => { setSubmitting(true) onSubmit(tasks) setSubmitted(true) setSubmitting(false) } if (submitted) return null return (
{/* Resize grip handle */}
{/* Header */}

Tasks {allHandled ? '✓ Ready' : `${doneCount}/${totalCount}`}

{/* Body */}
{/* ── Questions Section ── */} {questionTasks.length > 0 && (
Questions {questionTasks.every(q => q.state === 'done' || q.state === 'skipped') && ( )}
{tasks.map((task, idx) => { if (task.type !== 'question') return null const q = task as QuestionResponse if (q.state === 'done') { return (
updateTask(idx, { state: 'active' })}>
{q.text}
"{q.value}"
) } if (q.state === 'skipped') { return (
{q.text}
Skipped
) } return (
{q.text}
{q.context && (
{q.context}
)} {q.state === 'active' ? (