import { VSCodeButton } from "@vscode/webview-ui-toolkit/react" import React, { memo, useEffect, useMemo, useRef, useState } from "react" import { useWindowSize } from "react-use" import { ClineMessage } from "../../../../src/shared/ExtensionMessage" import { useExtensionState } from "../../context/ExtensionStateContext" import { vscode } from "../../utils/vscode" import Thumbnails from "../common/Thumbnails" import { mentionRegexGlobal } from "../../../../src/shared/context-mentions" import { formatLargeNumber } from "../../utils/format" import { normalizeApiConfiguration } from "../settings/ApiOptions" interface TaskHeaderProps { task: ClineMessage tokensIn: number tokensOut: number doesModelSupportPromptCache: boolean cacheWrites?: number cacheReads?: number totalCost: number contextTokens: number onClose: () => void } const TaskHeader: React.FC = ({ task, tokensIn, tokensOut, doesModelSupportPromptCache, cacheWrites, cacheReads, totalCost, contextTokens, onClose, }) => { const { apiConfiguration } = useExtensionState() const { selectedModelInfo } = useMemo(() => normalizeApiConfiguration(apiConfiguration), [apiConfiguration]) const [isTaskExpanded, setIsTaskExpanded] = useState(true) const [isTextExpanded, setIsTextExpanded] = useState(false) const [showSeeMore, setShowSeeMore] = useState(false) const textContainerRef = useRef(null) const textRef = useRef(null) const contextWindow = selectedModelInfo?.contextWindow || 1 const contextPercentage = Math.round((contextTokens / contextWindow) * 100) /* When dealing with event listeners in React components that depend on state variables, we face a challenge. We want our listener to always use the most up-to-date version of a callback function that relies on current state, but we don't want to constantly add and remove event listeners as that function updates. This scenario often arises with resize listeners or other window events. Simply adding the listener in a useEffect with an empty dependency array risks using stale state, while including the callback in the dependencies can lead to unnecessary re-registrations of the listener. There are react hook libraries that provide a elegant solution to this problem by utilizing the useRef hook to maintain a reference to the latest callback function without triggering re-renders or effect re-runs. This approach ensures that our event listener always has access to the most current state while minimizing performance overhead and potential memory leaks from multiple listener registrations. Sources - https://usehooks-ts.com/react-hook/use-event-listener - https://streamich.github.io/react-use/?path=/story/sensors-useevent--docs - https://github.com/streamich/react-use/blob/master/src/useEvent.ts - https://stackoverflow.com/questions/55565444/how-to-register-event-with-useeffect-hooks Before: const updateMaxHeight = useCallback(() => { if (isExpanded && textContainerRef.current) { const maxHeight = window.innerHeight * (3 / 5) textContainerRef.current.style.maxHeight = `${maxHeight}px` } }, [isExpanded]) useEffect(() => { updateMaxHeight() }, [isExpanded, updateMaxHeight]) useEffect(() => { window.removeEventListener("resize", updateMaxHeight) window.addEventListener("resize", updateMaxHeight) return () => { window.removeEventListener("resize", updateMaxHeight) } }, [updateMaxHeight]) After: */ const { height: windowHeight, width: windowWidth } = useWindowSize() useEffect(() => { if (isTextExpanded && textContainerRef.current) { const maxHeight = windowHeight * (1 / 2) textContainerRef.current.style.maxHeight = `${maxHeight}px` } }, [isTextExpanded, windowHeight]) useEffect(() => { if (textRef.current && textContainerRef.current) { let textContainerHeight = textContainerRef.current.clientHeight if (!textContainerHeight) { textContainerHeight = textContainerRef.current.getBoundingClientRect().height } const isOverflowing = textRef.current.scrollHeight > textContainerHeight // necessary to show see more button again if user resizes window to expand and then back to collapse if (!isOverflowing) { setIsTextExpanded(false) } setShowSeeMore(isOverflowing) } }, [task.text, windowWidth]) const isCostAvailable = useMemo(() => { return ( apiConfiguration?.apiProvider !== "openai" && apiConfiguration?.apiProvider !== "ollama" && apiConfiguration?.apiProvider !== "lmstudio" && apiConfiguration?.apiProvider !== "gemini" ) }, [apiConfiguration?.apiProvider]) const shouldShowPromptCacheInfo = doesModelSupportPromptCache && apiConfiguration?.apiProvider !== "openrouter" return (
setIsTaskExpanded(!isTaskExpanded)}>
Task{!isTaskExpanded && ":"} {!isTaskExpanded && ( {highlightMentions(task.text, false)} )}
{!isTaskExpanded && isCostAvailable && (
${totalCost?.toFixed(4)}
)}
{isTaskExpanded && ( <>
{highlightMentions(task.text, false)}
{!isTextExpanded && showSeeMore && (
setIsTextExpanded(!isTextExpanded)}> See more
)}
{isTextExpanded && showSeeMore && (
setIsTextExpanded(!isTextExpanded)}> See less
)} {task.images && task.images.length > 0 && }
Tokens: {formatLargeNumber(tokensIn || 0)} {formatLargeNumber(tokensOut || 0)}
{!isCostAvailable && }
Context: {contextTokens ? `${formatLargeNumber(contextTokens)} (${contextPercentage}%)` : "-"}
{shouldShowPromptCacheInfo && (cacheReads !== undefined || cacheWrites !== undefined) && (
Cache: +{formatLargeNumber(cacheWrites || 0)} {formatLargeNumber(cacheReads || 0)}
)} {isCostAvailable && (
API Cost: ${totalCost?.toFixed(4)}
)}
)}
{/* {apiProvider === "" && (
Credits Remaining:
{formatPrice(Credits || 0)} {(Credits || 0) < 1 && ( <> {" "} (get more?) )}
)} */}
) } export const highlightMentions = (text?: string, withShadow = true) => { if (!text) return text const parts = text.split(mentionRegexGlobal) return parts.map((part, index) => { if (index % 2 === 0) { // This is regular text return part } else { // This is a mention return ( vscode.postMessage({ type: "openMention", text: part })}> @{part} ) } }) } const ExportButton = () => ( vscode.postMessage({ type: "exportCurrentTask" })} style={ { // marginBottom: "-2px", // marginRight: "-2.5px", } }>
EXPORT
) export default memo(TaskHeader)