Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | import { cn } from "@heroui/react" import { parseFocusChainItem } from "@shared/focus-chain-utils" import { CheckIcon, CircleIcon } from "lucide-react" import React, { useCallback, useEffect, useRef, useState } from "react" import LightMarkdown from "./LightMarkdown" interface ChecklistRendererProps { text: string } interface ChecklistItem { checked: boolean text: string } const ChecklistRenderer: React.FC<ChecklistRendererProps> = ({ text }) => { const containerRef = useRef<HTMLDivElement>(null) const [lastCompletedIndex, setLastCompletedIndex] = useState(-1) const [isUserScrolling, setIsUserScrolling] = useState(false) const scrollTimeoutRef = useRef<NodeJS.Timeout>() const parseChecklistItems = (text: string): ChecklistItem[] => { const lines = text.split("\n").filter((line) => line.trim()) const items: ChecklistItem[] = [] for (const line of lines) { const trimmedLine = line.trim() const parsed = parseFocusChainItem(trimmedLine) if (parsed) { items.push({ checked: parsed.checked, text: parsed.text }) } } return items } const items = parseChecklistItems(text) // Handle user scroll detection // This prevents jumpy scrolling when the task is streaming and users are viewing the focus chain list const handleScroll = useCallback(() => { setIsUserScrolling(true) if (scrollTimeoutRef.current) { clearTimeout(scrollTimeoutRef.current) } scrollTimeoutRef.current = setTimeout(() => { setIsUserScrolling(false) }, 1000) // Reset after 1 second of no scrolling }, []) // Auto-scroll to show the most recently completed item when in scroll mode useEffect(() => { if (items.length >= 10 && containerRef.current && !isUserScrolling) { // Find the last completed item let currentLastCompletedIndex = -1 for (let i = items.length - 1; i >= 0; i--) { if (items[i].checked) { currentLastCompletedIndex = i break } } // Only auto-scroll if there's a new completion or first time if (currentLastCompletedIndex >= 0 && currentLastCompletedIndex !== lastCompletedIndex) { setLastCompletedIndex(currentLastCompletedIndex) // Use scrollIntoView for more accurate positioning const container = containerRef.current const itemElements = container.children if (itemElements[currentLastCompletedIndex]) { itemElements[currentLastCompletedIndex].scrollIntoView({ behavior: "smooth", block: "start", }) } } } }, [items, lastCompletedIndex, isUserScrolling]) // Cleanup timeout on unmount useEffect(() => { return () => { if (scrollTimeoutRef.current) { clearTimeout(scrollTimeoutRef.current) } } }, []) if (items.length === 0) { // If no checklist items found, return the original text return <div style={{ whiteSpace: "pre-wrap" }}>{text}</div> } return ( <div onScroll={handleScroll} ref={containerRef} style={{ display: "flex", flexDirection: "column", gap: "2px", fontSize: "12px", lineHeight: "1.3", maxHeight: items.length >= 10 ? "200px" : "auto", overflowY: items.length >= 10 ? "auto" : "visible", }}> {items.map((item, index) => ( // biome-ignore lint/suspicious/noArrayIndexKey: Using index as key for checklist items <div className="flex items-start gap-1.5 p-0.5" key={`checklist-item-${index}`}> <span className={cn("text-xs shrink-0 mt-0.5", item.checked ? "text-success" : "text-foreground")}> {item.checked ? <CheckIcon size={10} /> : <CircleIcon size={10} />} </span> <div className={cn("text-xs break-words flex-1", item.checked ? "text-description" : "text-foreground")} style={{ textDecoration: item.checked ? "line-through" : "none", lineHeight: "1.3", }}> <LightMarkdown compact text={item.text} /> </div> </div> ))} </div> ) } export default ChecklistRenderer |