'use client'; import { useEffect, useMemo, useState } from 'react'; import { Edit3, MessageSquareText, Send, X } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { ScrollArea } from '@/components/ui/scroll-area'; import { buildEditableIssueDraft, buildIssueUpdatePayload, validateEditableIssueDraft, type EditableIssueDraft, type EditableIssueFieldErrors } from '../../lib/issue-editor'; import type { UpdateMutationPayload } from '../../lib/mutations'; import type { BeadIssue } from '../../lib/types'; import { ThreadView, type ThreadItem } from './thread-view'; interface ThreadDrawerProps { isOpen: boolean; onClose: () => void; title: string; id: string; items?: ThreadItem[]; embedded?: boolean; issue?: BeadIssue | null; projectRoot?: string; onIssueUpdated?: (issueId: string) => Promise | void; } const SAMPLE_ITEMS: ThreadItem[] = [ { id: '1', type: 'comment', author: 'sarah.lee', content: 'Pushed a first pass for the left rail hierarchy. Need readability check on status chips.', timestamp: new Date(Date.now() - 6 * 60 * 1000), }, { id: '2', type: 'status_change', from: 'open', to: 'in_progress', timestamp: new Date(Date.now() - 31 * 60 * 1000), }, { id: '3', type: 'protocol_event', event: 'HANDOFF', content: 'Swarm integrator picked up follow-up work.', timestamp: new Date(Date.now() - 55 * 60 * 1000), }, ]; const STATUS_OPTIONS: EditableIssueDraft['status'][] = ['open', 'in_progress', 'blocked', 'deferred', 'closed']; const PRIORITY_OPTIONS = [0, 1, 2, 3, 4] as const; async function postIssueUpdate(body: UpdateMutationPayload): Promise { const response = await fetch('/api/beads/update', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), }); const payload = (await response.json()) as { ok: boolean; error?: { message?: string } }; if (!response.ok || !payload.ok) { throw new Error(payload.error?.message ?? 'Update failed'); } } function saveStateTone(state: 'ready' | 'saving' | 'saved' | 'error'): string { if (state === 'saving') return 'border-[#5BA8A0]/50 bg-[#5BA8A0]/20 text-[#D6EEEA]'; if (state === 'saved') return 'border-[#7CB97A]/50 bg-[#7CB97A]/20 text-[#D4ECD2]'; if (state === 'error') return 'border-[#E24A3A]/50 bg-[#E24A3A]/20 text-[#F3C2BC]'; return 'border-white/10 bg-white/5 text-[#B8B8B8]'; } export function ThreadDrawer({ isOpen, onClose, title, id, items = SAMPLE_ITEMS, embedded = false, issue, projectRoot, onIssueUpdated, }: ThreadDrawerProps) { const [comment, setComment] = useState(''); const [editMode, setEditMode] = useState(false); const [draft, setDraft] = useState(issue ? buildEditableIssueDraft(issue) : null); const [fieldErrors, setFieldErrors] = useState({}); const [saveError, setSaveError] = useState(null); const [saveState, setSaveState] = useState<'ready' | 'saving' | 'saved' | 'error'>('ready'); useEffect(() => { if (!issue) { setDraft(null); setEditMode(false); setFieldErrors({}); setSaveError(null); setSaveState('ready'); return; } setDraft(buildEditableIssueDraft(issue)); setEditMode(false); setFieldErrors({}); setSaveError(null); setSaveState('ready'); }, [issue]); const canEdit = Boolean(issue && projectRoot && draft); const participants = useMemo(() => { const names = new Set(); for (const item of items) { if (item.author && item.author.trim()) { names.add(item.author.trim()); } } return Array.from(names).slice(0, 4); }, [items]); const handleSave = async () => { if (!issue || !projectRoot || !draft) { return; } const validation = validateEditableIssueDraft(draft); if (!validation.ok) { setFieldErrors(validation.errors); setSaveState('error'); return; } const payload = buildIssueUpdatePayload(issue, draft, projectRoot); if (!payload) { setEditMode(false); setSaveState('saved'); setTimeout(() => setSaveState('ready'), 900); return; } setSaveState('saving'); setSaveError(null); setFieldErrors({}); try { await postIssueUpdate(payload); await onIssueUpdated?.(issue.id); setEditMode(false); setSaveState('saved'); setTimeout(() => setSaveState('ready'), 900); } catch (error) { setSaveError(error instanceof Error ? error.message : 'Save failed'); setSaveState('error'); } }; if (!isOpen) { return null; } return (

Open Thread

{title}

{id} ยท {items.length} events

Conversation
{participants.map((name) => ( {name} ))}

Task summary

{saveState}
{!issue ? (

No task details available for this thread context.

) : !editMode ? (

{issue.title}

{issue.description ?? 'No description provided.'}

{issue.status} P{issue.priority} {issue.issue_type} {issue.assignee ? @{issue.assignee} : null}
) : (
{fieldErrors.title ?

{fieldErrors.title}

: null}