/ src / components / tasks / task-sheet.tsx
task-sheet.tsx
   1  'use client'
   2  
   3  import { useEffect, useMemo, useRef, useState } from 'react'
   4  import { useRouter } from 'next/navigation'
   5  import ReactMarkdown from 'react-markdown'
   6  import remarkGfm from 'remark-gfm'
   7  import { useAppStore } from '@/stores/use-app-store'
   8  import { useAgentsQuery } from '@/features/agents/queries'
   9  import {
  10    useAppendTaskCommentMutation,
  11    useCreateTaskMutation,
  12    useTasksQuery,
  13    useUpdateTaskMutation,
  14  } from '@/features/tasks/queries'
  15  import { useProjectsQuery } from '@/features/projects/queries'
  16  import { useAppSettingsQuery } from '@/features/settings/queries'
  17  import { useProtocolRunsQuery } from '@/features/protocols/queries'
  18  import { BottomSheet } from '@/components/shared/bottom-sheet'
  19  import { AgentPickerList } from '@/components/shared/agent-picker-list'
  20  import { DirBrowser } from '@/components/shared/dir-browser'
  21  import { SheetFooter } from '@/components/shared/sheet-footer'
  22  import { inputClass } from '@/components/shared/form-styles'
  23  import { StructuredSessionLauncher } from '@/components/protocols/structured-session-launcher'
  24  import type { BoardTask, TaskComment, TaskQualityGateConfig } from '@/types'
  25  import { dedup, errorMessage } from '@/lib/shared-utils'
  26  import { SectionLabel } from '@/components/shared/section-label'
  27  import { AgentAvatar } from '@/components/agents/agent-avatar'
  28  
  29  function fmtTime(ts: number) {
  30    const d = new Date(ts)
  31    const now = new Date()
  32    const isToday = d.toDateString() === now.toDateString()
  33    if (isToday) return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
  34    return d.toLocaleDateString([], { month: 'short', day: 'numeric' }) + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
  35  }
  36  
  37  function normalizeGateNumber(value: unknown, fallback: number, min: number, max: number): number {
  38    const parsed = typeof value === 'number'
  39      ? value
  40      : typeof value === 'string'
  41        ? Number.parseInt(value, 10)
  42        : Number.NaN
  43    if (!Number.isFinite(parsed)) return fallback
  44    return Math.max(min, Math.min(max, Math.trunc(parsed)))
  45  }
  46  
  47  export function TaskSheet() {
  48    const router = useRouter()
  49    const open = useAppStore((s) => s.taskSheetOpen)
  50    const setOpen = useAppStore((s) => s.setTaskSheetOpen)
  51    const editingId = useAppStore((s) => s.editingTaskId)
  52    const setEditingId = useAppStore((s) => s.setEditingTaskId)
  53    const activeProjectFilter = useAppStore((s) => s.activeProjectFilter)
  54  
  55    const viewOnly = useAppStore((s) => s.taskSheetViewOnly)
  56    const setViewOnly = useAppStore((s) => s.setTaskSheetViewOnly)
  57    const { data: tasks = {}, isLoading: tasksLoading } = useTasksQuery({ includeArchived: true, enabled: open })
  58    const { data: agents = {}, isLoading: agentsLoading } = useAgentsQuery({ enabled: open })
  59    const { data: projects = {}, isLoading: projectsLoading } = useProjectsQuery({ enabled: open })
  60    const { data: appSettings = {}, isLoading: settingsLoading } = useAppSettingsQuery({ enabled: open })
  61    const createTaskMutation = useCreateTaskMutation()
  62    const updateTaskMutation = useUpdateTaskMutation()
  63    const appendCommentMutation = useAppendTaskCommentMutation()
  64  
  65    const [title, setTitle] = useState('')
  66    const [description, setDescription] = useState('')
  67    const [agentId, setAgentId] = useState('')
  68    const [commentText, setCommentText] = useState('')
  69    const [images, setImages] = useState<string[]>([])
  70    const [uploading, setUploading] = useState(false)
  71    const [cwd, setCwd] = useState('')
  72    const [file, setFile] = useState<string | null>(null)
  73    const [projectId, setProjectId] = useState('')
  74    const [tags, setTags] = useState<string[]>([])
  75    const [tagInput, setTagInput] = useState('')
  76    const [blockedBy, setBlockedBy] = useState<string[]>([])
  77    const [depSearch, setDepSearch] = useState('')
  78    const [depError, setDepError] = useState<string | null>(null)
  79    const [dueAt, setDueAt] = useState<string>('')
  80    const [customFields, setCustomFields] = useState<Record<string, string | number | boolean>>({})
  81    const [priority, setPriority] = useState<'low' | 'medium' | 'high' | 'critical' | ''>('')
  82    const [qualityGateEnabled, setQualityGateEnabled] = useState(true)
  83    const [qualityGateMinResultChars, setQualityGateMinResultChars] = useState(80)
  84    const [qualityGateMinEvidenceItems, setQualityGateMinEvidenceItems] = useState(2)
  85    const [qualityGateRequireVerification, setQualityGateRequireVerification] = useState(false)
  86    const [qualityGateRequireArtifact, setQualityGateRequireArtifact] = useState(false)
  87    const [qualityGateRequireReport, setQualityGateRequireReport] = useState(false)
  88    const [structuredSessionOpen, setStructuredSessionOpen] = useState(false)
  89    const formInitRef = useRef<string | null>(null)
  90  
  91    const editing = editingId ? tasks[editingId] : null
  92    const agentList = useMemo(
  93      () => Object.values(agents).sort((a, b) => a.name.localeCompare(b.name)),
  94      [agents],
  95    )
  96    const { data: linkedProtocolRuns = [] } = useProtocolRunsQuery({
  97      taskId: editing?.id || null,
  98      limit: 6,
  99      enabled: open && !!editing?.id,
 100    })
 101    const activeStructuredRunId =
 102      linkedProtocolRuns.find((run) => !['completed', 'failed', 'cancelled', 'archived'].includes(run.status))?.id || null
 103  
 104    useEffect(() => {
 105      if (!open) return
 106      if (tasksLoading || agentsLoading || projectsLoading || settingsLoading) return
 107  
 108      const initKey = editingId ?? '__new__'
 109      if (formInitRef.current === initKey) return
 110  
 111      const defaultGateEnabled = appSettings.taskQualityGateEnabled ?? true
 112      const defaultGateMinResult = normalizeGateNumber(appSettings.taskQualityGateMinResultChars, 80, 10, 2000)
 113      const defaultGateMinEvidence = normalizeGateNumber(appSettings.taskQualityGateMinEvidenceItems, 2, 0, 8)
 114      const defaultGateRequireVerification = appSettings.taskQualityGateRequireVerification ?? false
 115      const defaultGateRequireArtifact = appSettings.taskQualityGateRequireArtifact ?? false
 116      const defaultGateRequireReport = appSettings.taskQualityGateRequireReport ?? false
 117  
 118      if (editingId && !editing) return
 119  
 120      if (editing) {
 121        setTitle(editing.title)
 122        setDescription(editing.description)
 123        setAgentId(editing.agentId)
 124        setProjectId(editing.projectId || '')
 125        setImages(editing.images || [])
 126        setCwd(editing.cwd || '')
 127        setFile(editing.file || null)
 128        setTags(editing.tags || [])
 129        setBlockedBy(editing.blockedBy || [])
 130        setDepSearch('')
 131        setDepError(null)
 132        setDueAt(editing.dueAt ? new Date(editing.dueAt).toISOString().slice(0, 10) : '')
 133        setCustomFields(editing.customFields || {})
 134        setPriority(editing.priority || '')
 135        const gate = (editing.qualityGate || null) as TaskQualityGateConfig | null
 136        setQualityGateEnabled(gate?.enabled ?? defaultGateEnabled)
 137        setQualityGateMinResultChars(normalizeGateNumber(gate?.minResultChars, defaultGateMinResult, 10, 2000))
 138        setQualityGateMinEvidenceItems(normalizeGateNumber(gate?.minEvidenceItems, defaultGateMinEvidence, 0, 8))
 139        setQualityGateRequireVerification(gate?.requireVerification ?? defaultGateRequireVerification)
 140        setQualityGateRequireArtifact(gate?.requireArtifact ?? defaultGateRequireArtifact)
 141        setQualityGateRequireReport(gate?.requireReport ?? defaultGateRequireReport)
 142        formInitRef.current = initKey
 143        return
 144      }
 145  
 146      setTitle('')
 147      setDescription('')
 148      setAgentId(agentList[0]?.id || '')
 149      setProjectId(activeProjectFilter || '')
 150      setImages([])
 151      setCwd('')
 152      setFile(null)
 153      setTags([])
 154      setBlockedBy([])
 155      setDepSearch('')
 156      setDepError(null)
 157      setDueAt('')
 158      setCustomFields({})
 159      setPriority('')
 160      setQualityGateEnabled(defaultGateEnabled)
 161      setQualityGateMinResultChars(defaultGateMinResult)
 162      setQualityGateMinEvidenceItems(defaultGateMinEvidence)
 163      setQualityGateRequireVerification(defaultGateRequireVerification)
 164      setQualityGateRequireArtifact(defaultGateRequireArtifact)
 165      setQualityGateRequireReport(defaultGateRequireReport)
 166      formInitRef.current = initKey
 167    }, [
 168      activeProjectFilter,
 169      agentList,
 170      agentsLoading,
 171      appSettings,
 172      editing,
 173      editingId,
 174      open,
 175      projectsLoading,
 176      settingsLoading,
 177      tasksLoading,
 178    ])
 179  
 180    // Update default agent when agents load (only if no agent selected yet)
 181    useEffect(() => {
 182      if (open && !editing && !agentId && agentList.length) {
 183        setAgentId(agentList[0].id)
 184      }
 185    }, [open, editing, agentId, agentList])
 186  
 187    const onClose = () => {
 188      formInitRef.current = null
 189      setDepError(null)
 190      setOpen(false)
 191      setEditingId(null)
 192    }
 193  
 194    const handleSave = async () => {
 195      const qualityGate: TaskQualityGateConfig | null = qualityGateEnabled
 196        ? {
 197            enabled: true,
 198            minResultChars: qualityGateMinResultChars,
 199            minEvidenceItems: qualityGateMinEvidenceItems,
 200            requireVerification: qualityGateRequireVerification,
 201            requireArtifact: qualityGateRequireArtifact,
 202            requireReport: qualityGateRequireReport,
 203          }
 204        : null
 205  
 206      // projectId uses null (not undefined) so the API can distinguish "clear" from "not sent"
 207      // projectId uses null (not undefined) so the API can distinguish "clear" from "not sent"
 208      const payload = {
 209        title: title.trim() || 'Untitled Task', description, agentId, projectId: projectId || null, images,
 210        cwd: cwd || undefined, file: file || undefined,
 211        tags, blockedBy, dueAt: dueAt ? new Date(dueAt).getTime() : null,
 212        customFields: Object.keys(customFields).length > 0 ? customFields : undefined,
 213        priority: priority || undefined,
 214        qualityGate,
 215      } as Partial<BoardTask> & { title: string; description: string; agentId: string }
 216      try {
 217        if (editing) {
 218          const res = await updateTaskMutation.mutateAsync({ id: editing.id, patch: payload })
 219          const errMsg = res && typeof res === 'object' ? (res as unknown as Record<string, unknown>).error : undefined
 220          if (typeof errMsg === 'string' && errMsg.trim()) {
 221            setDepError(errMsg)
 222            return
 223          }
 224        } else {
 225          const res = await createTaskMutation.mutateAsync(payload)
 226          const errMsg = res && typeof res === 'object' ? (res as unknown as Record<string, unknown>).error : undefined
 227          if (typeof errMsg === 'string' && errMsg.trim()) {
 228            setDepError(errMsg)
 229            return
 230          }
 231        }
 232      } catch (err: unknown) {
 233        setDepError(errorMessage(err))
 234        return
 235      }
 236      setDepError(null)
 237      onClose()
 238    }
 239  
 240    const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
 241      const file = e.target.files?.[0]
 242      if (!file) return
 243      setUploading(true)
 244      try {
 245        const res = await fetch('/api/upload', {
 246          method: 'POST',
 247          headers: { 'x-filename': file.name },
 248          body: await file.arrayBuffer(),
 249        })
 250        const data = await res.json()
 251        if (data.url) setImages((prev) => [...prev, data.url])
 252      } catch (err: unknown) {
 253        console.error('Image upload failed:', errorMessage(err))
 254      }
 255      setUploading(false)
 256      e.target.value = ''
 257    }
 258  
 259    const handleArchive = async () => {
 260      if (editing) {
 261        await updateTaskMutation.mutateAsync({ id: editing.id, patch: { status: 'archived' } })
 262        onClose()
 263      }
 264    }
 265  
 266    const handleUnarchive = async () => {
 267      if (editing) {
 268        await updateTaskMutation.mutateAsync({ id: editing.id, patch: { status: 'backlog' } })
 269        onClose()
 270      }
 271    }
 272  
 273    const handleQueue = async () => {
 274      if (editing && editing.status === 'backlog') {
 275        await updateTaskMutation.mutateAsync({ id: editing.id, patch: { status: 'queued' } })
 276        onClose()
 277      }
 278    }
 279  
 280    const handleAddComment = async () => {
 281      if (!editing || !commentText.trim()) return
 282      const c: TaskComment = {
 283        id: Math.random().toString(36).slice(2, 10),
 284        author: 'You',
 285        text: commentText.trim(),
 286        createdAt: Date.now(),
 287      }
 288      // Use atomic append to avoid race conditions with queue-added comments
 289      await appendCommentMutation.mutateAsync({ id: editing.id, comment: c })
 290      setCommentText('')
 291    }
 292  
 293    const PRIORITY_STYLES: Record<string, string> = {
 294      low: 'bg-sky-500/10 border-sky-500/20 text-sky-400',
 295      medium: 'bg-amber-500/10 border-amber-500/20 text-amber-400',
 296      high: 'bg-orange-500/10 border-orange-500/20 text-orange-400',
 297      critical: 'bg-red-500/10 border-red-500/20 text-red-400',
 298    }
 299    const STATUS_STYLES: Record<string, string> = {
 300      backlog: 'bg-white/[0.06] text-text-3',
 301      queued: 'bg-amber-500/10 text-amber-400',
 302      'in-progress': 'bg-sky-500/10 text-sky-400',
 303      completed: 'bg-emerald-500/10 text-emerald-400',
 304      failed: 'bg-red-500/10 text-red-400',
 305      archived: 'bg-white/[0.04] text-text-3/60',
 306    }
 307  
 308    const taskAgent = editing ? agents[editing.agentId] : null
 309    const taskProject = editing?.projectId ? projects[editing.projectId] : null
 310  
 311    /* ───── View-only mode ───── */
 312    if (viewOnly && editing) {
 313      return (
 314        <BottomSheet open={open} onClose={onClose}>
 315          {/* Header: title + badges + timestamps */}
 316          <div className="mb-8">
 317            <h2 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-3">
 318              {editing.title}
 319            </h2>
 320            <div className="flex flex-wrap items-center gap-2 mb-3">
 321              <span className={`px-2.5 py-1 rounded-[8px] text-[12px] font-600 border border-transparent ${STATUS_STYLES[editing.status] || 'bg-white/[0.06] text-text-3'}`}>
 322                {editing.status}
 323              </span>
 324              {editing.priority && (
 325                <span className={`px-2.5 py-1 rounded-[8px] text-[12px] font-600 border ${PRIORITY_STYLES[editing.priority] || ''}`}>
 326                  {editing.priority}
 327                </span>
 328              )}
 329            </div>
 330            <div className="flex flex-wrap gap-x-4 gap-y-1 text-[12px] text-text-3">
 331              <span>Created {fmtTime(editing.createdAt)}</span>
 332              {editing.startedAt && <span>Started {fmtTime(editing.startedAt)}</span>}
 333              {editing.completedAt && <span>Completed {fmtTime(editing.completedAt)}</span>}
 334            </div>
 335          </div>
 336  
 337          {/* Description */}
 338          {editing.description && (
 339            <div className="mb-8">
 340              <SectionLabel>Description</SectionLabel>
 341              <div className="msg-content text-[14px] leading-[1.7] text-text-2 break-words p-4 rounded-[14px] border border-white/[0.06] bg-surface">
 342                <ReactMarkdown remarkPlugins={[remarkGfm]}>{editing.description}</ReactMarkdown>
 343              </div>
 344            </div>
 345          )}
 346  
 347          {editing.objective && (
 348            <div className="mb-8">
 349              <SectionLabel>Objective</SectionLabel>
 350              <div className="rounded-[14px] border border-white/[0.06] bg-surface px-4 py-3">
 351                <div className="text-[14px] font-600 text-text">{editing.objective}</div>
 352              </div>
 353            </div>
 354          )}
 355  
 356          {/* Agent */}
 357          {taskAgent && (
 358            <div className="mb-8">
 359              <SectionLabel>Agent</SectionLabel>
 360              <div className="flex items-center gap-2.5 px-4 py-3 rounded-[14px] border border-white/[0.06] bg-surface">
 361                <AgentAvatar seed={taskAgent.avatarSeed || null} avatarUrl={taskAgent.avatarUrl} name={taskAgent.name} size={24} />
 362                <span className="text-[14px] font-600 text-text">{taskAgent.name}</span>
 363              </div>
 364            </div>
 365          )}
 366  
 367          {/* Project */}
 368          {taskProject && (
 369            <div className="mb-8">
 370              <SectionLabel>Project</SectionLabel>
 371              <span className="inline-flex items-center gap-2 px-3 py-2 rounded-[10px] border border-white/[0.06] bg-surface text-[13px] font-600 text-text-2">
 372                <span className="w-2.5 h-2.5 rounded-full shrink-0" style={{ backgroundColor: taskProject.color || '#6366F1' }} />
 373                {taskProject.name}
 374              </span>
 375            </div>
 376          )}
 377  
 378          {/* Directory / File */}
 379          {(editing.cwd || editing.file) && (
 380            <div className="mb-8">
 381              <SectionLabel>{editing.file ? 'File' : 'Directory'}</SectionLabel>
 382              <code className="block px-4 py-3 rounded-[14px] border border-white/[0.06] bg-surface text-[13px] text-text-2 font-mono break-all">
 383                {editing.file || editing.cwd}
 384              </code>
 385            </div>
 386          )}
 387  
 388          {/* Tags */}
 389          {editing.tags && editing.tags.length > 0 && (
 390            <div className="mb-8">
 391              <SectionLabel>Tags</SectionLabel>
 392              <div className="flex flex-wrap gap-1.5">
 393                {editing.tags.map((tag) => (
 394                  <span key={tag} className="px-2.5 py-1 rounded-[8px] bg-indigo-500/10 text-indigo-400 text-[12px] font-600">
 395                    {tag}
 396                  </span>
 397                ))}
 398              </div>
 399            </div>
 400          )}
 401  
 402          {/* Blocked By */}
 403          {editing.blockedBy && editing.blockedBy.length > 0 && (
 404            <div className="mb-8">
 405              <SectionLabel>Blocked By</SectionLabel>
 406              <div className="flex flex-wrap gap-1.5">
 407                {editing.blockedBy.map((bid) => {
 408                  const bt = tasks[bid]
 409                  return (
 410                    <span key={bid} className="px-2.5 py-1 rounded-[8px] bg-white/[0.04] text-text-3 text-[12px] font-600">
 411                      {bt ? bt.title : bid}
 412                    </span>
 413                  )
 414                })}
 415              </div>
 416            </div>
 417          )}
 418  
 419          {/* Blocks */}
 420          {editing.blocks && editing.blocks.length > 0 && (
 421            <div className="mb-8">
 422              <SectionLabel>Blocks</SectionLabel>
 423              <div className="flex flex-wrap gap-1.5">
 424                {editing.blocks.map((bid) => {
 425                  const bt = tasks[bid]
 426                  return bt ? (
 427                    <span key={bid} className="px-2.5 py-1 rounded-[8px] bg-white/[0.04] text-text-3 text-[12px] font-600">{bt.title}</span>
 428                  ) : null
 429                })}
 430              </div>
 431            </div>
 432          )}
 433  
 434          {/* Due Date */}
 435          {editing.dueAt && (
 436            <div className="mb-8">
 437              <SectionLabel>Due Date</SectionLabel>
 438              <span className="text-[14px] text-text-2">{new Date(editing.dueAt).toLocaleDateString([], { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' })}</span>
 439            </div>
 440          )}
 441  
 442          {/* Custom Fields */}
 443          {editing.customFields && Object.keys(editing.customFields).length > 0 && (
 444            <div className="mb-8">
 445              <SectionLabel>Custom Fields</SectionLabel>
 446              <div className="space-y-2">
 447                {Object.entries(editing.customFields).map(([key, val]) => {
 448                  const def = appSettings.taskCustomFieldDefs?.find((d) => d.key === key)
 449                  return (
 450                    <div key={key} className="flex items-baseline gap-2">
 451                      <span className="text-[12px] font-600 text-text-3">{def?.label || key}:</span>
 452                      <span className="text-[13px] text-text-2">{String(val)}</span>
 453                    </div>
 454                  )
 455                })}
 456              </div>
 457            </div>
 458          )}
 459  
 460          {editing.qualityGate?.enabled && (
 461            <div className="mb-8">
 462              <SectionLabel>Quality Gate</SectionLabel>
 463              <div className="p-4 rounded-[14px] border border-white/[0.06] bg-surface space-y-1.5 text-[12px] text-text-2">
 464                <p>Min result chars: {editing.qualityGate.minResultChars ?? 80}</p>
 465                <p>Min evidence signals: {editing.qualityGate.minEvidenceItems ?? 2}</p>
 466                <p>Verification required: {(editing.qualityGate.requireVerification ?? false) ? 'Yes' : 'No'}</p>
 467                <p>Artifact required: {(editing.qualityGate.requireArtifact ?? false) ? 'Yes' : 'No'}</p>
 468                <p>Task report required: {(editing.qualityGate.requireReport ?? false) ? 'Yes' : 'No'}</p>
 469              </div>
 470            </div>
 471          )}
 472  
 473          {/* Images (thumbnails only, no remove/upload) */}
 474          {editing.images && editing.images.length > 0 && (
 475            <div className="mb-8">
 476              <SectionLabel>Images</SectionLabel>
 477              <div className="flex gap-2 flex-wrap">
 478                {editing.images.map((url, i) => (
 479                  // eslint-disable-next-line @next/next/no-img-element
 480                  <img key={i} src={url} alt="" className="w-20 h-20 rounded-[10px] object-cover border border-white/[0.08]" />
 481                ))}
 482              </div>
 483            </div>
 484          )}
 485  
 486          {/* Result */}
 487          {editing.result && (
 488            <div className="mb-8">
 489              <SectionLabel>Result</SectionLabel>
 490              <div className="p-4 rounded-[14px] border border-white/[0.06] bg-surface text-[13px] text-text-2 whitespace-pre-wrap max-h-[200px] overflow-y-auto">
 491                {editing.result}
 492              </div>
 493            </div>
 494          )}
 495  
 496          {Array.isArray(editing.outputFiles) && editing.outputFiles.length > 0 && (
 497            <div className="mb-8">
 498              <SectionLabel>Output Files</SectionLabel>
 499              <div className="flex flex-col gap-1.5">
 500                {editing.outputFiles.map((fileRef) => (
 501                  <code key={fileRef} className="text-[12px] text-text-3 font-mono break-all">
 502                    {fileRef}
 503                  </code>
 504                ))}
 505              </div>
 506            </div>
 507          )}
 508  
 509          {editing.completionReportPath && (
 510            <div className="mb-8">
 511              <SectionLabel>Task Report</SectionLabel>
 512              <code className="text-[12px] text-text-3 font-mono break-all">{editing.completionReportPath}</code>
 513            </div>
 514          )}
 515  
 516          {/* CLI Sessions */}
 517          {(editing.claudeResumeId || editing.codexResumeId || editing.opencodeResumeId || editing.geminiResumeId || editing.cliResumeId) && (
 518            <div className="mb-8">
 519              <SectionLabel>CLI Sessions</SectionLabel>
 520              <div className="flex flex-wrap gap-2">
 521                {editing.claudeResumeId && (
 522                  <div className="flex items-center gap-2 px-3 py-2 rounded-[10px] border border-white/[0.06] bg-surface">
 523                    <span className="text-[11px] font-600 text-amber-400">Claude</span>
 524                    <code className="text-[11px] text-text-3 font-mono">{editing.claudeResumeId}</code>
 525                  </div>
 526                )}
 527                {editing.codexResumeId && (
 528                  <div className="flex items-center gap-2 px-3 py-2 rounded-[10px] border border-white/[0.06] bg-surface">
 529                    <span className="text-[11px] font-600 text-emerald-400">Codex</span>
 530                    <code className="text-[11px] text-text-3 font-mono">{editing.codexResumeId}</code>
 531                  </div>
 532                )}
 533                {editing.opencodeResumeId && (
 534                  <div className="flex items-center gap-2 px-3 py-2 rounded-[10px] border border-white/[0.06] bg-surface">
 535                    <span className="text-[11px] font-600 text-sky-400">OpenCode</span>
 536                    <code className="text-[11px] text-text-3 font-mono">{editing.opencodeResumeId}</code>
 537                  </div>
 538                )}
 539                {editing.geminiResumeId && (
 540                  <div className="flex items-center gap-2 px-3 py-2 rounded-[10px] border border-white/[0.06] bg-surface">
 541                    <span className="text-[11px] font-600 text-fuchsia-400">Gemini</span>
 542                    <code className="text-[11px] text-text-3 font-mono">{editing.geminiResumeId}</code>
 543                  </div>
 544                )}
 545                {!(editing.claudeResumeId || editing.codexResumeId || editing.opencodeResumeId || editing.geminiResumeId) && editing.cliResumeId && (
 546                  <div className="flex items-center gap-2 px-3 py-2 rounded-[10px] border border-white/[0.06] bg-surface">
 547                    <span className="text-[11px] font-600 text-text-2">{editing.cliProvider || 'CLI'}</span>
 548                    <code className="text-[11px] text-text-3 font-mono">{editing.cliResumeId}</code>
 549                  </div>
 550                )}
 551              </div>
 552            </div>
 553          )}
 554  
 555          {/* Error / Retry notice */}
 556          {editing.error && (() => {
 557            const retryPending =
 558              editing.status !== 'failed' &&
 559              !editing.deadLetteredAt &&
 560              (editing.retryScheduledAt != null || /^Retry scheduled after failure/i.test(editing.error))
 561            const label = retryPending ? 'Retry Pending' : 'Error'
 562            const tone = retryPending
 563              ? 'border-amber-500/15 bg-amber-500/[0.04] text-amber-300/80'
 564              : 'border-red-500/10 bg-red-500/[0.03] text-red-400/80'
 565            const labelTone = retryPending ? 'text-amber-400' : 'text-red-400'
 566            return (
 567              <div className="mb-8">
 568                <label className={`block font-display text-[12px] font-600 uppercase tracking-[0.08em] mb-3 ${labelTone}`}>{label}</label>
 569                <div className={`p-4 rounded-[14px] border text-[13px] whitespace-pre-wrap ${tone}`}>
 570                  {editing.error}
 571                </div>
 572              </div>
 573            )
 574          })()}
 575  
 576          {/* Comments (with input — adding comments from view mode is useful) */}
 577          <div className="mb-8">
 578            <SectionLabel>Comments {editing.comments?.length ? `(${editing.comments.length})` : ''}</SectionLabel>
 579  
 580            {editing.comments && editing.comments.length > 0 && (
 581              <div className="space-y-3 mb-4 max-h-[300px] overflow-y-auto">
 582                {editing.comments.map((c) => (
 583                  <div key={c.id} className="p-3.5 rounded-[12px] border border-white/[0.06] bg-surface">
 584                    <div className="flex items-center gap-2 mb-1.5">
 585                      <span className={`text-[12px] font-600 ${c.agentId ? 'text-accent-bright' : 'text-text-2'}`}>
 586                        {c.author}
 587                      </span>
 588                      <span className="text-[10px] text-text-3/50 font-mono">{fmtTime(c.createdAt)}</span>
 589                    </div>
 590                    <p className="text-[13px] text-text-2 leading-[1.5] whitespace-pre-wrap">{c.text}</p>
 591                  </div>
 592                ))}
 593              </div>
 594            )}
 595  
 596            <div className="flex gap-2">
 597              <input
 598                type="text"
 599                value={commentText}
 600                onChange={(e) => setCommentText(e.target.value)}
 601                placeholder="Add a comment..."
 602                className={`${inputClass} flex-1`}
 603                style={{ fontFamily: 'inherit' }}
 604                onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleAddComment() } }}
 605              />
 606              <button
 607                onClick={handleAddComment}
 608                disabled={!commentText.trim()}
 609                className="px-4 py-3 rounded-[14px] border-none bg-accent-soft text-accent-bright text-[13px] font-600 cursor-pointer disabled:opacity-30 hover:brightness-110 transition-all shrink-0"
 610                style={{ fontFamily: 'inherit' }}
 611              >
 612                Post
 613              </button>
 614            </div>
 615          </div>
 616  
 617          {/* Footer: Edit + Close */}
 618          <div className="flex gap-3 pt-2 border-t border-white/[0.04]">
 619            {activeStructuredRunId && (
 620              <button
 621                onClick={() => router.push(`/protocols?runId=${encodeURIComponent(activeStructuredRunId)}`)}
 622                className="flex-1 py-3.5 rounded-[14px] border border-sky-500/20 bg-sky-500/10 text-sky-100 text-[15px] font-600 cursor-pointer hover:bg-sky-500/14 transition-all"
 623                style={{ fontFamily: 'inherit' }}
 624              >
 625                Open Session
 626              </button>
 627            )}
 628            <button
 629              onClick={() => setStructuredSessionOpen(true)}
 630              className="flex-1 py-3.5 rounded-[14px] border border-accent-bright/20 bg-accent-bright/10 text-accent-bright text-[15px] font-600 cursor-pointer hover:bg-accent-bright/14 transition-all"
 631              style={{ fontFamily: 'inherit' }}
 632            >
 633              {activeStructuredRunId ? 'Run Another Session' : 'Run Structured Session'}
 634            </button>
 635            <button
 636              onClick={onClose}
 637              className="flex-1 py-3.5 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[15px] font-600 cursor-pointer hover:bg-surface-2 transition-all"
 638              style={{ fontFamily: 'inherit' }}
 639            >
 640              Close
 641            </button>
 642            <button
 643              onClick={() => setViewOnly(false)}
 644              className="flex-1 py-3.5 rounded-[14px] border-none bg-accent-bright text-white text-[15px] font-600 cursor-pointer active:scale-[0.97] transition-all shadow-[0_4px_20px_rgba(99,102,241,0.25)] hover:brightness-110"
 645              style={{ fontFamily: 'inherit' }}
 646            >
 647              Edit
 648            </button>
 649          </div>
 650        </BottomSheet>
 651      )
 652    }
 653  
 654    /* ───── Edit / Create mode ───── */
 655    return (
 656      <BottomSheet open={open} onClose={onClose}>
 657        <div className="mb-8">
 658          <h2 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-2">
 659            {editing ? 'Edit Task' : 'New Task'}
 660          </h2>
 661          <p className="text-[14px] text-text-3">
 662            {editing ? `Status: ${editing.status}` : 'Create a task and assign an agent'}
 663          </p>
 664        </div>
 665  
 666        <div className="mb-8">
 667          <SectionLabel>Title</SectionLabel>
 668          <input
 669            type="text"
 670            value={title}
 671            onChange={(e) => setTitle(e.target.value)}
 672            placeholder="e.g. Run full site audit"
 673            className={inputClass}
 674            style={{ fontFamily: 'inherit' }}
 675          />
 676        </div>
 677  
 678        <div className="mb-8">
 679          <SectionLabel>Description</SectionLabel>
 680          <textarea
 681            value={description}
 682            onChange={(e) => setDescription(e.target.value)}
 683            placeholder="Detailed task instructions... Use @AgentName to auto-assign"
 684            rows={4}
 685            className={`${inputClass} resize-y min-h-[100px]`}
 686            style={{ fontFamily: 'inherit' }}
 687          />
 688        </div>
 689  
 690        {editing?.objective && (
 691          <div className="mb-8">
 692            <SectionLabel>Objective</SectionLabel>
 693            <div className="rounded-[14px] border border-white/[0.06] bg-surface px-4 py-3 text-[12px] leading-[1.7] text-text-3/75">
 694              <div className="font-600 text-text">{editing.objective}</div>
 695            </div>
 696          </div>
 697        )}
 698  
 699        {/* Priority */}
 700        <div className="mb-8">
 701          <SectionLabel>Priority <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span></SectionLabel>
 702          <div className="flex flex-wrap gap-2">
 703            {([['', 'None', 'bg-surface border-white/[0.06] text-text-2'],
 704              ['low', 'Low', 'bg-sky-500/10 border-sky-500/20 text-sky-400'],
 705              ['medium', 'Medium', 'bg-amber-500/10 border-amber-500/20 text-amber-400'],
 706              ['high', 'High', 'bg-orange-500/10 border-orange-500/20 text-orange-400'],
 707              ['critical', 'Critical', 'bg-red-500/10 border-red-500/20 text-red-400'],
 708            ] as const).map(([val, label, cls]) => (
 709              <button
 710                key={val}
 711                onClick={() => setPriority(val as typeof priority)}
 712                className={`px-4 py-3 rounded-[12px] text-[14px] font-600 cursor-pointer transition-all border
 713                  ${priority === val
 714                    ? `${cls} ring-1 ring-current`
 715                    : 'bg-surface border-white/[0.06] text-text-2 hover:bg-surface-2'}`}
 716                style={{ fontFamily: 'inherit' }}
 717              >
 718                {label}
 719              </button>
 720            ))}
 721          </div>
 722        </div>
 723  
 724        {/* Images */}
 725        <div className="mb-8">
 726          <SectionLabel>Images <span className="normal-case tracking-normal font-normal text-text-3">(optional — reference designs, mockups, etc.)</span></SectionLabel>
 727          {images.length > 0 && (
 728            <div className="flex gap-2 flex-wrap mb-3">
 729              {images.map((url, i) => (
 730                <div key={i} className="relative group">
 731                  <img src={url} alt="" className="w-20 h-20 rounded-[10px] object-cover border border-white/[0.08]" />
 732                  <button
 733                    onClick={() => setImages((prev) => prev.filter((_, idx) => idx !== i))}
 734                    className="absolute -top-1.5 -right-1.5 w-5 h-5 rounded-full bg-red-500 text-white flex items-center justify-center text-[11px] font-700 cursor-pointer
 735                      opacity-0 group-hover:opacity-100 transition-opacity border-none"
 736                  >
 737                    x
 738                  </button>
 739                </div>
 740              ))}
 741            </div>
 742          )}
 743          <label className={`inline-flex items-center gap-2 px-4 py-2.5 rounded-[12px] border border-white/[0.06] bg-surface text-text-3 text-[13px] font-600 cursor-pointer hover:bg-surface-2 transition-colors ${uploading ? 'opacity-50 pointer-events-none' : ''}`}>
 744            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
 745              <rect x="3" y="3" width="18" height="18" rx="2" />
 746              <circle cx="8.5" cy="8.5" r="1.5" />
 747              <polyline points="21 15 16 10 5 21" />
 748            </svg>
 749            {uploading ? 'Uploading...' : 'Add Image'}
 750            <input type="file" accept="image/*" onChange={handleImageUpload} className="hidden" />
 751          </label>
 752        </div>
 753  
 754        <div className="mb-8">
 755          <SectionLabel>Agent</SectionLabel>
 756          <AgentPickerList
 757            agents={agentList}
 758            selected={agentId}
 759            onSelect={(id) => setAgentId(id)}
 760          />
 761        </div>
 762  
 763        {/* Project (optional) */}
 764        <div className="mb-8">
 765          <SectionLabel>Project <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span></SectionLabel>
 766          <div className="flex flex-wrap gap-2">
 767            <button
 768              onClick={() => setProjectId('')}
 769              className={`px-4 py-3 rounded-[12px] text-[14px] font-600 cursor-pointer transition-all border
 770                ${!projectId
 771                  ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
 772                  : 'bg-surface border-white/[0.06] text-text-2 hover:bg-surface-2'}`}
 773              style={{ fontFamily: 'inherit' }}
 774            >
 775              None
 776            </button>
 777            {Object.values(projects).map((p) => (
 778              <button
 779                key={p.id}
 780                onClick={() => setProjectId(p.id)}
 781                className={`px-4 py-3 rounded-[12px] text-[14px] font-600 cursor-pointer transition-all border flex items-center gap-2
 782                  ${projectId === p.id
 783                    ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
 784                    : 'bg-surface border-white/[0.06] text-text-2 hover:bg-surface-2'}`}
 785                style={{ fontFamily: 'inherit' }}
 786              >
 787                <span className="w-2.5 h-2.5 rounded-full shrink-0" style={{ backgroundColor: p.color || '#6366F1' }} />
 788                {p.name}
 789              </button>
 790            ))}
 791          </div>
 792        </div>
 793  
 794        {/* Directory (optional) */}
 795        <div className="mb-8">
 796          <SectionLabel>Directory <span className="normal-case tracking-normal font-normal text-text-3">(optional — project to work in)</span></SectionLabel>
 797          <DirBrowser
 798            value={cwd || null}
 799            file={file}
 800            onChange={(dir, f) => {
 801              setCwd(dir)
 802              setFile(f ?? null)
 803              if (!title) {
 804                const dirName = dir.split('/').pop() || ''
 805                setTitle(dirName)
 806              }
 807            }}
 808            onClear={() => { setCwd(''); setFile(null) }}
 809          />
 810        </div>
 811  
 812        {/* Tags */}
 813        <div className="mb-8">
 814          <SectionLabel>Tags <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span></SectionLabel>
 815          {tags.length > 0 && (
 816            <div className="flex flex-wrap gap-1.5 mb-3">
 817              {tags.map((tag) => (
 818                <span key={tag} className="inline-flex items-center gap-1 px-2 py-1 rounded-[8px] bg-indigo-500/10 text-indigo-400 text-[12px] font-600">
 819                  {tag}
 820                  <button onClick={() => setTags((prev) => prev.filter((t) => t !== tag))} className="text-indigo-400/60 hover:text-indigo-400 cursor-pointer border-none bg-transparent p-0 text-[14px] leading-none">&times;</button>
 821                </span>
 822              ))}
 823            </div>
 824          )}
 825          <div className="relative">
 826            <input
 827              type="text"
 828              value={tagInput}
 829              onChange={(e) => setTagInput(e.target.value)}
 830              onKeyDown={(e) => {
 831                if (e.key === 'Enter' && tagInput.trim()) {
 832                  e.preventDefault()
 833                  const t = tagInput.trim().toLowerCase()
 834                  if (!tags.includes(t)) setTags((prev) => [...prev, t])
 835                  setTagInput('')
 836                }
 837              }}
 838              placeholder="Type and press Enter to add..."
 839              className={inputClass}
 840              style={{ fontFamily: 'inherit' }}
 841              list="tag-suggestions"
 842            />
 843            <datalist id="tag-suggestions">
 844              {dedup(Object.values(tasks).flatMap((t) => t.tags || []))
 845                .filter((t) => !tags.includes(t) && t.includes(tagInput.toLowerCase()))
 846                .slice(0, 10)
 847                .map((t) => <option key={t} value={t} />)}
 848            </datalist>
 849          </div>
 850        </div>
 851  
 852        {/* Dependencies */}
 853        <div className="mb-8">
 854          <SectionLabel>Blocked By <span className="normal-case tracking-normal font-normal text-text-3">(tasks that must complete first)</span></SectionLabel>
 855          {/* Selected blockers as removable chips */}
 856          {blockedBy.length > 0 && (
 857            <div className="flex flex-wrap gap-1.5 mb-3">
 858              {blockedBy.map((bid) => {
 859                const bt = tasks[bid]
 860                return (
 861                  <span key={bid} className="inline-flex items-center gap-1 px-2.5 py-1 rounded-[8px] bg-rose-500/10 text-rose-400 text-[12px] font-600">
 862                    {bt ? bt.title : bid}
 863                    <button
 864                      onClick={() => setBlockedBy((prev) => prev.filter((b) => b !== bid))}
 865                      className="text-rose-400/60 hover:text-rose-400 cursor-pointer border-none bg-transparent p-0 text-[14px] leading-none"
 866                    >
 867                      &times;
 868                    </button>
 869                  </span>
 870                )
 871              })}
 872            </div>
 873          )}
 874          {/* Searchable dropdown for adding dependencies */}
 875          <div className="relative">
 876            <input
 877              type="text"
 878              value={depSearch}
 879              onChange={(e) => setDepSearch(e.target.value)}
 880              placeholder="Search tasks to add as dependency..."
 881              className={inputClass}
 882              style={{ fontFamily: 'inherit' }}
 883            />
 884            {depSearch.trim() && (
 885              <div className="absolute z-20 top-full left-0 right-0 mt-1 max-h-[200px] overflow-y-auto rounded-[12px] border border-white/[0.08] bg-surface shadow-xl">
 886                {Object.values(tasks)
 887                  .filter((t) =>
 888                    t.id !== editingId &&
 889                    t.status !== 'archived' &&
 890                    !blockedBy.includes(t.id) &&
 891                    t.title.toLowerCase().includes(depSearch.toLowerCase())
 892                  )
 893                  .slice(0, 10)
 894                  .map((t) => (
 895                    <button
 896                      key={t.id}
 897                      onClick={() => {
 898                        setBlockedBy((prev) => [...prev, t.id])
 899                        setDepSearch('')
 900                      }}
 901                      className="w-full text-left px-4 py-2.5 text-[13px] text-text-2 hover:bg-surface-2 cursor-pointer border-none bg-transparent transition-colors flex items-center gap-2"
 902                      style={{ fontFamily: 'inherit' }}
 903                    >
 904                      <span className="flex-1 truncate">{t.title}</span>
 905                      <span className="text-[10px] text-text-3 shrink-0">({t.status})</span>
 906                    </button>
 907                  ))}
 908                {Object.values(tasks).filter((t) =>
 909                  t.id !== editingId &&
 910                  t.status !== 'archived' &&
 911                  !blockedBy.includes(t.id) &&
 912                  t.title.toLowerCase().includes(depSearch.toLowerCase())
 913                ).length === 0 && (
 914                  <div className="px-4 py-3 text-[13px] text-text-3">No matching tasks</div>
 915                )}
 916              </div>
 917            )}
 918          </div>
 919          {depError && (
 920            <p className="mt-2 text-[12px] text-red-400 font-600">{depError}</p>
 921          )}
 922          {editing && Array.isArray(editing.blocks) && editing.blocks.length > 0 && (
 923            <div className="mt-3">
 924              <span className="text-[11px] font-600 text-text-3 uppercase tracking-[0.06em]">Blocks:</span>
 925              <div className="flex flex-wrap gap-1.5 mt-1.5">
 926                {editing.blocks.map((bid) => {
 927                  const bt = tasks[bid]
 928                  return bt ? (
 929                    <span key={bid} className="px-2 py-1 rounded-[6px] bg-white/[0.04] text-text-3 text-[11px] font-600">{bt.title}</span>
 930                  ) : null
 931                })}
 932              </div>
 933            </div>
 934          )}
 935        </div>
 936  
 937        {/* Due Date */}
 938        <div className="mb-8">
 939          <SectionLabel>Due Date <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span></SectionLabel>
 940          <input
 941            type="date"
 942            value={dueAt}
 943            onChange={(e) => setDueAt(e.target.value)}
 944            className={`${inputClass} appearance-none`}
 945            style={{ fontFamily: 'inherit', colorScheme: 'dark' }}
 946          />
 947        </div>
 948  
 949        <div className="mb-8">
 950          <SectionLabel>Quality Gate</SectionLabel>
 951          <p className="text-[12px] text-text-3 mb-3">
 952            Checks that must pass before this task can be marked completed.
 953          </p>
 954          <div className="p-4 rounded-[14px] border border-white/[0.06] bg-surface">
 955            <button
 956              onClick={() => setQualityGateEnabled((prev) => !prev)}
 957              className={`relative w-10 h-[22px] rounded-full transition-colors duration-200 cursor-pointer ${qualityGateEnabled ? 'bg-accent' : 'bg-white/[0.12]'}`}
 958            >
 959              <span className={`absolute top-[3px] left-[3px] w-4 h-4 rounded-full bg-white transition-transform duration-200 ${qualityGateEnabled ? 'translate-x-[18px]' : ''}`} />
 960            </button>
 961            <span className="ml-2 text-[12px] text-text-2">{qualityGateEnabled ? 'Enabled' : 'Disabled'}</span>
 962  
 963            {qualityGateEnabled && (
 964              <div className="grid grid-cols-1 md:grid-cols-2 gap-3 mt-4">
 965                <div>
 966                  <label className="block text-[11px] text-text-3 mb-1.5">Min Result Chars</label>
 967                  <input
 968                    type="number"
 969                    min={10}
 970                    max={2000}
 971                    value={qualityGateMinResultChars}
 972                    onChange={(e) => setQualityGateMinResultChars(normalizeGateNumber(e.target.value, 80, 10, 2000))}
 973                    className={inputClass}
 974                    style={{ fontFamily: 'inherit' }}
 975                  />
 976                </div>
 977                <div>
 978                  <label className="block text-[11px] text-text-3 mb-1.5">Min Evidence Signals</label>
 979                  <input
 980                    type="number"
 981                    min={0}
 982                    max={8}
 983                    value={qualityGateMinEvidenceItems}
 984                    onChange={(e) => setQualityGateMinEvidenceItems(normalizeGateNumber(e.target.value, 2, 0, 8))}
 985                    className={inputClass}
 986                    style={{ fontFamily: 'inherit' }}
 987                  />
 988                </div>
 989                <label className="flex items-center gap-2 text-[12px] text-text-2">
 990                  <input
 991                    type="checkbox"
 992                    checked={qualityGateRequireVerification}
 993                    onChange={(e) => setQualityGateRequireVerification(e.target.checked)}
 994                    className="h-4 w-4 rounded border-white/20 accent-accent"
 995                  />
 996                  Require verification evidence (tests/lint/build)
 997                </label>
 998                <label className="flex items-center gap-2 text-[12px] text-text-2">
 999                  <input
1000                    type="checkbox"
1001                    checked={qualityGateRequireArtifact}
1002                    onChange={(e) => setQualityGateRequireArtifact(e.target.checked)}
1003                    className="h-4 w-4 rounded border-white/20 accent-accent"
1004                  />
1005                  Require artifact evidence (upload URL or task artifacts)
1006                </label>
1007                <label className="flex items-center gap-2 text-[12px] text-text-2 md:col-span-2">
1008                  <input
1009                    type="checkbox"
1010                    checked={qualityGateRequireReport}
1011                    onChange={(e) => setQualityGateRequireReport(e.target.checked)}
1012                    className="h-4 w-4 rounded border-white/20 accent-accent"
1013                  />
1014                  Require generated task report
1015                </label>
1016              </div>
1017            )}
1018          </div>
1019        </div>
1020  
1021        {/* Custom Fields */}
1022        {appSettings.taskCustomFieldDefs && appSettings.taskCustomFieldDefs.length > 0 && (
1023          <div className="mb-8">
1024            <SectionLabel>Custom Fields</SectionLabel>
1025            <div className="space-y-4">
1026              {appSettings.taskCustomFieldDefs.map((def) => (
1027                <div key={def.key}>
1028                  <label className="block text-[12px] text-text-3 mb-1.5">{def.label}</label>
1029                  {def.type === 'select' ? (
1030                    <select
1031                      value={String(customFields[def.key] ?? '')}
1032                      onChange={(e) => setCustomFields((prev) => ({ ...prev, [def.key]: e.target.value }))}
1033                      className={inputClass}
1034                      style={{ fontFamily: 'inherit' }}
1035                    >
1036                      <option value="">—</option>
1037                      {def.options?.map((opt) => <option key={opt} value={opt}>{opt}</option>)}
1038                    </select>
1039                  ) : (
1040                    <input
1041                      type={def.type === 'number' ? 'number' : 'text'}
1042                      value={String(customFields[def.key] ?? '')}
1043                      onChange={(e) => setCustomFields((prev) => ({
1044                        ...prev,
1045                        [def.key]: def.type === 'number' ? (e.target.value === '' ? '' : Number(e.target.value)) : e.target.value,
1046                      }))}
1047                      className={inputClass}
1048                      style={{ fontFamily: 'inherit' }}
1049                    />
1050                  )}
1051                </div>
1052              ))}
1053            </div>
1054          </div>
1055        )}
1056  
1057        {editing?.result && (
1058          <div className="mb-8">
1059            <SectionLabel>Result</SectionLabel>
1060            <div className="p-4 rounded-[14px] border border-white/[0.06] bg-surface text-[13px] text-text-2 whitespace-pre-wrap max-h-[200px] overflow-y-auto">
1061              {editing.result}
1062            </div>
1063          </div>
1064        )}
1065  
1066        {editing && (editing.claudeResumeId || editing.codexResumeId || editing.opencodeResumeId || editing.geminiResumeId || editing.cliResumeId) && (
1067          <div className="mb-8">
1068            <SectionLabel>CLI Sessions</SectionLabel>
1069            <div className="flex flex-wrap gap-2">
1070              {editing.claudeResumeId && (
1071                <div className="flex items-center gap-2 px-3 py-2 rounded-[10px] border border-white/[0.06] bg-surface">
1072                  <span className="text-[11px] font-600 text-amber-400">Claude</span>
1073                  <code className="text-[11px] text-text-3 font-mono">{editing.claudeResumeId}</code>
1074                </div>
1075              )}
1076              {editing.codexResumeId && (
1077                <div className="flex items-center gap-2 px-3 py-2 rounded-[10px] border border-white/[0.06] bg-surface">
1078                  <span className="text-[11px] font-600 text-emerald-400">Codex</span>
1079                  <code className="text-[11px] text-text-3 font-mono">{editing.codexResumeId}</code>
1080                </div>
1081              )}
1082              {editing.opencodeResumeId && (
1083                <div className="flex items-center gap-2 px-3 py-2 rounded-[10px] border border-white/[0.06] bg-surface">
1084                  <span className="text-[11px] font-600 text-sky-400">OpenCode</span>
1085                  <code className="text-[11px] text-text-3 font-mono">{editing.opencodeResumeId}</code>
1086                </div>
1087              )}
1088              {editing.geminiResumeId && (
1089                <div className="flex items-center gap-2 px-3 py-2 rounded-[10px] border border-white/[0.06] bg-surface">
1090                  <span className="text-[11px] font-600 text-fuchsia-400">Gemini</span>
1091                  <code className="text-[11px] text-text-3 font-mono">{editing.geminiResumeId}</code>
1092                </div>
1093              )}
1094              {!(editing.claudeResumeId || editing.codexResumeId || editing.opencodeResumeId || editing.geminiResumeId) && editing.cliResumeId && (
1095                <div className="flex items-center gap-2 px-3 py-2 rounded-[10px] border border-white/[0.06] bg-surface">
1096                  <span className="text-[11px] font-600 text-text-2">{editing.cliProvider || 'CLI'}</span>
1097                  <code className="text-[11px] text-text-3 font-mono">{editing.cliResumeId}</code>
1098                </div>
1099              )}
1100            </div>
1101          </div>
1102        )}
1103  
1104        {editing?.error && (
1105          <div className="mb-8">
1106            <label className="block font-display text-[12px] font-600 text-red-400 uppercase tracking-[0.08em] mb-3">Error</label>
1107            <div className="p-4 rounded-[14px] border border-red-500/10 bg-red-500/[0.03] text-[13px] text-red-400/80 whitespace-pre-wrap">
1108              {editing.error}
1109            </div>
1110          </div>
1111        )}
1112  
1113        {/* Comments */}
1114        {editing && (
1115          <div className="mb-8">
1116            <SectionLabel>Comments {editing.comments?.length ? `(${editing.comments.length})` : ''}</SectionLabel>
1117  
1118            {editing.comments && editing.comments.length > 0 && (
1119              <div className="space-y-3 mb-4 max-h-[300px] overflow-y-auto">
1120                {editing.comments.map((c) => (
1121                  <div key={c.id} className="p-3.5 rounded-[12px] border border-white/[0.06] bg-surface">
1122                    <div className="flex items-center gap-2 mb-1.5">
1123                      <span className={`text-[12px] font-600 ${c.agentId ? 'text-accent-bright' : 'text-text-2'}`}>
1124                        {c.author}
1125                      </span>
1126                      <span className="text-[10px] text-text-3/50 font-mono">{fmtTime(c.createdAt)}</span>
1127                    </div>
1128                    <p className="text-[13px] text-text-2 leading-[1.5] whitespace-pre-wrap">{c.text}</p>
1129                  </div>
1130                ))}
1131              </div>
1132            )}
1133  
1134            <div className="flex gap-2">
1135              <input
1136                type="text"
1137                value={commentText}
1138                onChange={(e) => setCommentText(e.target.value)}
1139                placeholder="Add a comment..."
1140                className={`${inputClass} flex-1`}
1141                style={{ fontFamily: 'inherit' }}
1142                onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleAddComment() } }}
1143              />
1144              <button
1145                onClick={handleAddComment}
1146                disabled={!commentText.trim()}
1147                className="px-4 py-3 rounded-[14px] border-none bg-accent-soft text-accent-bright text-[13px] font-600 cursor-pointer disabled:opacity-30 hover:brightness-110 transition-all shrink-0"
1148                style={{ fontFamily: 'inherit' }}
1149              >
1150                Post
1151              </button>
1152            </div>
1153          </div>
1154        )}
1155  
1156        <SheetFooter
1157          onCancel={onClose}
1158          onSave={handleSave}
1159          saveLabel={editing ? 'Save' : 'Create'}
1160          saveDisabled={!title.trim() || !agentId}
1161          left={<>
1162            {editing && activeStructuredRunId && (
1163              <button onClick={() => router.push(`/protocols?runId=${encodeURIComponent(activeStructuredRunId)}`)} className="py-3.5 px-6 rounded-[14px] border border-sky-500/20 bg-transparent text-sky-100 text-[15px] font-600 cursor-pointer hover:bg-sky-500/10 transition-all" style={{ fontFamily: 'inherit' }}>
1164                Open Session
1165              </button>
1166            )}
1167            {editing && (
1168              <button onClick={() => setStructuredSessionOpen(true)} className="py-3.5 px-6 rounded-[14px] border border-accent-bright/20 bg-transparent text-accent-bright text-[15px] font-600 cursor-pointer hover:bg-accent-bright/10 transition-all" style={{ fontFamily: 'inherit' }}>
1169                {activeStructuredRunId ? 'Run Another Session' : 'Run Structured Session'}
1170              </button>
1171            )}
1172            {editing && editing.status !== 'archived' && (
1173              <button onClick={handleArchive} className="py-3.5 px-6 rounded-[14px] border border-white/[0.08] bg-transparent text-text-3 text-[15px] font-600 cursor-pointer hover:bg-white/[0.04] transition-all" style={{ fontFamily: 'inherit' }}>
1174                Archive
1175              </button>
1176            )}
1177            {editing && editing.status === 'archived' && (
1178              <button onClick={handleUnarchive} className="py-3.5 px-6 rounded-[14px] border border-accent-bright/20 bg-transparent text-accent-bright text-[15px] font-600 cursor-pointer hover:bg-accent-bright/10 transition-all" style={{ fontFamily: 'inherit' }}>
1179                Unarchive
1180              </button>
1181            )}
1182            {editing && editing.status === 'backlog' && (
1183              <button onClick={handleQueue} className="py-3.5 px-6 rounded-[14px] border border-amber-500/20 bg-transparent text-amber-400 text-[15px] font-600 cursor-pointer hover:bg-amber-500/10 transition-all" style={{ fontFamily: 'inherit' }}>
1184                Queue
1185              </button>
1186            )}
1187          </>}
1188        />
1189        <StructuredSessionLauncher
1190          open={structuredSessionOpen}
1191          onClose={() => setStructuredSessionOpen(false)}
1192          onCreated={(run) => {
1193            router.push(`/protocols?runId=${encodeURIComponent(run.id)}`)
1194          }}
1195          initialContext={{
1196            taskId: editing?.id || null,
1197            taskLabel: editing?.title || null,
1198            participantAgentIds: editing?.agentId ? [editing.agentId] : agentId ? [agentId] : [],
1199            facilitatorAgentId: editing?.agentId || agentId || null,
1200            title: editing ? `Structured session: ${editing.title}` : title ? `Structured session: ${title}` : null,
1201            goal: editing?.description || description || title || null,
1202          }}
1203        />
1204      </BottomSheet>
1205    )
1206  }